From ca6c1f977042102382ad89963ffcbcd6c2f1d2f7 Mon Sep 17 00:00:00 2001 From: Vanessa Landayan Date: Thu, 17 Oct 2024 16:18:41 -0400 Subject: [PATCH] Release version 13.1.0 --- BrazeProject/BrazeProject.tsx | 45 ++++++----- BrazeProject/ios/BrazeProject/AppDelegate.mm | 12 ++- BrazeProject/ios/Podfile | 4 +- BrazeProject/ios/Podfile.lock | 44 +++++------ CHANGELOG.md | 17 +++++ __tests__/index.test.js | 53 ++++++++++++- __tests__/jest.setup.js | 1 + .../com/braze/reactbridge/BrazeReactBridge.kt | 4 + .../com/braze/reactbridge/BrazeReactBridge.kt | 5 ++ braze-react-native-sdk.podspec | 6 +- .../BrazeReactBridge/BrazeReactBridge.mm | 64 ++++------------ .../BrazeReactBridge/BrazeReactUtils.h | 31 +++++++- .../BrazeReactBridge/BrazeReactUtils.m | 74 ++++++++++++++++++- package.json | 2 +- src/NativeBrazeReactModule.ts | 1 + src/braze.js | 34 +++++++-- src/index.d.ts | 19 +++-- yarn.lock | 23 +++--- 18 files changed, 313 insertions(+), 126 deletions(-) diff --git a/BrazeProject/BrazeProject.tsx b/BrazeProject/BrazeProject.tsx index 57172b8..09371e1 100644 --- a/BrazeProject/BrazeProject.tsx +++ b/BrazeProject/BrazeProject.tsx @@ -152,6 +152,19 @@ export const BrazeProject = (): ReactElement => { ]); }; + const handlePushPayload = (pushPayload) => { + if (pushPayload) { + console.log( + `Received push notification payload: + - type: ${pushPayload.payload_type} + - title: ${pushPayload.title} + - is_silent: ${pushPayload.is_silent} + - url: ${pushPayload.url}`, + ); + console.log(JSON.stringify(pushPayload, undefined, 2)); + } + }; + const showToast = (msg: string, duration: number = 2000) => { setMessage(msg); setToastVisible(true); @@ -177,15 +190,18 @@ export const BrazeProject = (): ReactElement => { }) .catch(err => console.error('Error getting initial URL', err)); - // Handles deep links when an iOS app is launched from hard close via push click. - // Note that this isn't handled by Linking.getInitialURL(), as the app is - // launched not from a deep link, but from clicking on the push notification. - // For more detail, see `Braze.getInitialURL` in `index.js`. - Braze.getInitialURL(url => { - if (url) { - console.log('Braze.getInitialURL is ' + url); - showToast('Braze.getInitialURL is ' + url); - handleOpenUrl({ url }); + // Handles push notification payloads and deep links when an iOS app is launched from terminated state via push click. + // For more detail, see `Braze.getInitialPushPayload`. + Braze.getInitialPushPayload(pushPayload => { + if (pushPayload) { + handlePushPayload(pushPayload); + + // If the push payload contains a URL, handle it + let initialURL = pushPayload.url; + if (initialURL) { + showToast('Initial URL is: ' + initialURL); + handleOpenUrl({ url: initialURL }); + } } }); @@ -242,16 +258,7 @@ export const BrazeProject = (): ReactElement => { const pushEventSubscription = Braze.addListener( Braze.Events.PUSH_NOTIFICATION_EVENT, - function (data) { - console.log( - `Push notification subscription triggered: - - type: ${data.payload_type} - - title: ${data.title} - - is_silent: ${data.is_silent} - `, - ); - console.log(JSON.stringify(data, undefined, 2)); - }, + data => handlePushPayload(data), ); return () => { diff --git a/BrazeProject/ios/BrazeProject/AppDelegate.mm b/BrazeProject/ios/BrazeProject/AppDelegate.mm index 901b04a..8b6b402 100644 --- a/BrazeProject/ios/BrazeProject/AppDelegate.mm +++ b/BrazeProject/ios/BrazeProject/AppDelegate.mm @@ -12,6 +12,13 @@ #import #import "BrazeReactGIFHelper.h" +@interface AppDelegate () + +// Keep a strong reference to the BrazeDelegate to ensure it is not deallocated. +@property (nonatomic, strong) BrazeReactDelegate *brazeDelegate; + +@end + @implementation AppDelegate static Braze *_braze; @@ -45,7 +52,8 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( } Braze *braze = [BrazeReactBridge initBraze:configuration]; - braze.delegate = [[BrazeReactDelegate alloc] init]; + self.brazeDelegate = [[BrazeReactDelegate alloc] init]; + braze.delegate = self.brazeDelegate; AppDelegate.braze = braze; // Use SDWebImage as the GIF provider. @@ -58,7 +66,7 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( [self registerForPushNotifications]; } - [[BrazeReactUtils sharedInstance] populateInitialUrlFromLaunchOptions:launchOptions]; + [[BrazeReactUtils sharedInstance] populateInitialPayloadFromLaunchOptions:launchOptions]; return [super application:application didFinishLaunchingWithOptions:launchOptions]; } diff --git a/BrazeProject/ios/Podfile b/BrazeProject/ios/Podfile index 15cb352..58da999 100644 --- a/BrazeProject/ios/Podfile +++ b/BrazeProject/ios/Podfile @@ -47,9 +47,9 @@ target 'BrazeProject' do end target 'BrazeProjectRichPush' do - pod 'BrazeNotificationService', '~> 11.0.0' + pod 'BrazeNotificationService', '~> 11.1.1' end target 'BrazeProjectPushStory' do - pod 'BrazePushStory', '~> 11.0.0' + pod 'BrazePushStory', '~> 11.1.1' end diff --git a/BrazeProject/ios/Podfile.lock b/BrazeProject/ios/Podfile.lock index a427d4e..6142364 100644 --- a/BrazeProject/ios/Podfile.lock +++ b/BrazeProject/ios/Podfile.lock @@ -1,9 +1,9 @@ PODS: - boost (1.84.0) - - braze-react-native-sdk (13.0.0): - - BrazeKit (~> 11.0.0) - - BrazeLocation (~> 11.0.0) - - BrazeUI (~> 11.0.0) + - braze-react-native-sdk (13.1.0): + - BrazeKit (~> 11.1.1) + - BrazeLocation (~> 11.1.1) + - BrazeUI (~> 11.1.1) - DoubleConversion - glog - hermes-engine @@ -24,13 +24,13 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - BrazeKit (11.0.0) - - BrazeLocation (11.0.0): - - BrazeKit (= 11.0.0) - - BrazeNotificationService (11.0.0) - - BrazePushStory (11.0.0) - - BrazeUI (11.0.0): - - BrazeKit (= 11.0.0) + - BrazeKit (11.1.1) + - BrazeLocation (11.1.1): + - BrazeKit (= 11.1.1) + - BrazeNotificationService (11.1.1) + - BrazePushStory (11.1.1) + - BrazeUI (11.1.1): + - BrazeKit (= 11.1.1) - DoubleConversion (1.1.6) - FBLazyVector (0.75.2) - fmt (9.1.0) @@ -1537,8 +1537,8 @@ PODS: DEPENDENCIES: - boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`) - "braze-react-native-sdk (from `../node_modules/@braze/react-native-sdk`)" - - BrazeNotificationService (~> 11.0.0) - - BrazePushStory (~> 11.0.0) + - BrazeNotificationService (~> 11.1.1) + - BrazePushStory (~> 11.1.1) - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) - fmt (from `../node_modules/react-native/third-party-podspecs/fmt.podspec`) @@ -1745,18 +1745,18 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: 4cb898d0bf20404aab1850c656dcea009429d6c1 - braze-react-native-sdk: 86de16e215b6c75d31a825c98052732108145412 - BrazeKit: eeef52b04bef0b5754c9cee6cae2db0021246b87 - BrazeLocation: f48cf3bec07a085e45b82026ac960671fa93ac46 - BrazeNotificationService: 3b5d374891ad4d61e8e5a816594681fc5d218a7c - BrazePushStory: c1520c52e51c29d261bbd29edcee81b376ede401 - BrazeUI: 5dd1f6598fb4b32449d69c0139dd86c320e3651d + braze-react-native-sdk: bc78ee374951dd88e9b916c53112d920f4696777 + BrazeKit: 879da791a0f4e247846a06c6de95f8b93f4579df + BrazeLocation: 59af48eafcf233a8ef16de3f6900955332113fe4 + BrazeNotificationService: 4e082e73a7b2ed8aa1488a1f9d9177e1051ea4b2 + BrazePushStory: eb83b837c290136c06d891d1d430acc02ca40ec1 + BrazeUI: 0785c09df3cc03aa6374ac3307215caef7e49bd3 DoubleConversion: 76ab83afb40bddeeee456813d9c04f67f78771b5 FBLazyVector: 38bb611218305c3bc61803e287b8a81c6f63b619 fmt: 4c2741a687cc09f0634a2e2c72a838b99f1ff120 - glog: 69ef571f3de08433d766d614c73a9838a06bf7eb + glog: c5d68082e772fa1c511173d6b30a9de2c05a69a2 hermes-engine: 3b6e0717ca847e2fc90a201e59db36caf04dee88 - RCT-Folly: 4464f4d875961fce86008d45f4ecf6cef6de0740 + RCT-Folly: 02617c592a293bd6d418e0a88ff4ee1f88329b47 RCTDeprecation: 34cbf122b623037ea9facad2e92e53434c5c7422 RCTRequired: 24c446d7bcd0f517d516b6265d8df04dc3eb1219 RCTTypeSafety: ef5e91bd791abd3a99b2c75fd565791102a66352 @@ -1815,6 +1815,6 @@ SPEC CHECKSUMS: SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d Yoga: a1d7895431387402a674fd0d1c04ec85e87909b8 -PODFILE CHECKSUM: 9d2830ef3c0cbe5823ce11c186bdb0a5f1b78514 +PODFILE CHECKSUM: d2de0a300a100d6e4a0f6663ec3607c40a5251fd COCOAPODS: 1.15.2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 650cfd8..19c57f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,24 @@ ⚠️ In version 2.0.0, we changed the iOS bridge from AppboyKit, which is written in Objective-C, to the new [Swift SDK](https://github.com/braze-inc/braze-swift-sdk). If you are upgrading from a version below 2.0.0 to a version above 2.0.0, please read [the instructions](https://github.com/braze-inc/braze-react-native-sdk/blob/master/CHANGELOG.md#200) to ensure a smooth transition and backward compatibility. +## 13.1.0 + +##### Fixed +- Updates the iOS sample app to properly retain the `BrazeReactDelegate` instance. Internally, the Braze SDK uses a weak reference to the delegate, which could be deallocated if not retained by the app. This change ensures the delegate is retained for the lifecycle of the app. + +##### Added +- Updates the native iOS version bindings [from Braze Swift SDK 11.0.0 to 11.1.1](https://github.com/braze-inc/braze-swift-sdk/compare/11.0.0...11.1.1#diff-06572a96a58dc510037d5efa622f9bec8519bc1beab13c9f251e97e657a9d4ed). +- Adds the method `Braze.getInitialPushPayload()` to get the push notification payload when opening the app via notification click while the application was in a terminated state. + - `Braze.getInitialURL()` is now deprecated in favor of `Braze.getInitialPushPayload()`. To access the initial URL, use the new method to receive the push notification payload, and access the value of the `url` key. + - If you are using `Braze.getInitialPushPayload()`, add the following code to your `application:didFinishLaunchingWithOptions:launchOptions:`: + ``` + [[BrazeReactUtils sharedInstance] populateInitialPayloadFromLaunchOptions:launchOptions]; + ``` + This replaces `populateInitialUrlFromLaunchOptions`, which is now deprecated. + ## 13.0.0 +⚠️ **Important:** This version includes a Swift SDK version with a known issue related to push subscription status. Upgrade to version `13.1.0` instead. + ##### Breaking - Updates the native Android version bindings [from Braze Android SDK 31.1.0 to 32.1.0](https://github.com/braze-inc/braze-android-sdk/compare/v31.1.0...v32.1.0#diff-06572a96a58dc510037d5efa622f9bec8519bc1beab13c9f251e97e657a9d4ed). - Updates the native iOS version bindings [from Braze Swift SDK 10.3.0 to 11.0.0](https://github.com/braze-inc/braze-swift-sdk/compare/10.3.0...11.0.0#diff-06572a96a58dc510037d5efa622f9bec8519bc1beab13c9f251e97e657a9d4ed). diff --git a/__tests__/index.test.js b/__tests__/index.test.js index d1244b6..4ca73d4 100644 --- a/__tests__/index.test.js +++ b/__tests__/index.test.js @@ -9,6 +9,28 @@ const testCallback = jest.fn(); const testInAppMessageJson = `{\"message\":\"body body\",\"type\":\"MODAL\",\"text_align_message\":\"CENTER\",\"click_action\":\"NONE\",\"message_close\":\"SWIPE\",\"extras\":{\"test\":\"123\",\"foo\":\"bar\"},\"header\":\"hello\",\"text_align_header\":\"CENTER\",\"image_url\":\"https:\\/\\/github.com\\/braze-inc\\/braze-react-native-sdk\\/blob\\/master\\/.github\\/assets\\/logo-dark.png?raw=true\",\"image_style\":\"TOP\",\"btns\":[{\"id\":0,\"text\":\"button 1\",\"click_action\":\"URI\",\"uri\":\"https:\\/\\/www.google.com\",\"use_webview\":true,\"bg_color\":4294967295,\"text_color\":4279990479,\"border_color\":4279990479},{\"id\":1,\"text\":\"button 2\",\"click_action\":\"NONE\",\"bg_color\":4279990479,\"text_color\":4294967295,\"border_color\":4279990479}],\"close_btn_color\":4291085508,\"bg_color\":4294243575,\"frame_color\":3207803699,\"text_color\":4280624421,\"header_text_color\":4280624421,\"trigger_id\":\"NWJhNTMxOThiZjVjZWE0NDZiMTUzYjZiXyRfbXY9NWJhNTMxOThiZjVjZWE0NDZiMTUzYjc1JnBpPWNtcA==\", \"is_test_send\":false}`; +const testPushPayloadJson = { + "use_webview": false, + "is_silent": false, + "ios": { + "aps": { + "alert": { + "title": "Test Message", + "body": "Hello World" + }, + "interruption-level": "active" + }, + "action_identifier": "com.apple.UNNotificationDefaultActionIdentifier" + }, + "payload_type": "push_opened", + "title": "Test Message", + "braze_properties": {}, + "is_braze_internal": false, + "body": "Hello World", + "timestamp": 1728060077, + "url": "www.braze.com" +}; + afterEach(() => { jest.clearAllMocks(); }); @@ -470,11 +492,20 @@ test('it calls BrazeReactBridge.getUnreadCardCountForCategories', () => { test('it calls BrazeReactBridge.getInitialURL if defined', () => { NativeBrazeReactModule.getInitialURL.mockImplementation((callback) => { - callback(null, "some_data"); + callback(null, testPushPayloadJson["url"]); }); Braze.getInitialURL(testCallback); expect(NativeBrazeReactModule.getInitialURL).toBeCalled(); - expect(testCallback).toBeCalledWith("some_data"); + expect(testCallback).toBeCalledWith(testPushPayloadJson["url"]); +}); + +test('it calls BrazeReactBridge.getInitialPushPayload if defined', () => { + NativeBrazeReactModule.getInitialPushPayload.mockImplementation((callback) => { + callback(null, testPushPayloadJson); + }); + Braze.getInitialPushPayload(testCallback); + expect(NativeBrazeReactModule.getInitialPushPayload).toBeCalled(); + expect(testCallback).toBeCalledWith(testPushPayloadJson); }); test('it calls BrazeReactBridge.getDeviceId', () => { @@ -496,6 +527,16 @@ test('it calls the callback with null and logs the error if BrazeReactBridge.get expect(console.log).toBeCalledWith("error"); }); +test('it calls the callback with null and logs the error if BrazeReactBridge.getInitialPushPayload returns an error', () => { + NativeBrazeReactModule.getInitialPushPayload.mockImplementation((callback) => { + callback("error", null); + }); + Braze.getInitialPushPayload(testCallback); + expect(NativeBrazeReactModule.getInitialPushPayload).toBeCalled(); + expect(testCallback).toBeCalledWith(null); + expect(console.log).toBeCalledWith("error"); +}); + test('it calls the callback with null if BrazeReactBridge.getInitialUrl is running on Android', () => { const platform = Platform.OS; Platform.OS = 'android'; @@ -504,6 +545,14 @@ test('it calls the callback with null if BrazeReactBridge.getInitialUrl is runni Platform.OS = platform; }); +test('it calls the callback with null if BrazeReactBridge.getInitialPushPayload is running on Android', () => { + const platform = Platform.OS; + Platform.OS = 'android'; + Braze.getInitialPushPayload(testCallback); + expect(testCallback).toBeCalledWith(null); + Platform.OS = platform; +}); + test('it calls BrazeReactBridge.subscribeToInAppMessage', () => { Braze.subscribeToInAppMessage(true, testCallback); expect(NativeBrazeReactModule.subscribeToInAppMessage).toBeCalledWith(true, testCallback); diff --git a/__tests__/jest.setup.js b/__tests__/jest.setup.js index 6ccf73e..06e7a90 100644 --- a/__tests__/jest.setup.js +++ b/__tests__/jest.setup.js @@ -70,6 +70,7 @@ jest.mock('react-native/Libraries/TurboModule/TurboModuleRegistry', () => { getCardCountForCategories: jest.fn(), getUnreadCardCountForCategories: jest.fn(), getInitialURL: jest.fn(), + getInitialPushPayload: jest.fn(), getDeviceId: jest.fn(), requestLocationInitialization: jest.fn(), requestGeofences: jest.fn(), diff --git a/android/src/newarch/com/braze/reactbridge/BrazeReactBridge.kt b/android/src/newarch/com/braze/reactbridge/BrazeReactBridge.kt index ac31eb6..87111d0 100644 --- a/android/src/newarch/com/braze/reactbridge/BrazeReactBridge.kt +++ b/android/src/newarch/com/braze/reactbridge/BrazeReactBridge.kt @@ -13,6 +13,10 @@ class BrazeReactBridge(reactContext: ReactApplicationContext): NativeBrazeReactM // iOS only } + override fun getInitialPushPayload(callback: Callback) { + // iOS only + } + override fun getDeviceId(callback: Callback) { return brazeImpl.getDeviceId(callback) } diff --git a/android/src/oldarch/com/braze/reactbridge/BrazeReactBridge.kt b/android/src/oldarch/com/braze/reactbridge/BrazeReactBridge.kt index 670091d..d1ace6a 100644 --- a/android/src/oldarch/com/braze/reactbridge/BrazeReactBridge.kt +++ b/android/src/oldarch/com/braze/reactbridge/BrazeReactBridge.kt @@ -13,6 +13,11 @@ class BrazeReactBridge(reactContext: ReactApplicationContext?) : ReactContextBas // iOS only } + @ReactMethod + fun getInitialPushPayload(callback: Callback) { + // iOS only + } + @ReactMethod fun getDeviceId(callback: Callback) { return brazeImpl.getDeviceId(callback) diff --git a/braze-react-native-sdk.podspec b/braze-react-native-sdk.podspec index 406cc8e..97c1c2f 100644 --- a/braze-react-native-sdk.podspec +++ b/braze-react-native-sdk.podspec @@ -20,9 +20,9 @@ Pod::Spec.new do |s| install_modules_dependencies(s) - s.dependency 'BrazeKit', '~> 11.0.0' - s.dependency 'BrazeLocation', '~> 11.0.0' - s.dependency 'BrazeUI', '~> 11.0.0' + s.dependency 'BrazeKit', '~> 11.1.1' + s.dependency 'BrazeLocation', '~> 11.1.1' + s.dependency 'BrazeUI', '~> 11.1.1' # Swift/Objective-C compatibility s.pod_target_xcconfig = { diff --git a/iOS/BrazeReactBridge/BrazeReactBridge/BrazeReactBridge.mm b/iOS/BrazeReactBridge/BrazeReactBridge/BrazeReactBridge.mm index fbf65d5..5cceff3 100644 --- a/iOS/BrazeReactBridge/BrazeReactBridge/BrazeReactBridge.mm +++ b/iOS/BrazeReactBridge/BrazeReactBridge/BrazeReactBridge.mm @@ -83,7 +83,7 @@ - (void)startObserving { self.notificationSubscription = [braze.notifications subscribeToUpdates:^(BRZNotificationsPayload * _Nonnull payload) { RCTLogInfo(@"Received push notification via subscription"); - NSDictionary *eventData = RCTFormatPushPayload(payload); + NSDictionary *eventData = [[BrazeReactUtils sharedInstance] formatPushPayload:payload withLaunchOptions:nil]; [self sendEventWithName:kPushNotificationEvent body:eventData]; }]; @@ -100,6 +100,7 @@ - (void)stopObserving { self.contentCardsSubscription = nil; self.newsFeedSubscription = nil; self.featureFlagsSubscription = nil; + self.notificationSubscription = nil; } - (NSArray *)supportedEvents { @@ -148,7 +149,7 @@ - (void)reportResultWithCallback:(RCTResponseSenderBlock)callback andError:(NSSt #pragma mark - Bridge bindings -// Returns push deep links from cold app starts. +// (Deprecated) Returns push deep links from cold app starts. // For more context see getInitialURL() in index.js RCT_EXPORT_METHOD(getInitialURL:(RCTResponseSenderBlock)callback) { if ([BrazeReactUtils sharedInstance].initialUrlString != nil) { @@ -158,6 +159,16 @@ - (void)reportResultWithCallback:(RCTResponseSenderBlock)callback andError:(NSSt } } +// Returns push payload from cold app starts. +// For more context see getInitialPushPayload() in index.js. +RCT_EXPORT_METHOD(getInitialPushPayload:(RCTResponseSenderBlock)callback) { + if ([BrazeReactUtils sharedInstance].initialPushPayload != nil) { + [self reportResultWithCallback:callback andError:nil andResult:[BrazeReactUtils sharedInstance].initialPushPayload]; + } else { + [self reportResultWithCallback:callback andError:nil andResult:nil]; + } +} + RCT_EXPORT_METHOD(getDeviceId:(RCTResponseSenderBlock)callback) { NSString *deviceId = [braze deviceId]; [self reportResultWithCallback:callback andError:nil andResult:deviceId]; @@ -628,55 +639,6 @@ - (nullable BRZNewsFeedCard *)getNewsFeedCardById:(NSString *)idString { return newsFeedCardData; } -#pragma mark - Push Notifications - -/// Formats the push notification payload into a JavaScript-readable object. -static NSDictionary *RCTFormatPushPayload(BRZNotificationsPayload *payload) { - NSMutableDictionary *eventData = [NSMutableDictionary dictionary]; - - // Uses the `"push_` prefix for consistency with Android. - switch (payload.type) { - case BRZNotificationsPayloadTypeOpened: - eventData[@"payload_type"] = @"push_opened"; - break; - case BRZNotificationsPayloadTypeReceived: - eventData[@"payload_type"] = @"push_received"; - break; - } - - eventData[@"url"] = [payload.urlContext.url absoluteString]; - eventData[@"use_webview"] = [NSNumber numberWithBool:payload.urlContext.useWebView]; - eventData[@"title"] = payload.title; - eventData[@"body"] = payload.body; - eventData[@"summary_text"] = payload.subtitle; - eventData[@"badge_count"] = payload.badge; - eventData[@"timestamp"] = [NSNumber numberWithInteger:(NSInteger)[payload.date timeIntervalSince1970]]; - eventData[@"is_silent"] = [NSNumber numberWithBool:payload.isSilent]; - eventData[@"is_braze_internal"] = [NSNumber numberWithBool:payload.isInternal]; - eventData[@"braze_properties"] = RCTFilterBrazeProperties(payload.userInfo); - - // Attaches the image URL from the `userInfo` payload if it exists. This is a no-op otherwise. - eventData[@"image_url"] = payload.userInfo[@"ab"][@"att"][@"url"]; - - // Adds relevant iOS-specific properties. - NSMutableDictionary *iosProperties = [NSMutableDictionary dictionary]; - iosProperties[@"action_identifier"] = payload.actionIdentifier; - iosProperties[@"aps"] = payload.userInfo[@"aps"]; - eventData[@"ios"] = iosProperties; - - return eventData; -} - -/// Strips the raw payload dictionary to only include Braze key-value pairs. -static NSDictionary *RCTFilterBrazeProperties(NSDictionary *userInfo) { - NSMutableDictionary *userInfoCopy = [userInfo mutableCopy]; - userInfoCopy[@"ab"] = nil; - userInfoCopy[@"ab_uri"] = nil; - userInfoCopy[@"aps"] = nil; - userInfoCopy[@"ab_use_webview"] = nil; - return userInfoCopy; -} - #pragma mark - Content Cards /// Returns the content card for the associated id, or nil if not found. diff --git a/iOS/BrazeReactBridge/BrazeReactBridge/BrazeReactUtils.h b/iOS/BrazeReactBridge/BrazeReactBridge/BrazeReactUtils.h index e5aa13a..1314d5d 100644 --- a/iOS/BrazeReactBridge/BrazeReactBridge/BrazeReactUtils.h +++ b/iOS/BrazeReactBridge/BrazeReactBridge/BrazeReactUtils.h @@ -1,12 +1,39 @@ #import #import +#import @interface BrazeReactUtils : NSObject + (BrazeReactUtils *)sharedInstance; -- (BOOL)populateInitialUrlFromLaunchOptions:(NSDictionary *)launchOptions; -- (BOOL)populateInitialUrlForCategories:(NSDictionary *)userInfo; +/** + * If the push dictionary from application:didFinishLaunchingWithOptions:launchOptions has a Braze push, we store it in initialPushPayload. + */ +- (BOOL)populateInitialPayloadFromLaunchOptions:(NSDictionary *)launchOptions; + +/** + * (Deprecated) Use method `populateInitialPayloadFromLaunchOptions` instead. + */ +- (BOOL)populateInitialUrlFromLaunchOptions:(NSDictionary *)launchOptions __deprecated_msg("use populateInitialPayloadFromLaunchOptions instead."); + +/** + * (Deprecated) Use method `populateInitialPayloadFromLaunchOptions` instead. + */ +- (BOOL)populateInitialUrlForCategories:(NSDictionary *)userInfo __deprecated_msg("use populateInitialPayloadFromLaunchOptions instead."); + +/** + * Formats the push notification payload into a JavaScript-readable object. + */ +- (NSDictionary *)formatPushPayload:(BRZNotificationsPayload *)payload withLaunchOptions:(NSDictionary *)launchOptions; + +/** + * The Braze push from application:didFinishLaunchingWithOptions:launchOptions. If there is no Braze push, this will be nil. + */ +@property NSDictionary *initialPushPayload; + +/** + * (Deprecated) Use property `initialPushPayload` instead. + */ @property NSString *initialUrlString; @end diff --git a/iOS/BrazeReactBridge/BrazeReactBridge/BrazeReactUtils.m b/iOS/BrazeReactBridge/BrazeReactBridge/BrazeReactUtils.m index 899bc49..6e04912 100644 --- a/iOS/BrazeReactBridge/BrazeReactBridge/BrazeReactUtils.m +++ b/iOS/BrazeReactBridge/BrazeReactBridge/BrazeReactUtils.m @@ -8,6 +8,7 @@ @implementation BrazeReactUtils - (instancetype)init { self = [super init]; self.initialUrlString = nil; + self.initialPushPayload = nil; return self; } @@ -18,7 +19,21 @@ + (BrazeReactUtils *)sharedInstance { return sharedInstance; } -// If the push dictionary from application:didFinishLaunchingWithOptions: launchOptions has a Braze deep link (ab_uri), we store it in initialUrlString +- (BOOL)populateInitialPayloadFromLaunchOptions:(NSDictionary *)launchOptions { + NSDictionary *pushDictionary = [launchOptions valueForKey:UIApplicationLaunchOptionsRemoteNotificationKey]; + BRZNotificationsPayload *notificationPayload = [[BRZNotificationsPayload alloc] initWithUserInfo:pushDictionary + type:BRZNotificationsPayloadTypeOpened + silent:NO]; + if (notificationPayload) { + sharedInstance.initialPushPayload = [self formatPushPayload:notificationPayload withLaunchOptions:launchOptions]; + NSLog(@"Initial iOS push payload set to %@.", sharedInstance.initialPushPayload); + } else { + sharedInstance.initialPushPayload = nil; + } + + return sharedInstance.initialPushPayload; +} + - (BOOL)populateInitialUrlFromLaunchOptions:(NSDictionary *)launchOptions { NSDictionary *pushDictionary = [launchOptions valueForKey:UIApplicationLaunchOptionsRemoteNotificationKey]; if (pushDictionary && pushDictionary[@"aps"] && pushDictionary[@"ab_uri"]) { @@ -49,4 +64,61 @@ - (BOOL)populateInitialUrlForCategories:(NSDictionary *)userInfo { return false; } +- (NSDictionary *)formatPushPayload:(BRZNotificationsPayload *)payload withLaunchOptions:(NSDictionary *) launchOptions { + NSMutableDictionary *eventData = [NSMutableDictionary dictionary]; + + // Uses the `"push_` prefix for consistency with Android. + switch (payload.type) { + case BRZNotificationsPayloadTypeOpened: + eventData[@"payload_type"] = @"push_opened"; + break; + case BRZNotificationsPayloadTypeReceived: + eventData[@"payload_type"] = @"push_received"; + break; + } + + // If the push was received while the app was in a terminated state, get the initial URL and sets it as the notification url. Otherwise, use the `urlContext`. + if (launchOptions) { + NSDictionary *pushDictionary = [launchOptions valueForKey:UIApplicationLaunchOptionsRemoteNotificationKey]; + if (pushDictionary && pushDictionary[@"aps"] && pushDictionary[@"ab_uri"]) { + eventData[@"url"] = pushDictionary[@"ab_uri"]; + } + } else { + eventData[@"url"] = [payload.urlContext.url absoluteString]; + } + + eventData[@"use_webview"] = [NSNumber numberWithBool:payload.urlContext.useWebView]; + eventData[@"title"] = payload.title; + eventData[@"body"] = payload.body; + eventData[@"summary_text"] = payload.subtitle; + eventData[@"badge_count"] = payload.badge; + eventData[@"timestamp"] = [NSNumber numberWithInteger:(NSInteger)[payload.date timeIntervalSince1970]]; + eventData[@"is_silent"] = [NSNumber numberWithBool:payload.isSilent]; + eventData[@"is_braze_internal"] = [NSNumber numberWithBool:payload.isInternal]; + eventData[@"braze_properties"] = filterBrazeProperties(payload.userInfo); + + // Attaches the image URL from the `userInfo` payload if it exists. This is a no-op otherwise. + eventData[@"image_url"] = payload.userInfo[@"ab"][@"att"][@"url"]; + + // Adds relevant iOS-specific properties. + NSMutableDictionary *iosProperties = [NSMutableDictionary dictionary]; + iosProperties[@"action_identifier"] = payload.actionIdentifier; + iosProperties[@"aps"] = payload.userInfo[@"aps"]; + eventData[@"ios"] = iosProperties; + + return eventData; +} + +/** + * Strips the raw payload dictionary to only include Braze key-value pairs. + */ +static NSDictionary *filterBrazeProperties(NSDictionary *userInfo) { + NSMutableDictionary *userInfoCopy = [userInfo mutableCopy]; + userInfoCopy[@"ab"] = nil; + userInfoCopy[@"ab_uri"] = nil; + userInfoCopy[@"aps"] = nil; + userInfoCopy[@"ab_use_webview"] = nil; + return userInfoCopy; +} + @end diff --git a/package.json b/package.json index bc64227..bdd4d6f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@braze/react-native-sdk", - "version": "13.0.0", + "version": "13.1.0", "description": "Braze SDK for React Native.", "main": "src/index.js", "types": "src/index.d.ts", diff --git a/src/NativeBrazeReactModule.ts b/src/NativeBrazeReactModule.ts index d1674e9..a294067 100644 --- a/src/NativeBrazeReactModule.ts +++ b/src/NativeBrazeReactModule.ts @@ -4,6 +4,7 @@ import type { FeatureFlag, NewsFeedCard, ContentCard } from './index'; export interface Spec extends TurboModule { getInitialURL(callback: (deepLink: string) => void): void; + getInitialPushPayload(callback: (pushPayload: Object) => void): void; getDeviceId(callback: (error?: Object, result?: string) => void): void; changeUser(userId: string, signature?: string | null): void; getUserId(callback: (error?: Object, result?: string) => void): void; diff --git a/src/braze.js b/src/braze.js index a119b2d..bb091e4 100644 --- a/src/braze.js +++ b/src/braze.js @@ -40,16 +40,38 @@ export class Braze { android: DeviceEventEmitter }); +/** + * @deprecated This method is deprecated in favor of `getInitialPushPayload`. + * + * To get the initial URL, call `getInitialPushPayload` and get the `url` key from the payload object. + */ + static getInitialURL(callback) { + if (Platform.OS === 'ios') { + this.bridge.getInitialURL((err, res) => { + if (err) { + console.log(err); + callback(null); + } else { + callback(res); + } + }); + } else { + // BrazeReactBridge.getInitialUrl not implemented on Android + callback(null); + } + } + /** * When launching an iOS application that has previously been force closed, React Native's Linking API doesn't - * support handling deep links embedded in push notifications. This is due to a race condition on startup between + * support handling push notifications and deep links in the payload. This is due to a race condition on startup between * the native call to RCTLinkingManager and React's loading of its JavaScript. This function provides a workaround: - * If an application is launched from a push notification click, we return any Braze deep links in the push payload. - * @param {function(string)} callback - A callback that retuns the deep link as a string. If there is no deep link, returns null. + * If an application is launched from a push notification click, we return the full push payload. + * @param {function(string)} callback - A callback that retuns the push notification as an Object. If there is no push payload, + * returns null. */ - static getInitialURL(callback) { + static getInitialPushPayload(callback) { if (Platform.OS === 'ios') { - this.bridge.getInitialURL((err, res) => { + this.bridge.getInitialPushPayload((err, res) => { if (err) { console.log(err); callback(null); @@ -58,7 +80,7 @@ export class Braze { } }); } else { - // BrazeReactBridge.getInitialUrl not implemented on Android + // BrazeReactBridge.getInitialPushPayload not implemented on Android callback(null); } } diff --git a/src/index.d.ts b/src/index.d.ts index 0364c8e..f9bd8a0 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -4,15 +4,22 @@ import { EmitterSubscription } from 'react-native'; /** - * When launching an iOS application that has previously been force closed, React Native's Linking API doesn't - * support handling deep links embedded in push notifications. This is due to a race condition on startup between - * the native call to RCTLinkingManager and React's loading of its JavaScript. This function provides a workaround: - * If an application is launched from a push notification click, we return any Braze deep links in the push payload. - * @param {function(string)} callback - A callback that retuns the deep link as a string. If there is no deep link, - * returns null. + * @deprecated This method is deprecated in favor of `getInitialPushPayload`. + * + * To get the initial URL, call `getInitialPushPayload` and get the `url` key from the payload object. */ export function getInitialURL(callback: (deepLink: string) => void): void; +/** + * When launching an iOS application that has previously been force closed, React Native's Linking API doesn't + * support handling push notifications and deep links in the payload. This is due to a race condition on startup between + * the call to `addListener` and React's loading of its JavaScript. This function provides a workaround: + * If an application is launched from a push notification click, we return the full push payload. + * @param {function(string)} callback - A callback that returns the formatted Braze push notification as a PushNotificationEvent. + * If there is no push payload, returns null. + */ +export function getInitialPushPayload(callback: (pushPayload: PushNotificationEvent) => void): void; + /** * @deprecated This method is deprecated in favor of `getDeviceId`. */ diff --git a/yarn.lock b/yarn.lock index 55c80d2..593f777 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2536,6 +2536,11 @@ encodeurl@~1.0.2: resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz" integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== +encodeurl@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" + integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== + end-of-stream@^1.1.0: version "1.4.4" resolved "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz" @@ -5258,10 +5263,10 @@ semver@^7.5.3, semver@^7.5.4: dependencies: lru-cache "^6.0.0" -send@0.18.0: - version "0.18.0" - resolved "https://registry.npmjs.org/send/-/send-0.18.0.tgz" - integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== +send@0.19.0: + version "0.19.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8" + integrity sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw== dependencies: debug "2.6.9" depd "2.0.0" @@ -5283,14 +5288,14 @@ serialize-error@^2.1.0: integrity sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw== serve-static@^1.13.1: - version "1.15.0" - resolved "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz" - integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== + version "1.16.2" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.2.tgz#b6a5343da47f6bdd2673848bf45754941e803296" + integrity sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw== dependencies: - encodeurl "~1.0.2" + encodeurl "~2.0.0" escape-html "~1.0.3" parseurl "~1.3.3" - send "0.18.0" + send "0.19.0" set-blocking@^2.0.0: version "2.0.0"