From e22f9d4647933becc79eb74a3b3b97f3ba37b011 Mon Sep 17 00:00:00 2001 From: turtledreams <62231246+turtledreams@users.noreply.github.com> Date: Sun, 18 Oct 2026 18:09:43 +0900 Subject: [PATCH] Generate np from staging fee7c2a Regenerated with `npm run generate:no-push` from staging at fee7c2a (#510). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Lbwg6rsYZ3rHLE7T1JC9Qx --- CHANGELOG.md | 12 +++++++++--- Countly.d.ts | 6 +++--- Countly.js | 10 +++++++--- Validators.js | 4 ++-- __tests__/countly.bridge.test.js | 26 +++++++++++++++++++++++++- __tests__/validators.test.js | 4 ++-- 6 files changed, 48 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4c2d1d6..532b6ee8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,13 +3,18 @@ * ! Minor breaking change ! `Countly.remoteConfig.getValue` now returns string values as strings in Android. Before, "2.10" came back as 2.1. * ! Minor breaking change ! `Countly.logException(exception, false)` now records a fatal crash. * ! Minor breaking change ! An uncaught JS error that crashes a release build is now recorded once, as a fatal crash with its JS stack, instead of twice. -* ! Minor breaking change ! The Firebase BoM is updated from 28.4.2 to 32.8.1 (firebase-messaging 23.4.1) in Android. Apps that use an older Firebase BoM get the versions of 32.8.1 for their Firebase libraries, and the Kotlin standard library 1.7.10 joins the build. The minimum Android version stays API 21. +* ! Minor breaking change ! Android now uses Firebase BoM 32.8.1 (firebase-messaging 23.4.1) instead of 28.4.2. Apps on an older Firebase BoM get its versions for their Firebase libraries, and the Kotlin standard library 1.7.10 joins the build. The minimum Android version stays API 21. * ! Minor breaking change ! `Countly.test` helpers now work only in debug builds. * ! Minor breaking change ! The package now ships only the files the SDK needs at runtime; the example app, the tests and the Xcode project for manual linking are no longer part of it. It declares `react-native` as a peer dependency and needs React Native 0.62 or newer. -* ! Minor breaking change ! The pod now makes only the bridge header and the iOS SDK's public headers public; other headers can no longer be imported from it. The `NotificationService` subspec no longer depends on React, and `pod 'CountlyReactNative'` installs only the `Core` subspec. With React Native 0.71 and newer, the pod's iOS deployment target now follows React Native's minimum iOS version. +* ! Minor breaking change ! The pod now makes only the bridge header and the iOS SDK's public headers public; other headers can no longer be imported from it. +* ! Minor breaking change ! The `NotificationService` subspec no longer depends on React, and `pod 'CountlyReactNative'` installs only the `Core` subspec. +* ! Minor breaking change ! With React Native 0.71 and newer, the pod's iOS deployment target now follows React Native's minimum iOS version. * Added a `{ success, error }` result to `Countly.remoteConfig.update`, `updateForKeysOnly` and `updateExceptKeys`. -* Added `Countly.registerForNotificationOpened(listener)`. It is called when the user taps a push, in Android and iOS, with the push data and the tapped action index: 0 for the notification, 1 and up for its buttons. A tap that started the app is delivered once a listener is registered. In Android, call `setIntent(intent)` in your MainActivity's `onNewIntent` so a tap that brings back a stopped app is delivered too. +* Added `Countly.registerForNotificationOpened(listener)`, called when the user taps a push in Android and iOS: + * The listener gets the push data and the tapped action index: 0 for the notification, 1 and up for its buttons. + * A tap that started the app is delivered once a listener is registered. + * In Android, call `setIntent(intent)` in your MainActivity's `onNewIntent` so a tap that brings back a stopped app is delivered too. * Added support for objects as campaign data in `recordDirectAttribution` and as values in `recordIndirectAttribution`, in `Countly` and in `CountlyConfig`. * Added the names "DEVELOPMENT", "PRODUCTION" and "ADHOC" as accepted values of `CountlyConfig.setPushTokenType`. @@ -36,6 +41,7 @@ * A push tap that starts the app now gets the intent redirection check in Android, as warm taps already did. * The push token is no longer sent before the user allows notifications in iOS. * Registering `Countly.registerForNotification` again now replaces the earlier listener, so each push reaches JS once. + * `Countly.registerForNotification` no longer throws, or removes the current listener, when it is given something that is not a function. * `Countly.sendPushToken` now returns an error message when it is called before init. * Mitigated data issues: * Event sums keep their full precision in iOS. diff --git a/Countly.d.ts b/Countly.d.ts index 8207e065..15263b5b 100644 --- a/Countly.d.ts +++ b/Countly.d.ts @@ -503,7 +503,7 @@ declare module "countly-sdk-react-native-bridge-np" { * To react to taps the same way on both platforms, for example to open a deep link, use 'registerForNotificationOpened'. * Registering again replaces the previous listener. * @param {callback listener } theListener - * @return {EventSubscription} subscription + * @return {EventSubscription} subscription, which does nothing when the listener is not a function */ export function registerForNotification(theListener: (theNotification: string) => void): EventSubscription; @@ -517,9 +517,9 @@ declare module "countly-sdk-react-native-bridge-np" { * Call 'setIntent(intent)' in your MainActivity's 'onNewIntent' so that such a tap is kept and delivered too. * @param {NotificationOpenedListener} theListener - called with the notification payload and the index of the tapped action: * 0 for the notification itself, 1 and up for its buttons - * @return {EventSubscription | void} subscription, or void when the listener is not a function + * @return {EventSubscription} subscription, which does nothing when the listener is not a function */ - export function registerForNotificationOpened(theListener: NotificationOpenedListener): EventSubscription | void; + export function registerForNotificationOpened(theListener: NotificationOpenedListener): EventSubscription; /** * Set to true if you want to enable countly internal debugging logs diff --git a/Countly.js b/Countly.js index 8da69278..4c345e12 100644 --- a/Countly.js +++ b/Countly.js @@ -357,12 +357,16 @@ Countly.askForNotificationPermission = function (customSoundPath = "null") { * To react to taps the same way on both platforms, for example to open a deep link, use 'registerForNotificationOpened'. * Registering again replaces the previous listener. * @param {callback listener } theListener - * @return {NativeEventEmitter} event + * @return {object} subscription with a 'remove' function, which does nothing when the listener is not a function */ Countly.registerForNotification = function (theListener) { if (Validate.PushAvailable("registerForNotification")) { return createInactiveSubscription(); } + if (typeof theListener !== "function") { + L.e("registerForNotification, listener should be a function"); + return createInactiveSubscription(); + } L.d("registerForNotification, Registering for notification"); removeSubscription("pushNotificationSubscription"); const event = eventEmitter.addListener(pushNotificationCallbackName, theListener); @@ -380,7 +384,7 @@ Countly.registerForNotification = function (theListener) { * Call 'setIntent(intent)' in your MainActivity's 'onNewIntent' so that such a tap is kept and delivered too. * @param {function(object, number): void} theListener - called with the notification payload and the index of the tapped action: * 0 for the notification itself, 1 and up for its buttons - * @return {object | void} subscription with a 'remove' function, or void when the listener is not a function + * @return {object} subscription with a 'remove' function, which does nothing when the listener is not a function */ Countly.registerForNotificationOpened = function (theListener) { if (Validate.PushAvailable("registerForNotificationOpened")) { @@ -388,7 +392,7 @@ Countly.registerForNotificationOpened = function (theListener) { } if (typeof theListener !== "function") { L.e("registerForNotificationOpened, listener should be a function"); - return; + return createInactiveSubscription(); } L.d("registerForNotificationOpened, Registering for notification taps"); removeSubscription("pushNotificationOpenedSubscription"); diff --git a/Validators.js b/Validators.js index d921d714..e523b1da 100644 --- a/Validators.js +++ b/Validators.js @@ -1,8 +1,8 @@ import * as L from "./Logger.js"; import { PUSH_NOTIFICATIONS_AVAILABLE } from "./CountlyFlavor.js"; -const INT32_MIN = -2147483648; -const INT32_MAX = 2147483647; +const INT32_MIN = -(2 ** 31); +const INT32_MAX = 2 ** 31 - 1; /** * Checks that push notification calls are part of this package. The no-push package ignores them with a warning. diff --git a/__tests__/countly.bridge.test.js b/__tests__/countly.bridge.test.js index 497bfbb1..0ee84665 100644 --- a/__tests__/countly.bridge.test.js +++ b/__tests__/countly.bridge.test.js @@ -119,6 +119,9 @@ function loadCountlyBridge({ platform = 'ios', nativeOverrides = {}, pushAvailab } addListener(eventName, handler) { + if (typeof handler !== 'function') { + throw new TypeError('EventEmitter.addListener(...): 2nd argument must be a function.'); + } const listener = { eventName, handler, removed: false }; const subscription = { remove: jest.fn(() => { @@ -945,6 +948,28 @@ test('registerForNotification keeps one subscription so each push reaches JS onc expect(listeners.filter((listener) => listener.eventName === 'pushNotificationCallback' && !listener.removed)).toHaveLength(1); }); +test('a push listener that is not a function is refused, keeps the current listener and gets a subscription that does nothing', () => { + const receivedListener = jest.fn(); + const openedListener = jest.fn(); + const { Countly, nativeModule, emit } = loadCountlyBridge({ platform: 'android' }); + + const receivedSubscription = Countly.registerForNotification(receivedListener); + const openedSubscription = Countly.registerForNotificationOpened(openedListener); + const refusedReceived = Countly.registerForNotification('not a function'); + const refusedOpened = Countly.registerForNotificationOpened('not a function'); + emit('pushNotificationCallback', '{"c.i":"push-1"}'); + emit('pushNotificationOpenedCallback', JSON.stringify({ notification: { 'c.i': 'push-1' }, actionIndex: 0 })); + + expect(() => refusedReceived.remove()).not.toThrow(); + expect(() => refusedOpened.remove()).not.toThrow(); + expect(receivedSubscription.remove).not.toHaveBeenCalled(); + expect(openedSubscription.remove).not.toHaveBeenCalled(); + expect(receivedListener).toHaveBeenCalledWith('{"c.i":"push-1"}'); + expect(openedListener).toHaveBeenCalledWith({ 'c.i': 'push-1' }, 0); + expect(nativeModule.registerForNotification).toHaveBeenCalledTimes(1); + expect(nativeModule.registerForNotificationOpened).toHaveBeenCalledTimes(1); +}); + test('push calls and push options reach the native module only in the push package', async () => { const noPushMessage = "push notifications are not part of the no-push package, use 'countly-sdk-react-native-bridge' for push notifications"; @@ -1010,7 +1035,6 @@ test('registerForNotificationOpened passes the tapped notification and button in expect(firstListener).not.toHaveBeenCalled(); expect(openedListener).toHaveBeenCalledTimes(1); expect(openedListener).toHaveBeenCalledWith({ 'c.i': 'push-1', title: 'Sale' }, 2); - expect(Countly.registerForNotificationOpened('not a function')).toBeUndefined(); }); test('recordMetrics returns before init without calling native code', () => { diff --git a/__tests__/validators.test.js b/__tests__/validators.test.js index 45c2b6fe..73846072 100644 --- a/__tests__/validators.test.js +++ b/__tests__/validators.test.js @@ -40,11 +40,11 @@ test('UserDataValue composes null, type, and parse-int validation', () => { }); test('UserDataInteger accepts whole numbers in the 32-bit range and refuses decimals, overflow and text', () => { - [7, -3, 0, '7', '-12', ' 42 ', 2147483647, -2147483648].forEach((value) => { + [7, -3, 0, '7', '-12', ' 42 ', 2 ** 31 - 1, -(2 ** 31)].forEach((value) => { expect(Validators.UserDataInteger(value, 'value', 'incrementBy')).toBeNull(); }); - [2.5, '2.5', 0.5, 3e9, '3000000000', -2147483649, NaN, Infinity, '', ' ', '7abc', '0x10', true, {}, null, undefined].forEach((value) => { + [2.5, '2.5', 0.5, 3e9, '3000000000', -(2 ** 31) - 1, NaN, Infinity, '', ' ', '7abc', '0x10', true, {}, null, undefined].forEach((value) => { expect(Validators.UserDataInteger(value, 'value', 'incrementBy')).toEqual(expect.any(String)); });