From 1ecb9397e8703f1fd1496db8567186934d3cc44a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Ferr=C3=A3o?= Date: Tue, 15 Sep 2026 09:45:52 +0100 Subject: [PATCH 1/2] feat(SDK-550): expose disableDeviceForAllUsers on the RN SDK Bridge Iterable.disableDeviceForAllUsers() so iOS can unregister this device's push token for every associated user. Android stays a logged no-op because the native SDK has no public equivalent. Co-authored-by: Cursor --- CHANGELOG.md | 8 ++++++++ .../reactnative/RNIterableAPIModuleImpl.java | 4 ++++ .../newarch/java/com/RNIterableAPIModule.java | 5 +++++ .../oldarch/java/com/RNIterableAPIModule.java | 5 +++++ ios/RNIterableAPI/RNIterableAPI.mm | 8 ++++++++ ios/RNIterableAPI/ReactIterableAPI.swift | 6 ++++++ src/__mocks__/MockRNIterableAPI.ts | 2 ++ src/api/NativeRNIterableAPI.ts | 1 + src/core/classes/Iterable.test.ts | 10 ++++++++++ src/core/classes/Iterable.ts | 20 +++++++++++++++++++ src/core/classes/IterableApi.test.ts | 11 ++++++++++ src/core/classes/IterableApi.ts | 11 ++++++++++ 12 files changed, 91 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f0b2f4c33..812560427 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## Unreleased + +### Updates + +- Added `Iterable.disableDeviceForAllUsers()` to unregister this device's push token from every user associated with the device (SDK-550). + - iOS: forwards to native `IterableAPI.disableDeviceForAllUsers()`. + - Android: graceful no-op that logs a warning; use `disableDeviceForCurrentUser()` to disable push for the current user. There is no public native "all users" equivalent. + ## 3.1.0 ### Fixes diff --git a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java index b25432d4b..ea7bae18b 100644 --- a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java +++ b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java @@ -373,6 +373,10 @@ public void disableDeviceForCurrentUser() { IterableApi.getInstance().disablePush(); } + public void disableDeviceForAllUsers() { + IterableLogger.w(TAG, "disableDeviceForAllUsers is not supported on Android; use disableDeviceForCurrentUser. There is no public native equivalent."); + } + public void registerDeviceToken(String token) { IterableLogger.v(TAG, "registerDeviceToken"); IterableApi.getInstance().registerDeviceToken(token); diff --git a/android/src/newarch/java/com/RNIterableAPIModule.java b/android/src/newarch/java/com/RNIterableAPIModule.java index fb71142b8..b25c39362 100644 --- a/android/src/newarch/java/com/RNIterableAPIModule.java +++ b/android/src/newarch/java/com/RNIterableAPIModule.java @@ -137,6 +137,11 @@ public void disableDeviceForCurrentUser() { moduleImpl.disableDeviceForCurrentUser(); } + @Override + public void disableDeviceForAllUsers() { + moduleImpl.disableDeviceForAllUsers(); + } + @Override public void registerDeviceToken(String token) { moduleImpl.registerDeviceToken(token); diff --git a/android/src/oldarch/java/com/RNIterableAPIModule.java b/android/src/oldarch/java/com/RNIterableAPIModule.java index c2f9b3161..cb290f3e4 100644 --- a/android/src/oldarch/java/com/RNIterableAPIModule.java +++ b/android/src/oldarch/java/com/RNIterableAPIModule.java @@ -138,6 +138,11 @@ public void disableDeviceForCurrentUser() { moduleImpl.disableDeviceForCurrentUser(); } + @ReactMethod + public void disableDeviceForAllUsers() { + moduleImpl.disableDeviceForAllUsers(); + } + @ReactMethod public void registerDeviceToken(String token) { moduleImpl.registerDeviceToken(token); diff --git a/ios/RNIterableAPI/RNIterableAPI.mm b/ios/RNIterableAPI/RNIterableAPI.mm index 965b2d81e..a766a6814 100644 --- a/ios/RNIterableAPI/RNIterableAPI.mm +++ b/ios/RNIterableAPI/RNIterableAPI.mm @@ -230,6 +230,10 @@ - (void)disableDeviceForCurrentUser { [_swiftAPI disableDeviceForCurrentUser]; } +- (void)disableDeviceForAllUsers { + [_swiftAPI disableDeviceForAllUsers]; +} + - (void)registerDeviceToken:(NSString *)token { [_swiftAPI registerDeviceToken:token]; } @@ -516,6 +520,10 @@ - (void)wakeApp { [_swiftAPI disableDeviceForCurrentUser]; } +RCT_EXPORT_METHOD(disableDeviceForAllUsers) { + [_swiftAPI disableDeviceForAllUsers]; +} + RCT_EXPORT_METHOD(registerDeviceToken : (NSString *)token) { [_swiftAPI registerDeviceToken:token]; } diff --git a/ios/RNIterableAPI/ReactIterableAPI.swift b/ios/RNIterableAPI/ReactIterableAPI.swift index a216a0e69..26def081d 100644 --- a/ios/RNIterableAPI/ReactIterableAPI.swift +++ b/ios/RNIterableAPI/ReactIterableAPI.swift @@ -144,6 +144,12 @@ import React IterableAPI.disableDeviceForCurrentUser() } + @objc(disableDeviceForAllUsers) + public func disableDeviceForAllUsers() { + ITBInfo() + IterableAPI.disableDeviceForAllUsers() + } + @objc(registerDeviceToken:) public func registerDeviceToken(token: String) { ITBInfo() diff --git a/src/__mocks__/MockRNIterableAPI.ts b/src/__mocks__/MockRNIterableAPI.ts index 60013bdbb..68355d2a1 100644 --- a/src/__mocks__/MockRNIterableAPI.ts +++ b/src/__mocks__/MockRNIterableAPI.ts @@ -34,6 +34,8 @@ export class MockRNIterableAPI { static disableDeviceForCurrentUser = jest.fn(); + static disableDeviceForAllUsers = jest.fn(); + static registerDeviceToken = jest.fn((token: string): void => { MockRNIterableAPI.token = token; }); diff --git a/src/api/NativeRNIterableAPI.ts b/src/api/NativeRNIterableAPI.ts index 7e96bc649..c902dc598 100644 --- a/src/api/NativeRNIterableAPI.ts +++ b/src/api/NativeRNIterableAPI.ts @@ -147,6 +147,7 @@ export interface Spec extends TurboModule { // Device management disableDeviceForCurrentUser(): void; + disableDeviceForAllUsers(): void; registerDeviceToken(token: string): void; getLastPushPayload(): Promise<{ [key: string]: string | number | boolean; diff --git a/src/core/classes/Iterable.test.ts b/src/core/classes/Iterable.test.ts index baccc3e41..6da4b9ca5 100644 --- a/src/core/classes/Iterable.test.ts +++ b/src/core/classes/Iterable.test.ts @@ -143,6 +143,16 @@ describe('Iterable', () => { }); }); + describe('disableDeviceForAllUsers', () => { + it('should disable the device for all users', () => { + // GIVEN no parameters + // WHEN Iterable.disableDeviceForAllUsers is called + Iterable.disableDeviceForAllUsers(); + // THEN corresponding method is called on RNIterableAPI + expect(MockRNIterableAPI.disableDeviceForAllUsers).toBeCalled(); + }); + }); + describe('registerDeviceToken', () => { it('should register the device token for the current user', () => { // GIVEN a device token diff --git a/src/core/classes/Iterable.ts b/src/core/classes/Iterable.ts index aaaa84073..ffd93295d 100644 --- a/src/core/classes/Iterable.ts +++ b/src/core/classes/Iterable.ts @@ -345,6 +345,26 @@ export class Iterable { IterableApi.disableDeviceForCurrentUser(); } + /** + * Disable this device's push token for every user associated with the device, + * not only the currently signed-in user. + * + * On iOS this forwards to native `IterableAPI.disableDeviceForAllUsers()`. + * On Android this is a no-op that logs a warning: the native Android SDK has + * no public "all users" equivalent. Use {@link disableDeviceForCurrentUser} to + * disable push for the current user on both platforms. + * + * Fire-and-forget: native success or failure is not surfaced to JS. + * + * @example + * ```typescript + * Iterable.disableDeviceForAllUsers(); + * ``` + */ + static disableDeviceForAllUsers() { + IterableApi.disableDeviceForAllUsers(); + } + /** * Register the device's token for the current user, re-enabling push notifications. * diff --git a/src/core/classes/IterableApi.test.ts b/src/core/classes/IterableApi.test.ts index d639d79ed..93cf80f6c 100644 --- a/src/core/classes/IterableApi.test.ts +++ b/src/core/classes/IterableApi.test.ts @@ -282,6 +282,17 @@ describe('IterableApi', () => { }); }); + describe('disableDeviceForAllUsers', () => { + it('should call RNIterableAPI.disableDeviceForAllUsers', () => { + // GIVEN no parameters + // WHEN disableDeviceForAllUsers is called + IterableApi.disableDeviceForAllUsers(); + + // THEN RNIterableAPI.disableDeviceForAllUsers is called + expect(MockRNIterableAPI.disableDeviceForAllUsers).toBeCalled(); + }); + }); + describe('registerDeviceToken', () => { it('should call RNIterableAPI.registerDeviceToken with the token', () => { // GIVEN a device token diff --git a/src/core/classes/IterableApi.ts b/src/core/classes/IterableApi.ts index 2fad862a7..aeb7def6b 100644 --- a/src/core/classes/IterableApi.ts +++ b/src/core/classes/IterableApi.ts @@ -137,6 +137,17 @@ export class IterableApi { return RNIterableAPI.disableDeviceForCurrentUser(); } + /** + * Disable this device's push token for every user associated with the device. + * + * iOS: forwards to native `IterableAPI.disableDeviceForAllUsers()`. + * Android: logged no-op — there is no public native "all users" equivalent. + */ + static disableDeviceForAllUsers() { + IterableLogger.log('disableDeviceForAllUsers'); + return RNIterableAPI.disableDeviceForAllUsers(); + } + /** * Register the device token for the current user, re-enabling push notifications. * From 8c0841bfbe9b3e41264b239893b2a61866911693 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Ferr=C3=A3o?= Date: Mon, 21 Sep 2026 10:33:14 +0100 Subject: [PATCH 2/2] fix(SDK-550): log Android disableDeviceForAllUsers no-op in Metro Keep the native bridge no-op, but emit the same unsupported warning from JS so the gap is visible without logcat when logReactNativeSdkCalls is on. Co-authored-by: Cursor --- src/core/classes/Iterable.ts | 7 +++-- src/core/classes/IterableApi.test.ts | 43 ++++++++++++++++++++++++++++ src/core/classes/IterableApi.ts | 10 ++++++- 3 files changed, 56 insertions(+), 4 deletions(-) diff --git a/src/core/classes/Iterable.ts b/src/core/classes/Iterable.ts index ffd93295d..e7119fcbd 100644 --- a/src/core/classes/Iterable.ts +++ b/src/core/classes/Iterable.ts @@ -350,9 +350,10 @@ export class Iterable { * not only the currently signed-in user. * * On iOS this forwards to native `IterableAPI.disableDeviceForAllUsers()`. - * On Android this is a no-op that logs a warning: the native Android SDK has - * no public "all users" equivalent. Use {@link disableDeviceForCurrentUser} to - * disable push for the current user on both platforms. + * On Android this is a no-op that logs a warning in Metro and logcat: the + * native Android SDK has no public "all users" equivalent. Use + * {@link disableDeviceForCurrentUser} to disable push for the current user + * on both platforms. The native Android method is still invoked. * * Fire-and-forget: native success or failure is not surfaced to JS. * diff --git a/src/core/classes/IterableApi.test.ts b/src/core/classes/IterableApi.test.ts index 93cf80f6c..b19954f73 100644 --- a/src/core/classes/IterableApi.test.ts +++ b/src/core/classes/IterableApi.test.ts @@ -3,6 +3,7 @@ import { Platform } from 'react-native'; import { MockRNIterableAPI } from '../../__mocks__/MockRNIterableAPI'; import { IterableApi } from './IterableApi'; import { IterableConfig } from './IterableConfig'; +import { IterableLogger } from './IterableLogger'; import { IterableAttributionInfo } from './IterableAttributionInfo'; import { IterableCommerceItem } from './IterableCommerceItem'; import { IterableInAppMessage } from '../../inApp/classes/IterableInAppMessage'; @@ -291,6 +292,48 @@ describe('IterableApi', () => { // THEN RNIterableAPI.disableDeviceForAllUsers is called expect(MockRNIterableAPI.disableDeviceForAllUsers).toBeCalled(); }); + + it('should log an unsupported warning on Android and still call native', () => { + const originalPlatform = Platform.OS; + Object.defineProperty(Platform, 'OS', { + value: 'android', + writable: true, + }); + const logSpy = jest.spyOn(IterableLogger, 'log'); + + IterableApi.disableDeviceForAllUsers(); + + expect(logSpy).toHaveBeenCalledWith( + 'disableDeviceForAllUsers is not supported on Android; use disableDeviceForCurrentUser. There is no public native equivalent.' + ); + expect(MockRNIterableAPI.disableDeviceForAllUsers).toBeCalled(); + + logSpy.mockRestore(); + Object.defineProperty(Platform, 'OS', { + value: originalPlatform, + writable: true, + }); + }); + + it('should log the method name on iOS and still call native', () => { + const originalPlatform = Platform.OS; + Object.defineProperty(Platform, 'OS', { + value: 'ios', + writable: true, + }); + const logSpy = jest.spyOn(IterableLogger, 'log'); + + IterableApi.disableDeviceForAllUsers(); + + expect(logSpy).toHaveBeenCalledWith('disableDeviceForAllUsers'); + expect(MockRNIterableAPI.disableDeviceForAllUsers).toBeCalled(); + + logSpy.mockRestore(); + Object.defineProperty(Platform, 'OS', { + value: originalPlatform, + writable: true, + }); + }); }); describe('registerDeviceToken', () => { diff --git a/src/core/classes/IterableApi.ts b/src/core/classes/IterableApi.ts index aeb7def6b..4d0650102 100644 --- a/src/core/classes/IterableApi.ts +++ b/src/core/classes/IterableApi.ts @@ -142,9 +142,17 @@ export class IterableApi { * * iOS: forwards to native `IterableAPI.disableDeviceForAllUsers()`. * Android: logged no-op — there is no public native "all users" equivalent. + * The warning is logged in JS (Metro) as well as native (logcat); the + * native method is still invoked. */ static disableDeviceForAllUsers() { - IterableLogger.log('disableDeviceForAllUsers'); + if (Platform.OS === 'android') { + IterableLogger.log( + 'disableDeviceForAllUsers is not supported on Android; use disableDeviceForCurrentUser. There is no public native equivalent.' + ); + } else { + IterableLogger.log('disableDeviceForAllUsers'); + } return RNIterableAPI.disableDeviceForAllUsers(); }