diff --git a/src/doctor/checks/ios.ts b/src/doctor/checks/ios.ts index f9f656a..0722faf 100644 --- a/src/doctor/checks/ios.ts +++ b/src/doctor/checks/ios.ts @@ -6,10 +6,13 @@ import { POD_MESSAGING_PUSH_FCM, POD_TRACKING, iOS_DEPLOYMENT_TARGET_MIN_REQUIRED, + iOS_SDK_PUSH_SWIZZLE_VERSION, } from '../constants'; import { Context, iOSProject } from '../core'; import { CheckGroup } from '../types'; import { + compareSemanticVersions, + extractSemanticVersion, extractVersionFromPodLock, getReadablePath, logger, @@ -35,7 +38,7 @@ export async function runChecks(group: CheckGroup): Promise { case CheckGroup.PushSetup: await runCatching(validatePushEntitlements)(project); await runCatching(analyzeNotificationServiceExtensionProperties)(project); - await runCatching(validateUserNotificationCenterDelegate)(project); + await runCatching(validateMessagingPushInitialization)(project); break; case CheckGroup.Dependencies: @@ -274,43 +277,52 @@ function getDeploymentTargetVersion( return null; } -async function validateUserNotificationCenterDelegate( +async function validateMessagingPushInitialization( project: iOSProject ): Promise { - let allRequirementsMet = false; - const userNotificationCenterPatternSwift = - /func\s+userNotificationCenter\(\s*_[^:]*:\s*UNUserNotificationCenter,\s*didReceive[^:]*:\s*UNNotificationResponse,\s*withCompletionHandler[^:]*:\s*@?escaping\s*\(?\)?\s*->\s*Void\s*\)?/; - const userNotificationCenterPatternObjC = - /-\s*\(\s*void\s*\)\s*userNotificationCenter:\s*\(\s*UNUserNotificationCenter\s*\s*\*\s*\)\s*[^:]*\s*didReceiveNotificationResponse:\s*\(\s*UNNotificationResponse\s*\*\s*\)\s*[^:]*\s*withCompletionHandler:\s*\(\s*void\s*\(\s*\^\s*\)\(\s*void\s*\)\s*\)\s*[^;]*;?/; + logger.debug(`Checking for MessagingPush Initialization in iOS`); + + // Search for any of the following patterns in the project files: + // - MessagingPush.initialize + // - MessagingPushAPN.initialize + // - MessagingPushFCM.initialize + const moduleInitializationPattern = /MessagingPush\w*\.initialize/; + const moduleInitializationFiles = searchFilesForCode( + { + codePatternByExtension: { + '.swift': moduleInitializationPattern, + '.m': moduleInitializationPattern, + '.mm': moduleInitializationPattern, + }, + ignoreDirectories: ['Images.xcassets'], + targetFileNames: ['AppDelegate'], + targetFilePatterns: ['cio', 'customerio', 'notification', 'push'], + }, + project.iOSProjectPath + ); - for (const appDelegateFile of project.appDelegateFiles) { - logger.debug( - `Checking AppDelegate at path: ${appDelegateFile.readablePath}` + if (moduleInitializationFiles.matchedFiles.length > 0) { + logger.success( + `MessagingPush Initialization found in ${moduleInitializationFiles.formattedMatchedFiles}` ); - const extension = appDelegateFile.args.get('extension'); - let pattern: RegExp; - switch (extension) { - case 'swift': - pattern = userNotificationCenterPatternSwift; - break; - case 'Objective-C': - case 'Objective-C++': - pattern = userNotificationCenterPatternObjC; - break; - default: - continue; - } - - allRequirementsMet = - allRequirementsMet || pattern.test(appDelegateFile.content!); - } - - if (allRequirementsMet) { - logger.success(`“Opened” metric tracking enabled`); } else { - logger.failure( - `Missing method in AppDelegate for tracking push "opened" metrics` + logger.debug(`Search Criteria:`); + logger.debug( + `Searching files with names: ${moduleInitializationFiles.formattedTargetFileNames}` + ); + logger.debug( + `Searching files with keywords: ${moduleInitializationFiles.formattedTargetPatterns}` ); + logger.debug( + `Looked into the following files: ${moduleInitializationFiles.formattedSearchedFiles}` + ); + if (logger.isDebug()) { + logger.failure('MessagingPush Module Initialization not found'); + } else { + logger.failure( + 'MessagingPush Module Initialization not found. For more details, run the script with the -v flag' + ); + } } } @@ -424,7 +436,14 @@ async function extractPodVersions(project: iOSProject): Promise { const podfileLock = project.podfileLock; const podfileLockContent = podfileLock.content; - const validatePod = (podName: string, optional: boolean = false): boolean => { + const validatePod = ( + podName: string, + optional: boolean = false, + // Minimum required version for new features + minRequiredVersion: string | undefined = undefined, + // Message to display when the pod needs to be updated for new features + updatePodMessage: string = `Please update ${podName} to latest version following our documentation` + ): boolean => { let podVersions: string | undefined; if (podfileLockContent) { podVersions = extractVersionFromPodLock(podfileLockContent, podName); @@ -432,6 +451,18 @@ async function extractPodVersions(project: iOSProject): Promise { if (podVersions) { logger.success(`${podName}: ${podVersions}`); + + // Check if the pod version is below the minimum required version + // If so, alert the user to update the pod for new features + if ( + minRequiredVersion && + compareSemanticVersions( + extractSemanticVersion(podVersions), + minRequiredVersion + ) < 0 + ) { + logger.alert(updatePodMessage); + } } else if (!optional) { logger.failure(`${podName} module not found`); } @@ -441,8 +472,21 @@ async function extractPodVersions(project: iOSProject): Promise { validatePod(POD_TRACKING); validatePod(POD_MESSAGING_IN_APP); - const pushMessagingAPNPod = validatePod(POD_MESSAGING_PUSH_APN, true); - const pushMessagingFCMPod = validatePod(POD_MESSAGING_PUSH_FCM, true); + // Alert message for updating Push Messaging pods + const pushMessagingPodUpdateMessage = (podName: string) => + `Please update ${podName} to latest version following our documentation for improved tracking of push notification metrics`; + const pushMessagingAPNPod = validatePod( + POD_MESSAGING_PUSH_APN, + true, + iOS_SDK_PUSH_SWIZZLE_VERSION, + pushMessagingPodUpdateMessage(POD_MESSAGING_PUSH_APN) + ); + const pushMessagingFCMPod = validatePod( + POD_MESSAGING_PUSH_FCM, + true, + iOS_SDK_PUSH_SWIZZLE_VERSION, + pushMessagingPodUpdateMessage(POD_MESSAGING_PUSH_FCM) + ); if (pushMessagingAPNPod && pushMessagingFCMPod) { logger.error( diff --git a/src/doctor/constants/sdk.ts b/src/doctor/constants/sdk.ts index 1b3f483..6f7b136 100644 --- a/src/doctor/constants/sdk.ts +++ b/src/doctor/constants/sdk.ts @@ -7,3 +7,8 @@ export const POD_MESSAGING_IN_APP = 'CustomerIOMessagingInApp'; export const POD_MESSAGING_PUSH = 'CustomerIOMessagingPush'; export const POD_MESSAGING_PUSH_APN = 'CustomerIOMessagingPushAPN'; export const POD_MESSAGING_PUSH_FCM = 'CustomerIOMessagingPushFCM'; +/** + * Specific versions of SDKs for which we have special handling in doctor tool + */ +// iOS SDK version that introduced support for swizzling in push module +export const iOS_SDK_PUSH_SWIZZLE_VERSION = '2.11'; diff --git a/src/doctor/utils/version.ts b/src/doctor/utils/version.ts index fa501b4..b8dcc7f 100644 --- a/src/doctor/utils/version.ts +++ b/src/doctor/utils/version.ts @@ -105,3 +105,52 @@ export function fetchLatestGitHubRelease( request.on('error', (error) => reject(error)); }); } + +/** + * Extracts the first semantic version number from a string that may contain multiple versions separated by commas. + * + * @param versions String containing multiple versions + * @returns First semantic version found in the string, or undefined if no version is found or input is undefined. + */ +export function extractSemanticVersion( + versions: string | undefined +): string | undefined { + if (versions === undefined) return undefined; + + // Regex pattern to match a semantic version number + const versionPattern = /\b\d+\.\d+(\.\d+)?\b/; + const match = versions.match(versionPattern); + return match ? match[0] : undefined; +} + +/** + * Compares two semantic version strings. + * + * @param version1 First version string to compare. + * @param version2 Second version string to compare. + * @returns -1 if version1 is less than version2, 1 if version1 is greater than version2, and 0 if they are equal. + */ +export function compareSemanticVersions( + version1: string | undefined, + version2: string | undefined +): number { + // Handle undefined values + if (version1 === undefined && version2 === undefined) return 0; + if (version1 === undefined) return -1; + if (version2 === undefined) return 1; + + const v1Parts = version1.split('.').map(Number); + const v2Parts = version2.split('.').map(Number); + + // Compare each part of the version numbers + for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) { + const part1 = v1Parts[i] ?? 0; // Use 0 for missing parts + const part2 = v2Parts[i] ?? 0; // Use 0 for missing parts + + if (part1 > part2) return 1; + if (part1 < part2) return -1; + } + + // If all parts are equal, return 0 + return 0; +}