Skip to content

Commit

Permalink
feat: automatically handle push click events and increase compatibili…
Browse files Browse the repository at this point in the history
…ty with 3rd party push modules (#112)
  • Loading branch information
levibostian authored Feb 12, 2024
1 parent 309e361 commit a67e345
Show file tree
Hide file tree
Showing 10 changed files with 111 additions and 67 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/deploy-sdk.yml
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ jobs:
ref: ${{ needs.deploy-git-tag.outputs.new_release_git_head }}
- uses: actions/setup-node@v4
with:
node-version: '16'
node-version: '20'
- run: npm ci

- name: Deploy to npm
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '16'
node-version: '20'
- run: npm ci

- name: Compile
Expand Down
18 changes: 9 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
"registry": "https://registry.npmjs.org/"
},
"peerDependencies": {
"customerio-reactnative": "^3.0.0"
"customerio-reactnative": ">=3.4.0"
},
"devDependencies": {
"@expo/config-plugins": "^4.1.4",
Expand Down
39 changes: 25 additions & 14 deletions src/helpers/constants/ios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,32 @@ export const CIO_CONFIGURECIOSDKPUSHNOTIFICATION_SNIPPET = `
[pnHandlerObj registerPushNotification];
`;

export const CIO_CONFIGURECIOSDKUSERNOTIFICATIONCENTER_SNIPPET = `
export const CIO_INITIALIZECIOSDK_SNIPPET = `
[pnHandlerObj initializeCioSdk];
// Code to make the CIO SDK compatible with expo-notifications package.
//
// The CIO SDK and expo-notifications both need to handle when a push gets clicked. However, iOS only allows one click handler to be set per app.
// To get around this limitation, we set the CIO SDK as the click handler. The CIO SDK sets itself up so that when another SDK or host iOS app
// sets itself as the click handler, the CIO SDK will still be able to handle when the push gets clicked, even though it's not the designated
// click handler in iOS at runtime.
//
// This should work for most SDKs. However, expo-notifications is unique in it's implementation. It will not setup push click handling it if detects
// that another SDK or host iOS app has already set itself as the click handler:
// https://github.com/expo/expo/blob/1b29637bec0b9888e8bc8c310476293a3e2d9786/packages/expo-notifications/ios/EXNotifications/Notifications/EXNotificationCenterDelegate.m#L31-L37
// ...to get around this, we must manually set it as the click handler after the CIO SDK. That's what this code block does.
//
// Note: Initialize the native iOS SDK and setup SDK push click handling before running this code.
# if __has_include(<EXNotifications/EXNotificationCenterDelegate.h>)
// Creating a new instance, as the comments in expo-notifications suggests, does not work. With this code, if you send a CIO push to device and click on it,
// no push metrics reporting will occur.
// EXNotificationCenterDelegate *notificationCenterDelegate = [[EXNotificationCenterDelegate alloc] init];
// ...instead, get the singleton reference from Expo.
id<UNUserNotificationCenterDelegate> notificationCenterDelegate = (id<UNUserNotificationCenterDelegate>) [EXModuleRegistryProvider getSingletonModuleForClass:[EXNotificationCenterDelegate class]];
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
center.delegate = self;
center.delegate = notificationCenterDelegate;
# endif
`;

export const CIO_CONFIGUREDEEPLINK_KILLEDSTATE_SNIPPET = `
Expand All @@ -103,18 +126,6 @@ NSMutableDictionary *modifiedLaunchOptions = [NSMutableDictionary dictionaryWith
//Deep link workaround for app killed state ends
`;

// Enable push handling - notification response
export const CIO_DIDRECEIVENOTIFICATIONRESPONSEHANDLER_SNIPPET = `
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)(void))completionHandler {
[pnHandlerObj userNotificationCenter:center didReceiveNotificationResponse:response withCompletionHandler:completionHandler];
}`;

// Foreground push handling
export const CIO_WILLPRESENTNOTIFICATIONHANDLER_SNIPPET = `
// show push when the app is in foreground
- (void)userNotificationCenter:(UNUserNotificationCenter* )center willPresentNotification:(UNNotification* )notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler {
completionHandler( UNNotificationPresentationOptionAlert + UNNotificationPresentationOptionSound);
}`;
export const CIO_REGISTER_PUSHNOTIFICATION_SNIPPET = `
@objc(registerPushNotification)
public func registerPushNotification() {
Expand Down
26 changes: 14 additions & 12 deletions src/helpers/native-files/ios/PushService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,20 @@ public class CIOAppPushNotificationsHandler : NSObject {

{{REGISTER_SNIPPET}}

@objc(initializeCioSdk)
public func initializeCioSdk() {
// Must initialize Customer.io before initializing MessagingPushAPN.
CustomerIO.initialize(siteId: "{{SITE_ID}}", apiKey: "{{API_KEY}}", region: .{{REGION}}) { config in
// Must configure auto track push events before initializing MessagingPushAPN.
// This is because after AppDelegate.didFinishLaunching is called, the app will start handling push click events.
// Configuring auto track push events after this point will not work.
config.autoTrackPushEvents = {{AUTO_TRACK_PUSH_EVENTS}}
}
MessagingPushAPN.initialize { config in
config.showPushAppInForeground = {{SHOW_PUSH_APP_IN_FOREGROUND}}
}
}

@objc(application:deviceToken:)
public func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
MessagingPush.shared.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken)
Expand All @@ -20,16 +34,4 @@ public class CIOAppPushNotificationsHandler : NSObject {
public func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
MessagingPush.shared.application(application, didFailToRegisterForRemoteNotificationsWithError: error)
}

@objc(userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:)
public func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
let handled = MessagingPush.shared.userNotificationCenter(center, didReceive: response,
withCompletionHandler: completionHandler)

// If the Customer.io SDK does not handle the push, it's up to you to handle it and call the
// completion handler. If the SDK did handle it, it called the completion handler for you.
if !handled {
completionHandler()
}
}
}
2 changes: 1 addition & 1 deletion src/helpers/utils/codeInjection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,5 @@ export function injectCodeByLineNumber(
content = [...lines.slice(0, index), snippet, ...lines.slice(index)];
}

return content;
return content.join('\n');
}
45 changes: 18 additions & 27 deletions src/ios/withAppDelegateModifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,19 @@ import {
CIO_APPDELEGATEHEADER_REGEX,
CIO_APPDELEGATEHEADER_USER_NOTIFICATION_CENTER_SNIPPET,
CIO_CONFIGURECIOSDKPUSHNOTIFICATION_SNIPPET,
CIO_CONFIGURECIOSDKUSERNOTIFICATIONCENTER_SNIPPET,
CIO_CONFIGUREDEEPLINK_KILLEDSTATE_SNIPPET,
CIO_DIDFAILTOREGISTERFORREMOTENOTIFICATIONSWITHERRORFULL_REGEX,
CIO_RCTBRIDGE_DEEPLINK_MODIFIEDOPTIONS_REGEX,
CIO_DIDFAILTOREGISTERFORREMOTENOTIFICATIONSWITHERROR_REGEX,
CIO_DIDFAILTOREGISTERFORREMOTENOTIFICATIONSWITHERROR_SNIPPET,
CIO_DIDFINISHLAUNCHINGMETHOD_REGEX,
CIO_DIDRECEIVENOTIFICATIONRESPONSEHANDLER_SNIPPET,
CIO_DIDREGISTERFORREMOTENOTIFICATIONSWITHDEVICETOKEN_REGEX,
CIO_DIDREGISTERFORREMOTENOTIFICATIONSWITHDEVICETOKEN_SNIPPET,
CIO_LAUNCHOPTIONS_DEEPLINK_MODIFIEDOPTIONS_REGEX,
CIO_PUSHNOTIFICATIONHANDLERDECLARATION_SNIPPET,
CIO_WILLPRESENTNOTIFICATIONHANDLER_SNIPPET,
CIO_LAUNCHOPTIONS_MODIFIEDOPTIONS_SNIPPET,
CIO_RCTBRIDGE_DEEPLINK_MODIFIEDOPTIONS_SNIPPET,
CIO_DEEPLINK_COMMENT_REGEX,
CIO_INITIALIZECIOSDK_SNIPPET,
} from '../helpers/constants/ios';
import {
injectCodeBeforeMultiLineRegex,
Expand All @@ -35,15 +32,6 @@ import {
import { FileManagement } from '../helpers/utils/fileManagement';
import type { CustomerIOPluginOptionsIOS } from '../types/cio-types';

const pushCodeSnippets = [
CIO_DIDRECEIVENOTIFICATIONRESPONSEHANDLER_SNIPPET,
CIO_WILLPRESENTNOTIFICATIONHANDLER_SNIPPET,
];

const additionalMethodsForPushNotifications = `${pushCodeSnippets.join(
'\n'
)}\n`; // Join newlines and ensure a newline at the end.

const addImport = (stringContents: string, appName: string) => {
const importRegex = /^(#import .*)\n/gm;
const addedImport = getImportSnippet(appName);
Expand All @@ -62,7 +50,7 @@ const addImport = (stringContents: string, appName: string) => {
stringContents,
endOfMatchIndex,
addedImport
).join('\n');
);

return stringContents;
};
Expand All @@ -87,11 +75,11 @@ const addNotificationConfiguration = (stringContents: string) => {
return stringContents;
};

const addUserNotificationCenterConfiguration = (stringContents: string) => {
const addInitializeNativeCioSdk = (stringContents: string) => {
stringContents = injectCodeBeforeMultiLineRegex(
stringContents,
CIO_DIDFINISHLAUNCHINGMETHOD_REGEX,
CIO_CONFIGURECIOSDKUSERNOTIFICATIONCENTER_SNIPPET
CIO_INITIALIZECIOSDK_SNIPPET
);

return stringContents;
Expand Down Expand Up @@ -134,11 +122,17 @@ const addDidRegisterForRemoteNotificationsWithDeviceToken = (
return stringContents;
};

const addAdditionalMethodsForPushNotifications = (stringContents: string) => {
stringContents = injectCodeByMultiLineRegex(
// Adds required import for Expo Notifications package in AppDelegate.
// Required to call functions from the package.
const addExpoNotificationsHeaderModification = (stringContents: string) => {
stringContents = injectCodeByLineNumber(
stringContents,
CIO_DIDFAILTOREGISTERFORREMOTENOTIFICATIONSWITHERRORFULL_REGEX,
additionalMethodsForPushNotifications
0,
`
#if __has_include(<EXNotifications/EXNotificationCenterDelegate.h>)
#import <EXNotifications/EXNotificationCenterDelegate.h>
#endif
`
);

return stringContents;
Expand Down Expand Up @@ -241,12 +235,8 @@ export const withAppDelegateModifications: ConfigPlugin<
) {
stringContents = addNotificationConfiguration(stringContents);
}
if (
props.handleNotificationClick === undefined ||
props.handleNotificationClick === true
) {
stringContents = addUserNotificationCenterConfiguration(stringContents);
}

stringContents = addInitializeNativeCioSdk(stringContents);

if (
props.handleDeeplinkInKilledState !== undefined &&
Expand All @@ -255,12 +245,13 @@ export const withAppDelegateModifications: ConfigPlugin<
stringContents = addHandleDeeplinkInKilledState(stringContents);
}

stringContents = addAdditionalMethodsForPushNotifications(stringContents);
stringContents =
addDidFailToRegisterForRemoteNotificationsWithError(stringContents);
stringContents =
addDidRegisterForRemoteNotificationsWithDeviceToken(stringContents);

stringContents = addExpoNotificationsHeaderModification(stringContents);

config.modResults.contents = stringContents;
} else {
console.log('Customerio AppDelegate changes already exist. Skipping...');
Expand Down
37 changes: 36 additions & 1 deletion src/ios/withNotificationsXcodeProject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -374,8 +374,43 @@ const updatePushFile = (
) {
snippet = CIO_REGISTER_PUSHNOTIFICATION_SNIPPET;
}

envFileContent = replaceCodeByRegex(envFileContent, REGISTER_RE, snippet);

if (options.pushNotification) {
envFileContent = replaceCodeByRegex(
envFileContent,
/\{\{SITE_ID\}\}/,
options.pushNotification.env.siteId
);
envFileContent = replaceCodeByRegex(
envFileContent,
/\{\{API_KEY\}\}/,
options.pushNotification.env.apiKey
);
envFileContent = replaceCodeByRegex(
envFileContent,
/\{\{REGION\}\}/,
options.pushNotification.env.region.toUpperCase()
);
}

const autoTrackPushEvents =
options.autoTrackPushEvents === undefined ||
options.autoTrackPushEvents === true;
envFileContent = replaceCodeByRegex(
envFileContent,
/\{\{AUTO_TRACK_PUSH_EVENTS\}\}/,
autoTrackPushEvents.toString()
);

const showPushAppInForeground =
options.showPushAppInForeground === undefined ||
options.showPushAppInForeground === true;
envFileContent = replaceCodeByRegex(
envFileContent,
/\{\{SHOW_PUSH_APP_IN_FOREGROUND\}\}/,
showPushAppInForeground.toString()
);

FileManagement.writeFile(envFileName, envFileContent);
};
5 changes: 5 additions & 0 deletions src/types/cio-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@ export type CustomerIOPluginOptionsIOS = {
appleTeamId?: string;
appName?: string;
disableNotificationRegistration?: boolean;
/**
* @deprecated No longer has any effect. Use autoTrackPushEvents to control if push metrics should be automatically tracked by SDK.
*/
handleNotificationClick?: boolean;
showPushAppInForeground?: boolean;
autoTrackPushEvents?: boolean;
handleDeeplinkInKilledState?: boolean;
useFrameworks?: 'static' | 'dynamic';
pushNotification?: {
Expand Down

0 comments on commit a67e345

Please sign in to comment.