Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- 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

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 @@ -750,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 @@ -850,6 +871,7 @@ enum EventName {
handleCustomActionCalled,
handleEmbeddedMessageUpdateCalled,
handleEmbeddedMessagingDisabledCalled,
handleUnknownUserCreatedCalled,
handleInAppCalled,
handleUrlCalled,
receivedIterableEmbeddedMessagesChanged,
Expand Down
19 changes: 19 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 @@ -677,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 @@ -875,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
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;
76 changes: 76 additions & 0 deletions src/core/classes/Iterable.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { NativeEventEmitter, Platform } from 'react-native';

import { MockLinking } from '../../__mocks__/MockLinking';
import { MockRNIterableAPI } from '../../__mocks__/MockRNIterableAPI';
import { testNativeEventEmitter } from '../../__mocks__/jest.setup';
// import from the same location that consumers import from
import {
Iterable,
Expand Down Expand Up @@ -49,6 +50,9 @@ describe('Iterable', () => {
nativeEmitter.removeAllListeners(
IterableEventName.handleEmbeddedMessagingDisabledCalled
);
nativeEmitter.removeAllListeners(
IterableEventName.handleUnknownUserCreatedCalled
);

// Clear any pending timers
jest.clearAllTimers();
Expand Down Expand Up @@ -1445,6 +1449,78 @@ describe('Iterable', () => {
});
});

describe('unknownUserHandler', () => {
it('should call unknownUserHandler when handleUnknownUserCreatedCalled is emitted with userId', () => {
const nativeEmitter = new NativeEventEmitter();
nativeEmitter.removeAllListeners(
IterableEventName.handleUnknownUserCreatedCalled
);
const config = new IterableConfig();
config.logReactNativeSdkCalls = false;
config.unknownUserHandler = jest.fn();
Iterable.initialize('apiKey', config);
expect(
testNativeEventEmitter.listenerCount(
IterableEventName.handleUnknownUserCreatedCalled
)
).toBe(1);
nativeEmitter.emit(IterableEventName.handleUnknownUserCreatedCalled, {
userId: 'generated-unknown-user-id',
});
expect(config.unknownUserHandler).toHaveBeenCalledWith(
'generated-unknown-user-id'
);
});

it('should not register a handleUnknownUserCreatedCalled listener when unknownUserHandler is omitted', () => {
testNativeEventEmitter.removeAllListeners(
IterableEventName.handleUnknownUserCreatedCalled
);
const config = new IterableConfig();
config.logReactNativeSdkCalls = false;
Iterable.initialize('apiKey', config);
expect(
testNativeEventEmitter.listenerCount(
IterableEventName.handleUnknownUserCreatedCalled
)
).toBe(0);
});

it('should include unknownUserHandlerPresent in toDict() when the handler is set', () => {
const config = new IterableConfig();
config.unknownUserHandler = jest.fn();
expect(config.toDict().unknownUserHandlerPresent).toBe(true);
});

it('should set unknownUserHandlerPresent to false when the handler is omitted', () => {
const config = new IterableConfig();
expect(config.toDict().unknownUserHandlerPresent).toBe(false);
});

it('should log without the userId when unknownUserHandler throws', () => {
const nativeEmitter = new NativeEventEmitter();
nativeEmitter.removeAllListeners(
IterableEventName.handleUnknownUserCreatedCalled
);
const secretUserId = 'secret-unknown-user-id-for-log-test';
const config = new IterableConfig();
config.logReactNativeSdkCalls = false;
config.unknownUserHandler = jest.fn(() => {
throw new Error(`handler failed for ${secretUserId}`);
});
const logSpy = jest.spyOn(IterableLogger, 'log');
Iterable.initialize('apiKey', config);
nativeEmitter.emit(IterableEventName.handleUnknownUserCreatedCalled, {
userId: secretUserId,
});
const loggedText = logSpy.mock.calls
.map((args) => String(args[0]))
.join('\n');
expect(loggedText).not.toContain(secretUserId);
logSpy.mockRestore();
});
});

describe('embedded messaging callbacks', () => {
describe('onEmbeddedMessageUpdate', () => {
it('should call onEmbeddedMessageUpdate when handleEmbeddedMessageUpdateCalled event is emitted', () => {
Expand Down
22 changes: 22 additions & 0 deletions src/core/classes/Iterable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1032,6 +1032,9 @@ export class Iterable {
RNEventEmitter.removeAllListeners(
IterableEventName.handleEmbeddedMessagingDisabledCalled
);
RNEventEmitter.removeAllListeners(
IterableEventName.handleUnknownUserCreatedCalled
);
}

/**
Expand Down Expand Up @@ -1305,6 +1308,25 @@ export class Iterable {
);
}

if (Iterable.savedConfig.unknownUserHandler) {
RNEventEmitter.addListener(
IterableEventName.handleUnknownUserCreatedCalled,
(dict: { userId?: string }) => {
const userId = dict?.userId;
if (userId === undefined || userId === null) {
return;
}
try {
Iterable.savedConfig.unknownUserHandler!(userId);
} catch {
IterableLogger?.log(
'IterableConfig.unknownUserHandler threw an error'
);
}
}
);
}

if (Iterable.savedConfig.enableEmbeddedMessaging) {
if (Iterable.savedConfig.onEmbeddedMessageUpdate) {
RNEventEmitter.addListener(
Expand Down
37 changes: 37 additions & 0 deletions src/core/classes/IterableConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,39 @@ export class IterableConfig {
mergeOnUnknownToKnown: true,
};

/**
* Called when the native SDK creates a generated user id after unknown-user
* activation criteria are met.
*
* JWT-enabled API keys: native logs in as that id immediately after this
* callback and invokes `authHandler` without passing the id. Store the
* `userId` argument **synchronously** here so `authHandler` can mint a token
* for that id in the same turn. Do not use `Iterable.getUserId()` in this
* callback; native has not switched identity yet. In `authHandler`, consume
* and clear that pending id on each call so later known-user logins and
* token refreshes use `Iterable.getUserId()` instead of a stale cached id.
*
* @param userId - The generated unknown user id from Iterable.
*
* @example
* ```typescript
* let pendingUnknownUserId: string | null = null;
* const config = new IterableConfig();
* config.enableUnknownUserActivation = true;
* config.unknownUserHandler = (userId) => {
* pendingUnknownUserId = userId;
* };
* config.authHandler = async () => {
* const unknownUserId = pendingUnknownUserId;
* pendingUnknownUserId = null;
* const id = unknownUserId ?? (await Iterable.getUserId());
* return fetchJwtForUser(id);
Comment thread
jferrao-itrbl marked this conversation as resolved.
* };
* Iterable.initialize('<YOUR_API_KEY>', config);
* ```
*/
unknownUserHandler?: (userId: string) => void;

/**
* Should the SDK enable and use embedded messaging?
*
Expand Down Expand Up @@ -504,6 +537,10 @@ export class IterableConfig {
* A boolean indicating if an authentication handler is present.
*/
authHandlerPresent: this.authHandler !== undefined,
/**
* A boolean indicating if an unknown-user handler is present.
*/
unknownUserHandlerPresent: this.unknownUserHandler !== undefined,
/**
* A boolean indicating if an embedded message update callback is present.
*/
Expand Down
2 changes: 2 additions & 0 deletions src/core/enums/IterableEventName.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,6 @@ export enum IterableEventName {
handleEmbeddedMessageUpdateCalled = 'handleEmbeddedMessageUpdateCalled',
/** Event that fires when embedded messaging is disabled */
handleEmbeddedMessagingDisabledCalled = 'handleEmbeddedMessagingDisabledCalled',
/** Event that fires when the native SDK creates an unknown user id */
handleUnknownUserCreatedCalled = 'handleUnknownUserCreatedCalled',
}
Loading