Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
d03e737
feat(SDK-554): expose IterableConfig.enableForegroundCriteriaFetch
jferrao-itrbl Oct 10, 2026
75272c6
test(SDK-554): tighten IterableConfig serialization coverage
jferrao-itrbl Oct 10, 2026
523be9b
feat(SDK-556): expose IterableConfig.identityResolution
jferrao-itrbl Oct 10, 2026
0889a67
test(SDK-556): fix partial identityResolution test title
jferrao-itrbl Oct 10, 2026
323f147
feat(SDK-553): expose IterableConfig.eventThresholdLimit
jferrao-itrbl Oct 10, 2026
c5143d6
docs(SDK-553): note eventThresholdLimit is inert until UUA is exposed
jferrao-itrbl Oct 10, 2026
a62987f
feat(SDK-561): expose visitor usage consent on Iterable
jferrao-itrbl Oct 10, 2026
37863f0
docs(SDK-561): fix visitor consent setter TSDoc
jferrao-itrbl Oct 10, 2026
2af34c8
Merge branch 'feature/SDK-548-uua-feature-parity' into feature/SDK-55…
jferrao-itrbl Oct 12, 2026
e90bc0e
Merge branch 'feature/SDK-548-uua-feature-parity' into feature/SDK-55…
jferrao-itrbl Oct 12, 2026
3111227
feat(SDK-794): expose IterableConfig.enableUnknownUserActivation
jferrao-itrbl Oct 13, 2026
e274a2b
Merge pull request #926 from Iterable/feature/SDK-553-event-threshold…
jferrao-itrbl Oct 14, 2026
8698273
Merge branch 'feature/SDK-548-uua-feature-parity' into feature/SDK-55…
jferrao-itrbl Oct 14, 2026
964a03e
Merge pull request #924 from Iterable/feature/SDK-554-enable-foregrou…
jferrao-itrbl Oct 14, 2026
48fa1cf
Merge branch 'feature/SDK-548-uua-feature-parity' into feature/SDK-79…
jferrao-itrbl Oct 14, 2026
9f4f52a
Merge pull request #928 from Iterable/feature/SDK-794-enable-unknown-…
jferrao-itrbl Oct 14, 2026
466b70e
Merge branch 'feature/SDK-548-uua-feature-parity' into feature/SDK-55…
jferrao-itrbl Oct 14, 2026
a50e406
Merge branch 'feature/SDK-548-uua-feature-parity' into feature/SDK-56…
jferrao-itrbl Oct 14, 2026
60bca2e
docs(SDK-561): align visitor consent TSDoc with enableUnknownUserActi…
jferrao-itrbl Oct 14, 2026
18afed9
fix(SDK-556): guard identityResolution in IterableConfig.toDict()
jferrao-itrbl Oct 19, 2026
973b06f
Merge pull request #925 from Iterable/feature/SDK-556-identity-resolu…
jferrao-itrbl Oct 19, 2026
f4f9702
Merge branch 'feature/SDK-548-uua-feature-parity' into feature/SDK-56…
jferrao-itrbl Oct 19, 2026
d95fa2c
refactor(SDK-561): move init reset helper off Iterable class
jferrao-itrbl Oct 19, 2026
bbf3840
Merge pull request #927 from Iterable/feature/SDK-561-visitor-usage-t…
jferrao-itrbl Oct 19, 2026
407ccbb
feat(SDK-804): expose IterableConfig.unknownUserHandler for JWT UUA
jferrao-itrbl Oct 22, 2026
4303f0a
test(SDK-804): assert no unknown-user listener when handler omitted
jferrao-itrbl Oct 22, 2026
550a60c
docs(SDK-804): clear pending unknown user id in authHandler example
jferrao-itrbl Oct 22, 2026
1402287
Merge pull request #930 from Iterable/feature/SDK-804-unknown-user-ha…
jferrao-itrbl Oct 22, 2026
b98442b
fix(SDK-804): forward-declare IterableUnknownUserHandler in RN bridge
jferrao-itrbl Oct 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
kind: decision
applies-to: rules/specifier.md
---

# Search for an existing owner before specifying a method body

Before a spec is finalized, search the tracker and open pull requests for every interface method this change must implement. Include long-lived branches, not only the default branch. If a ticket already defines that method, the spec depends on it and keeps that behaviour. A no-op is allowed only when the search finds no owner.

A compile stub is not "too small for a spec" when the class already implements the interface and the pinned SDK requires a new method body. That body is a bridge contract another ticket may already own. Specifying an empty override on the default branch will overwrite the real behaviour when the branches meet.
10 changes: 10 additions & 0 deletions .artifacts/feedback/search-before-stubbing-interface-method.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
kind: decision
applies-to: rules/developer.md
---

# Search for an existing owner before stubbing an interface method

Before writing a body for an interface method the current ticket did not name, search the tracker and branches other than the one you are on, including long-lived feature branches and open pull requests. If a ticket already defines the method, implement that contract or stop and ask. Do not invent a no-op.

A green build of the current branch only proves the stub compiles. It cannot see an implementation that already emits, forwards, or otherwise behaves, sitting on another branch. Record the search in the handoff: the ticket, the branch, and whether the method already has a behaviour or is still absent.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
## Unreleased

### Updates

- Added `IterableConfig.enableUnknownUserActivation` (default `false`) to opt in to Unknown User Activation on the native SDK (SDK-794). Visitor consent via `Iterable.setVisitorUsageTracked` is also required before the native SDK stores unknown-user events.
- Added `IterableConfig.enableForegroundCriteriaFetch` (default `true`) to control whether the SDK fetches unknown-user targeting criteria when the app enters the foreground (SDK-554).
- Added `IterableConfig.eventThresholdLimit` (default `100`) to cap locally stored unknown-user events before identification (SDK-553). The value is forwarded to the native SDK and applies when unknown-user activation is enabled.
- Added `Iterable.setVisitorUsageTracked` and `Iterable.getVisitorUsageTracked` to record and read visitor consent for Unknown User Activation (SDK-561). Consent is persisted by the native SDK.
- Added `IterableConfig.identityResolution` and the `IterableIdentityResolution` type so RN apps can configure default visitor replay and unknown-to-known merge behavior (SDK-556).
- Added `IterableConfig.unknownUserHandler` so JWT-enabled apps can receive the generated unknown user id before native requests a token (SDK-804). Store the id synchronously in the handler and mint in `authHandler` for that id.

## 3.2.1

### Fixes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import com.iterable.iterableapi.IterableInAppMessage;
import com.iterable.iterableapi.IterableInboxSession;
import com.iterable.iterableapi.IterableLogger;
import com.iterable.iterableapi.IterableUnknownUserHandler;
import com.iterable.iterableapi.IterableUrlHandler;
import com.iterable.iterableapi.RNIterableInternal;

Expand All @@ -53,7 +54,7 @@
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

public class RNIterableAPIModuleImpl implements IterableUrlHandler, IterableCustomActionHandler, IterableInAppHandler, IterableAuthHandler, IterableInAppManager.Listener, IterableEmbeddedUpdateHandler {
public class RNIterableAPIModuleImpl implements IterableUrlHandler, IterableCustomActionHandler, IterableInAppHandler, IterableAuthHandler, IterableUnknownUserHandler, IterableInAppManager.Listener, IterableEmbeddedUpdateHandler {
public static final String NAME = "RNIterableAPI";

private static String TAG = "RNIterableAPIModule";
Expand Down Expand Up @@ -93,6 +94,10 @@ public void initializeWithApiKey(String apiKey, ReadableMap configReadableMap, S
configBuilder.setAuthHandler(this);
}

if (configReadableMap.hasKey("unknownUserHandlerPresent") && configReadableMap.getBoolean("unknownUserHandlerPresent") == true) {
configBuilder.setUnknownUserHandler(this);
}

// Check if embedded messaging is enabled before building config
boolean enableEmbeddedMessaging = configReadableMap.hasKey("enableEmbeddedMessaging") && configReadableMap.getBoolean("enableEmbeddedMessaging");

Expand Down Expand Up @@ -161,6 +166,10 @@ public void initialize2WithApiKey(String apiKey, ReadableMap configReadableMap,
configBuilder.setAuthHandler(this);
}

if (configReadableMap.hasKey("unknownUserHandlerPresent") && configReadableMap.getBoolean("unknownUserHandlerPresent") == true) {
configBuilder.setUnknownUserHandler(this);
}

// NOTE: There does not seem to be a way to set the API endpoint
// override in the Android SDK. Check with @Ayyanchira and @evantk91 to
// see what the best approach is.
Expand Down Expand Up @@ -249,6 +258,15 @@ public void getUserId(Promise promise) {
promise.resolve(RNIterableInternal.getUserId());
}

public void setVisitorUsageTracked(boolean tracked) {
IterableLogger.d(TAG, "setVisitorUsageTracked: " + tracked);
IterableApi.getInstance().setVisitorUsageTracked(tracked);
}

public void getVisitorUsageTracked(Promise promise) {
promise.resolve(IterableApi.getInstance().getVisitorUsageTracked());
}

public void trackEvent(String name, @Nullable ReadableMap dataFields) {
IterableLogger.v(TAG, "trackEvent");
IterableApi.getInstance().track(name, optSerializedDataFields(dataFields));
Expand Down Expand Up @@ -741,6 +759,18 @@ public void onEmbeddedMessagingDisabled() {
sendEvent(EventName.handleEmbeddedMessagingDisabledCalled.name(), null);
}

@Override
public void onUnknownUserCreated(@NonNull String userId) {
JSONObject messageJson = new JSONObject();
try {
messageJson.put("userId", userId);
WritableMap eventData = Serialization.convertJsonToMap(messageJson);
sendEvent(EventName.handleUnknownUserCreatedCalled.name(), eventData);
} catch (JSONException e) {
IterableLogger.e(TAG, "Failed to emit unknown user created event");
}
}

@Override
public void onEmbeddedMessagingSyncSucceeded() {
IterableLogger.d(TAG, "onEmbeddedMessagingSyncSucceeded");
Expand Down Expand Up @@ -841,6 +871,7 @@ enum EventName {
handleCustomActionCalled,
handleEmbeddedMessageUpdateCalled,
handleEmbeddedMessagingDisabledCalled,
handleUnknownUserCreatedCalled,
handleInAppCalled,
handleUrlCalled,
receivedIterableEmbeddedMessagesChanged,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -201,6 +202,11 @@ static IterableConfig.Builder getConfigFromReadableMap(ReadableMap iterableConte
configBuilder.setExpiringAuthTokenRefreshPeriod(iterableContextJSON.optLong("expiringAuthTokenRefreshPeriod"));
}

if (iterableContextJSON.has("eventThresholdLimit")) {
configBuilder.setEventThresholdLimit(
iterableContextJSON.optInt("eventThresholdLimit", 100));
}

if (iterableContextJSON.has("useInMemoryStorageForInApps") || iterableContextJSON.has("androidSdkUseInMemoryStorageForInApps")) {
configBuilder.setUseInMemoryStorageForInApps(iterableContextJSON.optBoolean("useInMemoryStorageForInApps"));
}
Expand Down Expand Up @@ -246,10 +252,19 @@ static IterableConfig.Builder getConfigFromReadableMap(ReadableMap iterableConte
configBuilder.setDataRegion(iterableDataRegion);
}

if (iterableContextJSON.has("enableForegroundCriteriaFetch")) {
configBuilder.setEnableForegroundCriteriaFetch(iterableContextJSON.optBoolean("enableForegroundCriteriaFetch"));
}

if (iterableContextJSON.has("enableEmbeddedMessaging")) {
configBuilder.setEnableEmbeddedMessaging(iterableContextJSON.optBoolean("enableEmbeddedMessaging"));
}

if (iterableContextJSON.has("enableUnknownUserActivation")) {
configBuilder.setEnableUnknownUserActivation(
iterableContextJSON.optBoolean("enableUnknownUserActivation"));
}

if (iterableContextJSON.has("retryPolicy")) {
JSONObject retryPolicyJson = iterableContextJSON.getJSONObject("retryPolicy");
int maxRetry = retryPolicyJson.getInt("maxRetry");
Expand All @@ -262,6 +277,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();
Expand Down
10 changes: 10 additions & 0 deletions android/src/newarch/java/com/RNIterableAPIModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,16 @@ public void getUserId(Promise promise) {
moduleImpl.getUserId(promise);
}

@Override
public void setVisitorUsageTracked(boolean tracked) {
moduleImpl.setVisitorUsageTracked(tracked);
}

@Override
public void getVisitorUsageTracked(Promise promise) {
moduleImpl.getVisitorUsageTracked(promise);
}

@Override
public void trackEvent(String name, @Nullable ReadableMap dataFields) {
moduleImpl.trackEvent(name, dataFields);
Expand Down
10 changes: 10 additions & 0 deletions android/src/oldarch/java/com/RNIterableAPIModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,16 @@ public void getUserId(Promise promise) {
moduleImpl.getUserId(promise);
}

@ReactMethod
public void setVisitorUsageTracked(boolean tracked) {
moduleImpl.setVisitorUsageTracked(tracked);
}

@ReactMethod
public void getVisitorUsageTracked(Promise promise) {
moduleImpl.getVisitorUsageTracked(promise);
}

@ReactMethod
public void trackEvent(String name, @Nullable ReadableMap dataFields) {
moduleImpl.trackEvent(name, dataFields);
Expand Down
19 changes: 19 additions & 0 deletions ios/RNIterableAPI/RNIterableAPI.mm
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
@protocol IterableInAppDelegate;
@protocol IterableCustomActionDelegate;
@protocol IterableAuthDelegate;
@protocol IterableUnknownUserHandler;
@protocol IterableURLDelegate;
@protocol IterableEmbeddedUpdateDelegate;
typedef NS_ENUM(NSInteger, InAppShowResponse) {
Expand Down Expand Up @@ -112,6 +113,15 @@ - (void)getUserId:(RCTPromiseResolveBlock)resolve
[_swiftAPI getUserId:resolve rejecter:reject];
}

- (void)setVisitorUsageTracked:(BOOL)tracked {
[_swiftAPI setVisitorUsageTracked:tracked];
}

- (void)getVisitorUsageTracked:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject {
[_swiftAPI getVisitorUsageTracked:resolve rejecter:reject];
}

- (void)setInAppShowResponse:(double)inAppShowResponse {
[_swiftAPI setInAppShowResponse:inAppShowResponse];
}
Expand Down Expand Up @@ -403,6 +413,15 @@ - (void)wakeApp {
[_swiftAPI getUserId:resolve rejecter:reject];
}

RCT_EXPORT_METHOD(setVisitorUsageTracked : (BOOL)tracked) {
[_swiftAPI setVisitorUsageTracked:tracked];
}

RCT_EXPORT_METHOD(getVisitorUsageTracked : (RCTPromiseResolveBlock)
resolve reject : (RCTPromiseRejectBlock)reject) {
[_swiftAPI getVisitorUsageTracked:resolve rejecter:reject];
}

RCT_EXPORT_METHOD(setInAppShowResponse : (double)inAppShowResponse) {
[_swiftAPI setInAppShowResponse:inAppShowResponse];
}
Expand Down
34 changes: 34 additions & 0 deletions ios/RNIterableAPI/ReactIterableAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import React
case handleAuthFailureCalled
case handleEmbeddedMessageUpdateCalled
case handleEmbeddedMessagingDisabledCalled
case handleUnknownUserCreatedCalled
}

@objc public static var supportedEvents: [String] {
Expand Down Expand Up @@ -129,6 +130,21 @@ import React
resolver(IterableAPI.userId)
}

@objc(setVisitorUsageTracked:)
public func setVisitorUsageTracked(tracked: Bool) {
ITBInfo()
IterableAPI.setVisitorUsageTracked(isVisitorUsageTracked: tracked)
}

@objc(getVisitorUsageTracked:rejecter:)
public func getVisitorUsageTracked(
resolver: @escaping RCTPromiseResolveBlock,
rejecter: @escaping RCTPromiseRejectBlock
) {
ITBInfo()
resolver(IterableAPI.getVisitorUsageTracked())
}

// MARK: - Iterable API Request Functions

@objc(setInAppShowResponse:)
Expand Down Expand Up @@ -662,6 +678,12 @@ import React
iterableConfig.authDelegate = self
}

if let unknownUserHandlerPresent = configDict["unknownUserHandlerPresent"] as? Bool,
unknownUserHandlerPresent == true
{
iterableConfig.unknownUserHandler = self
}

// connect new inbox in-app payloads to the RN SDK
NotificationCenter.default.addObserver(
self, selector: #selector(receivedIterableInboxChanged),
Expand Down Expand Up @@ -860,6 +882,18 @@ extension ReactIterableAPI: IterableAuthDelegate {
}
}

extension ReactIterableAPI: IterableUnknownUserHandler {
public func onUnknownUserCreated(userId: String) {
ITBInfo()
guard shouldEmit else {
return
}
delegate?.sendEvent(
withName: EventName.handleUnknownUserCreatedCalled.rawValue,
body: ["userId": userId])
}
}

extension ReactIterableAPI: IterableEmbeddedUpdateDelegate {
public func onMessagesUpdated() {
ITBInfo()
Expand Down
22 changes: 22 additions & 0 deletions ios/RNIterableAPI/Serialization.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ extension IterableConfig {
config.expiringAuthTokenRefreshPeriod = expiringAuthTokenRefreshPeriod
}

if let eventThresholdLimit = dict["eventThresholdLimit"] as? Int {
config.eventThresholdLimit = eventThresholdLimit
}

if let logLevelNumber = dict["logLevel"] as? NSNumber {
config.logDelegate = createLogDelegate(logLevelNumber: logLevelNumber)
}
Expand All @@ -83,10 +87,18 @@ extension IterableConfig {
config.useInMemoryStorageForInApps = useInMemoryStorageForInApp
}

if let enableForegroundCriteriaFetch = dict["enableForegroundCriteriaFetch"] as? Bool {
config.enableForegroundCriteriaFetch = enableForegroundCriteriaFetch
}

if let enableEmbeddedMessaging = dict["enableEmbeddedMessaging"] as? Bool {
config.enableEmbeddedMessaging = enableEmbeddedMessaging
}

if let enableUnknownUserActivation = dict["enableUnknownUserActivation"] as? Bool {
config.enableUnknownUserActivation = enableUnknownUserActivation
}

if let dataRegion = dict["dataRegion"] as? NSNumber {
switch dataRegion {
case 0:
Expand All @@ -110,6 +122,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
}

Expand Down
15 changes: 15 additions & 0 deletions src/__mocks__/MockRNIterableAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ export class MockRNIterableAPI {
static attributionInfo?: IterableAttributionInfo;
static messages?: IterableInAppMessage[];
static clickedUrl?: string;
static visitorUsageTracked = false;
static unknownVisitorEvents: string[] = [];

static async getEmail(): Promise<string | undefined> {
return await new Promise((resolve) => {
Expand All @@ -32,6 +34,19 @@ export class MockRNIterableAPI {
MockRNIterableAPI.token = authToken;
});

static setVisitorUsageTracked = jest.fn((tracked: boolean): void => {
MockRNIterableAPI.visitorUsageTracked = tracked;
MockRNIterableAPI.unknownVisitorEvents = [];
});

static getVisitorUsageTracked = jest.fn(async (): Promise<boolean> => {
return await Promise.resolve(MockRNIterableAPI.visitorUsageTracked);
});

static pushUnknownVisitorEventForTests(eventId: string): void {
MockRNIterableAPI.unknownVisitorEvents.push(eventId);
}

static disableDeviceForCurrentUser = jest.fn();

static registerDeviceToken = jest.fn((token: string): void => {
Expand Down
3 changes: 3 additions & 0 deletions src/__mocks__/jest.setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,6 @@ jest.doMock('react-native', () => {
ReactNative
);
});

/** Shared backing store for all NativeEventEmitter instances in tests. */
export const testNativeEventEmitter = mockNativeEventEmitter;
Loading
Loading