diff --git a/src/__mocks__/MockRNIterableAPI.ts b/src/__mocks__/MockRNIterableAPI.ts index 1949c15bf..c7f325677 100644 --- a/src/__mocks__/MockRNIterableAPI.ts +++ b/src/__mocks__/MockRNIterableAPI.ts @@ -16,10 +16,10 @@ export class MockRNIterableAPI { }); } - static setEmail(email: string, authToken?: string): void { + static setEmail = jest.fn((email: string, authToken?: string): void => { MockRNIterableAPI.email = email; MockRNIterableAPI.token = authToken; - } + }); static async getUserId(): Promise { return await new Promise((resolve) => { @@ -27,10 +27,10 @@ export class MockRNIterableAPI { }); } - static setUserId(userId: string, authToken?: string): void { + static setUserId = jest.fn((userId: string, authToken?: string): void => { MockRNIterableAPI.userId = userId; MockRNIterableAPI.token = authToken; - } + }); static disableDeviceForCurrentUser = jest.fn(); @@ -62,9 +62,11 @@ export class MockRNIterableAPI { }); } - static setAttributionInfo(attributionInfo?: IterableAttributionInfo): void { - MockRNIterableAPI.attributionInfo = attributionInfo; - } + static setAttributionInfo = jest.fn( + (attributionInfo?: IterableAttributionInfo): void => { + MockRNIterableAPI.attributionInfo = attributionInfo; + } + ); static initializeWithApiKey = jest.fn().mockResolvedValue(true); @@ -86,14 +88,16 @@ export class MockRNIterableAPI { static setAutoDisplayPaused = jest.fn(); - static async showMessage( - _message: IterableInAppMessage, - _consume: boolean - ): Promise { - return await new Promise((resolve) => { - resolve(MockRNIterableAPI.clickedUrl); - }); - } + static showMessage = jest.fn( + async ( + _messageId: string, + _consume: boolean + ): Promise => { + return await new Promise((resolve) => { + resolve(MockRNIterableAPI.clickedUrl); + }); + } + ); static removeMessage = jest.fn(); @@ -109,6 +113,22 @@ export class MockRNIterableAPI { static updateSubscriptions = jest.fn(); + static getInboxMessages = jest.fn( + async (): Promise => { + return await new Promise((resolve) => { + resolve(MockRNIterableAPI.messages); + }); + } + ); + + static startSession = jest.fn(); + + static endSession = jest.fn(); + + static updateVisibleRows = jest.fn(); + + static getHtmlInAppContentForMessage = jest.fn(); + // set messages function is to set the messages static property // this is for testing purposes only static setMessages(messages: IterableInAppMessage[]): void { diff --git a/src/core/classes/Iterable.ts b/src/core/classes/Iterable.ts index a893236b8..0b28c2f8b 100644 --- a/src/core/classes/Iterable.ts +++ b/src/core/classes/Iterable.ts @@ -3,27 +3,23 @@ import { Linking, NativeEventEmitter, Platform } from 'react-native'; import { buildInfo } from '../../itblBuildInfo'; import { RNIterableAPI } from '../../api'; -// TODO: Organize these so that there are no circular dependencies -// See https://github.com/expo/expo/issues/35100 +import { IterableInAppManager } from '../../inApp/classes/IterableInAppManager'; import { IterableInAppMessage } from '../../inApp/classes/IterableInAppMessage'; import { IterableInAppCloseSource } from '../../inApp/enums/IterableInAppCloseSource'; import { IterableInAppDeleteSource } from '../../inApp/enums/IterableInAppDeleteSource'; import { IterableInAppLocation } from '../../inApp/enums/IterableInAppLocation'; import { IterableAuthResponseResult } from '../enums/IterableAuthResponseResult'; import { IterableEventName } from '../enums/IterableEventName'; - -// Add this type-only import to avoid circular dependency -import type { IterableInAppManager } from '../../inApp/classes/IterableInAppManager'; - +import type { IterableAuthFailure } from '../types/IterableAuthFailure'; import { IterableAction } from './IterableAction'; import { IterableActionContext } from './IterableActionContext'; +import { IterableApi } from './IterableApi'; import { IterableAttributionInfo } from './IterableAttributionInfo'; +import { IterableAuthManager } from './IterableAuthManager'; import { IterableAuthResponse } from './IterableAuthResponse'; import type { IterableCommerceItem } from './IterableCommerceItem'; import { IterableConfig } from './IterableConfig'; import { IterableLogger } from './IterableLogger'; -import type { IterableAuthFailure } from '../types/IterableAuthFailure'; -import { IterableAuthManager } from './IterableAuthManager'; const RNEventEmitter = new NativeEventEmitter(RNIterableAPI); @@ -74,21 +70,7 @@ export class Iterable { * Iterable.inAppManager.showMessage(message, true); * ``` */ - static get inAppManager() { - // Lazy initialization to avoid circular dependency - if (!this._inAppManager) { - // Import here to avoid circular dependency at module level - - const { - IterableInAppManager, - // eslint-disable-next-line @typescript-eslint/no-var-requires,@typescript-eslint/no-require-imports - } = require('../../inApp/classes/IterableInAppManager'); - this._inAppManager = new IterableInAppManager(); - } - return this._inAppManager; - } - - private static _inAppManager: IterableInAppManager | undefined; + static inAppManager: IterableInAppManager = new IterableInAppManager(); /** * Authentication manager for the current user. @@ -139,16 +121,13 @@ export class Iterable { config: IterableConfig = new IterableConfig() ): Promise { Iterable.savedConfig = config; - Iterable.logger = new IterableLogger(Iterable.savedConfig); - Iterable?.logger?.log('initialize: ' + apiKey); - this.setupEventHandlers(); const version = this.getVersionFromPackageJson(); - return RNIterableAPI.initializeWithApiKey(apiKey, config.toDict(), version); + return IterableApi.initializeWithApiKey(apiKey, { config, version }); } /** @@ -163,20 +142,17 @@ export class Iterable { apiEndPoint: string ): Promise { Iterable.savedConfig = config; - Iterable.logger = new IterableLogger(Iterable.savedConfig); - Iterable?.logger?.log('initialize2: ' + apiKey); - this.setupEventHandlers(); + const version = this.getVersionFromPackageJson(); - return RNIterableAPI.initialize2WithApiKey( - apiKey, - config.toDict(), + return IterableApi.initialize2WithApiKey(apiKey, { + config, version, - apiEndPoint - ); + apiEndPoint, + }); } /** @@ -229,9 +205,7 @@ export class Iterable { * ``` */ static setEmail(email: string | null, authToken?: string | null) { - Iterable?.logger?.log('setEmail: ' + email); - - RNIterableAPI.setEmail(email, authToken); + IterableApi.setEmail(email, authToken); } /** @@ -245,9 +219,7 @@ export class Iterable { * ``` */ static getEmail(): Promise { - Iterable?.logger?.log('getEmail'); - - return RNIterableAPI.getEmail(); + return IterableApi.getEmail(); } /** @@ -294,9 +266,7 @@ export class Iterable { * taken */ static setUserId(userId?: string | null, authToken?: string | null) { - Iterable?.logger?.log('setUserId: ' + userId); - - RNIterableAPI.setUserId(userId, authToken); + IterableApi.setUserId(userId, authToken); } /** @@ -310,9 +280,7 @@ export class Iterable { * ``` */ static getUserId(): Promise { - Iterable?.logger?.log('getUserId'); - - return RNIterableAPI.getUserId(); + return IterableApi.getUserId(); } /** @@ -324,9 +292,7 @@ export class Iterable { * ``` */ static disableDeviceForCurrentUser() { - Iterable?.logger?.log('disableDeviceForCurrentUser'); - - RNIterableAPI.disableDeviceForCurrentUser(); + IterableApi.disableDeviceForCurrentUser(); } /** @@ -341,9 +307,7 @@ export class Iterable { * ``` */ static getLastPushPayload(): Promise { - Iterable?.logger?.log('getLastPushPayload'); - - return RNIterableAPI.getLastPushPayload(); + return IterableApi.getLastPushPayload(); } /** @@ -369,9 +333,7 @@ export class Iterable { * ``` */ static getAttributionInfo(): Promise { - Iterable?.logger?.log('getAttributionInfo'); - - return RNIterableAPI.getAttributionInfo().then( + return IterableApi.getAttributionInfo().then( ( dict: { campaignId: number; @@ -417,13 +379,7 @@ export class Iterable { * ``` */ static setAttributionInfo(attributionInfo?: IterableAttributionInfo) { - Iterable?.logger?.log('setAttributionInfo'); - - RNIterableAPI.setAttributionInfo( - attributionInfo as unknown as { - [key: string]: string | number | boolean; - } | null - ); + IterableApi.setAttributionInfo(attributionInfo); } /** @@ -462,15 +418,13 @@ export class Iterable { appAlreadyRunning: boolean, dataFields?: unknown ) { - Iterable?.logger?.log('trackPushOpenWithCampaignId'); - - RNIterableAPI.trackPushOpenWithCampaignId( + IterableApi.trackPushOpenWithCampaignId({ campaignId, templateId, - messageId as string, + messageId, appAlreadyRunning, - dataFields as { [key: string]: string | number | boolean } | undefined - ); + dataFields, + }); } /** @@ -500,11 +454,7 @@ export class Iterable { * ``` */ static updateCart(items: IterableCommerceItem[]) { - Iterable?.logger?.log('updateCart'); - - RNIterableAPI.updateCart( - items as unknown as { [key: string]: string | number | boolean }[] - ); + IterableApi.updateCart(items); } /** @@ -519,9 +469,7 @@ export class Iterable { */ static wakeApp() { if (Platform.OS === 'android') { - Iterable?.logger?.log('Attempting to wake the app'); - - RNIterableAPI.wakeApp(); + IterableApi.wakeApp(); } } @@ -556,11 +504,7 @@ export class Iterable { ) { Iterable?.logger?.log('trackPurchase'); - RNIterableAPI.trackPurchase( - total, - items as unknown as { [key: string]: string | number | boolean }[], - dataFields as { [key: string]: string | number | boolean } | undefined - ); + IterableApi.trackPurchase({ total, items, dataFields }); } /** @@ -586,9 +530,13 @@ export class Iterable { message: IterableInAppMessage, location: IterableInAppLocation ) { - Iterable?.logger?.log('trackInAppOpen'); - - RNIterableAPI.trackInAppOpen(message.messageId, location); + if (!message?.messageId) { + Iterable?.logger?.log( + `Skipping trackInAppOpen because message ID is required, but received ${message}.` + ); + return; + } + IterableApi.trackInAppOpen({ message, location }); } /** @@ -617,9 +565,7 @@ export class Iterable { location: IterableInAppLocation, clickedUrl: string ) { - Iterable?.logger?.log('trackInAppClick'); - - RNIterableAPI.trackInAppClick(message.messageId, location, clickedUrl); + IterableApi.trackInAppClick({ message, location, clickedUrl }); } /** @@ -650,14 +596,7 @@ export class Iterable { source: IterableInAppCloseSource, clickedUrl?: string ) { - Iterable?.logger?.log('trackInAppClose'); - - RNIterableAPI.trackInAppClose( - message.messageId, - location, - source, - clickedUrl - ); + IterableApi.trackInAppClose({ message, location, source, clickedUrl }); } /** @@ -701,9 +640,7 @@ export class Iterable { location: IterableInAppLocation, source: IterableInAppDeleteSource ) { - Iterable?.logger?.log('inAppConsume'); - - RNIterableAPI.inAppConsume(message.messageId, location, source); + IterableApi.inAppConsume(message, location, source); } /** @@ -727,12 +664,7 @@ export class Iterable { * ``` */ static trackEvent(name: string, dataFields?: unknown) { - Iterable?.logger?.log('trackEvent'); - - RNIterableAPI.trackEvent( - name, - dataFields as { [key: string]: string | number | boolean } | undefined - ); + IterableApi.trackEvent({ name, dataFields }); } /** @@ -778,12 +710,7 @@ export class Iterable { dataFields: unknown | undefined, mergeNestedObjects: boolean ) { - Iterable?.logger?.log('updateUser'); - - RNIterableAPI.updateUser( - dataFields as { [key: string]: string | number | boolean }, - mergeNestedObjects - ); + IterableApi.updateUser(dataFields, mergeNestedObjects); } /** @@ -804,9 +731,7 @@ export class Iterable { * ``` */ static updateEmail(email: string, authToken?: string) { - Iterable?.logger?.log('updateEmail'); - - RNIterableAPI.updateEmail(email, authToken); + IterableApi.updateEmail(email, authToken); } /** @@ -888,9 +813,7 @@ export class Iterable { */ /* eslint-enable tsdoc/syntax */ static handleAppLink(link: string): Promise { - Iterable?.logger?.log('handleAppLink'); - - return RNIterableAPI.handleAppLink(link); + return IterableApi.handleAppLink(link); } /** @@ -935,16 +858,14 @@ export class Iterable { campaignId: number, templateId: number ) { - Iterable?.logger?.log('updateSubscriptions'); - - RNIterableAPI.updateSubscriptions( + IterableApi.updateSubscriptions({ emailListIds, unsubscribedChannelIds, unsubscribedMessageTypeIds, subscribedMessageTypeIds, campaignId, - templateId - ); + templateId, + }); } /** @@ -1011,7 +932,7 @@ export class Iterable { const message = IterableInAppMessage.fromDict(messageDict); // MOB-10423: Check if we can use chain operator (?.) here instead const result = Iterable.savedConfig.inAppHandler!(message); - RNIterableAPI.setInAppShowResponse(result); + IterableApi.setInAppShowResponse(result); } ); } diff --git a/src/core/classes/IterableApi.test.ts b/src/core/classes/IterableApi.test.ts new file mode 100644 index 000000000..ee41c3784 --- /dev/null +++ b/src/core/classes/IterableApi.test.ts @@ -0,0 +1,1132 @@ +import { Platform } from 'react-native'; + +import { MockRNIterableAPI } from '../../__mocks__/MockRNIterableAPI'; +import { IterableApi } from './IterableApi'; +import { IterableConfig } from './IterableConfig'; +import { IterableAttributionInfo } from './IterableAttributionInfo'; +import { IterableCommerceItem } from './IterableCommerceItem'; +import { IterableInAppMessage } from '../../inApp/classes/IterableInAppMessage'; +import { IterableInAppTrigger } from '../../inApp/classes/IterableInAppTrigger'; +import { IterableInAppTriggerType } from '../../inApp/enums/IterableInAppTriggerType'; +import { IterableInAppLocation } from '../../inApp/enums/IterableInAppLocation'; +import { IterableInAppCloseSource } from '../../inApp/enums/IterableInAppCloseSource'; +import { IterableInAppDeleteSource } from '../../inApp/enums/IterableInAppDeleteSource'; +import { IterableInAppShowResponse } from '../../inApp/enums/IterableInAppShowResponse'; +import { type IterableInboxImpressionRowInfo } from '../../inbox/types/IterableInboxImpressionRowInfo'; + +// Mock the RNIterableAPI module +jest.mock('../../api', () => ({ + __esModule: true, + default: MockRNIterableAPI, +})); + +describe('IterableApi', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + // ====================================================== // + // ===================== INITIALIZE ===================== // + // ====================================================== // + + describe('initializeWithApiKey', () => { + it('should call RNIterableAPI.initializeWithApiKey with correct parameters', async () => { + // GIVEN an API key, config, and version + const apiKey = 'test-api-key'; + const config = new IterableConfig(); + const version = '1.0.0'; + + // WHEN initializeWithApiKey is called + const result = await IterableApi.initializeWithApiKey(apiKey, { + config, + version, + }); + + // THEN RNIterableAPI.initializeWithApiKey is called with correct parameters + expect(MockRNIterableAPI.initializeWithApiKey).toBeCalledWith( + apiKey, + config.toDict(), + version + ); + expect(result).toBe(true); + }); + + it('should use default config when not provided', async () => { + // GIVEN an API key and version + const apiKey = 'test-api-key'; + const version = '1.0.0'; + + // WHEN initializeWithApiKey is called without config + const result = await IterableApi.initializeWithApiKey(apiKey, { + config: new IterableConfig(), + version, + }); + + // THEN RNIterableAPI.initializeWithApiKey is called with default config + expect(MockRNIterableAPI.initializeWithApiKey).toBeCalledWith( + apiKey, + expect.any(Object), + version + ); + expect(result).toBe(true); + }); + }); + + describe('initialize2WithApiKey', () => { + it('should call RNIterableAPI.initialize2WithApiKey with correct parameters', async () => { + // GIVEN an API key, config, version, and endpoint + const apiKey = 'test-api-key'; + const config = new IterableConfig(); + const version = '1.0.0'; + const apiEndPoint = 'https://api.staging.iterable.com'; + + // WHEN initialize2WithApiKey is called + const result = await IterableApi.initialize2WithApiKey(apiKey, { + config, + version, + apiEndPoint, + }); + + // THEN RNIterableAPI.initialize2WithApiKey is called with correct parameters + expect(MockRNIterableAPI.initialize2WithApiKey).toBeCalledWith( + apiKey, + config.toDict(), + version, + apiEndPoint + ); + expect(result).toBe(true); + }); + + it('should use default config when not provided', async () => { + // GIVEN an API key, version, and endpoint + const apiKey = 'test-api-key'; + const version = '1.0.0'; + const apiEndPoint = 'https://api.staging.iterable.com'; + + // WHEN initialize2WithApiKey is called without config + const result = await IterableApi.initialize2WithApiKey(apiKey, { + version, + apiEndPoint, + config: new IterableConfig(), + }); + + // THEN RNIterableAPI.initialize2WithApiKey is called with default config + expect(MockRNIterableAPI.initialize2WithApiKey).toBeCalledWith( + apiKey, + expect.any(Object), + version, + apiEndPoint + ); + expect(result).toBe(true); + }); + }); + + // ====================================================== // + // ===================== USER MANAGEMENT ================ // + // ====================================================== // + + describe('setEmail', () => { + it('should call RNIterableAPI.setEmail with email only', () => { + // GIVEN an email + const email = 'user@example.com'; + + // WHEN setEmail is called + IterableApi.setEmail(email); + + // THEN RNIterableAPI.setEmail is called with email + expect(MockRNIterableAPI.setEmail).toBeCalledWith(email, undefined); + }); + + it('should call RNIterableAPI.setEmail with email and auth token', () => { + // GIVEN an email and auth token + const email = 'user@example.com'; + const authToken = 'jwt-token'; + + // WHEN setEmail is called + IterableApi.setEmail(email, authToken); + + // THEN RNIterableAPI.setEmail is called with email and auth token + expect(MockRNIterableAPI.setEmail).toBeCalledWith(email, authToken); + }); + + it('should call RNIterableAPI.setEmail with null email', () => { + // GIVEN null email + const email = null; + + // WHEN setEmail is called + IterableApi.setEmail(email); + + // THEN RNIterableAPI.setEmail is called with null email + expect(MockRNIterableAPI.setEmail).toBeCalledWith(null, undefined); + }); + }); + + describe('getEmail', () => { + it('should return the email from RNIterableAPI', async () => { + // GIVEN a mock email + const expectedEmail = 'user@example.com'; + MockRNIterableAPI.email = expectedEmail; + + // WHEN getEmail is called + const result = await IterableApi.getEmail(); + + // THEN the email is returned + expect(result).toBe(expectedEmail); + }); + }); + + describe('setUserId', () => { + it('should call RNIterableAPI.setUserId with userId only', () => { + // GIVEN a userId + const userId = 'user123'; + + // WHEN setUserId is called + IterableApi.setUserId(userId); + + // THEN RNIterableAPI.setUserId is called with userId + expect(MockRNIterableAPI.setUserId).toBeCalledWith(userId, undefined); + }); + + it('should call RNIterableAPI.setUserId with userId and auth token', () => { + // GIVEN a userId and auth token + const userId = 'user123'; + const authToken = 'jwt-token'; + + // WHEN setUserId is called + IterableApi.setUserId(userId, authToken); + + // THEN RNIterableAPI.setUserId is called with userId and auth token + expect(MockRNIterableAPI.setUserId).toBeCalledWith(userId, authToken); + }); + + it('should call RNIterableAPI.setUserId with null userId', () => { + // GIVEN null userId + const userId = null; + + // WHEN setUserId is called + IterableApi.setUserId(userId); + + // THEN RNIterableAPI.setUserId is called with null userId + expect(MockRNIterableAPI.setUserId).toBeCalledWith(null, undefined); + }); + + it('should call RNIterableAPI.setUserId with undefined userId', () => { + // GIVEN undefined userId + const userId = undefined; + + // WHEN setUserId is called + IterableApi.setUserId(userId); + + // THEN RNIterableAPI.setUserId is called with undefined userId + expect(MockRNIterableAPI.setUserId).toBeCalledWith(undefined, undefined); + }); + }); + + describe('getUserId', () => { + it('should return the userId from RNIterableAPI', async () => { + // GIVEN a mock userId + const expectedUserId = 'user123'; + MockRNIterableAPI.userId = expectedUserId; + + // WHEN getUserId is called + const result = await IterableApi.getUserId(); + + // THEN the userId is returned + expect(result).toBe(expectedUserId); + }); + }); + + describe('disableDeviceForCurrentUser', () => { + it('should call RNIterableAPI.disableDeviceForCurrentUser', () => { + // GIVEN no parameters + // WHEN disableDeviceForCurrentUser is called + IterableApi.disableDeviceForCurrentUser(); + + // THEN RNIterableAPI.disableDeviceForCurrentUser is called + expect(MockRNIterableAPI.disableDeviceForCurrentUser).toBeCalled(); + }); + }); + + describe('updateUser', () => { + it('should call RNIterableAPI.updateUser with data fields and merge flag', () => { + // GIVEN data fields and merge flag + const dataFields = { name: 'John', age: 30 }; + const mergeNestedObjects = true; + + // WHEN updateUser is called + IterableApi.updateUser(dataFields, mergeNestedObjects); + + // THEN RNIterableAPI.updateUser is called with correct parameters + expect(MockRNIterableAPI.updateUser).toBeCalledWith( + dataFields, + mergeNestedObjects + ); + }); + + it('should call RNIterableAPI.updateUser with mergeNestedObjects false', () => { + // GIVEN data fields and merge flag set to false + const dataFields = { name: 'Jane' }; + const mergeNestedObjects = false; + + // WHEN updateUser is called + IterableApi.updateUser(dataFields, mergeNestedObjects); + + // THEN RNIterableAPI.updateUser is called with correct parameters + expect(MockRNIterableAPI.updateUser).toBeCalledWith( + dataFields, + mergeNestedObjects + ); + }); + }); + + describe('updateEmail', () => { + it('should call RNIterableAPI.updateEmail with email only', () => { + // GIVEN a new email + const email = 'newuser@example.com'; + + // WHEN updateEmail is called + IterableApi.updateEmail(email); + + // THEN RNIterableAPI.updateEmail is called with email + expect(MockRNIterableAPI.updateEmail).toBeCalledWith(email, undefined); + }); + + it('should call RNIterableAPI.updateEmail with email and auth token', () => { + // GIVEN a new email and auth token + const email = 'newuser@example.com'; + const authToken = 'new-jwt-token'; + + // WHEN updateEmail is called + IterableApi.updateEmail(email, authToken); + + // THEN RNIterableAPI.updateEmail is called with email and auth token + expect(MockRNIterableAPI.updateEmail).toBeCalledWith(email, authToken); + }); + + it('should call RNIterableAPI.updateEmail with null auth token', () => { + // GIVEN a new email and null auth token + const email = 'newuser@example.com'; + const authToken = null; + + // WHEN updateEmail is called + IterableApi.updateEmail(email, authToken); + + // THEN RNIterableAPI.updateEmail is called with email and null auth token + expect(MockRNIterableAPI.updateEmail).toBeCalledWith(email, null); + }); + }); + + // ====================================================== // + // ===================== TRACKING ====================== // + // ====================================================== // + + describe('trackPushOpenWithCampaignId', () => { + it('should call RNIterableAPI.trackPushOpenWithCampaignId with all parameters', () => { + // GIVEN push open parameters + const params = { + campaignId: 123, + templateId: 456, + messageId: 'msg123', + appAlreadyRunning: false, + dataFields: { source: 'push' }, + }; + + // WHEN trackPushOpenWithCampaignId is called + IterableApi.trackPushOpenWithCampaignId(params); + + // THEN RNIterableAPI.trackPushOpenWithCampaignId is called with correct parameters + expect(MockRNIterableAPI.trackPushOpenWithCampaignId).toBeCalledWith( + params.campaignId, + params.templateId, + params.messageId, + params.appAlreadyRunning, + params.dataFields + ); + }); + + it('should call RNIterableAPI.trackPushOpenWithCampaignId without dataFields', () => { + // GIVEN push open parameters without dataFields + const params = { + campaignId: 123, + templateId: 456, + messageId: 'msg123', + appAlreadyRunning: true, + }; + + // WHEN trackPushOpenWithCampaignId is called + IterableApi.trackPushOpenWithCampaignId(params); + + // THEN RNIterableAPI.trackPushOpenWithCampaignId is called with correct parameters + expect(MockRNIterableAPI.trackPushOpenWithCampaignId).toBeCalledWith( + params.campaignId, + params.templateId, + params.messageId, + params.appAlreadyRunning, + undefined + ); + }); + + it('should call RNIterableAPI.trackPushOpenWithCampaignId with null messageId', () => { + // GIVEN push open parameters with null messageId + const params = { + campaignId: 123, + templateId: 456, + messageId: null, + appAlreadyRunning: false, + }; + + // WHEN trackPushOpenWithCampaignId is called + IterableApi.trackPushOpenWithCampaignId(params); + + // THEN RNIterableAPI.trackPushOpenWithCampaignId is called with correct parameters + expect(MockRNIterableAPI.trackPushOpenWithCampaignId).toBeCalledWith( + params.campaignId, + params.templateId, + null, + params.appAlreadyRunning, + undefined + ); + }); + }); + + describe('trackPurchase', () => { + it('should call RNIterableAPI.trackPurchase with all parameters', () => { + // GIVEN purchase parameters + const total = 99.99; + const items = [ + new IterableCommerceItem('item1', 'Product 1', 49.99, 1), + new IterableCommerceItem('item2', 'Product 2', 49.99, 1), + ]; + const dataFields = { currency: 'USD', discount: 10 }; + + // WHEN trackPurchase is called + IterableApi.trackPurchase({ total, items, dataFields }); + + // THEN RNIterableAPI.trackPurchase is called with correct parameters + expect(MockRNIterableAPI.trackPurchase).toBeCalledWith( + total, + items, + dataFields + ); + }); + + it('should call RNIterableAPI.trackPurchase without dataFields', () => { + // GIVEN purchase parameters without dataFields + const total = 50.0; + const items = [new IterableCommerceItem('item1', 'Product 1', 50.0, 1)]; + + // WHEN trackPurchase is called + IterableApi.trackPurchase({ total, items }); + + // THEN RNIterableAPI.trackPurchase is called with correct parameters + expect(MockRNIterableAPI.trackPurchase).toBeCalledWith( + total, + items, + undefined + ); + }); + }); + + describe('trackInAppOpen', () => { + it('should call RNIterableAPI.trackInAppOpen with message and location', () => { + // GIVEN an in-app message and location + const message = new IterableInAppMessage( + 'msg123', + 456, + new IterableInAppTrigger(IterableInAppTriggerType.immediate), + new Date(), + new Date(), + false, + undefined, + undefined, + false, + 0 + ); + const location = IterableInAppLocation.inApp; + + // WHEN trackInAppOpen is called + IterableApi.trackInAppOpen({ message, location }); + + // THEN RNIterableAPI.trackInAppOpen is called with correct parameters + expect(MockRNIterableAPI.trackInAppOpen).toBeCalledWith( + message.messageId, + location + ); + }); + }); + + describe('trackInAppClick', () => { + it('should call RNIterableAPI.trackInAppClick with message, location, and clickedUrl', () => { + // GIVEN an in-app message, location, and clicked URL + const message = new IterableInAppMessage( + 'msg123', + 456, + new IterableInAppTrigger(IterableInAppTriggerType.immediate), + new Date(), + new Date(), + false, + undefined, + undefined, + false, + 0 + ); + const location = IterableInAppLocation.inApp; + const clickedUrl = 'https://example.com'; + + // WHEN trackInAppClick is called + IterableApi.trackInAppClick({ message, location, clickedUrl }); + + // THEN RNIterableAPI.trackInAppClick is called with correct parameters + expect(MockRNIterableAPI.trackInAppClick).toBeCalledWith( + message.messageId, + location, + clickedUrl + ); + }); + }); + + describe('trackInAppClose', () => { + it('should call RNIterableAPI.trackInAppClose with message, location, and source', () => { + // GIVEN an in-app message, location, and source + const message = new IterableInAppMessage( + 'msg123', + 456, + new IterableInAppTrigger(IterableInAppTriggerType.immediate), + new Date(), + new Date(), + false, + undefined, + undefined, + false, + 0 + ); + const location = IterableInAppLocation.inApp; + const source = IterableInAppCloseSource.back; + + // WHEN trackInAppClose is called + IterableApi.trackInAppClose({ message, location, source }); + + // THEN RNIterableAPI.trackInAppClose is called with correct parameters + expect(MockRNIterableAPI.trackInAppClose).toBeCalledWith( + message.messageId, + location, + source, + undefined + ); + }); + + it('should call RNIterableAPI.trackInAppClose with clickedUrl when provided', () => { + // GIVEN an in-app message, location, source, and clicked URL + const message = new IterableInAppMessage( + 'msg123', + 456, + new IterableInAppTrigger(IterableInAppTriggerType.immediate), + new Date(), + new Date(), + false, + undefined, + undefined, + false, + 0 + ); + const location = IterableInAppLocation.inApp; + const source = IterableInAppCloseSource.link; + const clickedUrl = 'https://example.com'; + + // WHEN trackInAppClose is called + IterableApi.trackInAppClose({ message, location, source, clickedUrl }); + + // THEN RNIterableAPI.trackInAppClose is called with correct parameters + expect(MockRNIterableAPI.trackInAppClose).toBeCalledWith( + message.messageId, + location, + source, + clickedUrl + ); + }); + }); + + describe('trackEvent', () => { + it('should call RNIterableAPI.trackEvent with name and dataFields', () => { + // GIVEN event name and data fields + const name = 'customEvent'; + const dataFields = { category: 'user_action', value: 100 }; + + // WHEN trackEvent is called + IterableApi.trackEvent({ name, dataFields }); + + // THEN RNIterableAPI.trackEvent is called with correct parameters + expect(MockRNIterableAPI.trackEvent).toBeCalledWith(name, dataFields); + }); + + it('should call RNIterableAPI.trackEvent with name only', () => { + // GIVEN event name only + const name = 'simpleEvent'; + + // WHEN trackEvent is called + IterableApi.trackEvent({ name }); + + // THEN RNIterableAPI.trackEvent is called with correct parameters + expect(MockRNIterableAPI.trackEvent).toBeCalledWith(name, undefined); + }); + }); + + // ====================================================== // + // ======================= AUTH ======================= // + // ====================================================== // + + describe('pauseAuthRetries', () => { + it('should call RNIterableAPI.pauseAuthRetries with true', () => { + // GIVEN pauseRetry is true + const pauseRetry = true; + + // WHEN pauseAuthRetries is called + IterableApi.pauseAuthRetries(pauseRetry); + + // THEN RNIterableAPI.pauseAuthRetries is called with true + expect(MockRNIterableAPI.pauseAuthRetries).toBeCalledWith(true); + }); + + it('should call RNIterableAPI.pauseAuthRetries with false', () => { + // GIVEN pauseRetry is false + const pauseRetry = false; + + // WHEN pauseAuthRetries is called + IterableApi.pauseAuthRetries(pauseRetry); + + // THEN RNIterableAPI.pauseAuthRetries is called with false + expect(MockRNIterableAPI.pauseAuthRetries).toBeCalledWith(false); + }); + }); + + describe('passAlongAuthToken', () => { + it('should call RNIterableAPI.passAlongAuthToken with valid token', () => { + // GIVEN a valid auth token + const authToken = 'valid-jwt-token'; + + // WHEN passAlongAuthToken is called + IterableApi.passAlongAuthToken(authToken); + + // THEN RNIterableAPI.passAlongAuthToken is called with the token + expect(MockRNIterableAPI.passAlongAuthToken).toBeCalledWith(authToken); + }); + + it('should call RNIterableAPI.passAlongAuthToken with null token', () => { + // GIVEN a null auth token + const authToken = null; + + // WHEN passAlongAuthToken is called + IterableApi.passAlongAuthToken(authToken); + + // THEN RNIterableAPI.passAlongAuthToken is called with null + expect(MockRNIterableAPI.passAlongAuthToken).toBeCalledWith(null); + }); + + it('should call RNIterableAPI.passAlongAuthToken with undefined token', () => { + // GIVEN an undefined auth token + const authToken = undefined; + + // WHEN passAlongAuthToken is called + IterableApi.passAlongAuthToken(authToken); + + // THEN RNIterableAPI.passAlongAuthToken is called with undefined + expect(MockRNIterableAPI.passAlongAuthToken).toBeCalledWith(undefined); + }); + }); + + // ====================================================== // + // ======================= IN-APP ======================= // + // ====================================================== // + + describe('inAppConsume', () => { + it('should call RNIterableAPI.inAppConsume with message, location, and source', () => { + // GIVEN an in-app message, location, and delete source + const message = new IterableInAppMessage( + 'msg123', + 456, + new IterableInAppTrigger(IterableInAppTriggerType.immediate), + new Date(), + new Date(), + false, + undefined, + undefined, + false, + 0 + ); + const location = IterableInAppLocation.inApp; + const source = IterableInAppDeleteSource.deleteButton; + + // WHEN inAppConsume is called + IterableApi.inAppConsume(message, location, source); + + // THEN RNIterableAPI.inAppConsume is called with correct parameters + expect(MockRNIterableAPI.inAppConsume).toBeCalledWith( + message.messageId, + location, + source + ); + }); + }); + + describe('getInAppMessages', () => { + it('should return in-app messages from RNIterableAPI', async () => { + // GIVEN mock in-app messages + const mockMessages = [ + new IterableInAppMessage( + 'msg1', + 123, + new IterableInAppTrigger(IterableInAppTriggerType.immediate), + new Date(), + new Date(), + false, + undefined, + undefined, + false, + 0 + ), + new IterableInAppMessage( + 'msg2', + 456, + new IterableInAppTrigger(IterableInAppTriggerType.event), + new Date(), + new Date(), + true, + undefined, + undefined, + false, + 0 + ), + ]; + MockRNIterableAPI.messages = mockMessages; + + // WHEN getInAppMessages is called + const result = await IterableApi.getInAppMessages(); + + // THEN the messages are returned + expect(result).toBe(mockMessages); + }); + }); + + describe('getInboxMessages', () => { + it('should return inbox messages from RNIterableAPI', async () => { + // GIVEN mock inbox messages + const mockMessages = [ + new IterableInAppMessage( + 'msg1', + 123, + new IterableInAppTrigger(IterableInAppTriggerType.immediate), + new Date(), + new Date(), + true, // saveToInbox + undefined, + undefined, + false, + 0 + ), + ]; + MockRNIterableAPI.messages = mockMessages; + + // WHEN getInboxMessages is called + const result = await IterableApi.getInboxMessages(); + + // THEN the messages are returned + expect(result).toBe(mockMessages); + }); + }); + + describe('showMessage', () => { + it('should call RNIterableAPI.showMessage with messageId and consume flag', async () => { + // GIVEN a message ID and consume flag + const messageId = 'msg123'; + const consume = true; + const expectedUrl = 'https://example.com'; + MockRNIterableAPI.clickedUrl = expectedUrl; + + // WHEN showMessage is called + const result = await IterableApi.showMessage(messageId, consume); + + // THEN RNIterableAPI.showMessage is called with correct parameters + expect(MockRNIterableAPI.showMessage).toBeCalledWith(messageId, consume); + expect(result).toBe(expectedUrl); + }); + + it('should call RNIterableAPI.showMessage with consume set to false', async () => { + // GIVEN a message ID and consume flag set to false + const messageId = 'msg123'; + const consume = false; + + // WHEN showMessage is called + await IterableApi.showMessage(messageId, consume); + + // THEN RNIterableAPI.showMessage is called with consume set to false + expect(MockRNIterableAPI.showMessage).toBeCalledWith(messageId, false); + }); + }); + + describe('removeMessage', () => { + it('should call RNIterableAPI.removeMessage with messageId, location, and source', () => { + // GIVEN a message ID, location, and source + const messageId = 'msg123'; + const location = 1; // IterableInAppLocation.inApp + const source = 2; // IterableInAppDeleteSource.deleteButton + + // WHEN removeMessage is called + IterableApi.removeMessage(messageId, location, source); + + // THEN RNIterableAPI.removeMessage is called with correct parameters + expect(MockRNIterableAPI.removeMessage).toBeCalledWith( + messageId, + location, + source + ); + }); + }); + + describe('setReadForMessage', () => { + it('should call RNIterableAPI.setReadForMessage with messageId and read status', () => { + // GIVEN a message ID and read status + const messageId = 'msg123'; + const read = true; + + // WHEN setReadForMessage is called + IterableApi.setReadForMessage(messageId, read); + + // THEN RNIterableAPI.setReadForMessage is called with correct parameters + expect(MockRNIterableAPI.setReadForMessage).toBeCalledWith( + messageId, + read + ); + }); + + it('should call RNIterableAPI.setReadForMessage with read set to false', () => { + // GIVEN a message ID and read status set to false + const messageId = 'msg123'; + const read = false; + + // WHEN setReadForMessage is called + IterableApi.setReadForMessage(messageId, read); + + // THEN RNIterableAPI.setReadForMessage is called with read set to false + expect(MockRNIterableAPI.setReadForMessage).toBeCalledWith( + messageId, + false + ); + }); + }); + + describe('setAutoDisplayPaused', () => { + it('should call RNIterableAPI.setAutoDisplayPaused with true', () => { + // GIVEN autoDisplayPaused is true + const autoDisplayPaused = true; + + // WHEN setAutoDisplayPaused is called + IterableApi.setAutoDisplayPaused(autoDisplayPaused); + + // THEN RNIterableAPI.setAutoDisplayPaused is called with true + expect(MockRNIterableAPI.setAutoDisplayPaused).toBeCalledWith(true); + }); + + it('should call RNIterableAPI.setAutoDisplayPaused with false', () => { + // GIVEN autoDisplayPaused is false + const autoDisplayPaused = false; + + // WHEN setAutoDisplayPaused is called + IterableApi.setAutoDisplayPaused(autoDisplayPaused); + + // THEN RNIterableAPI.setAutoDisplayPaused is called with false + expect(MockRNIterableAPI.setAutoDisplayPaused).toBeCalledWith(false); + }); + }); + + describe('getHtmlInAppContentForMessage', () => { + it('should call RNIterableAPI.getHtmlInAppContentForMessage with messageId', async () => { + // GIVEN a message ID + const messageId = 'msg123'; + const mockContent = { html: '
Test content
' }; + MockRNIterableAPI.getHtmlInAppContentForMessage = jest + .fn() + .mockResolvedValue(mockContent); + + // WHEN getHtmlInAppContentForMessage is called + const result = await IterableApi.getHtmlInAppContentForMessage(messageId); + + // THEN RNIterableAPI.getHtmlInAppContentForMessage is called with messageId + expect(MockRNIterableAPI.getHtmlInAppContentForMessage).toBeCalledWith( + messageId + ); + expect(result).toBe(mockContent); + }); + }); + + describe('setInAppShowResponse', () => { + it('should call RNIterableAPI.setInAppShowResponse with response', () => { + // GIVEN an in-app show response + const response = IterableInAppShowResponse.show; + + // WHEN setInAppShowResponse is called + IterableApi.setInAppShowResponse(response); + + // THEN RNIterableAPI.setInAppShowResponse is called with response + expect(MockRNIterableAPI.setInAppShowResponse).toBeCalledWith(response); + }); + }); + + describe('startSession', () => { + it('should call RNIterableAPI.startSession with visible rows', () => { + // GIVEN visible rows + const visibleRows: IterableInboxImpressionRowInfo[] = [ + { messageId: 'msg1', silentInbox: true }, + { messageId: 'msg2', silentInbox: false }, + ]; + + // WHEN startSession is called + IterableApi.startSession(visibleRows); + + // THEN RNIterableAPI.startSession is called with visible rows + expect(MockRNIterableAPI.startSession).toBeCalledWith(visibleRows); + }); + }); + + describe('endSession', () => { + it('should call RNIterableAPI.endSession', () => { + // GIVEN no parameters + // WHEN endSession is called + IterableApi.endSession(); + + // THEN RNIterableAPI.endSession is called + expect(MockRNIterableAPI.endSession).toBeCalled(); + }); + }); + + describe('updateVisibleRows', () => { + it('should call RNIterableAPI.updateVisibleRows with visible rows', () => { + // GIVEN visible rows + const visibleRows: IterableInboxImpressionRowInfo[] = [ + { messageId: 'msg1', silentInbox: true }, + ]; + + // WHEN updateVisibleRows is called + IterableApi.updateVisibleRows(visibleRows); + + // THEN RNIterableAPI.updateVisibleRows is called with visible rows + expect(MockRNIterableAPI.updateVisibleRows).toBeCalledWith(visibleRows); + }); + + it('should call RNIterableAPI.updateVisibleRows with empty array when no rows provided', () => { + // GIVEN no visible rows + // WHEN updateVisibleRows is called without parameters + IterableApi.updateVisibleRows(); + + // THEN RNIterableAPI.updateVisibleRows is called with empty array + expect(MockRNIterableAPI.updateVisibleRows).toBeCalledWith([]); + }); + }); + + // ====================================================== // + // ======================= MOSC ======================= // + // ====================================================== // + + describe('updateCart', () => { + it('should call RNIterableAPI.updateCart with items', () => { + // GIVEN cart items + const items = [ + new IterableCommerceItem('item1', 'Product 1', 25.99, 2), + new IterableCommerceItem('item2', 'Product 2', 15.99, 1), + ]; + + // WHEN updateCart is called + IterableApi.updateCart(items); + + // THEN RNIterableAPI.updateCart is called with items + expect(MockRNIterableAPI.updateCart).toBeCalledWith(items); + }); + }); + + describe('wakeApp', () => { + it('should call RNIterableAPI.wakeApp on Android', () => { + // GIVEN Android platform + const originalPlatform = Platform.OS; + Object.defineProperty(Platform, 'OS', { + value: 'android', + writable: true, + }); + + // WHEN wakeApp is called + IterableApi.wakeApp(); + + // THEN RNIterableAPI.wakeApp is called + expect(MockRNIterableAPI.wakeApp).toBeCalled(); + + // Restore original platform + Object.defineProperty(Platform, 'OS', { + value: originalPlatform, + writable: true, + }); + }); + + it('should not call RNIterableAPI.wakeApp on iOS', () => { + // GIVEN iOS platform + const originalPlatform = Platform.OS; + Object.defineProperty(Platform, 'OS', { + value: 'ios', + writable: true, + }); + + // WHEN wakeApp is called + IterableApi.wakeApp(); + + // THEN RNIterableAPI.wakeApp is not called + expect(MockRNIterableAPI.wakeApp).not.toBeCalled(); + + // Restore original platform + Object.defineProperty(Platform, 'OS', { + value: originalPlatform, + writable: true, + }); + }); + }); + + describe('handleAppLink', () => { + it('should call RNIterableAPI.handleAppLink with link', () => { + // GIVEN a link + const link = 'https://example.com/deep-link'; + + // WHEN handleAppLink is called + IterableApi.handleAppLink(link); + + // THEN RNIterableAPI.handleAppLink is called with link + expect(MockRNIterableAPI.handleAppLink).toBeCalledWith(link); + }); + }); + + describe('updateSubscriptions', () => { + it('should call RNIterableAPI.updateSubscriptions with all parameters', () => { + // GIVEN subscription parameters + const params = { + emailListIds: [1, 2, 3], + unsubscribedChannelIds: [4, 5], + unsubscribedMessageTypeIds: [6, 7, 8], + subscribedMessageTypeIds: [9, 10], + campaignId: 11, + templateId: 12, + }; + + // WHEN updateSubscriptions is called + IterableApi.updateSubscriptions(params); + + // THEN RNIterableAPI.updateSubscriptions is called with correct parameters + expect(MockRNIterableAPI.updateSubscriptions).toBeCalledWith( + params.emailListIds, + params.unsubscribedChannelIds, + params.unsubscribedMessageTypeIds, + params.subscribedMessageTypeIds, + params.campaignId, + params.templateId + ); + }); + + it('should call RNIterableAPI.updateSubscriptions with null arrays', () => { + // GIVEN subscription parameters with null arrays + const params = { + emailListIds: null, + unsubscribedChannelIds: null, + unsubscribedMessageTypeIds: null, + subscribedMessageTypeIds: null, + campaignId: 11, + templateId: 12, + }; + + // WHEN updateSubscriptions is called + IterableApi.updateSubscriptions(params); + + // THEN RNIterableAPI.updateSubscriptions is called with null arrays + expect(MockRNIterableAPI.updateSubscriptions).toBeCalledWith( + null, + null, + null, + null, + params.campaignId, + params.templateId + ); + }); + }); + + describe('getLastPushPayload', () => { + it('should return the last push payload from RNIterableAPI', async () => { + // GIVEN a mock push payload + const mockPayload = { + campaignId: 123, + templateId: 456, + messageId: 'msg123', + customData: { key: 'value' }, + }; + MockRNIterableAPI.lastPushPayload = mockPayload; + + // WHEN getLastPushPayload is called + const result = await IterableApi.getLastPushPayload(); + + // THEN the push payload is returned + expect(result).toBe(mockPayload); + }); + }); + + describe('getAttributionInfo', () => { + it('should return IterableAttributionInfo when attribution info exists', async () => { + // GIVEN mock attribution info + const mockAttributionDict = { + campaignId: 123, + templateId: 456, + messageId: 'msg123', + }; + MockRNIterableAPI.getAttributionInfo = jest + .fn() + .mockResolvedValue(mockAttributionDict); + + // WHEN getAttributionInfo is called + const result = await IterableApi.getAttributionInfo(); + + // THEN IterableAttributionInfo is returned + expect(result).toBeInstanceOf(IterableAttributionInfo); + expect(result?.campaignId).toBe(123); + expect(result?.templateId).toBe(456); + expect(result?.messageId).toBe('msg123'); + }); + + it('should return undefined when attribution info is null', async () => { + // GIVEN null attribution info + MockRNIterableAPI.getAttributionInfo = jest.fn().mockResolvedValue(null); + + // WHEN getAttributionInfo is called + const result = await IterableApi.getAttributionInfo(); + + // THEN undefined is returned + expect(result).toBeUndefined(); + }); + }); + + describe('setAttributionInfo', () => { + it('should call RNIterableAPI.setAttributionInfo with attribution info', () => { + // GIVEN attribution info + const attributionInfo = new IterableAttributionInfo(123, 456, 'msg123'); + + // WHEN setAttributionInfo is called + IterableApi.setAttributionInfo(attributionInfo); + + // THEN RNIterableAPI.setAttributionInfo is called with attribution info + expect(MockRNIterableAPI.setAttributionInfo).toBeCalledWith( + attributionInfo + ); + }); + + it('should call RNIterableAPI.setAttributionInfo with undefined', () => { + // GIVEN undefined attribution info + const attributionInfo = undefined; + + // WHEN setAttributionInfo is called + IterableApi.setAttributionInfo(attributionInfo); + + // THEN RNIterableAPI.setAttributionInfo is called with undefined + expect(MockRNIterableAPI.setAttributionInfo).toBeCalledWith(undefined); + }); + }); +}); diff --git a/src/core/classes/IterableApi.ts b/src/core/classes/IterableApi.ts new file mode 100644 index 000000000..122b5e33a --- /dev/null +++ b/src/core/classes/IterableApi.ts @@ -0,0 +1,633 @@ +import { Platform } from 'react-native'; + +import RNIterableAPI from '../../api'; +import type { IterableHtmlInAppContent } from '../../inApp/classes/IterableHtmlInAppContent'; +import type { IterableInAppMessage } from '../../inApp/classes/IterableInAppMessage'; +import type { IterableInAppCloseSource } from '../../inApp/enums/IterableInAppCloseSource'; +import type { IterableInAppDeleteSource } from '../../inApp/enums/IterableInAppDeleteSource'; +import type { IterableInAppLocation } from '../../inApp/enums/IterableInAppLocation'; +import type { IterableInAppShowResponse } from '../../inApp/enums/IterableInAppShowResponse'; +import type { IterableInboxImpressionRowInfo } from '../../inbox/types/IterableInboxImpressionRowInfo'; +import { IterableAttributionInfo } from './IterableAttributionInfo'; +import type { IterableCommerceItem } from './IterableCommerceItem'; +import { IterableConfig } from './IterableConfig'; + +/** + * Contains functions that directly interact with the native layer. + */ +export class IterableApi { + // ====================================================== // + // ===================== INITIALIZE ===================== // + // ====================================================== // + + /** + * Initializes the Iterable React Native SDK in your app's Javascript or Typescript code. + * + * @param apiKey - The [*mobile* API + * key](https://support.iterable.com/hc/en-us/articles/360043464871-API-Keys) + * for your application + * @param config - Configuration object for the SDK + * @param version - Version of the SDK, derived from the package.json file + */ + static initializeWithApiKey( + apiKey: string, + { + config = new IterableConfig(), + version, + }: { + config: IterableConfig; + version: string; + } + ): Promise { + // IterableLogger.log('initializeWithApiKey: ', apiKey); + return RNIterableAPI.initializeWithApiKey(apiKey, config.toDict(), version); + } + + /** + * DO NOT CALL THIS METHOD. + * This method is used internally to connect to staging environment. + * + * @internal + */ + static initialize2WithApiKey( + apiKey: string, + { + config = new IterableConfig(), + version, + apiEndPoint, + }: { + config: IterableConfig; + version: string; + apiEndPoint: string; + } + ): Promise { + // IterableLogger.log('initialize2WithApiKey: ', apiKey); + return RNIterableAPI.initialize2WithApiKey( + apiKey, + config.toDict(), + version, + apiEndPoint + ); + } + + // ---- End INITIALIZE ---- // + + // ====================================================== // + // ===================== USER MANAGEMENT ================ // + // ====================================================== // + + /** + * Associate the current user with the passed in email parameter. + * + * @param email - Email address to associate with + * the current user + * @param authToken - Valid, pre-fetched JWT the SDK + * can use to authenticate API requests, optional - If null/undefined, no JWT + * related action will be taken + */ + static setEmail(email: string | null, authToken?: string | null) { + // IterableLogger.log('setEmail: ', email); + return RNIterableAPI.setEmail(email, authToken); + } + + /** + * Get the email associated with the current user. + * + * @returns The email associated with the current user + */ + static getEmail() { + // IterableLogger.log('getEmail'); + return RNIterableAPI.getEmail(); + } + + /** + * Associate the current user with the passed in `userId` parameter. + * + * WARNING: specify a user by calling `Iterable.setEmail` or + * `Iterable.setUserId`, but **NOT** both. + * + * @param userId - User ID to associate with the current user + * @param authToken - Valid, pre-fetched JWT the SDK + * can use to authenticate API requests, optional - If null/undefined, no JWT + * related action will be taken + */ + static setUserId( + userId: string | null | undefined, + authToken?: string | null + ) { + // IterableLogger.log('setUserId: ', userId); + return RNIterableAPI.setUserId(userId, authToken); + } + + /** + * Get the `userId` associated with the current user. + */ + static getUserId() { + // IterableLogger.log('getUserId'); + return RNIterableAPI.getUserId(); + } + + /** + * Disable the device for the current user. + */ + static disableDeviceForCurrentUser() { + // IterableLogger.log('disableDeviceForCurrentUser'); + return RNIterableAPI.disableDeviceForCurrentUser(); + } + + /** + * Save data to the current user's Iterable profile. + * + * @param dataFields - The data fields to update + * @param mergeNestedObjects - Whether to merge nested objects + */ + static updateUser(dataFields: unknown, mergeNestedObjects: boolean) { + // IterableLogger.log('updateUser: ', dataFields, mergeNestedObjects); + return RNIterableAPI.updateUser(dataFields, mergeNestedObjects); + } + + /** + * Change the value of the email field on the current user's Iterable profile. + * + * @param email - The new email to set + * @param authToken - The new auth token (JWT) to set with the new email, optional - If null/undefined, no JWT-related action will be taken + */ + static updateEmail(email: string, authToken?: string | null) { + // IterableLogger.log('updateEmail: ', email, authToken); + return RNIterableAPI.updateEmail(email, authToken); + } + + // ---- End USER MANAGEMENT ---- // + + // ====================================================== // + // ===================== TRACKING ====================== // + // ====================================================== // + + /** + * Create a `pushOpen` event on the current user's Iterable profile, populating + * it with data provided to the method call. + * + * @param campaignId - The campaign ID + * @param templateId - The template ID + * @param messageId - The message ID + * @param appAlreadyRunning - Whether the app is already running + * @param dataFields - The data fields to track + */ + static trackPushOpenWithCampaignId({ + campaignId, + templateId, + messageId, + appAlreadyRunning, + dataFields, + }: { + campaignId: number; + templateId: number; + messageId: string | null | undefined; + appAlreadyRunning: boolean; + dataFields?: unknown; + }) { + // IterableLogger.log( + // 'trackPushOpenWithCampaignId: ', + // campaignId, + // templateId, + // messageId, + // appAlreadyRunning, + // dataFields + // ); + return RNIterableAPI.trackPushOpenWithCampaignId( + campaignId, + templateId, + messageId, + appAlreadyRunning, + dataFields + ); + } + + /** + * Create a `purchase` event on the current user's Iterable profile, populating + * it with data provided to the method call. + * + * @param total - The total cost of the purchase + * @param items - The items included in the purchase + * @param dataFields - The data fields to track + */ + static trackPurchase({ + total, + items, + dataFields, + }: { + total: number; + items: IterableCommerceItem[]; + dataFields?: unknown; + }) { + // IterableLogger.log('trackPurchase: ', total, items, dataFields); + return RNIterableAPI.trackPurchase(total, items, dataFields); + } + + /** + * Create an `inAppOpen` event for the specified message on the current user's profile + * for manual tracking purposes. Iterable's SDK automatically tracks in-app message opens when you use the + * SDK's default rendering. + * + * @param message - The in-app message (an {@link IterableInAppMessage} object) + * @param location - The location of the in-app message (an IterableInAppLocation enum) + */ + static trackInAppOpen({ + message, + location, + }: { + message: IterableInAppMessage; + location: IterableInAppLocation; + }) { + // IterableLogger.log('trackInAppOpen: ', message, location); + return RNIterableAPI.trackInAppOpen(message.messageId, location); + } + + /** + * Create an `inAppClick` event for the specified message on the current user's profile + * for manual tracking purposes. Iterable's SDK automatically tracks in-app message clicks when you use the + * SDK's default rendering. Click events refer to click events within the in-app message to distinguish + * from `inAppOpen` events. + * + * @param message - The in-app message. + * @param location - The location of the in-app message. + * @param clickedUrl - The URL clicked by the user. + */ + static trackInAppClick({ + message, + location, + clickedUrl, + }: { + message: IterableInAppMessage; + location: IterableInAppLocation; + clickedUrl: string; + }) { + // IterableLogger.log('trackInAppClick: ', message, location, clickedUrl); + return RNIterableAPI.trackInAppClick( + message.messageId, + location, + clickedUrl + ); + } + + /** + * Create an `inAppClose` event for the specified message on the current user's profile + * for manual tracking purposes. Iterable's SDK automatically tracks in-app message close events when you use the + * SDK's default rendering. + * + * @param message - The in-app message. + * @param location - The location of the in-app message. + * @param source - The way the in-app was closed. + * @param clickedUrl - The URL clicked by the user. + */ + static trackInAppClose({ + message, + location, + source, + clickedUrl, + }: { + message: IterableInAppMessage; + location: IterableInAppLocation; + source: IterableInAppCloseSource; + clickedUrl?: string; + }) { + // IterableLogger.log( + // 'trackInAppClose: ', + // message, + // location, + // source, + // clickedUrl + // ); + return RNIterableAPI.trackInAppClose( + message.messageId, + location, + source, + clickedUrl + ); + } + + /** + * Create a custom event on the current user's Iterable profile, populating + * it with data provided to the method call. + * + * @param name - The name of the event + * @param dataFields - The data fields to track + */ + static trackEvent({ + name, + dataFields, + }: { + name: string; + dataFields?: unknown; + }) { + // IterableLogger.log('trackEvent: ', name, dataFields); + return RNIterableAPI.trackEvent(name, dataFields); + } + + // ---- End TRACKING ---- // + + // ====================================================== // + // ======================= AUTH ======================= // + // ====================================================== // + + /** + * Pause or resume the automatic retrying of authentication requests. + * + * @param pauseRetry - Whether to pause or resume the automatic retrying of authentication requests + */ + static pauseAuthRetries(pauseRetry: boolean) { + // IterableLogger.log('pauseAuthRetries: ', pauseRetry); + return RNIterableAPI.pauseAuthRetries(pauseRetry); + } + + /** + * Pass along an auth token to the SDK. + * + * @param authToken - The auth token to pass along + */ + static passAlongAuthToken(authToken: string | null | undefined) { + // IterableLogger.log('passAlongAuthToken: ', authToken); + return RNIterableAPI.passAlongAuthToken(authToken); + } + + // ---- End AUTH ---- // + + // ====================================================== // + // ======================= IN-APP ======================= // + // ====================================================== // + + /** + * Remove the specified message from the current user's message queue. + * + * @param message - The in-app message. + * @param location - The location of the in-app message. + * @param source - The way the in-app was consumed. + */ + static inAppConsume( + message: IterableInAppMessage, + location: IterableInAppLocation, + source: IterableInAppDeleteSource + ) { + // IterableLogger.log('inAppConsume: ', message, location, source); + return RNIterableAPI.inAppConsume(message.messageId, location, source); + } + + /** + * Retrieve the current user's list of in-app messages stored in the local queue. + * + * @returns A Promise that resolves to an array of in-app messages. + */ + static getInAppMessages(): Promise { + // IterableLogger.log('getInAppMessages'); + return RNIterableAPI.getInAppMessages() as unknown as Promise< + IterableInAppMessage[] + >; + } + + /** + * Retrieve the current user's list of in-app messages designated for the + * mobile inbox and stored in the local queue. + * + * @returns A Promise that resolves to an array of messages marked as `saveToInbox`. + */ + static getInboxMessages(): Promise { + // IterableLogger.log('getInboxMessages'); + return RNIterableAPI.getInboxMessages() as unknown as Promise< + IterableInAppMessage[] + >; + } + + /** + * Renders an in-app message and consumes it from the user's message queue if necessary. + * + * If you skip showing an in-app message when it arrives, you can show it at + * another time by calling this method. + * + * @param messageId - The message to show (an {@link IterableInAppMessage} object) + * @param consume - Whether or not the message should be consumed from the user's message queue after being shown. This should be defaulted to true. + */ + static showMessage( + messageId: string, + consume: boolean + ): Promise { + // IterableLogger.log('showMessage: ', messageId, consume); + return RNIterableAPI.showMessage(messageId, consume); + } + + /** + * Remove the specified message from the current user's message queue. + * + * @param messageId - The message to remove. + * @param location - The location of the message. + * @param source - The way the message was removed. + */ + static removeMessage( + messageId: string, + location: number, + source: number + ): void { + // IterableLogger.log('removeMessage: ', messageId, location, source); + return RNIterableAPI.removeMessage(messageId, location, source); + } + + /** + * Set the read status of the specified message. + * + * @param messageId - The message to set the read status of. + * @param read - Whether the message is read. + */ + static setReadForMessage(messageId: string, read: boolean): void { + // IterableLogger.log('setReadForMessage: ', messageId, read); + return RNIterableAPI.setReadForMessage(messageId, read); + } + + /** + * Pause or unpause the automatic display of incoming in-app messages + * + * @param autoDisplayPaused - Whether to pause or unpause the automatic display of incoming in-app messages + */ + static setAutoDisplayPaused(autoDisplayPaused: boolean): void { + // IterableLogger.log('setAutoDisplayPaused: ', autoDisplayPaused); + return RNIterableAPI.setAutoDisplayPaused(autoDisplayPaused); + } + + /** + * Retrieve HTML in-app content for a specified in-app message. + * + * @param messageId - The message from which to get HTML content. + * + * @returns A Promise that resolves to an {@link IterableHtmlInAppContent} object. + */ + static getHtmlInAppContentForMessage( + messageId: string + ): Promise { + // IterableLogger.log('getHtmlInAppContentForMessage: ', messageId); + return RNIterableAPI.getHtmlInAppContentForMessage(messageId); + } + + /** + * Set the response to an in-app message. + * + * @param inAppShowResponse - The response to an in-app message. + */ + static setInAppShowResponse(inAppShowResponse: IterableInAppShowResponse) { + // IterableLogger.log('setInAppShowResponse: ', inAppShowResponse); + return RNIterableAPI.setInAppShowResponse(inAppShowResponse); + } + + /** + * Start a session. + * + * @param visibleRows - The visible rows. + */ + static startSession(visibleRows: IterableInboxImpressionRowInfo[]) { + // IterableLogger.log('startSession: ', visibleRows); + return RNIterableAPI.startSession(visibleRows); + } + + /** + * End a session. + */ + static endSession() { + // IterableLogger.log('endSession'); + return RNIterableAPI.endSession(); + } + + /** + * Update the visible rows. + * + * @param visibleRows - The visible rows. + */ + static updateVisibleRows(visibleRows: IterableInboxImpressionRowInfo[] = []) { + // IterableLogger.log('updateVisibleRows: ', visibleRows); + return RNIterableAPI.updateVisibleRows(visibleRows); + } + + // ---- End IN-APP ---- // + + // ====================================================== // + // ======================= MOSC ======================= // + // ====================================================== // + + /** + * Update the cart. + * + * @param items - The items. + */ + static updateCart(items: IterableCommerceItem[]) { + // IterableLogger.log('updateCart: ', items); + return RNIterableAPI.updateCart(items); + } + + /** + * Wake the app. + * ANDROID ONLY + */ + static wakeApp() { + if (Platform.OS === 'android') { + // IterableLogger.log('wakeApp'); + return RNIterableAPI.wakeApp(); + } + } + + /** + * Handle an app link -- this is used to handle deep links. + * + * @param link - The link. + */ + static handleAppLink(link: string) { + // IterableLogger.log('handleAppLink: ', link); + return RNIterableAPI.handleAppLink(link); + } + + /** + * Update the subscriptions. + * + * @param emailListIds - The email list IDs. + * @param unsubscribedChannelIds - The unsubscribed channel IDs. + * @param unsubscribedMessageTypeIds - The unsubscribed message type IDs. + * @param subscribedMessageTypeIds - The subscribed message type IDs. + * @param campaignId - The campaign ID. + * @param templateId - The template ID. + */ + static updateSubscriptions({ + emailListIds, + unsubscribedChannelIds, + unsubscribedMessageTypeIds, + subscribedMessageTypeIds, + campaignId, + templateId, + }: { + emailListIds: number[] | null; + unsubscribedChannelIds: number[] | null; + unsubscribedMessageTypeIds: number[] | null; + subscribedMessageTypeIds: number[] | null; + campaignId: number; + templateId: number; + }) { + // IterableLogger.log( + // 'updateSubscriptions: ', + // emailListIds, + // unsubscribedChannelIds, + // unsubscribedMessageTypeIds, + // subscribedMessageTypeIds, + // campaignId, + // templateId + // ); + return RNIterableAPI.updateSubscriptions( + emailListIds, + unsubscribedChannelIds, + unsubscribedMessageTypeIds, + subscribedMessageTypeIds, + campaignId, + templateId + ); + } + + /** + * Get the last push payload. + */ + static getLastPushPayload() { + // IterableLogger.log('getLastPushPayload'); + return RNIterableAPI.getLastPushPayload(); + } + + /** + * Get the attribution info. + */ + static getAttributionInfo() { + // IterableLogger.log('getAttributionInfo'); + // FIXME: What if this errors? + return RNIterableAPI.getAttributionInfo().then( + ( + dict: { + campaignId: number; + templateId: number; + messageId: string; + } | null + ) => { + if (dict) { + return new IterableAttributionInfo( + dict.campaignId as number, + dict.templateId as number, + dict.messageId as string + ); + } else { + return undefined; + } + } + ); + } + + /** + * Set the attribution info. + * + * @param attributionInfo - The attribution info. + */ + static setAttributionInfo(attributionInfo?: IterableAttributionInfo) { + // IterableLogger.log('setAttributionInfo: ', attributionInfo); + return RNIterableAPI.setAttributionInfo(attributionInfo); + } + + // ---- End MOSC ---- // +} diff --git a/src/core/classes/IterableAuthManager.ts b/src/core/classes/IterableAuthManager.ts index 9df15c03a..44ece1af0 100644 --- a/src/core/classes/IterableAuthManager.ts +++ b/src/core/classes/IterableAuthManager.ts @@ -1,5 +1,5 @@ -import RNIterableAPI from '../../api'; import { IterableAuthResponse } from './IterableAuthResponse'; +import { IterableApi } from './IterableApi'; /** * Manages the authentication for the Iterable SDK. @@ -22,7 +22,7 @@ export class IterableAuthManager { * ``` */ pauseAuthRetries(pauseRetry: boolean) { - return RNIterableAPI.pauseAuthRetries(pauseRetry); + return IterableApi.pauseAuthRetries(pauseRetry); } /** @@ -39,6 +39,6 @@ export class IterableAuthManager { passAlongAuthToken( authToken: string | null | undefined ): Promise { - return RNIterableAPI.passAlongAuthToken(authToken); + return IterableApi.passAlongAuthToken(authToken); } } diff --git a/src/core/classes/index.ts b/src/core/classes/index.ts index 60c26047c..c201f3d7b 100644 --- a/src/core/classes/index.ts +++ b/src/core/classes/index.ts @@ -1,7 +1,9 @@ export * from './Iterable'; export * from './IterableAction'; export * from './IterableActionContext'; +export * from './IterableApi'; export * from './IterableAttributionInfo'; +export * from './IterableAuthManager'; export * from './IterableAuthResponse'; export * from './IterableCommerceItem'; export * from './IterableConfig'; diff --git a/src/inApp/classes/IterableInAppManager.ts b/src/inApp/classes/IterableInAppManager.ts index f376b35bc..ae34f80d1 100644 --- a/src/inApp/classes/IterableInAppManager.ts +++ b/src/inApp/classes/IterableInAppManager.ts @@ -1,4 +1,4 @@ -import { RNIterableAPI } from '../../api'; +import { IterableApi } from '../../core/classes/IterableApi'; import type { IterableInAppDeleteSource, IterableInAppLocation, @@ -13,6 +13,20 @@ import { IterableInAppMessage } from './IterableInAppMessage'; * displaying messages, removing messages, setting read status, and more. * * The `inAppManager` property of an `Iterable` instance is set to an instance of this class. + * + * @example + * ```typescript + * const inAppManager = new IterableInAppManager(); + * + * inAppManager.getMessages().then(messages => { + * console.log('Messages:', messages); + * }); + * + * // You can also access an instance on `Iterable.inAppManager` + * Iterable.inAppManager.getMessages().then(messages => { + * console.log('Messages:', messages); + * }); + * ``` */ export class IterableInAppManager { /** @@ -32,9 +46,7 @@ export class IterableInAppManager { * @returns A Promise that resolves to an array of in-app messages. */ getMessages(): Promise { - // Iterable?.logger?.log('InAppManager.getMessages'); - - return RNIterableAPI.getInAppMessages() as unknown as Promise< + return IterableApi.getInAppMessages() as unknown as Promise< IterableInAppMessage[] >; } @@ -57,9 +69,7 @@ export class IterableInAppManager { * @returns A Promise that resolves to an array of messages marked as `saveToInbox`. */ getInboxMessages(): Promise { - // Iterable?.logger?.log('InAppManager.getInboxMessages'); - - return RNIterableAPI.getInboxMessages() as unknown as Promise< + return IterableApi.getInboxMessages() as unknown as Promise< IterableInAppMessage[] >; } @@ -86,9 +96,7 @@ export class IterableInAppManager { message: IterableInAppMessage, consume: boolean ): Promise { - // Iterable?.logger?.log('InAppManager.show'); - - return RNIterableAPI.showMessage(message.messageId, consume); + return IterableApi.showMessage(message.messageId, consume); } /** @@ -114,9 +122,7 @@ export class IterableInAppManager { location: IterableInAppLocation, source: IterableInAppDeleteSource ): void { - // Iterable?.logger?.log('InAppManager.remove'); - - return RNIterableAPI.removeMessage(message.messageId, location, source); + return IterableApi.removeMessage(message.messageId, location, source); } /** @@ -131,9 +137,7 @@ export class IterableInAppManager { * ``` */ setReadForMessage(message: IterableInAppMessage, read: boolean) { - // Iterable?.logger?.log('InAppManager.setRead'); - - RNIterableAPI.setReadForMessage(message.messageId, read); + return IterableApi.setReadForMessage(message.messageId, read); } /** @@ -151,11 +155,7 @@ export class IterableInAppManager { getHtmlContentForMessage( message: IterableInAppMessage ): Promise { - // Iterable?.logger?.log('InAppManager.getHtmlContentForMessage'); - - return RNIterableAPI.getHtmlInAppContentForMessage( - message.messageId - ) as unknown as Promise; + return IterableApi.getHtmlInAppContentForMessage(message.messageId); } /** @@ -173,8 +173,6 @@ export class IterableInAppManager { * ``` */ setAutoDisplayPaused(paused: boolean) { - // Iterable?.logger?.log('InAppManager.setAutoDisplayPaused'); - - RNIterableAPI.setAutoDisplayPaused(paused); + return IterableApi.setAutoDisplayPaused(paused); } } diff --git a/src/inbox/classes/IterableInboxDataModel.ts b/src/inbox/classes/IterableInboxDataModel.ts index 311f5cc7c..4b81d5b22 100644 --- a/src/inbox/classes/IterableInboxDataModel.ts +++ b/src/inbox/classes/IterableInboxDataModel.ts @@ -1,5 +1,4 @@ -import { RNIterableAPI } from '../../api'; -import { Iterable } from '../../core/classes/Iterable'; +import { IterableApi } from '../../core/classes/IterableApi'; import { IterableHtmlInAppContent, IterableInAppDeleteSource, @@ -94,11 +93,7 @@ export class IterableInboxDataModel { * @returns A promise that resolves to the HTML content of the specified message. */ getHtmlContentForMessageId(id: string): Promise { - Iterable?.logger?.log( - 'IterableInboxDataModel.getHtmlContentForItem messageId: ' + id - ); - - return RNIterableAPI.getHtmlInAppContentForMessage(id).then( + return IterableApi.getHtmlInAppContentForMessage(id).then( (content: IterableHtmlInAppContentRaw) => { return IterableHtmlInAppContent.fromDict(content); } @@ -111,9 +106,7 @@ export class IterableInboxDataModel { * @param id - The unique identifier of the message to be marked as read. */ setMessageAsRead(id: string) { - Iterable?.logger?.log('IterableInboxDataModel.setMessageAsRead'); - - RNIterableAPI.setReadForMessage(id, true); + return IterableApi.setReadForMessage(id, true); } /** @@ -123,9 +116,11 @@ export class IterableInboxDataModel { * @param deleteSource - The source from which the delete action is initiated. */ deleteItemById(id: string, deleteSource: IterableInAppDeleteSource) { - Iterable?.logger?.log('IterableInboxDataModel.deleteItemById'); - - RNIterableAPI.removeMessage(id, IterableInAppLocation.inbox, deleteSource); + return IterableApi.removeMessage( + id, + IterableInAppLocation.inbox, + deleteSource + ); } /** @@ -135,7 +130,7 @@ export class IterableInboxDataModel { * If the fetch operation fails, the promise resolves to an empty array. */ async refresh(): Promise { - return RNIterableAPI.getInboxMessages().then( + return IterableApi.getInboxMessages().then( (messages: IterableInAppMessage[]) => { return this.processMessages(messages); }, @@ -151,7 +146,7 @@ export class IterableInboxDataModel { * @param visibleRows - An array of `IterableInboxImpressionRowInfo` objects representing the rows that are currently visible. */ startSession(visibleRows: IterableInboxImpressionRowInfo[] = []) { - RNIterableAPI.startSession(visibleRows as unknown as { [key: string]: string | number | boolean }[]); + return IterableApi.startSession(visibleRows); } /** @@ -162,7 +157,7 @@ export class IterableInboxDataModel { */ async endSession(visibleRows: IterableInboxImpressionRowInfo[] = []) { await this.updateVisibleRows(visibleRows); - RNIterableAPI.endSession(); + return IterableApi.endSession(); } /** @@ -178,7 +173,7 @@ export class IterableInboxDataModel { * Defaults to an empty array if not provided. */ updateVisibleRows(visibleRows: IterableInboxImpressionRowInfo[] = []) { - RNIterableAPI.updateVisibleRows(visibleRows as unknown as { [key: string]: string | number | boolean }[]); + return IterableApi.updateVisibleRows(visibleRows); } /**