Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support for automatic push handling on iOS #29

Merged
merged 11 commits into from
Apr 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 79 additions & 35 deletions src/doctor/checks/ios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -35,7 +38,7 @@ export async function runChecks(group: CheckGroup): Promise<void> {
case CheckGroup.PushSetup:
await runCatching(validatePushEntitlements)(project);
await runCatching(analyzeNotificationServiceExtensionProperties)(project);
await runCatching(validateUserNotificationCenterDelegate)(project);
await runCatching(validateMessagingPushInitialization)(project);
break;

case CheckGroup.Dependencies:
Expand Down Expand Up @@ -274,43 +277,52 @@ function getDeploymentTargetVersion(
return null;
}

async function validateUserNotificationCenterDelegate(
async function validateMessagingPushInitialization(
project: iOSProject
): Promise<void> {
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'
);
}
}
}

Expand Down Expand Up @@ -424,14 +436,33 @@ async function extractPodVersions(project: iOSProject): Promise<void> {
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);
}

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`);
}
Expand All @@ -441,8 +472,21 @@ async function extractPodVersions(project: iOSProject): Promise<void> {
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(
Expand Down
5 changes: 5 additions & 0 deletions src/doctor/constants/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
49 changes: 49 additions & 0 deletions src/doctor/utils/version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Loading