diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 7f3c2703..812d77ca 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -4,6 +4,10 @@ `react-native-onesignal` `5.4.x` registers its iOS TurboModule through the `codegenConfig.ios.modulesProvider` field, which was introduced in React Native `0.79`. Apps on React Native `0.76`–`0.78` (including Expo SDK `52`) will hit `Invariant Violation: TurboModuleRegistry.getEnforcing(...): 'OneSignal' could not be found` at runtime when the New Architecture is enabled. Stay on `5.3.x` until you can move to React Native `0.79+` (Expo SDK `53+`); `5.3.x` uses the legacy bridge module and works on the New Architecture via the interop layer. +#### Notification `rawPayload` on Android + +Notification `rawPayload` is now an object on Android, matching iOS. Remove any `JSON.parse(notification.rawPayload)` calls and access its properties directly. + #### ⚠️ Migration Advisory for current OneSignal customers Our new [user-centric APIs and v5.x.x SDKs](https://onesignal.com/blog/unify-your-users-across-channels-and-devices/) offer an improved user and data management experience. However, they may not be at 1:1 feature parity with our previous versions yet. diff --git a/android/build.gradle b/android/build.gradle index 50df5d1f..1d594620 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -66,4 +66,5 @@ dependencies { } testImplementation 'junit:junit:4.12' + testImplementation 'org.json:json:20240303' } diff --git a/android/src/main/java/com/onesignal/rnonesignalandroid/RNUtils.java b/android/src/main/java/com/onesignal/rnonesignalandroid/RNUtils.java index e6494daf..d2027583 100644 --- a/android/src/main/java/com/onesignal/rnonesignalandroid/RNUtils.java +++ b/android/src/main/java/com/onesignal/rnonesignalandroid/RNUtils.java @@ -7,6 +7,7 @@ import com.facebook.react.bridge.ReadableType; import com.facebook.react.bridge.WritableArray; import com.facebook.react.bridge.WritableMap; +import com.onesignal.debug.internal.logging.Logging; import com.onesignal.inAppMessages.IInAppMessage; import com.onesignal.inAppMessages.IInAppMessageClickEvent; import com.onesignal.inAppMessages.IInAppMessageClickResult; @@ -126,11 +127,22 @@ public static HashMap convertNotificationToMap(INotification not } notificationHash.put("actionButtons", actionButtons); } - notificationHash.put("rawPayload", notification.getRawPayload()); + notificationHash.put("rawPayload", convertRawPayloadToHashMap(notification.getRawPayload())); return notificationHash; } + static HashMap convertRawPayloadToHashMap(String rawPayload) { + if (rawPayload == null || rawPayload.trim().isEmpty()) return new HashMap<>(); + + try { + return convertJSONObjectToHashMap(new JSONObject(rawPayload)); + } catch (JSONException exception) { + Logging.error("Failed to parse notification raw payload", exception); + return new HashMap<>(); + } + } + private static HashMap convertInAppMessageToMap(IInAppMessage message) { HashMap hash = new HashMap<>(); hash.put("messageId", message.getMessageId()); diff --git a/android/src/test/java/com/onesignal/rnonesignalandroid/RNUtilsTest.java b/android/src/test/java/com/onesignal/rnonesignalandroid/RNUtilsTest.java new file mode 100644 index 00000000..a0f24ad5 --- /dev/null +++ b/android/src/test/java/com/onesignal/rnonesignalandroid/RNUtilsTest.java @@ -0,0 +1,41 @@ +package com.onesignal.rnonesignalandroid; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.HashMap; +import java.util.List; +import org.junit.Test; + +public class RNUtilsTest { + @Test + public void convertRawPayloadToHashMapConvertsNestedJson() { + HashMap payload = RNUtils.convertRawPayloadToHashMap( + "{\"message\":\"hello\",\"count\":2,\"enabled\":true,\"nested\":{\"value\":\"inside\"}," + + "\"items\":[1,{\"name\":\"item\"}]}"); + + assertEquals("hello", payload.get("message")); + assertEquals(2, payload.get("count")); + assertEquals(true, payload.get("enabled")); + + HashMap nested = (HashMap) payload.get("nested"); + assertEquals("inside", nested.get("value")); + + List items = (List) payload.get("items"); + assertEquals(1, items.get(0)); + assertEquals("item", ((HashMap) items.get(1)).get("name")); + } + + @Test + public void convertRawPayloadToHashMapReturnsEmptyMapForEmptyPayload() { + assertTrue(RNUtils.convertRawPayloadToHashMap("").isEmpty()); + assertTrue(RNUtils.convertRawPayloadToHashMap(" ").isEmpty()); + assertTrue(RNUtils.convertRawPayloadToHashMap(null).isEmpty()); + } + + @Test + public void convertRawPayloadToHashMapReturnsEmptyMapForInvalidPayload() { + assertTrue(RNUtils.convertRawPayloadToHashMap("not-json").isEmpty()); + assertTrue(RNUtils.convertRawPayloadToHashMap("[1, 2]").isEmpty()); + } +} diff --git a/examples/demo/README.md b/examples/demo/README.md index 3e2c3f85..9cd7000a 100644 --- a/examples/demo/README.md +++ b/examples/demo/README.md @@ -73,6 +73,18 @@ When you want to forcefully reload, for example to reset the state of your app, - **Android**: Press the R key twice or select **"Reload"** from the **Dev Menu**, accessed via Ctrl + M (Windows/Linux) or Cmd ⌘ + M (macOS). - **iOS**: Press R in iOS Simulator. +## Testing iOS Action Button Icons + +The iOS demo bundles shared action button icons in both the `demo` and +`OneSignalNotificationServiceExtension` targets. Create an image notification +in the OneSignal dashboard with these action buttons: + +- **Save Story** with icon `template-bookmark-icon` +- **Share** with icon `template-share-icon` + +Enter the icon names without the `.png` extension. On an iOS 15 or newer +physical device, expand the notification to verify that both icons render. + ## Congratulations! :tada: You've successfully run and modified your React Native App. :partying_face: diff --git a/examples/demo/bun.lock b/examples/demo/bun.lock index 01127819..058e57f8 100644 --- a/examples/demo/bun.lock +++ b/examples/demo/bun.lock @@ -1006,7 +1006,7 @@ "react-native-dotenv": ["react-native-dotenv@3.4.11", "", { "dependencies": { "dotenv": "^16.4.5" }, "peerDependencies": { "@babel/runtime": "^7.20.6" } }, "sha512-6vnIE+WHABSeHCaYP6l3O1BOEhWxKH6nHAdV7n/wKn/sciZ64zPPp2NUdEUf1m7g4uuzlLbjgr+6uDt89q2DOg=="], - "react-native-onesignal": ["react-native-onesignal@../../react-native-onesignal.tgz", { "dependencies": { "invariant": "^2.2.4" }, "peerDependencies": { "react-native": ">=0.79.0" } }, "sha512-UiPEj1EY1ghqHM/iUrcAIFr5YRnVqKQsOVLl2G7Y/Dq8BxE8S8jkPeMJpyFCgjZzQgKk9B3yCuWwTqECucaQAg=="], + "react-native-onesignal": ["react-native-onesignal@../../react-native-onesignal.tgz", { "dependencies": { "invariant": "^2.2.4" }, "peerDependencies": { "react-native": ">=0.79.0" } }, "sha512-GitSpK1jeWw44O5Uyh+U2C7xNGIXmOgTgOE0e8lVJeKQYSkte7HULSzWgcGWsTECAVkdBrglhOB6DTnA6faaSA=="], "react-native-safe-area-context": ["react-native-safe-area-context@5.7.0", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-/9/MtQz8ODphjsLdZ+GZAIcC/RtoqW9EeShf7Uvnfgm/pzYrJ75y3PV/J1wuAV1T5Dye5ygq4EAW20RoBq0ABQ=="], diff --git a/examples/demo/ios/demo.xcodeproj/project.pbxproj b/examples/demo/ios/demo.xcodeproj/project.pbxproj index 8246a9ca..1a7dfaf2 100644 --- a/examples/demo/ios/demo.xcodeproj/project.pbxproj +++ b/examples/demo/ios/demo.xcodeproj/project.pbxproj @@ -21,6 +21,10 @@ A10000000000000000000007 /* libPods-OneSignalWidgetExtension.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A20000000000000000000010 /* libPods-OneSignalWidgetExtension.a */; }; A10000000000000000000008 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A20000000000000000000009 /* WidgetKit.framework */; }; A10000000000000000000009 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A2000000000000000000000A /* SwiftUI.framework */; }; + B10000000000000000000001 /* template-bookmark-icon.png in Resources */ = {isa = PBXBuildFile; fileRef = B20000000000000000000001 /* template-bookmark-icon.png */; }; + B10000000000000000000002 /* template-share-icon.png in Resources */ = {isa = PBXBuildFile; fileRef = B20000000000000000000002 /* template-share-icon.png */; }; + B10000000000000000000003 /* template-bookmark-icon.png in Resources */ = {isa = PBXBuildFile; fileRef = B20000000000000000000001 /* template-bookmark-icon.png */; }; + B10000000000000000000004 /* template-share-icon.png in Resources */ = {isa = PBXBuildFile; fileRef = B20000000000000000000002 /* template-share-icon.png */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -82,6 +86,8 @@ A2000000000000000000000F /* libPods-OneSignalNotificationServiceExtension.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-OneSignalNotificationServiceExtension.a"; sourceTree = BUILT_PRODUCTS_DIR; }; A20000000000000000000010 /* libPods-OneSignalWidgetExtension.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-OneSignalWidgetExtension.a"; sourceTree = BUILT_PRODUCTS_DIR; }; A20000000000000000000011 /* demo.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = demo.entitlements; path = demo/demo.entitlements; sourceTree = ""; }; + B20000000000000000000001 /* template-bookmark-icon.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = "template-bookmark-icon.png"; path = "demo/template-bookmark-icon.png"; sourceTree = ""; }; + B20000000000000000000002 /* template-share-icon.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = "template-share-icon.png"; path = "demo/template-share-icon.png"; sourceTree = ""; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; /* End PBXFileReference section */ @@ -124,6 +130,8 @@ 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */, 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */, A20000000000000000000011 /* demo.entitlements */, + B20000000000000000000001 /* template-bookmark-icon.png */, + B20000000000000000000002 /* template-share-icon.png */, ); name = demo; sourceTree = ""; @@ -317,6 +325,8 @@ 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */, 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, 5F8F7D5C84F8947D278D5B06 /* PrivacyInfo.xcprivacy in Resources */, + B10000000000000000000001 /* template-bookmark-icon.png in Resources */, + B10000000000000000000002 /* template-share-icon.png in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -324,6 +334,8 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + B10000000000000000000003 /* template-bookmark-icon.png in Resources */, + B10000000000000000000004 /* template-share-icon.png in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/examples/demo/ios/demo/template-bookmark-icon.png b/examples/demo/ios/demo/template-bookmark-icon.png new file mode 100644 index 00000000..99281835 Binary files /dev/null and b/examples/demo/ios/demo/template-bookmark-icon.png differ diff --git a/examples/demo/ios/demo/template-share-icon.png b/examples/demo/ios/demo/template-share-icon.png new file mode 100644 index 00000000..2457e187 Binary files /dev/null and b/examples/demo/ios/demo/template-share-icon.png differ diff --git a/examples/demo/src/hooks/useOneSignal.ts b/examples/demo/src/hooks/useOneSignal.ts index b7ee9932..06177ef6 100644 --- a/examples/demo/src/hooks/useOneSignal.ts +++ b/examples/demo/src/hooks/useOneSignal.ts @@ -191,15 +191,35 @@ function useOneSignalState(): UseOneSignalReturn { const handleNotificationClick = (e: NotificationClickEvent) => { console.log(`Notification click: ${e.notification.title ?? ''}`); + + // uncomment to see the full event object + // console.log('[OneSignal] click event: ', e); }; const handleForegroundWillDisplay = (e: NotificationWillDisplayEvent) => { console.log(`Notification foregroundWillDisplay: ${e.getNotification().title ?? ''}`); + + // uncomment to see the full event object + // console.log('[OneSignal] will display event: ', e.getNotification()); + // uncomment to test preventing the default display behavior // e.preventDefault(); // can call this after preventDefault (within ~25 seconds) to force display of notification // e.getNotification().display(); + + // example with a delay (assumes preventDefault was called) + // console.log('[OneSignal] forcing display of notification in x seconds'); + // let seconds = 24; + // const interval = setInterval(() => { + // seconds--; + // console.log(`[OneSignal] displaying notification in ${seconds} seconds`); + // if (seconds <= 0) { + // console.log('[OneSignal] displaying notification'); + // e.getNotification().display(); + // clearInterval(interval); + // } + // }, 1000); }; const pushSubHandler = (event: PushSubscriptionChangedState) => { diff --git a/src/OSNotification.test.ts b/src/OSNotification.test.ts index 75e0aac5..a64080f2 100644 --- a/src/OSNotification.test.ts +++ b/src/OSNotification.test.ts @@ -40,7 +40,7 @@ describe('OSNotification', () => { test('should initialize with optional common properties as undefined', () => { const notificationData = { body: 'Test body', - rawPayload: '', + rawPayload: {}, notificationId: 'id-123', }; const notification = new OSNotification(notificationData); @@ -77,7 +77,7 @@ describe('OSNotification', () => { collapseId: 'collapse-1', fromProjectNumber: '123456789', smallIconAccentColor: 'FFFF0000', - lockScreenVisibility: '1', + lockScreenVisibility: 1, androidNotificationId: 456, }; const notification = new OSNotification(androidData); @@ -93,7 +93,7 @@ describe('OSNotification', () => { expect(notification.collapseId).toBe('collapse-1'); expect(notification.fromProjectNumber).toBe('123456789'); expect(notification.smallIconAccentColor).toBe('FFFF0000'); - expect(notification.lockScreenVisibility).toBe('1'); + expect(notification.lockScreenVisibility).toBe(1); expect(notification.androidNotificationId).toBe(456); }); @@ -198,7 +198,7 @@ describe('OSNotification', () => { }); }); - describe('rawPayload types', () => { + describe('rawPayload', () => { test('should accept object as rawPayload', () => { const notificationData = { ...baseNotificationData, @@ -212,16 +212,6 @@ describe('OSNotification', () => { }); }); - test('should accept string as rawPayload', () => { - const notificationData = { - ...baseNotificationData, - rawPayload: '{"key":"value"}', - }; - const notification = new OSNotification(notificationData); - - expect(notification.rawPayload).toBe('{"key":"value"}'); - }); - test('should accept empty object as rawPayload', () => { const notificationData = { ...baseNotificationData, @@ -231,15 +221,5 @@ describe('OSNotification', () => { expect(notification.rawPayload).toEqual({}); }); - - test('should accept empty string as rawPayload', () => { - const notificationData = { - ...baseNotificationData, - rawPayload: '', - }; - const notification = new OSNotification(notificationData); - - expect(notification.rawPayload).toBe(''); - }); }); }); diff --git a/src/OSNotification.ts b/src/OSNotification.ts index 646f8a29..d7363b3a 100644 --- a/src/OSNotification.ts +++ b/src/OSNotification.ts @@ -8,7 +8,7 @@ export interface BaseNotificationData { sound?: string; title?: string; launchURL?: string; - rawPayload: object | string; // platform bridges return different types + rawPayload: object; actionButtons?: object[]; additionalData?: object; notificationId: string; @@ -26,7 +26,7 @@ interface AndroidNotificationData extends BaseNotificationData { collapseId?: string; fromProjectNumber?: string; smallIconAccentColor?: string; - lockScreenVisibility?: string; + lockScreenVisibility?: number; androidNotificationId?: number; } @@ -52,7 +52,7 @@ export default class OSNotification { sound?: string; title?: string; launchURL?: string; - rawPayload: object | string; // platform bridges return different types + rawPayload: object; actionButtons?: object[]; additionalData?: object; notificationId: string; @@ -68,7 +68,7 @@ export default class OSNotification { collapseId?: string; fromProjectNumber?: string; smallIconAccentColor?: string; - lockScreenVisibility?: string; + lockScreenVisibility?: number; androidNotificationId?: number; // ios only badge?: string;