From 523be9b55c5ac4fd5c75834172bfb1bc2a5a38c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Ferr=C3=A3o?= Date: Sat, 10 Oct 2026 11:53:50 +0100 Subject: [PATCH 1/2] feat(SDK-556): expose IterableConfig.identityResolution Add IterableIdentityResolution type and wire defaults through iOS/Android Serialization. --- CHANGELOG.md | 6 +++ .../iterable/reactnative/Serialization.java | 12 +++++ ios/RNIterableAPI/Serialization.swift | 10 ++++ src/core/classes/Iterable.test.ts | 51 +++++++++++++++++++ src/core/classes/IterableConfig.ts | 19 +++++++ src/core/types/IterableIdentityResolution.ts | 20 ++++++++ src/core/types/index.ts | 1 + src/index.tsx | 1 + 8 files changed, 120 insertions(+) create mode 100644 src/core/types/IterableIdentityResolution.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 771a9fca4..ce27548ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## Unreleased + +### Updates + +- Added `IterableConfig.identityResolution` and the `IterableIdentityResolution` type so RN apps can configure default visitor replay and unknown-to-known merge behavior (SDK-556). + ## 3.2.1 ### Fixes diff --git a/android/src/main/java/com/iterable/reactnative/Serialization.java b/android/src/main/java/com/iterable/reactnative/Serialization.java index 97aa52f49..8306e5feb 100644 --- a/android/src/main/java/com/iterable/reactnative/Serialization.java +++ b/android/src/main/java/com/iterable/reactnative/Serialization.java @@ -22,6 +22,7 @@ import com.iterable.iterableapi.IterableInAppHandler; import com.iterable.iterableapi.IterableInAppLocation; import com.iterable.iterableapi.IterableInAppMessage; +import com.iterable.iterableapi.IterableIdentityResolution; import com.iterable.iterableapi.IterableInboxSession; import com.iterable.iterableapi.IterableLogger; import com.iterable.iterableapi.RetryPolicy; @@ -262,6 +263,17 @@ static IterableConfig.Builder getConfigFromReadableMap(ReadableMap iterableConte configBuilder.setAuthRetryPolicy(new RetryPolicy(maxRetry, retryInterval, retryPolicyType)); } + if (iterableContextJSON.has("identityResolution")) { + JSONObject identityResolutionJson = + iterableContextJSON.getJSONObject("identityResolution"); + boolean replayOnVisitorToKnown = + identityResolutionJson.optBoolean("replayOnVisitorToKnown", true); + boolean mergeOnUnknownToKnown = + identityResolutionJson.optBoolean("mergeOnUnknownToKnown", true); + configBuilder.setIdentityResolution( + new IterableIdentityResolution(replayOnVisitorToKnown, mergeOnUnknownToKnown)); + } + return configBuilder; } catch (JSONException e) { e.printStackTrace(); diff --git a/ios/RNIterableAPI/Serialization.swift b/ios/RNIterableAPI/Serialization.swift index fee4c49b6..0e53aa23a 100644 --- a/ios/RNIterableAPI/Serialization.swift +++ b/ios/RNIterableAPI/Serialization.swift @@ -110,6 +110,16 @@ extension IterableConfig { } } + if let identityResolutionDict = dict["identityResolution"] as? [AnyHashable: Any] { + let replayOnVisitorToKnown = + identityResolutionDict["replayOnVisitorToKnown"] as? Bool ?? true + let mergeOnUnknownToKnown = + identityResolutionDict["mergeOnUnknownToKnown"] as? Bool ?? true + config.identityResolution = IterableIdentityResolution( + replayOnVisitorToKnown: replayOnVisitorToKnown, + mergeOnUnknownToKnown: mergeOnUnknownToKnown) + } + return config } diff --git a/src/core/classes/Iterable.test.ts b/src/core/classes/Iterable.test.ts index baccc3e41..51001e1f7 100644 --- a/src/core/classes/Iterable.test.ts +++ b/src/core/classes/Iterable.test.ts @@ -337,6 +337,10 @@ describe('Iterable', () => { expect(config.useInMemoryStorageForInApps).toBe(false); expect(config.androidWakeDelayMs).toBe(1000); expect(config.authCallbackTimeoutMs).toBe(6000); + expect(config.identityResolution).toEqual({ + replayOnVisitorToKnown: true, + mergeOnUnknownToKnown: true, + }); const configDict = config.toDict(); expect(configDict.allowedProtocols).toEqual([]); expect(configDict.androidSdkUseInMemoryStorageForInApps).toBe(false); @@ -355,6 +359,10 @@ describe('Iterable', () => { expect(configDict.useInMemoryStorageForInApps).toBe(false); expect(configDict.androidWakeDelayMs).toBe(1000); expect(configDict.authCallbackTimeoutMs).toBe(6000); + expect(configDict.identityResolution).toEqual({ + replayOnVisitorToKnown: true, + mergeOnUnknownToKnown: true, + }); }); it('should allow overriding androidWakeDelayMs and authCallbackTimeoutMs', () => { @@ -365,6 +373,49 @@ describe('Iterable', () => { expect(configDict.androidWakeDelayMs).toBe(1500); expect(configDict.authCallbackTimeoutMs).toBe(2500); }); + + it('should include identityResolution in toDict() with overridden values', () => { + const config = new IterableConfig(); + config.identityResolution = { + replayOnVisitorToKnown: false, + mergeOnUnknownToKnown: false, + }; + const configDict = config.toDict(); + expect(configDict.identityResolution).toEqual({ + replayOnVisitorToKnown: false, + mergeOnUnknownToKnown: false, + }); + }); + + it('should default missing identityResolution keys to true in toDict()', () => { + const config = new IterableConfig(); + config.identityResolution = { mergeOnUnknownToKnown: false }; + const configDict = config.toDict(); + expect(configDict.identityResolution).toEqual({ + replayOnVisitorToKnown: true, + mergeOnUnknownToKnown: false, + }); + }); + + it('should default replayOnVisitorToKnown to true when only merge is set', () => { + const config = new IterableConfig(); + config.identityResolution = { replayOnVisitorToKnown: false }; + const configDict = config.toDict(); + expect(configDict.identityResolution).toEqual({ + replayOnVisitorToKnown: false, + mergeOnUnknownToKnown: true, + }); + }); + + it('should default both identityResolution keys to true for an empty object', () => { + const config = new IterableConfig(); + config.identityResolution = {}; + const configDict = config.toDict(); + expect(configDict.identityResolution).toEqual({ + replayOnVisitorToKnown: true, + mergeOnUnknownToKnown: true, + }); + }); }); describe('urlHandler', () => { diff --git a/src/core/classes/IterableConfig.ts b/src/core/classes/IterableConfig.ts index b4de2fb41..0085cf5b5 100644 --- a/src/core/classes/IterableConfig.ts +++ b/src/core/classes/IterableConfig.ts @@ -4,6 +4,7 @@ import { IterableDataRegion } from '../enums/IterableDataRegion'; import { IterableLogLevel } from '../enums/IterableLogLevel'; import { IterablePushPlatform } from '../enums/IterablePushPlatform'; import type { IterableAuthFailure } from '../types/IterableAuthFailure'; +import type { IterableIdentityResolution } from '../types/IterableIdentityResolution'; import type { IterableRetryPolicy } from '../types/IterableRetryPolicy'; import { IterableAction } from './IterableAction'; import type { IterableActionContext } from './IterableActionContext'; @@ -391,6 +392,18 @@ export class IterableConfig { */ authCallbackTimeoutMs = 6000; + /** + * Default identity-resolution behavior when identifying a user with + * `Iterable.setEmail` or `Iterable.setUserId`. + * + * Applies when unknown-user activation and visitor tracking are enabled on the + * native SDK. Omitted keys in a partial object default to `true`. + */ + identityResolution: IterableIdentityResolution = { + replayOnVisitorToKnown: true, + mergeOnUnknownToKnown: true, + }; + /** * Should the SDK enable and use embedded messaging? * @@ -484,6 +497,12 @@ export class IterableConfig { pushPlatform: this.pushPlatform, encryptionEnforced: this.encryptionEnforced, retryPolicy: this.retryPolicy, + identityResolution: { + replayOnVisitorToKnown: + this.identityResolution.replayOnVisitorToKnown ?? true, + mergeOnUnknownToKnown: + this.identityResolution.mergeOnUnknownToKnown ?? true, + }, enableEmbeddedMessaging: this.enableEmbeddedMessaging, androidWakeDelayMs: this.androidWakeDelayMs, authCallbackTimeoutMs: this.authCallbackTimeoutMs, diff --git a/src/core/types/IterableIdentityResolution.ts b/src/core/types/IterableIdentityResolution.ts new file mode 100644 index 000000000..e6b45639c --- /dev/null +++ b/src/core/types/IterableIdentityResolution.ts @@ -0,0 +1,20 @@ +/** + * Controls how the SDK resolves anonymous (visitor / unknown-user) data when a + * user is identified with `setEmail` or `setUserId`. + * + * Values set on `IterableConfig.identityResolution` are defaults; native + * `setEmail` / `setUserId` can override them per call (not exposed in RN yet). + */ +export interface IterableIdentityResolution { + /** + * When `true`, locally stored visitor events are replayed to the known user + * profile on identification. + */ + replayOnVisitorToKnown?: boolean; + + /** + * When `true`, the generated unknown-user profile is merged into the known + * user profile on identification. + */ + mergeOnUnknownToKnown?: boolean; +} diff --git a/src/core/types/index.ts b/src/core/types/index.ts index 7659a76e4..f133be55e 100644 --- a/src/core/types/index.ts +++ b/src/core/types/index.ts @@ -1,3 +1,4 @@ export * from './IterableAuthFailure'; export * from './IterableEdgeInsetDetails'; +export * from './IterableIdentityResolution'; export * from './IterableRetryPolicy'; diff --git a/src/index.tsx b/src/index.tsx index 13cb6915d..92a262a3a 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -30,6 +30,7 @@ export { export type { IterableAuthFailure, IterableEdgeInsetDetails, + IterableIdentityResolution, IterableRetryPolicy, } from './core/types'; export { From 0889a67f37bfd55ee7fe1509b590b6fa939d0143 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Ferr=C3=A3o?= Date: Sat, 10 Oct 2026 11:58:10 +0100 Subject: [PATCH 2/2] test(SDK-556): fix partial identityResolution test title Name the case that sets only replayOnVisitorToKnown. --- src/core/classes/Iterable.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/classes/Iterable.test.ts b/src/core/classes/Iterable.test.ts index 51001e1f7..420bff79e 100644 --- a/src/core/classes/Iterable.test.ts +++ b/src/core/classes/Iterable.test.ts @@ -397,7 +397,7 @@ describe('Iterable', () => { }); }); - it('should default replayOnVisitorToKnown to true when only merge is set', () => { + it('should default mergeOnUnknownToKnown to true when only replayOnVisitorToKnown is set', () => { const config = new IterableConfig(); config.identityResolution = { replayOnVisitorToKnown: false }; const configDict = config.toDict();