From 0b83ee205e1499b8938e7a0d2c03578af1c169b6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 28 Jan 2026 23:21:33 +0000 Subject: [PATCH 01/12] Update Android SDK to 5.6.0 --- android/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/build.gradle b/android/build.gradle index d48e354e..22f69e64 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -31,7 +31,7 @@ dependencies { // api is used instead of implementation so the parent :app project can access any of the OneSignal Java // classes if needed. Such as com.onesignal.NotificationExtenderService - api 'com.onesignal:OneSignal:5.4.2' + api 'com.onesignal:OneSignal:5.6.0' testImplementation 'junit:junit:4.12' } \ No newline at end of file From 8b49729d61fbc9e5280eeea0b36bc1406c7acf31 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 28 Jan 2026 23:21:35 +0000 Subject: [PATCH 02/12] Update iOS SDK to 5.4.0 --- react-native-onesignal.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/react-native-onesignal.podspec b/react-native-onesignal.podspec index b7861442..3f6014e6 100644 --- a/react-native-onesignal.podspec +++ b/react-native-onesignal.podspec @@ -22,5 +22,5 @@ Pod::Spec.new do |s| # pod 'React', :path => '../node_modules/react-native/' # The Native OneSignal-iOS-SDK XCFramework from cocoapods. - s.dependency 'OneSignalXCFramework', '5.2.16' + s.dependency 'OneSignalXCFramework', '5.4.0' end From 35433a6102d406e6dce5e8f8d67d7ac49130db19 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Wed, 28 Jan 2026 16:13:40 -0800 Subject: [PATCH 03/12] feat: add custom events support --- .../rnonesignalandroid/RNOneSignal.java | 9 ++++ .../onesignal/rnonesignalandroid/RNUtils.java | 46 +++++++++++++++++++ examples/RNOneSignalTS/OSButtons.tsx | 22 +++++++++ ios/RCTOneSignal/RCTOneSignalEventEmitter.m | 4 ++ src/index.ts | 18 ++++++++ 5 files changed, 99 insertions(+) diff --git a/android/src/main/java/com/onesignal/rnonesignalandroid/RNOneSignal.java b/android/src/main/java/com/onesignal/rnonesignalandroid/RNOneSignal.java index 35b3a059..ae80c39b 100644 --- a/android/src/main/java/com/onesignal/rnonesignalandroid/RNOneSignal.java +++ b/android/src/main/java/com/onesignal/rnonesignalandroid/RNOneSignal.java @@ -35,6 +35,9 @@ of this software and associated documentation files (the "Software"), to deal package com.onesignal.rnonesignalandroid; +import java.util.HashMap; +import java.util.Map; + import android.content.Context; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.LifecycleEventListener; @@ -754,4 +757,10 @@ public void addListener(String eventName) { public void removeListeners(int count) { // Keep: Required for RN built in Event Emitter Calls. } + + + @ReactMethod + public void trackEvent(String name, @Nullable ReadableMap properties) { + OneSignal.getUser().trackEvent(name, properties != null ? RNUtils.convertReadableMapToMap(properties) : null); + } } diff --git a/android/src/main/java/com/onesignal/rnonesignalandroid/RNUtils.java b/android/src/main/java/com/onesignal/rnonesignalandroid/RNUtils.java index 29d1f722..c1331265 100644 --- a/android/src/main/java/com/onesignal/rnonesignalandroid/RNUtils.java +++ b/android/src/main/java/com/onesignal/rnonesignalandroid/RNUtils.java @@ -268,6 +268,52 @@ public static HashMap convertReadableMapIntoStringMap(ReadableMa return stringMap; } + public static Map convertReadableMapToMap(ReadableMap readableMap) { + Map map = new HashMap<>(); + ReadableMapKeySetIterator iterator = readableMap.keySetIterator(); + + while (iterator.hasNextKey()) { + String key = iterator.nextKey(); + ReadableType type = readableMap.getType(key); + map.put(key, convertValue(type, readableMap, key)); + } + + return map; + } + + public static List convertReadableArrayToList(ReadableArray readableArray) { + List list = new ArrayList<>(); + + for (int i = 0; i < readableArray.size(); i++) { + ReadableType type = readableArray.getType(i); + list.add(convertValue(type, readableArray, i)); + } + + return list; + } + + private static Object convertValue(ReadableType type, ReadableMap map, String key) { + switch (type) { + case Boolean: return map.getBoolean(key); + case Number: return map.getDouble(key); + case String: return map.getString(key); + case Map: return convertReadableMapToMap(map.getMap(key)); + case Array: return convertReadableArrayToList(map.getArray(key)); + default: return null; + } + } + + private static Object convertValue(ReadableType type, ReadableArray array, int index) { + switch (type) { + case Boolean: return array.getBoolean(index); + case Number: return array.getDouble(index); + case String: return array.getString(index); + case Map: return convertReadableMapToMap(array.getMap(index)); + case Array: return convertReadableArrayToList(array.getArray(index)); + default: return null; + } + } + public static HashMap convertPermissionToMap(boolean granted) { HashMap hash = new HashMap<>(); diff --git a/examples/RNOneSignalTS/OSButtons.tsx b/examples/RNOneSignalTS/OSButtons.tsx index 00781d8a..949acda8 100644 --- a/examples/RNOneSignalTS/OSButtons.tsx +++ b/examples/RNOneSignalTS/OSButtons.tsx @@ -396,11 +396,33 @@ const OSButtons: React.FC = ({ loggingFunction, inputFieldValue }) => { }, ); + const trackEventButton = renderButtonView('Track Event', () => { + loggingFunction('Tracking event: ', 'ReactNative'); + const platform = Platform.OS; // This will be 'ios' or 'android' + OneSignal.User.trackEvent(`ReactNative-${platform}-noprops`); + OneSignal.User.trackEvent(`ReactNative-${platform}`, { + someNum: 123, + someFloat: 3.14159, + someString: 'abc', + someBool: true, + someObject: { + abc: '123', + nested: { + def: '456', + }, + }, + someArray: [1, 2], + someMixedArray: [1, '2', { abc: '123' }], + someNull: null, + }); + }); + return [ loginButton, logoutButton, addEmailButton, removeEmailButton, + trackEventButton, sendTagWithKeyButton, deleteTagWithKeyButton, addTagsButton, diff --git a/ios/RCTOneSignal/RCTOneSignalEventEmitter.m b/ios/RCTOneSignal/RCTOneSignalEventEmitter.m index 93abb6fe..9f76c15a 100644 --- a/ios/RCTOneSignal/RCTOneSignalEventEmitter.m +++ b/ios/RCTOneSignal/RCTOneSignalEventEmitter.m @@ -628,4 +628,8 @@ - (void)removeUserStateObserver { } } +RCT_EXPORT_METHOD(trackEvent:(NSString *)name withProperties:(NSDictionary * _Nullable)properties) { + [OneSignal.User trackEventWithName:name properties:properties]; +} + @end diff --git a/src/index.ts b/src/index.ts index 2fc85415..e848e5c5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -590,6 +590,24 @@ export namespace OneSignal { return RNOneSignal.getTags(); } + + /** + * Track custom events for the current user. + * Note: Currently, null values will be omitted for Android. + * */ + export function trackEvent( + name: string, + properties: Record = {}, + ) { + if (!isNativeModuleLoaded(RNOneSignal)) return; + + if (!isObjectSerializable(properties)) { + console.error('Properties must be JSON-serializable'); + return; + } + + RNOneSignal.trackEvent(name, properties); + } } export namespace Notifications { From 62ab8510326d85d761320a51b09eb94d9075acb2 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Wed, 28 Jan 2026 16:24:18 -0800 Subject: [PATCH 04/12] add object serializable check --- src/index.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/index.ts b/src/index.ts index e848e5c5..c2ae3dd1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -956,6 +956,21 @@ export namespace OneSignal { } } +/** + * Returns true if the value is a JSON-serializable object. + */ +function isObjectSerializable(value: unknown): boolean { + if (!(typeof value === 'object' && value !== null && !Array.isArray(value))) { + return false; + } + try { + JSON.stringify(value); + return true; + } catch (e) { + return false; + } +} + export { OSNotificationPermission } from './constants/subscription'; export { NotificationWillDisplayEvent, From 61624a70769be8e061183fe48e34f34295a3df3c Mon Sep 17 00:00:00 2001 From: Fadi George Date: Wed, 28 Jan 2026 16:35:28 -0800 Subject: [PATCH 05/12] fix logic for android track event --- .../rnonesignalandroid/RNOneSignal.java | 10 ++-- .../onesignal/rnonesignalandroid/RNUtils.java | 50 ++++++++++++------- build.gradle | 2 +- examples/RNOneSignalTS/OSButtons.tsx | 5 +- ios/RCTOneSignal/RCTOneSignalEventEmitter.m | 5 +- 5 files changed, 43 insertions(+), 29 deletions(-) diff --git a/android/src/main/java/com/onesignal/rnonesignalandroid/RNOneSignal.java b/android/src/main/java/com/onesignal/rnonesignalandroid/RNOneSignal.java index ae80c39b..14a8ce79 100644 --- a/android/src/main/java/com/onesignal/rnonesignalandroid/RNOneSignal.java +++ b/android/src/main/java/com/onesignal/rnonesignalandroid/RNOneSignal.java @@ -35,10 +35,8 @@ of this software and associated documentation files (the "Software"), to deal package com.onesignal.rnonesignalandroid; -import java.util.HashMap; -import java.util.Map; - import android.content.Context; +import androidx.annotation.Nullable; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.LifecycleEventListener; import com.facebook.react.bridge.Promise; @@ -758,9 +756,11 @@ public void removeListeners(int count) { // Keep: Required for RN built in Event Emitter Calls. } - @ReactMethod public void trackEvent(String name, @Nullable ReadableMap properties) { - OneSignal.getUser().trackEvent(name, properties != null ? RNUtils.convertReadableMapToMap(properties) : null); + OneSignal.getUser() + .trackEvent( + name, + properties != null ? RNUtils.convertReadableMapToMap(properties) : new java.util.HashMap<>()); } } diff --git a/android/src/main/java/com/onesignal/rnonesignalandroid/RNUtils.java b/android/src/main/java/com/onesignal/rnonesignalandroid/RNUtils.java index c1331265..e46e748d 100644 --- a/android/src/main/java/com/onesignal/rnonesignalandroid/RNUtils.java +++ b/android/src/main/java/com/onesignal/rnonesignalandroid/RNUtils.java @@ -271,46 +271,58 @@ public static HashMap convertReadableMapIntoStringMap(ReadableMa public static Map convertReadableMapToMap(ReadableMap readableMap) { Map map = new HashMap<>(); ReadableMapKeySetIterator iterator = readableMap.keySetIterator(); - + while (iterator.hasNextKey()) { String key = iterator.nextKey(); ReadableType type = readableMap.getType(key); map.put(key, convertValue(type, readableMap, key)); } - + return map; } - + public static List convertReadableArrayToList(ReadableArray readableArray) { List list = new ArrayList<>(); - + for (int i = 0; i < readableArray.size(); i++) { ReadableType type = readableArray.getType(i); list.add(convertValue(type, readableArray, i)); } - + return list; } - + private static Object convertValue(ReadableType type, ReadableMap map, String key) { switch (type) { - case Boolean: return map.getBoolean(key); - case Number: return map.getDouble(key); - case String: return map.getString(key); - case Map: return convertReadableMapToMap(map.getMap(key)); - case Array: return convertReadableArrayToList(map.getArray(key)); - default: return null; + case Boolean: + return map.getBoolean(key); + case Number: + return map.getDouble(key); + case String: + return map.getString(key); + case Map: + return convertReadableMapToMap(map.getMap(key)); + case Array: + return convertReadableArrayToList(map.getArray(key)); + default: + return null; } } - + private static Object convertValue(ReadableType type, ReadableArray array, int index) { switch (type) { - case Boolean: return array.getBoolean(index); - case Number: return array.getDouble(index); - case String: return array.getString(index); - case Map: return convertReadableMapToMap(array.getMap(index)); - case Array: return convertReadableArrayToList(array.getArray(index)); - default: return null; + case Boolean: + return array.getBoolean(index); + case Number: + return array.getDouble(index); + case String: + return array.getString(index); + case Map: + return convertReadableMapToMap(array.getMap(index)); + case Array: + return convertReadableArrayToList(array.getArray(index)); + default: + return null; } } diff --git a/build.gradle b/build.gradle index 3168af42..33087e95 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ spotless { java { target 'android/**/*.java', 'examples/RNOneSignalTS/android/app/src/**/*.java' targetExclude '**/build/**' - palantirJavaFormat('2.28.0') + palantirJavaFormat('2.47.0') removeUnusedImports() trimTrailingWhitespace() endWithNewline() diff --git a/examples/RNOneSignalTS/OSButtons.tsx b/examples/RNOneSignalTS/OSButtons.tsx index 949acda8..7ef7f803 100644 --- a/examples/RNOneSignalTS/OSButtons.tsx +++ b/examples/RNOneSignalTS/OSButtons.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Text, View } from 'react-native'; +import { Platform, Text, View } from 'react-native'; import { OneSignal } from 'react-native-onesignal'; import { renderButtonView } from './Helpers'; // Remove: import {Text, Divider} from '@react-native-material/core'; @@ -410,9 +410,10 @@ const OSButtons: React.FC = ({ loggingFunction, inputFieldValue }) => { nested: { def: '456', }, + ghi: null, }, someArray: [1, 2], - someMixedArray: [1, '2', { abc: '123' }], + someMixedArray: [1, '2', { abc: '123' }, null], someNull: null, }); }); diff --git a/ios/RCTOneSignal/RCTOneSignalEventEmitter.m b/ios/RCTOneSignal/RCTOneSignalEventEmitter.m index 9f76c15a..46b13810 100644 --- a/ios/RCTOneSignal/RCTOneSignalEventEmitter.m +++ b/ios/RCTOneSignal/RCTOneSignalEventEmitter.m @@ -628,8 +628,9 @@ - (void)removeUserStateObserver { } } -RCT_EXPORT_METHOD(trackEvent:(NSString *)name withProperties:(NSDictionary * _Nullable)properties) { - [OneSignal.User trackEventWithName:name properties:properties]; +RCT_EXPORT_METHOD(trackEvent : (NSString *)name withProperties : ( + NSDictionary *_Nullable)properties) { + [OneSignal.User trackEventWithName:name properties:properties]; } @end From 682017ddc1fa12f6a70ecf54de97413d90615896 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Wed, 28 Jan 2026 16:42:12 -0800 Subject: [PATCH 06/12] filter out null for android custom events mapping --- .../java/com/onesignal/rnonesignalandroid/RNUtils.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/android/src/main/java/com/onesignal/rnonesignalandroid/RNUtils.java b/android/src/main/java/com/onesignal/rnonesignalandroid/RNUtils.java index e46e748d..c48af5c5 100644 --- a/android/src/main/java/com/onesignal/rnonesignalandroid/RNUtils.java +++ b/android/src/main/java/com/onesignal/rnonesignalandroid/RNUtils.java @@ -275,7 +275,10 @@ public static Map convertReadableMapToMap(ReadableMap readableMa while (iterator.hasNextKey()) { String key = iterator.nextKey(); ReadableType type = readableMap.getType(key); - map.put(key, convertValue(type, readableMap, key)); + Object value = convertValue(type, readableMap, key); + if (value != null) { + map.put(key, value); + } } return map; @@ -286,7 +289,10 @@ public static List convertReadableArrayToList(ReadableArray readableArra for (int i = 0; i < readableArray.size(); i++) { ReadableType type = readableArray.getType(i); - list.add(convertValue(type, readableArray, i)); + Object value = convertValue(type, readableArray, i); + if (value != null) { + list.add(value); + } } return list; From cf551ec3a29a675373137dc61c90535f64ebe8a6 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Wed, 28 Jan 2026 18:04:10 -0800 Subject: [PATCH 07/12] update podfile --- examples/RNOneSignalTS/ios/Podfile.lock | 34 ++++++++++++------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/examples/RNOneSignalTS/ios/Podfile.lock b/examples/RNOneSignalTS/ios/Podfile.lock index 41650a06..d1cebe62 100644 --- a/examples/RNOneSignalTS/ios/Podfile.lock +++ b/examples/RNOneSignalTS/ios/Podfile.lock @@ -8,9 +8,9 @@ PODS: - hermes-engine (0.82.1): - hermes-engine/Pre-built (= 0.82.1) - hermes-engine/Pre-built (0.82.1) - - OneSignalXCFramework (5.2.16): - - OneSignalXCFramework/OneSignalComplete (= 5.2.16) - - OneSignalXCFramework/OneSignal (5.2.16): + - OneSignalXCFramework (5.4.0): + - OneSignalXCFramework/OneSignalComplete (= 5.4.0) + - OneSignalXCFramework/OneSignal (5.4.0): - OneSignalXCFramework/OneSignalCore - OneSignalXCFramework/OneSignalExtension - OneSignalXCFramework/OneSignalLiveActivities @@ -18,38 +18,38 @@ PODS: - OneSignalXCFramework/OneSignalOSCore - OneSignalXCFramework/OneSignalOutcomes - OneSignalXCFramework/OneSignalUser - - OneSignalXCFramework/OneSignalComplete (5.2.16): + - OneSignalXCFramework/OneSignalComplete (5.4.0): - OneSignalXCFramework/OneSignal - OneSignalXCFramework/OneSignalInAppMessages - OneSignalXCFramework/OneSignalLocation - - OneSignalXCFramework/OneSignalCore (5.2.16) - - OneSignalXCFramework/OneSignalExtension (5.2.16): + - OneSignalXCFramework/OneSignalCore (5.4.0) + - OneSignalXCFramework/OneSignalExtension (5.4.0): - OneSignalXCFramework/OneSignalCore - OneSignalXCFramework/OneSignalOutcomes - - OneSignalXCFramework/OneSignalInAppMessages (5.2.16): + - OneSignalXCFramework/OneSignalInAppMessages (5.4.0): - OneSignalXCFramework/OneSignalCore - OneSignalXCFramework/OneSignalNotifications - OneSignalXCFramework/OneSignalOSCore - OneSignalXCFramework/OneSignalOutcomes - OneSignalXCFramework/OneSignalUser - - OneSignalXCFramework/OneSignalLiveActivities (5.2.16): + - OneSignalXCFramework/OneSignalLiveActivities (5.4.0): - OneSignalXCFramework/OneSignalCore - OneSignalXCFramework/OneSignalOSCore - OneSignalXCFramework/OneSignalUser - - OneSignalXCFramework/OneSignalLocation (5.2.16): + - OneSignalXCFramework/OneSignalLocation (5.4.0): - OneSignalXCFramework/OneSignalCore - OneSignalXCFramework/OneSignalNotifications - OneSignalXCFramework/OneSignalOSCore - OneSignalXCFramework/OneSignalUser - - OneSignalXCFramework/OneSignalNotifications (5.2.16): + - OneSignalXCFramework/OneSignalNotifications (5.4.0): - OneSignalXCFramework/OneSignalCore - OneSignalXCFramework/OneSignalExtension - OneSignalXCFramework/OneSignalOutcomes - - OneSignalXCFramework/OneSignalOSCore (5.2.16): + - OneSignalXCFramework/OneSignalOSCore (5.4.0): - OneSignalXCFramework/OneSignalCore - - OneSignalXCFramework/OneSignalOutcomes (5.2.16): + - OneSignalXCFramework/OneSignalOutcomes (5.4.0): - OneSignalXCFramework/OneSignalCore - - OneSignalXCFramework/OneSignalUser (5.2.16): + - OneSignalXCFramework/OneSignalUser (5.4.0): - OneSignalXCFramework/OneSignalCore - OneSignalXCFramework/OneSignalNotifications - OneSignalXCFramework/OneSignalOSCore @@ -1828,8 +1828,8 @@ PODS: - React-RCTFBReactNativeSpec - ReactCommon/turbomodule/core - SocketRocket - - react-native-onesignal (5.2.17): - - OneSignalXCFramework (= 5.2.16) + - react-native-onesignal (5.3.0): + - OneSignalXCFramework (= 5.4.0) - React (< 1.0.0, >= 0.13.0) - react-native-safe-area-context (5.6.2): - boost @@ -2768,7 +2768,7 @@ SPEC CHECKSUMS: fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd glog: 5683914934d5b6e4240e497e0f4a3b42d1854183 hermes-engine: 273e30e7fb618279934b0b95ffab60ecedb7acf5 - OneSignalXCFramework: 8ed6648481bee0bd973a138fecd80331b798524f + OneSignalXCFramework: 95b6391df5a91b448003149c1a633ade42ceca1e RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669 RCTDeprecation: f17e2ebc07876ca9ab8eb6e4b0a4e4647497ae3a RCTRequired: e2c574c1b45231f7efb0834936bd609d75072b63 @@ -2802,7 +2802,7 @@ SPEC CHECKSUMS: React-logger: 500f2fa5697d224e63c33d913c8a4765319e19bf React-Mapbuffer: 06d59c448da7e34eb05b3fb2189e12f6a30fec57 React-microtasksnativemodule: d1ee999dc9052e23f6488b730fa2d383a4ea40e5 - react-native-onesignal: 3b6cd199ec0db87166ef7fb595715627a35b3244 + react-native-onesignal: 68c8423063cc8ead827e09bc71d139c14850feaf react-native-safe-area-context: c00143b4823773bba23f2f19f85663ae89ceb460 React-NativeModulesApple: 46690a0fe94ec28fc6fc686ec797b911d251ded0 React-oscompat: 95875e81f5d4b3c7b2c888d5bd2c9d83450d8bdb From ba2b5c14cfa29491952e80b7e560901398a82ed1 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Wed, 28 Jan 2026 18:36:16 -0800 Subject: [PATCH 08/12] add tests for custom events --- __mocks__/react-native.ts | 1 + src/helpers.test.ts | 44 ++++++++++++++++++++++++++++++++++++++- src/helpers.ts | 15 +++++++++++++ src/index.test.ts | 43 ++++++++++++++++++++++++++++++++++++++ src/index.ts | 23 ++++++-------------- 5 files changed, 108 insertions(+), 18 deletions(-) diff --git a/__mocks__/react-native.ts b/__mocks__/react-native.ts index 517f1d10..4170f2ee 100644 --- a/__mocks__/react-native.ts +++ b/__mocks__/react-native.ts @@ -86,6 +86,7 @@ const mockRNOneSignal = { addOutcomeWithValue: vi.fn(), displayNotification: vi.fn(), preventDefault: vi.fn(), + trackEvent: vi.fn(), }; const mockPlatform = { diff --git a/src/helpers.test.ts b/src/helpers.test.ts index b3e5fb6c..ab31fabf 100644 --- a/src/helpers.test.ts +++ b/src/helpers.test.ts @@ -1,6 +1,10 @@ import type { NativeModule } from 'react-native'; import type { MockInstance } from 'vitest'; -import { isNativeModuleLoaded, isValidCallback } from './helpers'; +import { + isNativeModuleLoaded, + isObjectSerializable, + isValidCallback, +} from './helpers'; describe('helpers', () => { let errorSpy: MockInstance; @@ -58,4 +62,42 @@ describe('helpers', () => { expect(result).toBe(true); }); }); + + describe('isObjectSerializable', () => { + test.each([ + { description: 'an empty object', value: {} }, + { description: 'an object with string values', value: { key: 'value' } }, + { description: 'an object with number values', value: { count: 42 } }, + { description: 'an object with boolean values', value: { active: true } }, + { description: 'an object with null values', value: { data: null } }, + { + description: 'a nested object', + value: { outer: { inner: 'value' } }, + }, + { + description: 'an object with array values', + value: { items: [1, 2, 3] }, + }, + ])('should return true for $description', ({ value }) => { + expect(isObjectSerializable(value)).toBe(true); + }); + + test.each([ + { description: 'null', value: null }, + { description: 'undefined', value: undefined }, + { description: 'a string', value: 'string' }, + { description: 'a number', value: 123 }, + { description: 'a boolean', value: true }, + { description: 'an array', value: [1, 2, 3] }, + { description: 'a function', value: () => {} }, + ])('should return false for $description', ({ value }) => { + expect(isObjectSerializable(value)).toBe(false); + }); + + test('should return false for objects with circular references', () => { + const circular: Record = {}; + circular.self = circular; + expect(isObjectSerializable(circular)).toBe(false); + }); + }); }); diff --git a/src/helpers.ts b/src/helpers.ts index 8c71c9e2..4bd3c22f 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -16,3 +16,18 @@ export function isNativeModuleLoaded(module: NativeModule): boolean { return true; } + +/** + * Returns true if the value is a JSON-serializable object. + */ +export function isObjectSerializable(value: unknown): boolean { + if (!(typeof value === 'object' && value !== null && !Array.isArray(value))) { + return false; + } + try { + JSON.stringify(value); + return true; + } catch { + return false; + } +} diff --git a/src/index.test.ts b/src/index.test.ts index 6b40bd7d..169c8077 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -894,6 +894,49 @@ describe('OneSignal', () => { }); }); + describe('trackEvent', () => { + test('should track event with name and properties', () => { + const properties = { key: 'value', count: 42 }; + OneSignal.User.trackEvent('purchase', properties); + expect(mockRNOneSignal.trackEvent).toHaveBeenCalledWith( + 'purchase', + properties, + ); + }); + + test('should track event with just name using default empty properties', () => { + OneSignal.User.trackEvent('page_view'); + expect(mockRNOneSignal.trackEvent).toHaveBeenCalledWith('page_view', {}); + }); + + test('should not track event if native module is not loaded', () => { + isNativeLoadedSpy.mockReturnValue(false); + OneSignal.User.trackEvent('event'); + expect(mockRNOneSignal.trackEvent).not.toHaveBeenCalled(); + }); + + test('should not track event if properties are not serializable', () => { + const circular: Record = {}; + circular.self = circular; + OneSignal.User.trackEvent('event', circular); + expect(errorSpy).toHaveBeenCalledWith( + 'Properties must be a JSON-serializable object', + ); + expect(mockRNOneSignal.trackEvent).not.toHaveBeenCalled(); + }); + + test('should not track event if properties is not an object', () => { + OneSignal.User.trackEvent( + 'event', + 'invalid' as unknown as Record, + ); + expect(errorSpy).toHaveBeenCalledWith( + 'Properties must be a JSON-serializable object', + ); + expect(mockRNOneSignal.trackEvent).not.toHaveBeenCalled(); + }); + }); + describe('Notifications', () => { describe('hasPermission (deprecated)', () => { test('should log deprecation warning', () => { diff --git a/src/index.ts b/src/index.ts index c2ae3dd1..7bced605 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,7 +14,11 @@ import { import type { OSNotificationPermission } from './constants/subscription'; import EventManager from './events/EventManager'; import NotificationWillDisplayEvent from './events/NotificationWillDisplayEvent'; -import { isNativeModuleLoaded, isValidCallback } from './helpers'; +import { + isNativeModuleLoaded, + isObjectSerializable, + isValidCallback, +} from './helpers'; import type { InAppMessage, InAppMessageClickEvent, @@ -602,7 +606,7 @@ export namespace OneSignal { if (!isNativeModuleLoaded(RNOneSignal)) return; if (!isObjectSerializable(properties)) { - console.error('Properties must be JSON-serializable'); + console.error('Properties must be a JSON-serializable object'); return; } @@ -956,21 +960,6 @@ export namespace OneSignal { } } -/** - * Returns true if the value is a JSON-serializable object. - */ -function isObjectSerializable(value: unknown): boolean { - if (!(typeof value === 'object' && value !== null && !Array.isArray(value))) { - return false; - } - try { - JSON.stringify(value); - return true; - } catch (e) { - return false; - } -} - export { OSNotificationPermission } from './constants/subscription'; export { NotificationWillDisplayEvent, From 27433bedfcb8e249387dd74d0c43cc826e9a8fc0 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Wed, 28 Jan 2026 19:10:37 -0800 Subject: [PATCH 09/12] formatting fixes --- build.gradle | 2 +- src/index.test.ts | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 33087e95..20878c6e 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ spotless { java { target 'android/**/*.java', 'examples/RNOneSignalTS/android/app/src/**/*.java' targetExclude '**/build/**' - palantirJavaFormat('2.47.0') + palantirJavaFormat('2.85.0') removeUnusedImports() trimTrailingWhitespace() endWithNewline() diff --git a/src/index.test.ts b/src/index.test.ts index 169c8077..4d877938 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -906,7 +906,10 @@ describe('OneSignal', () => { test('should track event with just name using default empty properties', () => { OneSignal.User.trackEvent('page_view'); - expect(mockRNOneSignal.trackEvent).toHaveBeenCalledWith('page_view', {}); + expect(mockRNOneSignal.trackEvent).toHaveBeenCalledWith( + 'page_view', + {}, + ); }); test('should not track event if native module is not loaded', () => { From 5bed190811907459829737823a45f5be6bea9e70 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Fri, 30 Jan 2026 11:15:37 -0800 Subject: [PATCH 10/12] Revert "filter out null for android custom events mapping" This reverts commit 682017ddc1fa12f6a70ecf54de97413d90615896. --- .../java/com/onesignal/rnonesignalandroid/RNUtils.java | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/android/src/main/java/com/onesignal/rnonesignalandroid/RNUtils.java b/android/src/main/java/com/onesignal/rnonesignalandroid/RNUtils.java index c48af5c5..e46e748d 100644 --- a/android/src/main/java/com/onesignal/rnonesignalandroid/RNUtils.java +++ b/android/src/main/java/com/onesignal/rnonesignalandroid/RNUtils.java @@ -275,10 +275,7 @@ public static Map convertReadableMapToMap(ReadableMap readableMa while (iterator.hasNextKey()) { String key = iterator.nextKey(); ReadableType type = readableMap.getType(key); - Object value = convertValue(type, readableMap, key); - if (value != null) { - map.put(key, value); - } + map.put(key, convertValue(type, readableMap, key)); } return map; @@ -289,10 +286,7 @@ public static List convertReadableArrayToList(ReadableArray readableArra for (int i = 0; i < readableArray.size(); i++) { ReadableType type = readableArray.getType(i); - Object value = convertValue(type, readableArray, i); - if (value != null) { - list.add(value); - } + list.add(convertValue(type, readableArray, i)); } return list; From cafdc582d798c92c2455616d0aa1d231b5a2a5f2 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Fri, 30 Jan 2026 11:42:09 -0800 Subject: [PATCH 11/12] Update Android SDK to 5.6.1 --- android/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/build.gradle b/android/build.gradle index 22f69e64..80e63f06 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -31,7 +31,7 @@ dependencies { // api is used instead of implementation so the parent :app project can access any of the OneSignal Java // classes if needed. Such as com.onesignal.NotificationExtenderService - api 'com.onesignal:OneSignal:5.6.0' + api 'com.onesignal:OneSignal:5.6.1' testImplementation 'junit:junit:4.12' } \ No newline at end of file From 115c14bdcac779aa9d08f48e0ea1ddd5b9ae02d8 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Fri, 30 Jan 2026 11:50:24 -0800 Subject: [PATCH 12/12] simplify properties conversion --- .../rnonesignalandroid/RNOneSignal.java | 5 +- .../onesignal/rnonesignalandroid/RNUtils.java | 58 ------------------- 2 files changed, 1 insertion(+), 62 deletions(-) diff --git a/android/src/main/java/com/onesignal/rnonesignalandroid/RNOneSignal.java b/android/src/main/java/com/onesignal/rnonesignalandroid/RNOneSignal.java index 14a8ce79..8c59d882 100644 --- a/android/src/main/java/com/onesignal/rnonesignalandroid/RNOneSignal.java +++ b/android/src/main/java/com/onesignal/rnonesignalandroid/RNOneSignal.java @@ -758,9 +758,6 @@ public void removeListeners(int count) { @ReactMethod public void trackEvent(String name, @Nullable ReadableMap properties) { - OneSignal.getUser() - .trackEvent( - name, - properties != null ? RNUtils.convertReadableMapToMap(properties) : new java.util.HashMap<>()); + OneSignal.getUser().trackEvent(name, properties != null ? properties.toHashMap() : new HashMap<>()); } } diff --git a/android/src/main/java/com/onesignal/rnonesignalandroid/RNUtils.java b/android/src/main/java/com/onesignal/rnonesignalandroid/RNUtils.java index e46e748d..29d1f722 100644 --- a/android/src/main/java/com/onesignal/rnonesignalandroid/RNUtils.java +++ b/android/src/main/java/com/onesignal/rnonesignalandroid/RNUtils.java @@ -268,64 +268,6 @@ public static HashMap convertReadableMapIntoStringMap(ReadableMa return stringMap; } - public static Map convertReadableMapToMap(ReadableMap readableMap) { - Map map = new HashMap<>(); - ReadableMapKeySetIterator iterator = readableMap.keySetIterator(); - - while (iterator.hasNextKey()) { - String key = iterator.nextKey(); - ReadableType type = readableMap.getType(key); - map.put(key, convertValue(type, readableMap, key)); - } - - return map; - } - - public static List convertReadableArrayToList(ReadableArray readableArray) { - List list = new ArrayList<>(); - - for (int i = 0; i < readableArray.size(); i++) { - ReadableType type = readableArray.getType(i); - list.add(convertValue(type, readableArray, i)); - } - - return list; - } - - private static Object convertValue(ReadableType type, ReadableMap map, String key) { - switch (type) { - case Boolean: - return map.getBoolean(key); - case Number: - return map.getDouble(key); - case String: - return map.getString(key); - case Map: - return convertReadableMapToMap(map.getMap(key)); - case Array: - return convertReadableArrayToList(map.getArray(key)); - default: - return null; - } - } - - private static Object convertValue(ReadableType type, ReadableArray array, int index) { - switch (type) { - case Boolean: - return array.getBoolean(index); - case Number: - return array.getDouble(index); - case String: - return array.getString(index); - case Map: - return convertReadableMapToMap(array.getMap(index)); - case Array: - return convertReadableArrayToList(array.getArray(index)); - default: - return null; - } - } - public static HashMap convertPermissionToMap(boolean granted) { HashMap hash = new HashMap<>();