diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e3897a76..de8835520 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ ### 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. + - **Android:** Scoped sync (`syncMessages([id, …])`) matches the pinned iterableapi behavior and may remove cached messages for placements not included in the sync response (SDK-798). iOS preserves unrequested placements. Prefer `syncMessages()` without placement IDs on Android until the native fix ships and the Android SDK pin is bumped. - Added `IterableInAppMessage.jsonOnly` so `inAppHandler` and in-app message fetches can distinguish JSON-only messages from HTML in-apps (SDK-750). - Added `IterableConfig.onEmbeddedMessagingSyncSucceeded` and `IterableConfig.onEmbeddedMessagingSyncFailed` so apps can observe embedded diff --git a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java index 33dd90069..9c1fddd39 100644 --- a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java +++ b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java @@ -796,9 +796,19 @@ public void onEmbeddedMessagingSyncFailed(@Nullable String reason) { // --------------------------------------------------------------------------------------- // 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 { + IterableLogger.w(TAG, "Scoped syncEmbeddedMessages may remove cached messages for placements not in the sync response (SDK-798). Prefer syncMessages() without placement IDs on Android until iterableapi is fixed."); + 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 f7e19daa4..05009ad37 100644 --- a/ios/RNIterableAPI/ReactIterableAPI.swift +++ b/ios/RNIterableAPI/ReactIterableAPI.swift @@ -537,10 +537,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..d77083712 100644 --- a/src/core/classes/IterableApi.test.ts +++ b/src/core/classes/IterableApi.test.ts @@ -1299,13 +1299,42 @@ 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 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]; + + // 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..58066eb5d 100644 --- a/src/core/classes/IterableApi.ts +++ b/src/core/classes/IterableApi.ts @@ -550,10 +550,16 @@ export class IterableApi { /** * Syncs embedded local cache with the server. + * + * @param placementIds - When provided, limits the sync to those placements. + * See {@link IterableEmbeddedManager.syncMessages} for Android scoped-sync + * caveats (SDK-798). */ - 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..80a910237 100644 --- a/src/embedded/classes/IterableEmbeddedManager.test.ts +++ b/src/embedded/classes/IterableEmbeddedManager.test.ts @@ -97,16 +97,49 @@ 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 + ); + }); + + 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', () => { diff --git a/src/embedded/classes/IterableEmbeddedManager.ts b/src/embedded/classes/IterableEmbeddedManager.ts index 8d53af49c..0575c6311 100644 --- a/src/embedded/classes/IterableEmbeddedManager.ts +++ b/src/embedded/classes/IterableEmbeddedManager.ts @@ -71,13 +71,23 @@ 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. + * + * @remarks On Android, scoped sync may clear cached messages for placements + * that are not in the sync response (iterableapi SDK-798). iOS merges + * unrequested placements from the existing cache. Until Android iterableapi + * is fixed and this SDK bumps its Android pin, prefer calling `syncMessages()` + * without placement IDs on Android when multiple placements are in use. + * * @example * ```typescript * IterableEmbeddedManager.syncMessages(); + * IterableEmbeddedManager.syncMessages([1, 2, 3]); * ``` */ - syncMessages() { - return IterableApi.syncEmbeddedMessages(); + syncMessages(placementIds?: number[] | null) { + return IterableApi.syncEmbeddedMessages(placementIds); } /**