From 70024a9dc9f282db0f768288615f4c881021680f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Ferr=C3=A3o?= Date: Thu, 1 Oct 2026 22:48:31 +0100 Subject: [PATCH 1/2] feat(embedded): add placement IDs to syncMessages (SDK-751) Expose optional placement scoping on embedded sync so RN matches native Android and iOS APIs; omit/null/empty still syncs all placements. --- CHANGELOG.md | 1 + .../reactnative/RNIterableAPIModuleImpl.java | 15 ++++++++--- .../newarch/java/com/RNIterableAPIModule.java | 4 +-- .../oldarch/java/com/RNIterableAPIModule.java | 4 +-- ios/RNIterableAPI/RNIterableAPI.mm | 8 +++--- ios/RNIterableAPI/ReactIterableAPI.swift | 11 +++++--- src/api/NativeRNIterableAPI.ts | 2 +- src/core/classes/IterableApi.test.ts | 27 ++++++++++++++++--- src/core/classes/IterableApi.ts | 8 +++--- .../classes/IterableEmbeddedManager.test.ts | 19 +++++++++++-- .../classes/IterableEmbeddedManager.ts | 8 ++++-- 11 files changed, 82 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3132e52c..727089101 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### Updates +- Added optional placement IDs to `Iterable.embeddedManager.syncMessages()` so apps can sync embedded messages for specific placements only (SDK-751). When omitted, null, or empty, behavior matches the previous sync-all call. - Added `Iterable.authManager.getAuthToken()` to read the JWT currently held by the native SDK (SDK-559). - iOS: `IterableAPI.authToken`. - Android: `IterableApi.getAuthToken()`. diff --git a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java index ae393863f..fc6c5cb15 100644 --- a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java +++ b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java @@ -762,9 +762,18 @@ public void onEmbeddedMessagingDisabled() { // --------------------------------------------------------------------------------------- // region Embedded messaging - public void syncEmbeddedMessages() { - IterableLogger.d(TAG, "syncEmbeddedMessages"); - IterableApi.getInstance().getEmbeddedManager().syncMessages(); + public void syncEmbeddedMessages(@Nullable ReadableArray placementIds) { + IterableLogger.d(TAG, "syncEmbeddedMessages for placements: " + placementIds); + + if (placementIds == null || placementIds.size() == 0) { + IterableApi.getInstance().getEmbeddedManager().syncMessages(); + } else { + Long[] ids = new Long[placementIds.size()]; + for (int i = 0; i < placementIds.size(); i++) { + ids[i] = (long) placementIds.getInt(i); + } + IterableApi.getInstance().getEmbeddedManager().syncMessages(ids); + } } public void startEmbeddedSession() { diff --git a/android/src/newarch/java/com/RNIterableAPIModule.java b/android/src/newarch/java/com/RNIterableAPIModule.java index 6e851fb54..ab07951ce 100644 --- a/android/src/newarch/java/com/RNIterableAPIModule.java +++ b/android/src/newarch/java/com/RNIterableAPIModule.java @@ -240,8 +240,8 @@ public void pauseAuthRetries(boolean pauseRetry) { } @Override - public void syncEmbeddedMessages() { - moduleImpl.syncEmbeddedMessages(); + public void syncEmbeddedMessages(@Nullable ReadableArray placementIds) { + moduleImpl.syncEmbeddedMessages(placementIds); } @Override diff --git a/android/src/oldarch/java/com/RNIterableAPIModule.java b/android/src/oldarch/java/com/RNIterableAPIModule.java index 9142967d5..4ee68aeb2 100644 --- a/android/src/oldarch/java/com/RNIterableAPIModule.java +++ b/android/src/oldarch/java/com/RNIterableAPIModule.java @@ -244,8 +244,8 @@ public void pauseAuthRetries(boolean pauseRetry) { } @ReactMethod - public void syncEmbeddedMessages() { - moduleImpl.syncEmbeddedMessages(); + public void syncEmbeddedMessages(@Nullable ReadableArray placementIds) { + moduleImpl.syncEmbeddedMessages(placementIds); } @ReactMethod diff --git a/ios/RNIterableAPI/RNIterableAPI.mm b/ios/RNIterableAPI/RNIterableAPI.mm index 825091661..ae7e47ba3 100644 --- a/ios/RNIterableAPI/RNIterableAPI.mm +++ b/ios/RNIterableAPI/RNIterableAPI.mm @@ -304,8 +304,8 @@ - (void)endEmbeddedSession { [_swiftAPI endEmbeddedSession]; } -- (void)syncEmbeddedMessages { - [_swiftAPI syncEmbeddedMessages]; +- (void)syncEmbeddedMessages:(NSArray *_Nullable)placementIds { + [_swiftAPI syncEmbeddedMessages:placementIds]; } - (void)getEmbeddedMessages:(NSArray *_Nullable)placementIds @@ -597,8 +597,8 @@ - (void)wakeApp { [_swiftAPI endEmbeddedSession]; } -RCT_EXPORT_METHOD(syncEmbeddedMessages) { - [_swiftAPI syncEmbeddedMessages]; +RCT_EXPORT_METHOD(syncEmbeddedMessages : (NSArray *_Nullable)placementIds) { + [_swiftAPI syncEmbeddedMessages:placementIds]; } RCT_EXPORT_METHOD(getEmbeddedMessages : (NSArray *_Nullable)placementIds resolve : (RCTPromiseResolveBlock)resolve reject : (RCTPromiseRejectBlock)reject) { diff --git a/ios/RNIterableAPI/ReactIterableAPI.swift b/ios/RNIterableAPI/ReactIterableAPI.swift index 1b78958e6..a72c7b894 100644 --- a/ios/RNIterableAPI/ReactIterableAPI.swift +++ b/ios/RNIterableAPI/ReactIterableAPI.swift @@ -535,10 +535,15 @@ import React EmbeddedSessionManager.shared.endSession() } - @objc(syncEmbeddedMessages) - public func syncEmbeddedMessages() { + @objc(syncEmbeddedMessages:) + public func syncEmbeddedMessages(placementIds: [NSNumber]?) { ITBInfo() - IterableAPI.embeddedManager.syncMessages { } + if let placementIds = placementIds, !placementIds.isEmpty { + let ids = placementIds.map { $0.intValue } + IterableAPI.embeddedManager.syncMessages(placementIds: ids) { } + } else { + IterableAPI.embeddedManager.syncMessages { } + } } @objc(getEmbeddedMessages:resolver:rejecter:) diff --git a/src/api/NativeRNIterableAPI.ts b/src/api/NativeRNIterableAPI.ts index d3ab874b0..8ef525f2a 100644 --- a/src/api/NativeRNIterableAPI.ts +++ b/src/api/NativeRNIterableAPI.ts @@ -184,7 +184,7 @@ export interface Spec extends TurboModule { pauseAuthRetries(pauseRetry: boolean): void; // Embedded Messaging - syncEmbeddedMessages(): void; + syncEmbeddedMessages(placementIds: Array | null): void; startEmbeddedSession(): void; endEmbeddedSession(): void; startEmbeddedImpression(messageId: string, placementId: number): void; diff --git a/src/core/classes/IterableApi.test.ts b/src/core/classes/IterableApi.test.ts index 77b1b68b5..64b7deeaa 100644 --- a/src/core/classes/IterableApi.test.ts +++ b/src/core/classes/IterableApi.test.ts @@ -1299,13 +1299,34 @@ describe('IterableApi', () => { }); describe('syncEmbeddedMessages', () => { - it('should call RNIterableAPI.syncEmbeddedMessages', () => { + it('should call RNIterableAPI.syncEmbeddedMessages with null when omitted', () => { // GIVEN no parameters // WHEN syncEmbeddedMessages is called IterableApi.syncEmbeddedMessages(); - // THEN RNIterableAPI.syncEmbeddedMessages is called - expect(MockRNIterableAPI.syncEmbeddedMessages).toBeCalled(); + // THEN RNIterableAPI.syncEmbeddedMessages is called with null (sync all) + expect(MockRNIterableAPI.syncEmbeddedMessages).toBeCalledWith(null); + }); + + it('should call RNIterableAPI.syncEmbeddedMessages with placement IDs', () => { + // GIVEN placement IDs + const placementIds = [1, 2]; + + // WHEN syncEmbeddedMessages is called + IterableApi.syncEmbeddedMessages(placementIds); + + // THEN RNIterableAPI.syncEmbeddedMessages is called with placement IDs + expect(MockRNIterableAPI.syncEmbeddedMessages).toBeCalledWith( + placementIds + ); + }); + + it('should call RNIterableAPI.syncEmbeddedMessages with empty array for sync all', () => { + // WHEN syncEmbeddedMessages is called with an empty array + IterableApi.syncEmbeddedMessages([]); + + // THEN the empty array is forwarded (native syncs all placements) + expect(MockRNIterableAPI.syncEmbeddedMessages).toBeCalledWith([]); }); }); diff --git a/src/core/classes/IterableApi.ts b/src/core/classes/IterableApi.ts index 9e01b6793..c0793b7d7 100644 --- a/src/core/classes/IterableApi.ts +++ b/src/core/classes/IterableApi.ts @@ -551,9 +551,11 @@ export class IterableApi { /** * Syncs embedded local cache with the server. */ - static syncEmbeddedMessages() { - IterableLogger.log('syncEmbeddedMessages'); - return RNIterableAPI.syncEmbeddedMessages(); + static syncEmbeddedMessages(placementIds?: number[] | null) { + IterableLogger.log('syncEmbeddedMessages: ', placementIds); + return RNIterableAPI.syncEmbeddedMessages( + placementIds === undefined ? null : placementIds + ); } /** diff --git a/src/embedded/classes/IterableEmbeddedManager.test.ts b/src/embedded/classes/IterableEmbeddedManager.test.ts index c460572f0..4da487a4a 100644 --- a/src/embedded/classes/IterableEmbeddedManager.test.ts +++ b/src/embedded/classes/IterableEmbeddedManager.test.ts @@ -97,16 +97,31 @@ describe('IterableEmbeddedManager', () => { }); describe('syncMessages', () => { - it('should call IterableApi.syncEmbeddedMessages', async () => { + it('should call IterableApi.syncEmbeddedMessages with null when omitted', async () => { // WHEN syncMessages is called const result = await embeddedManager.syncMessages(); - // THEN IterableApi.syncEmbeddedMessages is called + // THEN IterableApi.syncEmbeddedMessages is called with null expect(MockRNIterableAPI.syncEmbeddedMessages).toHaveBeenCalledTimes(1); + expect(MockRNIterableAPI.syncEmbeddedMessages).toHaveBeenCalledWith(null); // AND the result is returned expect(result).toBeUndefined(); }); + + it('should call IterableApi.syncEmbeddedMessages with placement IDs', () => { + // GIVEN placement IDs + const placementIds = [1, 2]; + + // WHEN syncMessages is called + embeddedManager.syncMessages(placementIds); + + // THEN IterableApi.syncEmbeddedMessages is called with placement IDs + expect(MockRNIterableAPI.syncEmbeddedMessages).toHaveBeenCalledTimes(1); + expect(MockRNIterableAPI.syncEmbeddedMessages).toHaveBeenCalledWith( + placementIds + ); + }); }); describe('getMessages', () => { diff --git a/src/embedded/classes/IterableEmbeddedManager.ts b/src/embedded/classes/IterableEmbeddedManager.ts index 8d53af49c..1359f46b3 100644 --- a/src/embedded/classes/IterableEmbeddedManager.ts +++ b/src/embedded/classes/IterableEmbeddedManager.ts @@ -71,13 +71,17 @@ export class IterableEmbeddedManager { * * However, do not poll for new embedded messages at a regular interval. * + * @param placementIds - Optional placement IDs to sync. When omitted, null, or + * empty, syncs all placements. + * * @example * ```typescript * IterableEmbeddedManager.syncMessages(); + * IterableEmbeddedManager.syncMessages([1, 2, 3]); * ``` */ - syncMessages() { - return IterableApi.syncEmbeddedMessages(); + syncMessages(placementIds?: number[] | null) { + return IterableApi.syncEmbeddedMessages(placementIds); } /** From f840cb0cab8f2d72f3b9c7ff6ebfb758fc25eff9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Ferr=C3=A3o?= Date: Thu, 1 Oct 2026 23:10:17 +0100 Subject: [PATCH 2/2] test(embedded): cover null and empty syncMessages paths (SDK-751) Lock AC2 sync-all inputs at IterableApi and IterableEmbeddedManager. --- src/core/classes/IterableApi.test.ts | 8 ++++++++ .../classes/IterableEmbeddedManager.test.ts | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/src/core/classes/IterableApi.test.ts b/src/core/classes/IterableApi.test.ts index 64b7deeaa..d77083712 100644 --- a/src/core/classes/IterableApi.test.ts +++ b/src/core/classes/IterableApi.test.ts @@ -1308,6 +1308,14 @@ describe('IterableApi', () => { expect(MockRNIterableAPI.syncEmbeddedMessages).toBeCalledWith(null); }); + it('should call RNIterableAPI.syncEmbeddedMessages with null when passed null', () => { + // WHEN syncEmbeddedMessages is called with null + IterableApi.syncEmbeddedMessages(null); + + // THEN RNIterableAPI.syncEmbeddedMessages is called with null (sync all) + expect(MockRNIterableAPI.syncEmbeddedMessages).toBeCalledWith(null); + }); + it('should call RNIterableAPI.syncEmbeddedMessages with placement IDs', () => { // GIVEN placement IDs const placementIds = [1, 2]; diff --git a/src/embedded/classes/IterableEmbeddedManager.test.ts b/src/embedded/classes/IterableEmbeddedManager.test.ts index 4da487a4a..80a910237 100644 --- a/src/embedded/classes/IterableEmbeddedManager.test.ts +++ b/src/embedded/classes/IterableEmbeddedManager.test.ts @@ -122,6 +122,24 @@ describe('IterableEmbeddedManager', () => { placementIds ); }); + + it('should call IterableApi.syncEmbeddedMessages with null placement IDs', () => { + // WHEN syncMessages is called with null + embeddedManager.syncMessages(null); + + // THEN IterableApi.syncEmbeddedMessages is called with null + expect(MockRNIterableAPI.syncEmbeddedMessages).toHaveBeenCalledTimes(1); + expect(MockRNIterableAPI.syncEmbeddedMessages).toHaveBeenCalledWith(null); + }); + + it('should call IterableApi.syncEmbeddedMessages with empty placement IDs', () => { + // WHEN syncMessages is called with an empty array + embeddedManager.syncMessages([]); + + // THEN the empty array is forwarded (native syncs all placements) + expect(MockRNIterableAPI.syncEmbeddedMessages).toHaveBeenCalledTimes(1); + expect(MockRNIterableAPI.syncEmbeddedMessages).toHaveBeenCalledWith([]); + }); }); describe('getMessages', () => {