Skip to content
Draft
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## Unreleased

### Updates

- 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; unknown-user activation is not enabled from React Native config yet.

## 3.2.1

### Fixes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,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
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
18 changes: 18 additions & 0 deletions ios/RNIterableAPI/RNIterableAPI.mm
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,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 +412,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
15 changes: 15 additions & 0 deletions ios/RNIterableAPI/ReactIterableAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,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
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
2 changes: 2 additions & 0 deletions src/api/NativeRNIterableAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ export interface Spec extends TurboModule {
getEmail(): Promise<string | null>;
setUserId(userId?: string | null, authToken?: string | null): void;
getUserId(): Promise<string | null | undefined>;
setVisitorUsageTracked(tracked: boolean): void;
getVisitorUsageTracked(): Promise<boolean>;

// In-app messaging
setInAppShowResponse(number: number): void;
Expand Down
95 changes: 95 additions & 0 deletions src/core/classes/Iterable.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,101 @@ describe('Iterable', () => {
});
});

describe('visitorUsageTracked', () => {
beforeEach(() => {
MockRNIterableAPI.visitorUsageTracked = false;
MockRNIterableAPI.unknownVisitorEvents = [];
Iterable.resetSdkInitializationStateForTests();
});

it('should expose setVisitorUsageTracked and getVisitorUsageTracked on Iterable', () => {
expect(typeof Iterable.setVisitorUsageTracked).toBe('function');
expect(typeof Iterable.getVisitorUsageTracked).toBe('function');
});

it('should forward true and false to RNIterableAPI after initialize', async () => {
const config = new IterableConfig();
config.logReactNativeSdkCalls = false;
await Iterable.initialize('test-key', config);

Iterable.setVisitorUsageTracked(true);
expect(MockRNIterableAPI.setVisitorUsageTracked).toHaveBeenCalledWith(true);

Iterable.setVisitorUsageTracked(false);
expect(MockRNIterableAPI.setVisitorUsageTracked).toHaveBeenCalledWith(false);
});

it('should resolve getVisitorUsageTracked to the value stored in the mock', async () => {
const config = new IterableConfig();
config.logReactNativeSdkCalls = false;
await Iterable.initialize('test-key', config);

MockRNIterableAPI.visitorUsageTracked = true;
await expect(Iterable.getVisitorUsageTracked()).resolves.toBe(true);

MockRNIterableAPI.visitorUsageTracked = false;
await expect(Iterable.getVisitorUsageTracked()).resolves.toBe(false);
});

it('should resolve to false when the integrator never calls the setter', async () => {
const config = new IterableConfig();
config.logReactNativeSdkCalls = false;
await Iterable.initialize('test-key', config);

await expect(Iterable.getVisitorUsageTracked()).resolves.toBe(false);
});

it('should return the persisted mock value after initialize runs again', async () => {
const config = new IterableConfig();
config.logReactNativeSdkCalls = false;
await Iterable.initialize('test-key', config);
Iterable.setVisitorUsageTracked(true);

Iterable.resetSdkInitializationStateForTests();
await Iterable.initialize('test-key', config);

await expect(Iterable.getVisitorUsageTracked()).resolves.toBe(true);
});

it('should clear unknown-visitor events in the mock on each setVisitorUsageTracked call', async () => {
const config = new IterableConfig();
config.logReactNativeSdkCalls = false;
MockRNIterableAPI.pushUnknownVisitorEventForTests('event-1');
await Iterable.initialize('test-key', config);

Iterable.setVisitorUsageTracked(false);
expect(MockRNIterableAPI.unknownVisitorEvents).toEqual([]);

MockRNIterableAPI.pushUnknownVisitorEventForTests('event-2');
Iterable.setVisitorUsageTracked(true);
expect(MockRNIterableAPI.unknownVisitorEvents).toEqual([]);
});

it('should not forward setVisitorUsageTracked before initialize resolves', async () => {
Iterable.setVisitorUsageTracked(true);
expect(MockRNIterableAPI.setVisitorUsageTracked).not.toHaveBeenCalled();
await expect(Iterable.getVisitorUsageTracked()).resolves.toBe(false);

const config = new IterableConfig();
config.logReactNativeSdkCalls = false;
await Iterable.initialize('test-key', config);
await expect(Iterable.getVisitorUsageTracked()).resolves.toBe(false);
});

it('should not call trackEvent, setEmail, or setUserId from visitor usage methods', async () => {
const config = new IterableConfig();
config.logReactNativeSdkCalls = false;
await Iterable.initialize('test-key', config);

Iterable.setVisitorUsageTracked(true);
await Iterable.getVisitorUsageTracked();

expect(MockRNIterableAPI.trackEvent).not.toHaveBeenCalled();
expect(MockRNIterableAPI.setEmail).not.toHaveBeenCalled();
expect(MockRNIterableAPI.setUserId).not.toHaveBeenCalled();
});
});

describe('logout', () => {
it('should call setEmail with null', () => {
// GIVEN no parameters
Expand Down
58 changes: 57 additions & 1 deletion src/core/classes/Iterable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,15 @@ export class Iterable {
*/
static savedConfig: IterableConfig = defaultConfig;

private static sdkInitialized = false;

/**
* @internal
*/
static resetSdkInitializationStateForTests(): void {
Iterable.sdkInitialized = false;
}

/**
* In-app message manager for the current user.
*
Expand Down Expand Up @@ -157,12 +166,18 @@ export class Iterable {
apiKey: string,
config: IterableConfig = new IterableConfig()
): Promise<boolean> {
Iterable.sdkInitialized = false;
Iterable.savedConfig = config;
this.setupIterable(config);

const version = this.getVersionFromPackageJson();

return IterableApi.initializeWithApiKey(apiKey, { config, version });
return IterableApi.initializeWithApiKey(apiKey, { config, version }).then(
(initialized) => {
Iterable.sdkInitialized = true;
return initialized;
}
);
}

/**
Expand All @@ -176,6 +191,7 @@ export class Iterable {
config: IterableConfig = new IterableConfig(),
apiEndPoint: string
): Promise<boolean> {
Iterable.sdkInitialized = false;
this.setupIterable(config);

const version = this.getVersionFromPackageJson();
Expand All @@ -184,6 +200,9 @@ export class Iterable {
config,
version,
apiEndPoint,
}).then((initialized) => {
Iterable.sdkInitialized = true;
return initialized;
});
}

Expand Down Expand Up @@ -333,6 +352,43 @@ export class Iterable {
return IterableApi.getUserId();
}

/**
* Record whether the end user has consented to visitor / unknown-user tracking.
*
* The value is persisted by the native SDK across app restarts. Each call
* clears locally stored unknown-visitor events on the native side.
*
* Unknown User Activation is not enabled from React Native config yet, so
* this only stores the consent flag until `enableUnknownUserActivation` is
* exposed. Use {@link Iterable.getVisitorUsageTracked} to read the stored value.
*
* No-op before `Iterable.initialize` has resolved (does not forward to native).
*/
static setVisitorUsageTracked(tracked: boolean): void {
if (!Iterable.sdkInitialized) {
return;
}
IterableApi.setVisitorUsageTracked(tracked);
}

/**
* Returns whether visitor / unknown-user tracking consent has been recorded.
*
* Reads the value persisted by the native SDK (including across app restarts
* after a prior `setVisitorUsageTracked` call).
*
* Resolves to `false` before `Iterable.initialize` has resolved or if the
* integrator has never called `setVisitorUsageTracked`. Until
* `enableUnknownUserActivation` is exposed on `IterableConfig`, the flag is
* stored only and does not enable unknown-user flows by itself.
*/
static getVisitorUsageTracked(): Promise<boolean> {
if (!Iterable.sdkInitialized) {
return Promise.resolve(false);
}
return IterableApi.getVisitorUsageTracked();
}

/**
* Disable the device's token for the current user. This will disable push notifications for the current user.
*
Expand Down
19 changes: 19 additions & 0 deletions src/core/classes/IterableApi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,25 @@ describe('IterableApi', () => {
});
});

describe('visitorUsageTracked', () => {
beforeEach(() => {
MockRNIterableAPI.visitorUsageTracked = false;
});

it('should call RNIterableAPI.setVisitorUsageTracked with the tracked flag', () => {
IterableApi.setVisitorUsageTracked(true);
expect(MockRNIterableAPI.setVisitorUsageTracked).toHaveBeenCalledWith(true);

IterableApi.setVisitorUsageTracked(false);
expect(MockRNIterableAPI.setVisitorUsageTracked).toHaveBeenCalledWith(false);
});

it('should return visitor usage tracked from RNIterableAPI', async () => {
MockRNIterableAPI.visitorUsageTracked = true;
await expect(IterableApi.getVisitorUsageTracked()).resolves.toBe(true);
});
});

describe('disableDeviceForCurrentUser', () => {
it('should call RNIterableAPI.disableDeviceForCurrentUser', () => {
// GIVEN no parameters
Expand Down
10 changes: 10 additions & 0 deletions src/core/classes/IterableApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,16 @@ export class IterableApi {
return RNIterableAPI.getUserId();
}

static setVisitorUsageTracked(tracked: boolean) {
IterableLogger.log('setVisitorUsageTracked: ', tracked);
return RNIterableAPI.setVisitorUsageTracked(tracked);
}

static getVisitorUsageTracked() {
IterableLogger.log('getVisitorUsageTracked');
return RNIterableAPI.getVisitorUsageTracked();
}

/**
* Disable the device for the current user.
*/
Expand Down
Loading