From 85e28d7004d89ad297549bc7ec1b1b43e7ed51f0 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 7 Oct 2025 00:23:02 -0700 Subject: [PATCH 01/45] feat: introduce IterableAuthManager for authentication handling --- src/core/classes/Iterable.ts | 59 ++++++++++++----------- src/core/classes/IterableAuthManager.ts | 29 +++++++++++ src/inApp/classes/IterableInAppManager.ts | 38 ++++++++++----- 3 files changed, 86 insertions(+), 40 deletions(-) create mode 100644 src/core/classes/IterableAuthManager.ts diff --git a/src/core/classes/Iterable.ts b/src/core/classes/Iterable.ts index 440f65e25..d17f55397 100644 --- a/src/core/classes/Iterable.ts +++ b/src/core/classes/Iterable.ts @@ -13,7 +13,7 @@ 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 { IterableInAppManager } from '../../inApp/classes/IterableInAppManager'; import { IterableAction } from './IterableAction'; import { IterableActionContext } from './IterableActionContext'; @@ -23,6 +23,7 @@ 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); @@ -56,6 +57,19 @@ export class Iterable { */ static savedConfig: IterableConfig = new IterableConfig(); + /** + * Auth manager for the Iterable SDK + * + * This property is set to an instance of provides access to authentication functionality including + * pausing authentication retries. + * + * @example + * ```typescript + * Iterable.authManager.pauseAuthRetries(true); + * ``` + */ + static authManager: IterableAuthManager | undefined; + /** * In-app message manager for the current user. * @@ -73,21 +87,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 | undefined; /** * Initializes the Iterable React Native SDK in your app's Javascript or Typescript code. @@ -124,13 +124,7 @@ export class Iterable { apiKey: string, config: IterableConfig = new IterableConfig() ): Promise { - Iterable.savedConfig = config; - - Iterable.logger = new IterableLogger(Iterable.savedConfig); - - Iterable?.logger?.log('initialize: ' + apiKey); - - this.setupEventHandlers(); + this.setupIterable(config); const version = this.getVersionFromPackageJson(); @@ -148,13 +142,8 @@ export class Iterable { config: IterableConfig = new IterableConfig(), apiEndPoint: string ): Promise { - Iterable.savedConfig = config; - - Iterable.logger = new IterableLogger(Iterable.savedConfig); + this.setupIterable(config); - Iterable?.logger?.log('initialize2: ' + apiKey); - - this.setupEventHandlers(); const version = this.getVersionFromPackageJson(); return RNIterableAPI.initialize2WithApiKey( @@ -165,6 +154,18 @@ export class Iterable { ); } + private static setupIterable(config: IterableConfig = new IterableConfig()) { + Iterable.savedConfig = config; + + const logger = new IterableLogger(Iterable.savedConfig); + + Iterable.logger = logger; + Iterable.authManager = new IterableAuthManager(logger); + Iterable.inAppManager = new IterableInAppManager(logger); + + this.setupEventHandlers(); + } + /** * Associate the current user with the passed in email parameter. * diff --git a/src/core/classes/IterableAuthManager.ts b/src/core/classes/IterableAuthManager.ts new file mode 100644 index 000000000..a81549667 --- /dev/null +++ b/src/core/classes/IterableAuthManager.ts @@ -0,0 +1,29 @@ +import RNIterableAPI from '../../api'; +import { IterableConfig } from './IterableConfig'; +import { IterableLogger } from './IterableLogger'; + +export class IterableAuthManager { + /** + * The logger for the Iterable SDK. + */ + private _logger: IterableLogger = new IterableLogger(new IterableConfig()); + + constructor(logger: IterableLogger) { + this._logger = logger; + } + + /** + * Pause the authentication retry mechanism. + * + * @param pauseRetry - Whether to pause the authentication retry mechanism + * + * @example + * ```typescript + * Iterable.pauseAuthRetries(true); + * ``` + */ + pauseAuthRetries(pauseRetry: boolean) { + this._logger?.log('pauseAuthRetries'); + RNIterableAPI.pauseAuthRetries(pauseRetry); + } +} diff --git a/src/inApp/classes/IterableInAppManager.ts b/src/inApp/classes/IterableInAppManager.ts index 2d555727f..29c99deed 100644 --- a/src/inApp/classes/IterableInAppManager.ts +++ b/src/inApp/classes/IterableInAppManager.ts @@ -1,5 +1,6 @@ import { RNIterableAPI } from '../../api'; -import { Iterable } from '../../core/classes/Iterable'; +import { IterableConfig } from '../../core/classes/IterableConfig'; +import { IterableLogger } from '../../core/classes/IterableLogger'; import type { IterableInAppDeleteSource, IterableInAppLocation, @@ -16,6 +17,15 @@ import { IterableInAppMessage } from './IterableInAppMessage'; * The `inAppManager` property of an `Iterable` instance is set to an instance of this class. */ export class IterableInAppManager { + /** + * The logger for the Iterable SDK. + */ + private _logger: IterableLogger = new IterableLogger(new IterableConfig()); + + constructor(logger: IterableLogger) { + this._logger = logger; + } + /** * Retrieve the current user's list of in-app messages stored in the local queue. * @@ -33,9 +43,11 @@ export class IterableInAppManager { * @returns A Promise that resolves to an array of in-app messages. */ getMessages(): Promise { - Iterable?.logger?.log('InAppManager.getMessages'); + this._logger?.log('InAppManager.getMessages'); - return RNIterableAPI.getInAppMessages() as unknown as Promise; + return RNIterableAPI.getInAppMessages() as unknown as Promise< + IterableInAppMessage[] + >; } /** @@ -56,9 +68,11 @@ export class IterableInAppManager { * @returns A Promise that resolves to an array of messages marked as `saveToInbox`. */ getInboxMessages(): Promise { - Iterable?.logger?.log('InAppManager.getInboxMessages'); + this?._logger?.log('InAppManager.getInboxMessages'); - return RNIterableAPI.getInboxMessages() as unknown as Promise; + return RNIterableAPI.getInboxMessages() as unknown as Promise< + IterableInAppMessage[] + >; } /** @@ -83,7 +97,7 @@ export class IterableInAppManager { message: IterableInAppMessage, consume: boolean ): Promise { - Iterable?.logger?.log('InAppManager.show'); + this?._logger?.log('InAppManager.show'); return RNIterableAPI.showMessage(message.messageId, consume); } @@ -111,7 +125,7 @@ export class IterableInAppManager { location: IterableInAppLocation, source: IterableInAppDeleteSource ): void { - Iterable?.logger?.log('InAppManager.remove'); + this?._logger?.log('InAppManager.remove'); return RNIterableAPI.removeMessage(message.messageId, location, source); } @@ -128,7 +142,7 @@ export class IterableInAppManager { * ``` */ setReadForMessage(message: IterableInAppMessage, read: boolean) { - Iterable?.logger?.log('InAppManager.setRead'); + this?._logger?.log('InAppManager.setRead'); RNIterableAPI.setReadForMessage(message.messageId, read); } @@ -148,9 +162,11 @@ export class IterableInAppManager { getHtmlContentForMessage( message: IterableInAppMessage ): Promise { - Iterable?.logger?.log('InAppManager.getHtmlContentForMessage'); + this?._logger?.log('InAppManager.getHtmlContentForMessage'); - return RNIterableAPI.getHtmlInAppContentForMessage(message.messageId) as unknown as Promise; + return RNIterableAPI.getHtmlInAppContentForMessage( + message.messageId + ) as unknown as Promise; } /** @@ -168,7 +184,7 @@ export class IterableInAppManager { * ``` */ setAutoDisplayPaused(paused: boolean) { - Iterable?.logger?.log('InAppManager.setAutoDisplayPaused'); + this?._logger?.log('InAppManager.setAutoDisplayPaused'); RNIterableAPI.setAutoDisplayPaused(paused); } From 86d9b41263a1cdc3a4e21a6d692f102749e1a29c Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 7 Oct 2025 01:11:36 -0700 Subject: [PATCH 02/45] refactor: initialize defaults for logger, config, authManager, and inAppManager in Iterable class --- src/core/classes/Iterable.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/core/classes/Iterable.ts b/src/core/classes/Iterable.ts index d17f55397..33b0118a0 100644 --- a/src/core/classes/Iterable.ts +++ b/src/core/classes/Iterable.ts @@ -27,6 +27,11 @@ import { IterableAuthManager } from './IterableAuthManager'; const RNEventEmitter = new NativeEventEmitter(RNIterableAPI); +const defaultConfig = new IterableConfig(); +const defaultLogger = new IterableLogger(defaultConfig); +const defaultAuthManager = new IterableAuthManager(defaultLogger); +const defaultInAppManager = new IterableInAppManager(defaultLogger); + /* eslint-disable tsdoc/syntax */ /** * The main class for the Iterable React Native SDK. @@ -50,12 +55,12 @@ export class Iterable { * Logger for the Iterable SDK * Log level is set with {@link IterableLogLevel} */ - static logger: IterableLogger = new IterableLogger(new IterableConfig()); + static logger: IterableLogger = defaultLogger; /** * Current configuration of the Iterable SDK */ - static savedConfig: IterableConfig = new IterableConfig(); + static savedConfig: IterableConfig = defaultConfig; /** * Auth manager for the Iterable SDK @@ -68,7 +73,7 @@ export class Iterable { * Iterable.authManager.pauseAuthRetries(true); * ``` */ - static authManager: IterableAuthManager | undefined; + static authManager: IterableAuthManager = defaultAuthManager; /** * In-app message manager for the current user. @@ -87,7 +92,7 @@ export class Iterable { * Iterable.inAppManager.showMessage(message, true); * ``` */ - static inAppManager: IterableInAppManager | undefined; + static inAppManager: IterableInAppManager = defaultInAppManager; /** * Initializes the Iterable React Native SDK in your app's Javascript or Typescript code. From 6b84340e189efdc36cea8b8aa3eff27973ea9db9 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 7 Oct 2025 01:47:53 -0700 Subject: [PATCH 03/45] fix: enhance error handling and logging for JWT authentication failures --- .../classes/IterableEmbeddedImpressionData.ts | 40 +++++++++++++++++++ src/embedded/classes/index.ts | 0 src/embedded/components/index.ts | 0 src/embedded/enums/index.ts | 0 src/embedded/index.ts | 0 src/embedded/types/index.ts | 0 6 files changed, 40 insertions(+) create mode 100644 src/embedded/classes/IterableEmbeddedImpressionData.ts create mode 100644 src/embedded/classes/index.ts create mode 100644 src/embedded/components/index.ts create mode 100644 src/embedded/enums/index.ts create mode 100644 src/embedded/index.ts create mode 100644 src/embedded/types/index.ts diff --git a/src/embedded/classes/IterableEmbeddedImpressionData.ts b/src/embedded/classes/IterableEmbeddedImpressionData.ts new file mode 100644 index 000000000..50f7a12ab --- /dev/null +++ b/src/embedded/classes/IterableEmbeddedImpressionData.ts @@ -0,0 +1,40 @@ +export interface IterableEmbeddedImpressionDataDict { + /** The message ID. */ + messageId: string; + /** The placement ID. */ + placementId: number; + /** The display count. */ + displayCount?: number; + /** The duration. */ + duration?: number; + /** The start date. */ + start?: Date | null; +} + +/** + * Represents the impression data for an embedded message. + */ +export class IterableEmbeddedImpressionData { + public messageId?: IterableEmbeddedImpressionDataDict['messageId']; + public placementId?: IterableEmbeddedImpressionDataDict['placementId']; + public displayCount?: IterableEmbeddedImpressionDataDict['displayCount'] = 0; + public duration?: IterableEmbeddedImpressionDataDict['duration'] = 0.0; + public start?: IterableEmbeddedImpressionDataDict['start']; + + constructor( + messageId: string, + placementId: number, + options: Pick< + IterableEmbeddedImpressionDataDict, + 'displayCount' | 'duration' | 'start' + > = {} + ) { + this.messageId = messageId; + this.placementId = placementId; + + const { displayCount = 0, duration = 0.0, start = null } = options; + this.displayCount = displayCount; + this.duration = duration; + this.start = start; + } +} diff --git a/src/embedded/classes/index.ts b/src/embedded/classes/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/src/embedded/components/index.ts b/src/embedded/components/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/src/embedded/enums/index.ts b/src/embedded/enums/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/src/embedded/index.ts b/src/embedded/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/src/embedded/types/index.ts b/src/embedded/types/index.ts new file mode 100644 index 000000000..e69de29bb From 4b4086160722acf125e13524438552ddc6108618 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 7 Oct 2025 02:45:11 -0700 Subject: [PATCH 04/45] feat: implement IterableTracking and session management for embedded impressions --- .eslintrc.js | 1 + src/core/classes/IterableTracking.ts | 8 ++ src/core/classes/IterableUtil.ts | 30 +++++ .../classes/IterableEmbeddedImpression.ts | 33 +++++ .../classes/IterableEmbeddedSession.ts | 33 +++++ .../classes/IterableEmbeddedSessionManager.ts | 115 ++++++++++++++++++ 6 files changed, 220 insertions(+) create mode 100644 src/core/classes/IterableTracking.ts create mode 100644 src/embedded/classes/IterableEmbeddedImpression.ts create mode 100644 src/embedded/classes/IterableEmbeddedSession.ts create mode 100644 src/embedded/classes/IterableEmbeddedSessionManager.ts diff --git a/.eslintrc.js b/.eslintrc.js index e0f808c23..4440e6c67 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -8,6 +8,7 @@ module.exports = { ], rules: { 'react/react-in-jsx-scope': 'off', + 'no-bitwise': 'off', }, overrides: [ { diff --git a/src/core/classes/IterableTracking.ts b/src/core/classes/IterableTracking.ts new file mode 100644 index 000000000..423aa4e51 --- /dev/null +++ b/src/core/classes/IterableTracking.ts @@ -0,0 +1,8 @@ +import { RNIterableAPI } from '../../api'; +import type { IterableEmbeddedSession } from '../../embedded/classes/IterableEmbeddedSession'; + +export class IterableTracking { + public static trackEmbeddedSession(session: IterableEmbeddedSession) { + RNIterableAPI.trackEmbeddedSession(session); + } +} diff --git a/src/core/classes/IterableUtil.ts b/src/core/classes/IterableUtil.ts index 6c2e39d01..b503c572f 100644 --- a/src/core/classes/IterableUtil.ts +++ b/src/core/classes/IterableUtil.ts @@ -18,4 +18,34 @@ export class IterableUtil { return false; } } + + static generateUUID(): string { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const g: any = typeof global !== 'undefined' ? (global as any) : undefined; + + if (g?.crypto?.getRandomValues) { + const bytes = new Uint8Array(16); + g.crypto.getRandomValues(bytes); + + // RFC 4122 compliance + bytes[6] = (bytes[6] ?? 0 & 0x0f) | 0x40; // version 4 + bytes[8] = (bytes[8] ?? 0 & 0x3f) | 0x80; // variant 10 + + const hex = Array.from(bytes, (b) => + b.toString(16).padStart(2, '0') + ).join(''); + return `${hex.substring(0, 8)}-${hex.substring(8, 12)}-${hex.substring(12, 16)}-${hex.substring(16, 20)}-${hex.substring(20)}`; + } + + // Fallback using Math.random (not cryptographically strong) + const hexDigits = '0123456789abcdef'; + const s: string[] = Array(36); + for (let i = 0; i < 36; i++) { + s[i] = hexDigits.charAt(Math.floor(Math.random() * 16)); + } + s[14] = '4'; + s[19] = hexDigits.charAt((parseInt(s[19] ?? '0', 16) & 0x3) | 0x8); + s[8] = s[13] = s[18] = s[23] = '-'; + return s.join(''); + } } diff --git a/src/embedded/classes/IterableEmbeddedImpression.ts b/src/embedded/classes/IterableEmbeddedImpression.ts new file mode 100644 index 000000000..eb45c49fd --- /dev/null +++ b/src/embedded/classes/IterableEmbeddedImpression.ts @@ -0,0 +1,33 @@ +export interface IterableEmbeddedImpressionDict { + /** The message ID. */ + messageId: string; + /** The placement ID. */ + placementId: number; + /** The display count. */ + displayCount: number; + /** The duration. */ + duration: number; +} + +/** + * Represents an embedded impression. + */ +export class IterableEmbeddedImpression { + public messageId: IterableEmbeddedImpressionDict['messageId']; + public placementId: IterableEmbeddedImpressionDict['placementId']; + public displayCount: IterableEmbeddedImpressionDict['displayCount']; + public duration: IterableEmbeddedImpressionDict['duration']; + + constructor(options: Partial = {}) { + const { + messageId = '', + placementId = 0, + displayCount = 0, + duration = 0, + } = options; + this.messageId = messageId; + this.placementId = placementId; + this.displayCount = displayCount; + this.duration = duration; + } +} diff --git a/src/embedded/classes/IterableEmbeddedSession.ts b/src/embedded/classes/IterableEmbeddedSession.ts new file mode 100644 index 000000000..5d81f7f19 --- /dev/null +++ b/src/embedded/classes/IterableEmbeddedSession.ts @@ -0,0 +1,33 @@ +import type { IterableEmbeddedImpressionData } from './IterableEmbeddedImpressionData'; +import { IterableUtil } from '../../core/classes/IterableUtil'; + +export interface IterableEmbeddedSessionDict { + /** The ID of the session. */ + id: string; + /** The start date of the session. */ + start: Date | null; + /** The end date of the session. */ + end: Date | null; + /** The impressions of the session. */ + impressions: IterableEmbeddedImpressionData[]; +} + +export class IterableEmbeddedSession { + public id: IterableEmbeddedSessionDict['id'] = ''; + public start: IterableEmbeddedSessionDict['start'] = null; + public end: IterableEmbeddedSessionDict['end'] = null; + public impressions: IterableEmbeddedSessionDict['impressions'] = []; + + constructor(options: Partial = {}) { + const { + id = IterableUtil.generateUUID(), + start = new Date(), + end = null, + impressions = [], + } = options; + this.id = id; + this.start = start; + this.end = end; + this.impressions = impressions; + } +} diff --git a/src/embedded/classes/IterableEmbeddedSessionManager.ts b/src/embedded/classes/IterableEmbeddedSessionManager.ts new file mode 100644 index 000000000..0ef9dd9dd --- /dev/null +++ b/src/embedded/classes/IterableEmbeddedSessionManager.ts @@ -0,0 +1,115 @@ +import { IterableConfig } from '../../core/classes/IterableConfig'; +import { IterableLogger } from '../../core/classes/IterableLogger'; +import { IterableTracking } from '../../core/classes/IterableTracking'; +import { IterableEmbeddedImpression } from './IterableEmbeddedImpression'; +import { IterableEmbeddedImpressionData } from './IterableEmbeddedImpressionData'; +import { IterableEmbeddedSession } from './IterableEmbeddedSession'; + +/** + * Manages the embedded session for the current user. + */ +export class IterableEmbeddedSessionManager { + private logger: IterableLogger = new IterableLogger(new IterableConfig()); + private impressions: Record = {}; + public session?: IterableEmbeddedSession; + constructor(logger: IterableLogger) { + this.logger = logger; + this.impressions = {}; + } + + public isTracking(): boolean { + return !!this.session?.start; + } + + public startSession() { + if (this.isTracking()) { + this.logger.log('Embedded session started twice'); + return; + } + + // TODO: figure out how to get a unique ID for the session + this.session = new IterableEmbeddedSession({ + start: new Date(), + }); + } + + public endSession() { + if (!this.isTracking()) { + this.logger.log('Embedded session ended without start'); + return; + } + + if (this.session?.impressions?.length) { + this.endAllImpressions(); + + const sessionToTrack = new IterableEmbeddedSession({ + start: this.session.start, + end: new Date(), + impressions: this.getImpressionList(), + }); + + IterableTracking.trackEmbeddedSession(sessionToTrack); + + //reset session for next session start + this.session = new IterableEmbeddedSession({ + start: null, + end: null, + impressions: [], + }); + + this.impressions = {}; + } + } + + public startImpression(messageId: string, placementId: number) { + let impressionData = this.impressions[messageId]; + if (!impressionData) { + impressionData = new IterableEmbeddedImpressionData( + messageId, + placementId + ); + this.impressions[messageId] = impressionData; + } + impressionData.start = new Date(); + } + + public pauseImpression(messageId: string) { + const impressionData = this.impressions[messageId]; + if (!impressionData) { + this.logger.log('onMessageImpressionEnded: impressionData not found'); + return; + } + + if (impressionData.start == null) { + this.logger.log('onMessageImpressionEnded: impressionStarted is null'); + return; + } + + this.updateDisplayCountAndDuration(impressionData); + } + + private updateDisplayCountAndDuration( + impressionData: IterableEmbeddedImpressionData + ): IterableEmbeddedImpressionData { + if (impressionData.start) { + impressionData.displayCount = (impressionData.displayCount || 0) + 1; + impressionData.duration = + (impressionData.duration || 0) + + (new Date().getTime() - impressionData.start.getTime()) / 1000; + impressionData.start = null; + } + return impressionData; + } + + private endAllImpressions() { + Object.values(this.impressions).forEach((impression) => { + this.updateDisplayCountAndDuration(impression); + }); + } + + private getImpressionList(): IterableEmbeddedImpressionData[] { + return Object.values(this.impressions).map((impression) => { + return new IterableEmbeddedImpression(impression); + }); + } +} From 26ddabd5e99ec2d4ccbd025ffd93bf25be7b8a39 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 7 Oct 2025 03:33:29 -0700 Subject: [PATCH 05/45] feat: implement IterableApi class for enhanced tracking and user management functionalities --- src/core/classes/IterableApi.ts | 244 +++++++++++++++++++++++++++++ src/core/classes/IterableLogger.ts | 4 +- src/core/constants/defaults.ts | 4 + 3 files changed, 250 insertions(+), 2 deletions(-) create mode 100644 src/core/classes/IterableApi.ts create mode 100644 src/core/constants/defaults.ts diff --git a/src/core/classes/IterableApi.ts b/src/core/classes/IterableApi.ts new file mode 100644 index 000000000..cb97b0ebf --- /dev/null +++ b/src/core/classes/IterableApi.ts @@ -0,0 +1,244 @@ +import RNIterableAPI from '../../api'; +import { IterableConfig } from './IterableConfig'; +import type { IterableLogger } from './IterableLogger'; +import { defaultLogger } from '../constants/defaults'; +import { IterableAttributionInfo } from './IterableAttributionInfo'; +import type { IterableCommerceItem } from './IterableCommerceItem'; +import type { IterableInAppMessage } from '../../inApp/classes/IterableInAppMessage'; +import type { IterableInAppLocation } from '../../inApp/enums/IterableInAppLocation'; +import type { IterableInAppCloseSource } from '../../inApp/enums/IterableInAppCloseSource'; +import type { IterableInAppDeleteSource } from '../../inApp/enums/IterableInAppDeleteSource'; +import { Platform } from 'react-native'; + +export class IterableApi { + private logger: IterableLogger = defaultLogger; + + constructor(logger: IterableLogger = defaultLogger) { + this.logger = logger; + } + + public static getInstance(): IterableApi { + return new IterableApi(); + } + + public initializeWithApiKey( + apiKey: string, + config: IterableConfig = new IterableConfig(), + version: string + ) { + this.logger.log('initializeWithApiKey: ', apiKey); + RNIterableAPI.initializeWithApiKey(apiKey, config.toDict(), version); + } + + public initialize2WithApiKey( + apiKey: string, + config: IterableConfig = new IterableConfig(), + version: string, + apiEndPoint: string + ) { + this.logger.log('initialize2WithApiKey: ', apiKey); + RNIterableAPI.initialize2WithApiKey( + apiKey, + config.toDict(), + version, + apiEndPoint + ); + } + + public setEmail(email: string | null, authToken?: string | null) { + this.logger.log('setEmail: ', email); + RNIterableAPI.setEmail(email, authToken); + } + + public getEmail() { + this.logger.log('getEmail'); + return RNIterableAPI.getEmail(); + } + + public setUserId(userId: string | null, authToken?: string | null) { + this.logger.log('setUserId: ', userId); + RNIterableAPI.setUserId(userId, authToken); + } + + public getUserId() { + this.logger.log('getUserId'); + return RNIterableAPI.getUserId(); + } + + public disableDeviceForCurrentUser() { + this.logger.log('disableDeviceForCurrentUser'); + RNIterableAPI.disableDeviceForCurrentUser(); + } + + public getLastPushPayload() { + this.logger.log('getLastPushPayload'); + return RNIterableAPI.getLastPushPayload(); + } + + public getAttributionInfo() { + this.logger.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; + } + } + ); + } + + public setAttributionInfo(attributionInfo: IterableAttributionInfo) { + this.logger.log('setAttributionInfo: ', attributionInfo); + RNIterableAPI.setAttributionInfo(attributionInfo); + } + + public trackPushOpenWithCampaignId( + campaignId: number, + templateId: number, + messageId: string | null, + appAlreadyRunning: boolean, + dataFields?: unknown + ) { + this.logger.log( + 'trackPushOpenWithCampaignId: ', + campaignId, + templateId, + messageId, + appAlreadyRunning, + dataFields + ); + RNIterableAPI.trackPushOpenWithCampaignId( + campaignId, + templateId, + messageId, + appAlreadyRunning, + dataFields + ); + } + + public updateCart(items: IterableCommerceItem[]) { + this.logger.log('updateCart: ', items); + RNIterableAPI.updateCart(items); + } + + public wakeApp() { + if (Platform.OS === 'android') { + this.logger.log('wakeApp'); + RNIterableAPI.wakeApp(); + } + } + + public trackPurchase( + total: number, + items: IterableCommerceItem[], + dataFields?: unknown + ) { + this.logger.log('trackPurchase: ', total, items, dataFields); + RNIterableAPI.trackPurchase(total, items, dataFields); + } + + public trackInAppOpen( + message: IterableInAppMessage, + location: IterableInAppLocation + ) { + this.logger.log('trackInAppOpen: ', message, location); + RNIterableAPI.trackInAppOpen(message.messageId, location); + } + + public trackInAppClick( + message: IterableInAppMessage, + location: IterableInAppLocation, + clickedUrl: string + ) { + this.logger.log('trackInAppClick: ', message, location, clickedUrl); + RNIterableAPI.trackInAppClick(message.messageId, location, clickedUrl); + } + + public trackInAppClose( + message: IterableInAppMessage, + location: IterableInAppLocation, + source: IterableInAppCloseSource, + clickedUrl?: string + ) { + this.logger.log('trackInAppClose: ', message, location, source, clickedUrl); + RNIterableAPI.trackInAppClose( + message.messageId, + location, + source, + clickedUrl + ); + } + + public inAppConsume( + message: IterableInAppMessage, + location: IterableInAppLocation, + source: IterableInAppDeleteSource + ) { + this.logger.log('inAppConsume: ', message, location, source); + RNIterableAPI.inAppConsume(message.messageId, location, source); + } + + public trackEvent(name: string, dataFields?: unknown) { + this.logger.log('trackEvent: ', name, dataFields); + RNIterableAPI.trackEvent(name, dataFields); + } + + public updateUser(dataFields: unknown, mergeNestedObjects: boolean) { + this.logger.log('updateUser: ', dataFields, mergeNestedObjects); + RNIterableAPI.updateUser(dataFields, mergeNestedObjects); + } + + public updateEmail(email: string, authToken?: string | null) { + this.logger.log('updateEmail: ', email, authToken); + RNIterableAPI.updateEmail(email, authToken); + } + + public handleAppLink(link: string) { + this.logger.log('handleAppLink: ', link); + RNIterableAPI.handleAppLink(link); + } + + public updateSubscriptions( + emailListIds: number[] | null, + unsubscribedChannelIds: number[] | null, + unsubscribedMessageTypeIds: number[] | null, + subscribedMessageTypeIds: number[] | null, + campaignId: number, + templateId: number + ) { + this.logger.log( + 'updateSubscriptions: ', + emailListIds, + unsubscribedChannelIds, + unsubscribedMessageTypeIds, + subscribedMessageTypeIds, + campaignId, + templateId + ); + RNIterableAPI.updateSubscriptions( + emailListIds, + unsubscribedChannelIds, + unsubscribedMessageTypeIds, + subscribedMessageTypeIds, + campaignId, + templateId + ); + } + + public pauseAuthRetries(pauseRetry: boolean) { + this.logger.log('pauseAuthRetries: ', pauseRetry); + RNIterableAPI.pauseAuthRetries(pauseRetry); + } +} diff --git a/src/core/classes/IterableLogger.ts b/src/core/classes/IterableLogger.ts index 3d9854888..f5a388fbb 100644 --- a/src/core/classes/IterableLogger.ts +++ b/src/core/classes/IterableLogger.ts @@ -39,13 +39,13 @@ export class IterableLogger { * * @param message - The message to be logged. */ - log(message: string) { + log(message?: unknown, ...optionalParams: unknown[]) { // default to `true` in the case of unit testing where `Iterable` is not initialized // which is most likely in a debug environment anyways const loggingEnabled = this.config.logReactNativeSdkCalls ?? true; if (loggingEnabled) { - console.log(message); + console.log(message, ...optionalParams); } } } diff --git a/src/core/constants/defaults.ts b/src/core/constants/defaults.ts new file mode 100644 index 000000000..f9eb6765c --- /dev/null +++ b/src/core/constants/defaults.ts @@ -0,0 +1,4 @@ +import { IterableLogger } from '../classes/IterableLogger'; +import { IterableConfig } from '../classes/IterableConfig'; + +export const defaultLogger = new IterableLogger(new IterableConfig()); From 2f19aefab3de3995d2cfdaa87a4ec9e6ac9afe0e Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 7 Oct 2025 03:59:41 -0700 Subject: [PATCH 06/45] refactor: update Iterable classes to use static logger --- src/core/classes/Iterable.ts | 2 + src/core/classes/IterableApi.ts | 231 ++++++++++++++------ src/core/classes/IterableAuthManager.ts | 8 +- src/inApp/classes/IterableInAppManager.ts | 20 +- src/inbox/classes/IterableInboxDataModel.ts | 11 +- 5 files changed, 183 insertions(+), 89 deletions(-) diff --git a/src/core/classes/Iterable.ts b/src/core/classes/Iterable.ts index 33b0118a0..9c2cebb46 100644 --- a/src/core/classes/Iterable.ts +++ b/src/core/classes/Iterable.ts @@ -24,6 +24,7 @@ import { IterableConfig } from './IterableConfig'; import { IterableLogger } from './IterableLogger'; import type { IterableAuthFailure } from '../types/IterableAuthFailure'; import { IterableAuthManager } from './IterableAuthManager'; +import { IterableApi } from './IterableApi'; const RNEventEmitter = new NativeEventEmitter(RNIterableAPI); @@ -167,6 +168,7 @@ export class Iterable { Iterable.logger = logger; Iterable.authManager = new IterableAuthManager(logger); Iterable.inAppManager = new IterableInAppManager(logger); + IterableApi.setLogger(logger); this.setupEventHandlers(); } diff --git a/src/core/classes/IterableApi.ts b/src/core/classes/IterableApi.ts index cb97b0ebf..f6d9a93c6 100644 --- a/src/core/classes/IterableApi.ts +++ b/src/core/classes/IterableApi.ts @@ -1,3 +1,5 @@ +import { Platform } from 'react-native'; + import RNIterableAPI from '../../api'; import { IterableConfig } from './IterableConfig'; import type { IterableLogger } from './IterableLogger'; @@ -8,36 +10,37 @@ import type { IterableInAppMessage } from '../../inApp/classes/IterableInAppMess import type { IterableInAppLocation } from '../../inApp/enums/IterableInAppLocation'; import type { IterableInAppCloseSource } from '../../inApp/enums/IterableInAppCloseSource'; import type { IterableInAppDeleteSource } from '../../inApp/enums/IterableInAppDeleteSource'; -import { Platform } from 'react-native'; +import type { IterableHtmlInAppContent } from '../../inApp/classes/IterableHtmlInAppContent'; +import type { IterableEmbeddedSession } from '../../embedded/classes/IterableEmbeddedSession'; export class IterableApi { - private logger: IterableLogger = defaultLogger; + static logger: IterableLogger = defaultLogger; constructor(logger: IterableLogger = defaultLogger) { - this.logger = logger; + IterableApi.logger = logger; } - public static getInstance(): IterableApi { - return new IterableApi(); + static setLogger(logger: IterableLogger) { + IterableApi.logger = logger; } - public initializeWithApiKey( + static initializeWithApiKey( apiKey: string, config: IterableConfig = new IterableConfig(), version: string ) { - this.logger.log('initializeWithApiKey: ', apiKey); + IterableApi.logger.log('initializeWithApiKey: ', apiKey); RNIterableAPI.initializeWithApiKey(apiKey, config.toDict(), version); } - public initialize2WithApiKey( + static initialize2WithApiKey( apiKey: string, config: IterableConfig = new IterableConfig(), version: string, apiEndPoint: string ) { - this.logger.log('initialize2WithApiKey: ', apiKey); - RNIterableAPI.initialize2WithApiKey( + IterableApi.logger.log('initialize2WithApiKey: ', apiKey); + return RNIterableAPI.initialize2WithApiKey( apiKey, config.toDict(), version, @@ -45,38 +48,38 @@ export class IterableApi { ); } - public setEmail(email: string | null, authToken?: string | null) { - this.logger.log('setEmail: ', email); - RNIterableAPI.setEmail(email, authToken); + static setEmail(email: string | null, authToken?: string | null) { + IterableApi.logger.log('setEmail: ', email); + return RNIterableAPI.setEmail(email, authToken); } - public getEmail() { - this.logger.log('getEmail'); + static getEmail() { + IterableApi.logger.log('getEmail'); return RNIterableAPI.getEmail(); } - public setUserId(userId: string | null, authToken?: string | null) { - this.logger.log('setUserId: ', userId); - RNIterableAPI.setUserId(userId, authToken); + static setUserId(userId: string | null, authToken?: string | null) { + IterableApi.logger.log('setUserId: ', userId); + return RNIterableAPI.setUserId(userId, authToken); } - public getUserId() { - this.logger.log('getUserId'); + static getUserId() { + IterableApi.logger.log('getUserId'); return RNIterableAPI.getUserId(); } - public disableDeviceForCurrentUser() { - this.logger.log('disableDeviceForCurrentUser'); - RNIterableAPI.disableDeviceForCurrentUser(); + static disableDeviceForCurrentUser() { + IterableApi.logger.log('disableDeviceForCurrentUser'); + return RNIterableAPI.disableDeviceForCurrentUser(); } - public getLastPushPayload() { - this.logger.log('getLastPushPayload'); + static getLastPushPayload() { + IterableApi.logger.log('getLastPushPayload'); return RNIterableAPI.getLastPushPayload(); } - public getAttributionInfo() { - this.logger.log('getAttributionInfo'); + static getAttributionInfo() { + IterableApi.logger.log('getAttributionInfo'); // FIXME: What if this errors? return RNIterableAPI.getAttributionInfo().then( ( @@ -99,19 +102,19 @@ export class IterableApi { ); } - public setAttributionInfo(attributionInfo: IterableAttributionInfo) { - this.logger.log('setAttributionInfo: ', attributionInfo); - RNIterableAPI.setAttributionInfo(attributionInfo); + static setAttributionInfo(attributionInfo: IterableAttributionInfo) { + IterableApi.logger.log('setAttributionInfo: ', attributionInfo); + return RNIterableAPI.setAttributionInfo(attributionInfo); } - public trackPushOpenWithCampaignId( + static trackPushOpenWithCampaignId( campaignId: number, templateId: number, messageId: string | null, appAlreadyRunning: boolean, dataFields?: unknown ) { - this.logger.log( + IterableApi.logger.log( 'trackPushOpenWithCampaignId: ', campaignId, templateId, @@ -119,7 +122,7 @@ export class IterableApi { appAlreadyRunning, dataFields ); - RNIterableAPI.trackPushOpenWithCampaignId( + return RNIterableAPI.trackPushOpenWithCampaignId( campaignId, templateId, messageId, @@ -128,52 +131,62 @@ export class IterableApi { ); } - public updateCart(items: IterableCommerceItem[]) { - this.logger.log('updateCart: ', items); - RNIterableAPI.updateCart(items); + static updateCart(items: IterableCommerceItem[]) { + IterableApi.logger.log('updateCart: ', items); + return RNIterableAPI.updateCart(items); } - public wakeApp() { + static wakeApp() { if (Platform.OS === 'android') { - this.logger.log('wakeApp'); - RNIterableAPI.wakeApp(); + IterableApi.logger.log('wakeApp'); + return RNIterableAPI.wakeApp(); } } - public trackPurchase( + static trackPurchase( total: number, items: IterableCommerceItem[], dataFields?: unknown ) { - this.logger.log('trackPurchase: ', total, items, dataFields); - RNIterableAPI.trackPurchase(total, items, dataFields); + IterableApi.logger.log('trackPurchase: ', total, items, dataFields); + return RNIterableAPI.trackPurchase(total, items, dataFields); } - public trackInAppOpen( + static trackInAppOpen( message: IterableInAppMessage, location: IterableInAppLocation ) { - this.logger.log('trackInAppOpen: ', message, location); - RNIterableAPI.trackInAppOpen(message.messageId, location); + IterableApi.logger.log('trackInAppOpen: ', message, location); + return RNIterableAPI.trackInAppOpen(message.messageId, location); } - public trackInAppClick( + static trackInAppClick( message: IterableInAppMessage, location: IterableInAppLocation, clickedUrl: string ) { - this.logger.log('trackInAppClick: ', message, location, clickedUrl); - RNIterableAPI.trackInAppClick(message.messageId, location, clickedUrl); + IterableApi.logger.log('trackInAppClick: ', message, location, clickedUrl); + return RNIterableAPI.trackInAppClick( + message.messageId, + location, + clickedUrl + ); } - public trackInAppClose( + static trackInAppClose( message: IterableInAppMessage, location: IterableInAppLocation, source: IterableInAppCloseSource, clickedUrl?: string ) { - this.logger.log('trackInAppClose: ', message, location, source, clickedUrl); - RNIterableAPI.trackInAppClose( + IterableApi.logger.log( + 'trackInAppClose: ', + message, + location, + source, + clickedUrl + ); + return RNIterableAPI.trackInAppClose( message.messageId, location, source, @@ -181,36 +194,36 @@ export class IterableApi { ); } - public inAppConsume( + static inAppConsume( message: IterableInAppMessage, location: IterableInAppLocation, source: IterableInAppDeleteSource ) { - this.logger.log('inAppConsume: ', message, location, source); - RNIterableAPI.inAppConsume(message.messageId, location, source); + IterableApi.logger.log('inAppConsume: ', message, location, source); + return RNIterableAPI.inAppConsume(message.messageId, location, source); } - public trackEvent(name: string, dataFields?: unknown) { - this.logger.log('trackEvent: ', name, dataFields); - RNIterableAPI.trackEvent(name, dataFields); + static trackEvent(name: string, dataFields?: unknown) { + IterableApi.logger.log('trackEvent: ', name, dataFields); + return RNIterableAPI.trackEvent(name, dataFields); } - public updateUser(dataFields: unknown, mergeNestedObjects: boolean) { - this.logger.log('updateUser: ', dataFields, mergeNestedObjects); - RNIterableAPI.updateUser(dataFields, mergeNestedObjects); + static updateUser(dataFields: unknown, mergeNestedObjects: boolean) { + IterableApi.logger.log('updateUser: ', dataFields, mergeNestedObjects); + return RNIterableAPI.updateUser(dataFields, mergeNestedObjects); } - public updateEmail(email: string, authToken?: string | null) { - this.logger.log('updateEmail: ', email, authToken); - RNIterableAPI.updateEmail(email, authToken); + static updateEmail(email: string, authToken?: string | null) { + IterableApi.logger.log('updateEmail: ', email, authToken); + return RNIterableAPI.updateEmail(email, authToken); } - public handleAppLink(link: string) { - this.logger.log('handleAppLink: ', link); - RNIterableAPI.handleAppLink(link); + static handleAppLink(link: string) { + IterableApi.logger.log('handleAppLink: ', link); + return RNIterableAPI.handleAppLink(link); } - public updateSubscriptions( + static updateSubscriptions( emailListIds: number[] | null, unsubscribedChannelIds: number[] | null, unsubscribedMessageTypeIds: number[] | null, @@ -218,7 +231,7 @@ export class IterableApi { campaignId: number, templateId: number ) { - this.logger.log( + IterableApi.logger.log( 'updateSubscriptions: ', emailListIds, unsubscribedChannelIds, @@ -227,7 +240,7 @@ export class IterableApi { campaignId, templateId ); - RNIterableAPI.updateSubscriptions( + return RNIterableAPI.updateSubscriptions( emailListIds, unsubscribedChannelIds, unsubscribedMessageTypeIds, @@ -237,8 +250,82 @@ export class IterableApi { ); } - public pauseAuthRetries(pauseRetry: boolean) { - this.logger.log('pauseAuthRetries: ', pauseRetry); - RNIterableAPI.pauseAuthRetries(pauseRetry); + static pauseAuthRetries(pauseRetry: boolean) { + IterableApi.logger.log('pauseAuthRetries: ', pauseRetry); + return RNIterableAPI.pauseAuthRetries(pauseRetry); + } + + static getInAppMessages(): Promise { + IterableApi.logger.log('getInAppMessages'); + return RNIterableAPI.getInAppMessages() as unknown as Promise< + IterableInAppMessage[] + >; + } + + static getInboxMessages(): Promise { + IterableApi.logger.log('getInboxMessages'); + return RNIterableAPI.getInboxMessages() as unknown as Promise< + IterableInAppMessage[] + >; + } + + static showMessage( + messageId: string, + consume: boolean + ): Promise { + IterableApi.logger.log('showMessage: ', messageId, consume); + return RNIterableAPI.showMessage(messageId, consume); + } + + static removeMessage( + messageId: string, + location: number, + source: number + ): void { + IterableApi.logger.log('removeMessage: ', messageId, location, source); + return RNIterableAPI.removeMessage(messageId, location, source); + } + + static setReadForMessage(messageId: string, read: boolean): void { + IterableApi.logger.log('setReadForMessage: ', messageId, read); + return RNIterableAPI.setReadForMessage(messageId, read); + } + + static setAutoDisplayPaused(autoDisplayPaused: boolean): void { + IterableApi.logger.log('setAutoDisplayPaused: ', autoDisplayPaused); + return RNIterableAPI.setAutoDisplayPaused(autoDisplayPaused); + } + + static getHtmlInAppContentForMessage( + messageId: string + ): Promise { + IterableApi.logger.log('getHtmlInAppContentForMessage: ', messageId); + return RNIterableAPI.getHtmlInAppContentForMessage(messageId); + } + + static trackEmbeddedSession(session: IterableEmbeddedSession): void { + IterableApi.logger.log('trackEmbeddedSession: ', session); + return RNIterableAPI.trackEmbeddedSession(session); + } + + static getHtmlInAppContentForMessageId( + messageId: string + ): Promise { + IterableApi.logger.log('getHtmlInAppContentForMessageId: ', messageId); + return RNIterableAPI.getHtmlInAppContentForMessage(messageId); + } + + static setMessageAsRead(messageId: string, read: boolean): void { + IterableApi.logger.log('setMessageAsRead: ', messageId, read); + return RNIterableAPI.setReadForMessage(messageId, read); + } + + static deleteItemById( + messageId: string, + location: number, + source: number + ): void { + IterableApi.logger.log('deleteItemById: ', messageId, location, source); + return RNIterableAPI.removeMessage(messageId, location, source); } } diff --git a/src/core/classes/IterableAuthManager.ts b/src/core/classes/IterableAuthManager.ts index a81549667..368f937bf 100644 --- a/src/core/classes/IterableAuthManager.ts +++ b/src/core/classes/IterableAuthManager.ts @@ -1,15 +1,15 @@ import RNIterableAPI from '../../api'; -import { IterableConfig } from './IterableConfig'; +import { defaultLogger } from '../constants/defaults'; import { IterableLogger } from './IterableLogger'; export class IterableAuthManager { /** * The logger for the Iterable SDK. */ - private _logger: IterableLogger = new IterableLogger(new IterableConfig()); + static logger: IterableLogger = defaultLogger; constructor(logger: IterableLogger) { - this._logger = logger; + IterableAuthManager.logger = logger; } /** @@ -23,7 +23,7 @@ export class IterableAuthManager { * ``` */ pauseAuthRetries(pauseRetry: boolean) { - this._logger?.log('pauseAuthRetries'); + IterableAuthManager.logger?.log('pauseAuthRetries'); RNIterableAPI.pauseAuthRetries(pauseRetry); } } diff --git a/src/inApp/classes/IterableInAppManager.ts b/src/inApp/classes/IterableInAppManager.ts index 29c99deed..4884eba3d 100644 --- a/src/inApp/classes/IterableInAppManager.ts +++ b/src/inApp/classes/IterableInAppManager.ts @@ -1,6 +1,6 @@ import { RNIterableAPI } from '../../api'; -import { IterableConfig } from '../../core/classes/IterableConfig'; import { IterableLogger } from '../../core/classes/IterableLogger'; +import { defaultLogger } from '../../core/constants/defaults'; import type { IterableInAppDeleteSource, IterableInAppLocation, @@ -20,10 +20,10 @@ export class IterableInAppManager { /** * The logger for the Iterable SDK. */ - private _logger: IterableLogger = new IterableLogger(new IterableConfig()); + static logger: IterableLogger = defaultLogger; constructor(logger: IterableLogger) { - this._logger = logger; + IterableInAppManager.logger = logger; } /** @@ -43,7 +43,7 @@ export class IterableInAppManager { * @returns A Promise that resolves to an array of in-app messages. */ getMessages(): Promise { - this._logger?.log('InAppManager.getMessages'); + IterableInAppManager.logger?.log('InAppManager.getMessages'); return RNIterableAPI.getInAppMessages() as unknown as Promise< IterableInAppMessage[] @@ -68,7 +68,7 @@ export class IterableInAppManager { * @returns A Promise that resolves to an array of messages marked as `saveToInbox`. */ getInboxMessages(): Promise { - this?._logger?.log('InAppManager.getInboxMessages'); + IterableInAppManager.logger?.log('InAppManager.getInboxMessages'); return RNIterableAPI.getInboxMessages() as unknown as Promise< IterableInAppMessage[] @@ -97,7 +97,7 @@ export class IterableInAppManager { message: IterableInAppMessage, consume: boolean ): Promise { - this?._logger?.log('InAppManager.show'); + IterableInAppManager.logger?.log('InAppManager.show'); return RNIterableAPI.showMessage(message.messageId, consume); } @@ -125,7 +125,7 @@ export class IterableInAppManager { location: IterableInAppLocation, source: IterableInAppDeleteSource ): void { - this?._logger?.log('InAppManager.remove'); + IterableInAppManager.logger?.log('InAppManager.remove'); return RNIterableAPI.removeMessage(message.messageId, location, source); } @@ -142,7 +142,7 @@ export class IterableInAppManager { * ``` */ setReadForMessage(message: IterableInAppMessage, read: boolean) { - this?._logger?.log('InAppManager.setRead'); + IterableInAppManager.logger?.log('InAppManager.setRead'); RNIterableAPI.setReadForMessage(message.messageId, read); } @@ -162,7 +162,7 @@ export class IterableInAppManager { getHtmlContentForMessage( message: IterableInAppMessage ): Promise { - this?._logger?.log('InAppManager.getHtmlContentForMessage'); + IterableInAppManager.logger?.log('InAppManager.getHtmlContentForMessage'); return RNIterableAPI.getHtmlInAppContentForMessage( message.messageId @@ -184,7 +184,7 @@ export class IterableInAppManager { * ``` */ setAutoDisplayPaused(paused: boolean) { - this?._logger?.log('InAppManager.setAutoDisplayPaused'); + IterableInAppManager.logger?.log('InAppManager.setAutoDisplayPaused'); RNIterableAPI.setAutoDisplayPaused(paused); } diff --git a/src/inbox/classes/IterableInboxDataModel.ts b/src/inbox/classes/IterableInboxDataModel.ts index 311f5cc7c..cbf4b5db7 100644 --- a/src/inbox/classes/IterableInboxDataModel.ts +++ b/src/inbox/classes/IterableInboxDataModel.ts @@ -11,6 +11,7 @@ import type { IterableInboxImpressionRowInfo, IterableInboxRowViewModel, } from '../types'; +import { IterableApi } from '../../core/classes/IterableApi'; /** * The `IterableInboxDataModel` class provides methods to manage and manipulate @@ -113,7 +114,7 @@ export class IterableInboxDataModel { setMessageAsRead(id: string) { Iterable?.logger?.log('IterableInboxDataModel.setMessageAsRead'); - RNIterableAPI.setReadForMessage(id, true); + IterableApi.setReadForMessage(id, true); } /** @@ -151,7 +152,9 @@ 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 }[]); + RNIterableAPI.startSession( + visibleRows as unknown as { [key: string]: string | number | boolean }[] + ); } /** @@ -178,7 +181,9 @@ 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 }[]); + RNIterableAPI.updateVisibleRows( + visibleRows as unknown as { [key: string]: string | number | boolean }[] + ); } /** From 7aef30aff02f8d9709d79a660545af9ee1aee7e6 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 7 Oct 2025 08:58:12 -0700 Subject: [PATCH 07/45] refactor: replace RNIterableAPI calls with IterableApi methods --- src/core/classes/Iterable.ts | 121 ++++++++------------------------ src/core/classes/IterableApi.ts | 11 +-- 2 files changed, 37 insertions(+), 95 deletions(-) diff --git a/src/core/classes/Iterable.ts b/src/core/classes/Iterable.ts index 9c2cebb46..ef5c06084 100644 --- a/src/core/classes/Iterable.ts +++ b/src/core/classes/Iterable.ts @@ -134,7 +134,7 @@ export class Iterable { const version = this.getVersionFromPackageJson(); - return RNIterableAPI.initializeWithApiKey(apiKey, config.toDict(), version); + return IterableApi.initializeWithApiKey(apiKey, config, version); } /** @@ -152,14 +152,18 @@ export class Iterable { const version = this.getVersionFromPackageJson(); - return RNIterableAPI.initialize2WithApiKey( + return IterableApi.initialize2WithApiKey( apiKey, - config.toDict(), + config, version, apiEndPoint ); } + /** + * Does basic setup of the Iterable SDK. + * @param config - The configuration object for the Iterable SDK + */ private static setupIterable(config: IterableConfig = new IterableConfig()) { Iterable.savedConfig = config; @@ -223,9 +227,7 @@ export class Iterable { * ``` */ static setEmail(email: string | null, authToken?: string | null) { - Iterable?.logger?.log('setEmail: ' + email); - - RNIterableAPI.setEmail(email, authToken); + return IterableApi.setEmail(email, authToken); } /** @@ -239,9 +241,7 @@ export class Iterable { * ``` */ static getEmail(): Promise { - Iterable?.logger?.log('getEmail'); - - return RNIterableAPI.getEmail(); + return IterableApi.getEmail(); } /** @@ -288,9 +288,7 @@ export class Iterable { * taken */ static setUserId(userId?: string | null, authToken?: string | null) { - Iterable?.logger?.log('setUserId: ' + userId); - - RNIterableAPI.setUserId(userId, authToken); + return IterableApi.setUserId(userId, authToken); } /** @@ -304,9 +302,7 @@ export class Iterable { * ``` */ static getUserId(): Promise { - Iterable?.logger?.log('getUserId'); - - return RNIterableAPI.getUserId(); + return IterableApi.getUserId(); } /** @@ -318,9 +314,7 @@ export class Iterable { * ``` */ static disableDeviceForCurrentUser() { - Iterable?.logger?.log('disableDeviceForCurrentUser'); - - RNIterableAPI.disableDeviceForCurrentUser(); + return IterableApi.disableDeviceForCurrentUser(); } /** @@ -335,9 +329,7 @@ export class Iterable { * ``` */ static getLastPushPayload(): Promise { - Iterable?.logger?.log('getLastPushPayload'); - - return RNIterableAPI.getLastPushPayload(); + return IterableApi.getLastPushPayload(); } /** @@ -363,9 +355,7 @@ export class Iterable { * ``` */ static getAttributionInfo(): Promise { - Iterable?.logger?.log('getAttributionInfo'); - - return RNIterableAPI.getAttributionInfo().then( + return IterableApi.getAttributionInfo().then( ( dict: { campaignId: number; @@ -411,12 +401,8 @@ export class Iterable { * ``` */ static setAttributionInfo(attributionInfo?: IterableAttributionInfo) { - Iterable?.logger?.log('setAttributionInfo'); - - RNIterableAPI.setAttributionInfo( - attributionInfo as unknown as { - [key: string]: string | number | boolean; - } | null + return IterableApi.setAttributionInfo( + attributionInfo as IterableAttributionInfo ); } @@ -456,9 +442,7 @@ export class Iterable { appAlreadyRunning: boolean, dataFields?: unknown ) { - Iterable?.logger?.log('trackPushOpenWithCampaignId'); - - RNIterableAPI.trackPushOpenWithCampaignId( + return IterableApi.trackPushOpenWithCampaignId( campaignId, templateId, messageId as string, @@ -494,11 +478,7 @@ export class Iterable { * ``` */ static updateCart(items: IterableCommerceItem[]) { - Iterable?.logger?.log('updateCart'); - - RNIterableAPI.updateCart( - items as unknown as { [key: string]: string | number | boolean }[] - ); + return IterableApi.updateCart(items); } /** @@ -512,11 +492,7 @@ export class Iterable { * ``` */ static wakeApp() { - if (Platform.OS === 'android') { - Iterable?.logger?.log('Attempting to wake the app'); - - RNIterableAPI.wakeApp(); - } + return IterableApi.wakeApp(); } /** @@ -548,13 +524,7 @@ export class Iterable { items: IterableCommerceItem[], dataFields?: unknown ) { - Iterable?.logger?.log('trackPurchase'); - - RNIterableAPI.trackPurchase( - total, - items as unknown as { [key: string]: string | number | boolean }[], - dataFields as { [key: string]: string | number | boolean } | undefined - ); + return IterableApi.trackPurchase(total, items, dataFields); } /** @@ -580,9 +550,7 @@ export class Iterable { message: IterableInAppMessage, location: IterableInAppLocation ) { - Iterable?.logger?.log('trackInAppOpen'); - - RNIterableAPI.trackInAppOpen(message.messageId, location); + return IterableApi.trackInAppOpen(message, location); } /** @@ -611,9 +579,7 @@ export class Iterable { location: IterableInAppLocation, clickedUrl: string ) { - Iterable?.logger?.log('trackInAppClick'); - - RNIterableAPI.trackInAppClick(message.messageId, location, clickedUrl); + return IterableApi.trackInAppClick(message, location, clickedUrl); } /** @@ -644,14 +610,7 @@ export class Iterable { source: IterableInAppCloseSource, clickedUrl?: string ) { - Iterable?.logger?.log('trackInAppClose'); - - RNIterableAPI.trackInAppClose( - message.messageId, - location, - source, - clickedUrl - ); + return IterableApi.trackInAppClose(message, location, source, clickedUrl); } /** @@ -695,9 +654,7 @@ export class Iterable { location: IterableInAppLocation, source: IterableInAppDeleteSource ) { - Iterable?.logger?.log('inAppConsume'); - - RNIterableAPI.inAppConsume(message.messageId, location, source); + return IterableApi.inAppConsume(message, location, source); } /** @@ -721,12 +678,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 - ); + return IterableApi.trackEvent(name, dataFields); } /** @@ -772,12 +724,7 @@ export class Iterable { dataFields: unknown | undefined, mergeNestedObjects: boolean ) { - Iterable?.logger?.log('updateUser'); - - RNIterableAPI.updateUser( - dataFields as { [key: string]: string | number | boolean }, - mergeNestedObjects - ); + return IterableApi.updateUser(dataFields, mergeNestedObjects); } /** @@ -798,9 +745,7 @@ export class Iterable { * ``` */ static updateEmail(email: string, authToken?: string) { - Iterable?.logger?.log('updateEmail'); - - RNIterableAPI.updateEmail(email, authToken); + return IterableApi.updateEmail(email, authToken); } /** @@ -882,9 +827,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); } /** @@ -929,9 +872,7 @@ export class Iterable { campaignId: number, templateId: number ) { - Iterable?.logger?.log('updateSubscriptions'); - - RNIterableAPI.updateSubscriptions( + return IterableApi.updateSubscriptions( emailListIds, unsubscribedChannelIds, unsubscribedMessageTypeIds, @@ -952,9 +893,7 @@ export class Iterable { * ``` */ static pauseAuthRetries(pauseRetry: boolean) { - Iterable?.logger?.log('pauseAuthRetries'); - - RNIterableAPI.pauseAuthRetries(pauseRetry); + return IterableApi.pauseAuthRetries(pauseRetry); } /** diff --git a/src/core/classes/IterableApi.ts b/src/core/classes/IterableApi.ts index f6d9a93c6..c7a5f0e19 100644 --- a/src/core/classes/IterableApi.ts +++ b/src/core/classes/IterableApi.ts @@ -28,9 +28,9 @@ export class IterableApi { apiKey: string, config: IterableConfig = new IterableConfig(), version: string - ) { + ): Promise { IterableApi.logger.log('initializeWithApiKey: ', apiKey); - RNIterableAPI.initializeWithApiKey(apiKey, config.toDict(), version); + return RNIterableAPI.initializeWithApiKey(apiKey, config.toDict(), version); } static initialize2WithApiKey( @@ -38,7 +38,7 @@ export class IterableApi { config: IterableConfig = new IterableConfig(), version: string, apiEndPoint: string - ) { + ): Promise { IterableApi.logger.log('initialize2WithApiKey: ', apiKey); return RNIterableAPI.initialize2WithApiKey( apiKey, @@ -58,7 +58,10 @@ export class IterableApi { return RNIterableAPI.getEmail(); } - static setUserId(userId: string | null, authToken?: string | null) { + static setUserId( + userId: string | null | undefined, + authToken?: string | null + ) { IterableApi.logger.log('setUserId: ', userId); return RNIterableAPI.setUserId(userId, authToken); } From 0adbc4453b7846c559f81c28248ca3881842e033 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 7 Oct 2025 11:43:57 -0700 Subject: [PATCH 08/45] feat: introduce tracking manager in Iterable class and refactor tracking methods --- src/core/classes/Iterable.ts | 88 ++++++-- src/core/classes/IterableApi.ts | 15 +- src/core/classes/IterableTracking.ts | 8 - src/core/utils/index.ts | 1 + src/core/utils/trackingUtils.ts | 213 ++++++++++++++++++ .../classes/IterableEmbeddedSessionManager.ts | 4 +- src/inbox/components/IterableInbox.tsx | 3 +- .../IterableInboxMessageDisplay.tsx | 6 +- 8 files changed, 301 insertions(+), 37 deletions(-) delete mode 100644 src/core/classes/IterableTracking.ts create mode 100644 src/core/utils/index.ts create mode 100644 src/core/utils/trackingUtils.ts diff --git a/src/core/classes/Iterable.ts b/src/core/classes/Iterable.ts index bd374df4f..1d24bb582 100644 --- a/src/core/classes/Iterable.ts +++ b/src/core/classes/Iterable.ts @@ -3,34 +3,39 @@ 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 { + defaultAuthManager, + defaultConfig, + defaultInAppManager, + defaultLogger, +} from '../constants/defaults'; import { IterableAuthResponseResult } from '../enums/IterableAuthResponseResult'; import { IterableEventName } from '../enums/IterableEventName'; - -// Add this type-only import to avoid circular dependency -import { IterableInAppManager } from '../../inApp/classes/IterableInAppManager'; - +import type { IterableAuthFailure } from '../types/IterableAuthFailure'; +import { + trackEmbeddedSession, + trackEvent, + trackInAppClick, + trackInAppClose, + trackInAppOpen, + trackPurchase, + trackPushOpenWithCampaignId, +} from '../utils'; 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 { - defaultAuthManager, - defaultConfig, - defaultInAppManager, - defaultLogger, -} from '../constants/defaults'; -import { IterableApi } from './IterableApi'; -import { IterableAuthManager } from './IterableAuthManager'; const RNEventEmitter = new NativeEventEmitter(RNIterableAPI); @@ -96,6 +101,26 @@ export class Iterable { */ static authManager: IterableAuthManager = defaultAuthManager; + /** + * Tracking manager for the current user. + * + * This property provides access to tracking functionality including + * tracking purchases, in-app messages, and more. + * + * @example + * ```typescript + * Iterable.tracker.trackPurchase(100, [new IterableCommerceItem('item1', 'Item 1', 10.0, 1)], { key: 'value' }); + * ``` + */ + static tracker = { + trackPushOpenWithCampaignId, + trackPurchase, + trackInAppOpen, + trackInAppClick, + trackInAppClose, + trackEvent, + trackEmbeddedSession, + }; /** * Initializes the Iterable React Native SDK in your app's Javascript or Typescript code. * @@ -391,6 +416,9 @@ export class Iterable { } /** + * @deprecated -- This method is deprecated and may be removed in a future + * release. Use `Iterable.tracker.trackPushOpenWithCampaignId` instead. + * * Create a `pushOpen` event on the current user's Iterable profile, populating * it with data provided to the method call. * @@ -480,6 +508,9 @@ export class Iterable { } /** + * @deprecated -- This method is deprecated and may be removed in a future + * release. Use `Iterable.tracker.trackPurchase` instead. + * * Create a purchase event on the current user's Iterable profile. * * Represent each item in the purchase event with an {@link IterableCommerceItem} object. @@ -508,10 +539,13 @@ export class Iterable { items: IterableCommerceItem[], dataFields?: unknown ) { - return IterableApi.trackPurchase(total, items, dataFields); + return Iterable.tracker.trackPurchase(total, items, dataFields); } /** + * @deprecated -- This method is deprecated and may be removed in a future + * release. Use `Iterable.tracker.trackInAppOpen` instead. + * * 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. @@ -534,10 +568,13 @@ export class Iterable { message: IterableInAppMessage, location: IterableInAppLocation ) { - return IterableApi.trackInAppOpen(message, location); + return Iterable.tracker.trackInAppOpen(message, location); } /** + * @deprecated -- This method is deprecated and may be removed in a future + * release. Use `Iterable.tracker.trackInAppClick` instead. + * * 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 @@ -563,10 +600,13 @@ export class Iterable { location: IterableInAppLocation, clickedUrl: string ) { - return IterableApi.trackInAppClick(message, location, clickedUrl); + return Iterable.tracker.trackInAppClick(message, location, clickedUrl); } /** + * @deprecated -- This method is deprecated and may be removed in a future + * release. Use `Iterable.tracker.trackInAppClose` instead. + * * 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 @@ -594,7 +634,12 @@ export class Iterable { source: IterableInAppCloseSource, clickedUrl?: string ) { - return IterableApi.trackInAppClose(message, location, source, clickedUrl); + return Iterable.tracker.trackInAppClose( + message, + location, + source, + clickedUrl + ); } /** @@ -642,6 +687,9 @@ export class Iterable { } /** + * @deprecated -- This method is deprecated and may be removed in a future + * release. Use `Iterable.tracker.trackEvent` instead. + * * Create a custom event to the current user's Iterable profile. * * Pass in the name of the event stored in eventName key and the data associated with the event. @@ -662,7 +710,7 @@ export class Iterable { * ``` */ static trackEvent(name: string, dataFields?: unknown) { - return IterableApi.trackEvent(name, dataFields); + return Iterable.tracker.trackEvent(name, dataFields); } /** diff --git a/src/core/classes/IterableApi.ts b/src/core/classes/IterableApi.ts index 2c698e85e..11f6894dc 100644 --- a/src/core/classes/IterableApi.ts +++ b/src/core/classes/IterableApi.ts @@ -13,6 +13,7 @@ import type { IterableInAppDeleteSource } from '../../inApp/enums/IterableInAppD import type { IterableHtmlInAppContent } from '../../inApp/classes/IterableHtmlInAppContent'; import type { IterableInAppShowResponse } from '../../inApp/enums/IterableInAppShowResponse'; import type { IterableInboxImpressionRowInfo } from '../../inbox/types/IterableInboxImpressionRowInfo'; +import type { IterableEmbeddedSession } from '../../embedded/classes/IterableEmbeddedSession'; export class IterableApi { static logger: IterableLogger = defaultLogger; @@ -105,8 +106,8 @@ export class IterableApi { /** * Associate the current user with the passed in `userId` parameter. * - * WARNING: specify a user by calling `Iterable.setEmail` or - * `Iterable.setUserId`, but **NOT** both. + * WARNING: specify a user by calling `IterableApi.setEmail` or + * `IterableApi.setUserId`, but **NOT** both. * * @param userId - User ID to associate with the current user * @param authToken - Valid, pre-fetched JWT the SDK @@ -298,6 +299,16 @@ export class IterableApi { return RNIterableAPI.trackEvent(name, dataFields); } + /** + * Track an embedded session. + * + * @param session - The session to track + */ + static trackEmbeddedSession(session: IterableEmbeddedSession) { + IterableApi.logger.log('trackEmbeddedSession: ', session); + return RNIterableAPI.trackEmbeddedSession(session); + } + // ---- End TRACKING ---- // // ====================================================== // diff --git a/src/core/classes/IterableTracking.ts b/src/core/classes/IterableTracking.ts deleted file mode 100644 index 423aa4e51..000000000 --- a/src/core/classes/IterableTracking.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { RNIterableAPI } from '../../api'; -import type { IterableEmbeddedSession } from '../../embedded/classes/IterableEmbeddedSession'; - -export class IterableTracking { - public static trackEmbeddedSession(session: IterableEmbeddedSession) { - RNIterableAPI.trackEmbeddedSession(session); - } -} diff --git a/src/core/utils/index.ts b/src/core/utils/index.ts new file mode 100644 index 000000000..8059f89c9 --- /dev/null +++ b/src/core/utils/index.ts @@ -0,0 +1 @@ +export * from './trackingUtils'; diff --git a/src/core/utils/trackingUtils.ts b/src/core/utils/trackingUtils.ts new file mode 100644 index 000000000..27d710cec --- /dev/null +++ b/src/core/utils/trackingUtils.ts @@ -0,0 +1,213 @@ +import { IterableApi } from '../classes/IterableApi'; +import type { IterableEmbeddedSession } from '../../embedded/classes/IterableEmbeddedSession'; +import type { IterableCommerceItem } from '../classes/IterableCommerceItem'; +import type { IterableInAppMessage } from '../../inApp/classes/IterableInAppMessage'; +import type { IterableInAppLocation } from '../../inApp/enums/IterableInAppLocation'; +import type { IterableInAppCloseSource } from '../../inApp/enums/IterableInAppCloseSource'; + +/** + * Create a `pushOpen` event on the current user's Iterable profile, populating + * it with data provided to the method call. + * + * **NOTE**: Iterable's SDK automatically tracks push notification opens. + * However, it's also possible to manually track these events by calling this + * method. + * + * @param campaignId - The ID of the campaign to associate with the push open + * @param templateId - The ID of the template to associate with the push open + * @param messageId - The ID of the message to associate with the push open + * @param appAlreadyRunning - Whether or not the app was already running when + * the push notification arrived + * @param dataFields - Information to store with the push open event + * + * @example + * ```typescript + * const CAMPAIGN_ID = 12345; + * const TEMPLATE_ID = 67890; + * const MESSAGE_ID = '0fc6657517c64014868ea2d15f23082b'; + * const APP_ALREADY_RUNNING = false; + * const DATA_FIELDS = { + * "discount": 0.99, + * "product": "cappuccino", + * }; + * + * trackPushOpen(CAMPAIGN_ID, TEMPLATE_ID, MESSAGE_ID, APP_ALREADY_RUNNING, DATA_FIELDS); + * ``` + */ +export const trackPushOpenWithCampaignId = ( + campaignId: number, + templateId: number, + messageId: string | undefined, + appAlreadyRunning: boolean, + dataFields?: unknown +) => { + return IterableApi.trackPushOpenWithCampaignId( + campaignId, + templateId, + messageId, + appAlreadyRunning, + dataFields + ); +}; + +/** + * Create a purchase event on the current user's Iterable profile. + * + * Represent each item in the purchase event with an {@link IterableCommerceItem} object. + * + * @see {@link IterableCommerceItem} + * + * **NOTE**: `total` is a parameter that is passed in. Iterable does not sum the `price` fields of the various items in the purchase event. + * + * @param total - The total cost of the purchase + * @param items - The items included in the purchase + * @param dataFields - Descriptive data to store on the purchase event + * + * @example + * ```typescript + * const items = [ + * new IterableCommerceItem('item1', 'Item 1', 10.0, 1), + * new IterableCommerceItem('item2', 'Item 2', 20.0, 2), + * ]; + * const dataFields = { 'key1': 'value1', }; + * + * trackPurchase(30.0, items, dataFields); + * ``` + */ +export const trackPurchase = ( + total: number, + items: IterableCommerceItem[], + dataFields?: unknown +) => { + return IterableApi.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) + * + * @example + * ```typescript + * const message = new IterableInAppMessage(1234, 4567, IterableInAppTrigger.auto, new Date(), new Date(), false, undefined, undefined, false, 0); + * trackInAppOpen(message, IterableInAppLocation.inApp); + * ``` + * + * @remarks + * Iterable's SDK automatically tracks in-app message opens when you use the + * SDK's default rendering. However, it's also possible to manually track + * these events by calling this method. + */ +export const trackInAppOpen = ( + message: IterableInAppMessage, + location: IterableInAppLocation +) => { + return IterableApi.trackInAppOpen(message, 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. + * + * @example + * ```typescript + * const message = new IterableInAppMessage(1234, 4567, IterableInAppTrigger.auto, new Date(), new Date(), false, undefined, undefined, false, 0); + * trackInAppClick(message, IterableInAppLocation.inApp, 'https://www.example.com'); + * ``` + * + * @remarks + * Iterable's SDK automatically tracks in-app message clicks when you use the + * SDK's default rendering. However, you can also manually track these events + * by calling this method. + */ +export const trackInAppClick = ( + message: IterableInAppMessage, + location: IterableInAppLocation, + clickedUrl: string +) => { + return IterableApi.trackInAppClick(message, 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. Useful for determining if the messages is in a mobile inbox. + * @param source - The way the in-app was closed. + * @param clickedUrl - The URL clicked by the user. + * + * @example + * ```typescript + * const message = new IterableInAppMessage(1234, 4567, IterableInAppTrigger.auto, new Date(), new Date(), false, undefined, undefined, false, 0); + * trackInAppClose(message, IterableInAppLocation.inApp, IterableInAppCloseSource.back, 'https://www.example.com'); + * ``` + * + * @remarks + * Iterable's SDK automatically tracks in-app message close events when you + * use the SDK's default rendering. However, it's also possible to manually + * track these events by calling this method. + */ +export const trackInAppClose = ( + message: IterableInAppMessage, + location: IterableInAppLocation, + source: IterableInAppCloseSource, + clickedUrl?: string +) => { + return IterableApi.trackInAppClose(message, location, source, clickedUrl); +}; + +/** + * Create a custom event to the current user's Iterable profile. + * + * Pass in the name of the event stored in eventName key and the data associated with the event. + * The eventType is set to "customEvent". + * + * @param name - The event name of the custom event + * @param dataFields - Descriptive data to store on the custom event + * + * @example + * ```typescript + * trackEvent("completedOnboarding", + * { + * "includedProfilePhoto": true, + * "favoriteColor": "red", + * "favoriteFlavor": "cinnamon", + * } + * ); + * ``` + */ +export const trackEvent = (name: string, dataFields?: unknown) => { + return IterableApi.trackEvent(name, dataFields); +}; + +/** + * Track an embedded session. + * + * @param session - The session to track + * + * @example + * ```typescript + * const session = new IterableEmbeddedSession({ + * start: new Date(), + * end: new Date(), + * impressions: [], + * }); + * + * trackEmbeddedSession(session); + * ``` + */ +export const trackEmbeddedSession = (session: IterableEmbeddedSession) => { + return IterableApi.trackEmbeddedSession(session); +}; diff --git a/src/embedded/classes/IterableEmbeddedSessionManager.ts b/src/embedded/classes/IterableEmbeddedSessionManager.ts index 0ef9dd9dd..fd77f7ba4 100644 --- a/src/embedded/classes/IterableEmbeddedSessionManager.ts +++ b/src/embedded/classes/IterableEmbeddedSessionManager.ts @@ -1,6 +1,6 @@ import { IterableConfig } from '../../core/classes/IterableConfig'; import { IterableLogger } from '../../core/classes/IterableLogger'; -import { IterableTracking } from '../../core/classes/IterableTracking'; +import { trackEmbeddedSession } from '../../core/utils/trackingUtils'; import { IterableEmbeddedImpression } from './IterableEmbeddedImpression'; import { IterableEmbeddedImpressionData } from './IterableEmbeddedImpressionData'; import { IterableEmbeddedSession } from './IterableEmbeddedSession'; @@ -48,7 +48,7 @@ export class IterableEmbeddedSessionManager { impressions: this.getImpressionList(), }); - IterableTracking.trackEmbeddedSession(sessionToTrack); + trackEmbeddedSession(sessionToTrack); //reset session for next session start this.session = new IterableEmbeddedSession({ diff --git a/src/inbox/components/IterableInbox.tsx b/src/inbox/components/IterableInbox.tsx index 3cf44d829..0085020c2 100644 --- a/src/inbox/components/IterableInbox.tsx +++ b/src/inbox/components/IterableInbox.tsx @@ -32,7 +32,6 @@ import { type IterableInboxMessageListProps, } from './IterableInboxMessageList'; - const RNEventEmitter = new NativeEventEmitter(RNIterableAPI); const DEFAULT_HEADLINE_HEIGHT = 60; @@ -363,7 +362,7 @@ export const IterableInbox = ({ inboxDataModel.setMessageAsRead(id); setSelectedRowViewModelIdx(index); - Iterable.trackInAppOpen( + Iterable.tracker.trackInAppOpen( // MOB-10428: Have a safety check for models[index].inAppMessage // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore diff --git a/src/inbox/components/IterableInboxMessageDisplay.tsx b/src/inbox/components/IterableInboxMessageDisplay.tsx index 7e6798c73..1abadeb95 100644 --- a/src/inbox/components/IterableInboxMessageDisplay.tsx +++ b/src/inbox/components/IterableInboxMessageDisplay.tsx @@ -179,12 +179,12 @@ export const IterableInboxMessageDisplay = ({ const source = IterableActionSource.inApp; const context = new IterableActionContext(action, source); - Iterable.trackInAppClick( + Iterable.tracker.trackInAppClick( rowViewModel.inAppMessage, IterableInAppLocation.inbox, URL ); - Iterable.trackInAppClose( + Iterable.tracker.trackInAppClose( rowViewModel.inAppMessage, IterableInAppLocation.inbox, IterableInAppCloseSource.link, @@ -225,7 +225,7 @@ export const IterableInboxMessageDisplay = ({ { returnToInbox(); - Iterable.trackInAppClose( + Iterable.tracker.trackInAppClose( rowViewModel.inAppMessage, IterableInAppLocation.inbox, IterableInAppCloseSource.back From e04f41f6ef14210c779b2af5a35bfa43ad7835ef Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 7 Oct 2025 12:35:56 -0700 Subject: [PATCH 09/45] feat: implement embedded messaging functionality with new classes and methods --- src/core/classes/IterableApi.ts | 16 ++++ src/core/enums/IterableActionSource.ts | 2 + .../classes/IterableEmbeddedMessage.ts | 51 +++++++++++++ .../IterableEmbeddedMessageElements.ts | 73 +++++++++++++++++++ .../IterableEmbeddedMessageElementsButton.ts | 53 ++++++++++++++ ...ableEmbeddedMessageElementsButtonAction.ts | 35 +++++++++ ...bleEmbeddedMessageElementsDefaultAction.ts | 35 +++++++++ .../IterableEmbeddedMessageElementsText.ts | 39 ++++++++++ .../IterableEmbeddedMessageMetadata.ts | 47 ++++++++++++ .../classes/IterableEmbeddedPlacement.ts | 35 +++++++++ .../classes/IterableEmbeddedSessionManager.ts | 1 + .../classes/IterableEmbeddedViewConfig.ts | 64 ++++++++++++++++ src/embedded/enums/IterableEmbeddedView.ts | 11 +++ 13 files changed, 462 insertions(+) create mode 100644 src/embedded/classes/IterableEmbeddedMessage.ts create mode 100644 src/embedded/classes/IterableEmbeddedMessageElements.ts create mode 100644 src/embedded/classes/IterableEmbeddedMessageElementsButton.ts create mode 100644 src/embedded/classes/IterableEmbeddedMessageElementsButtonAction.ts create mode 100644 src/embedded/classes/IterableEmbeddedMessageElementsDefaultAction.ts create mode 100644 src/embedded/classes/IterableEmbeddedMessageElementsText.ts create mode 100644 src/embedded/classes/IterableEmbeddedMessageMetadata.ts create mode 100644 src/embedded/classes/IterableEmbeddedPlacement.ts create mode 100644 src/embedded/classes/IterableEmbeddedViewConfig.ts create mode 100644 src/embedded/enums/IterableEmbeddedView.ts diff --git a/src/core/classes/IterableApi.ts b/src/core/classes/IterableApi.ts index 11f6894dc..9daed568e 100644 --- a/src/core/classes/IterableApi.ts +++ b/src/core/classes/IterableApi.ts @@ -490,6 +490,22 @@ export class IterableApi { // ---- End IN-APP ---- // + // ====================================================== // + // ======================= EMBEDDED ======================= // + // ====================================================== // + + /** + * Start an embedded session. + * + * @param visibleRows - The visible rows. + */ + static startEmbeddedSession(visibleRows: IterableInboxImpressionRowInfo[]) { + IterableApi.logger.log('startEmbeddedSession: ', visibleRows); + return RNIterableAPI.startEmbeddedSession(visibleRows); + } + + // ---- End EMBEDDED ---- // + // ====================================================== // // ======================= MOSC ======================= // // ====================================================== // diff --git a/src/core/enums/IterableActionSource.ts b/src/core/enums/IterableActionSource.ts index 3692e6361..437bb9808 100644 --- a/src/core/enums/IterableActionSource.ts +++ b/src/core/enums/IterableActionSource.ts @@ -8,4 +8,6 @@ export enum IterableActionSource { appLink = 1, /** The action source was an in-app message */ inApp = 2, + /** The action source was an embedded message */ + embedded = 3, } diff --git a/src/embedded/classes/IterableEmbeddedMessage.ts b/src/embedded/classes/IterableEmbeddedMessage.ts new file mode 100644 index 000000000..418569b49 --- /dev/null +++ b/src/embedded/classes/IterableEmbeddedMessage.ts @@ -0,0 +1,51 @@ +import { IterableEmbeddedMessageMetadata } from './IterableEmbeddedMessageMetadata'; +import type { IterableEmbeddedMessageMetadataDict } from './IterableEmbeddedMessageMetadata'; +import { IterableEmbeddedMessageElements } from './IterableEmbeddedMessageElements'; +import type { IterableEmbeddedMessageElementsDict } from './IterableEmbeddedMessageElements'; + +export interface IterableEmbeddedMessageDict { + metadata: IterableEmbeddedMessageMetadataDict; + elements?: IterableEmbeddedMessageElementsDict | null; + payload?: Record | null; +} + +export class IterableEmbeddedMessage { + public metadata: IterableEmbeddedMessageMetadata; + public elements?: IterableEmbeddedMessageElements | null = null; + public payload?: Record | null = null; + + constructor( + metadata: IterableEmbeddedMessageMetadata, + options: { + elements?: IterableEmbeddedMessageElements | null; + payload?: Record | null; + } = {} + ) { + const { elements = null, payload = null } = options; + this.metadata = metadata; + this.elements = elements; + this.payload = payload; + } + + toDict(): IterableEmbeddedMessageDict { + return { + metadata: this.metadata.toDict(), + elements: this.elements ? this.elements.toDict() : null, + payload: this.payload ?? null, + }; + } + + static fromDict( + jsonObject: IterableEmbeddedMessageDict + ): IterableEmbeddedMessage { + const metadata = IterableEmbeddedMessageMetadata.fromDict( + jsonObject.metadata + ); + const elements = IterableEmbeddedMessageElements.fromDict( + jsonObject.elements ?? null + ); + const payload = jsonObject.payload ?? null; + + return new IterableEmbeddedMessage(metadata, { elements, payload }); + } +} diff --git a/src/embedded/classes/IterableEmbeddedMessageElements.ts b/src/embedded/classes/IterableEmbeddedMessageElements.ts new file mode 100644 index 000000000..2fe4c7159 --- /dev/null +++ b/src/embedded/classes/IterableEmbeddedMessageElements.ts @@ -0,0 +1,73 @@ +import { IterableEmbeddedMessageElementsButton } from './IterableEmbeddedMessageElementsButton'; +import { IterableEmbeddedMessageElementsDefaultAction } from './IterableEmbeddedMessageElementsDefaultAction'; +import { IterableEmbeddedMessageElementsText } from './IterableEmbeddedMessageElementsText'; + +export interface IterableEmbeddedMessageElementsDict { + title?: string | null; + body?: string | null; + mediaURL?: string | null; + mediaUrlCaption?: string | null; + defaultAction?: IterableEmbeddedMessageElementsDefaultAction | null; + buttons?: IterableEmbeddedMessageElementsButton[] | null; + text?: IterableEmbeddedMessageElementsText[] | null; +} + +export class IterableEmbeddedMessageElements { + public title?: IterableEmbeddedMessageElementsDict['title'] = null; + public body?: IterableEmbeddedMessageElementsDict['body'] = null; + public mediaURL?: IterableEmbeddedMessageElementsDict['mediaURL'] = null; + public mediaUrlCaption?: IterableEmbeddedMessageElementsDict['mediaUrlCaption'] = + null; + public defaultAction?: IterableEmbeddedMessageElementsDict['defaultAction'] = + null; + public buttons?: IterableEmbeddedMessageElementsDict['buttons'] = null; + public text?: IterableEmbeddedMessageElementsDict['text'] = null; + + constructor(options: Partial = {}) { + const { + title = null, + body = null, + mediaURL = null, + mediaUrlCaption = null, + defaultAction = null, + buttons = null, + text = null, + } = options; + + this.title = title; + this.body = body; + this.mediaURL = mediaURL; + this.mediaUrlCaption = mediaUrlCaption; + this.defaultAction = defaultAction; + this.buttons = buttons; + this.text = text; + } + + toDict(): IterableEmbeddedMessageElementsDict { + return { + title: this.title, + body: this.body, + mediaURL: this.mediaURL, + mediaUrlCaption: this.mediaUrlCaption, + defaultAction: this.defaultAction, + buttons: this.buttons, + text: this.text, + }; + } + + static fromDict( + jsonObject?: IterableEmbeddedMessageElementsDict | null + ): IterableEmbeddedMessageElements | null { + if (!jsonObject) return null; + + return new IterableEmbeddedMessageElements({ + title: jsonObject.title ?? null, + body: jsonObject.body ?? null, + mediaURL: jsonObject.mediaURL ?? null, + mediaUrlCaption: jsonObject.mediaUrlCaption ?? null, + defaultAction: jsonObject.defaultAction ?? null, + buttons: jsonObject.buttons ?? null, + text: jsonObject.text ?? null, + }); + } +} diff --git a/src/embedded/classes/IterableEmbeddedMessageElementsButton.ts b/src/embedded/classes/IterableEmbeddedMessageElementsButton.ts new file mode 100644 index 000000000..a79be30a1 --- /dev/null +++ b/src/embedded/classes/IterableEmbeddedMessageElementsButton.ts @@ -0,0 +1,53 @@ +import { IterableEmbeddedMessageElementsButtonAction } from './IterableEmbeddedMessageElementsButtonAction'; + +export interface IterableEmbeddedMessageElementsButtonDict { + /** The ID. */ + id: string; + /** The title. */ + title?: string | null; + /** The action. */ + action?: IterableEmbeddedMessageElementsButtonAction | null; +} + +export class IterableEmbeddedMessageElementsButton { + public id: string; + public title?: string | null; + public action?: IterableEmbeddedMessageElementsButtonAction | null; + + constructor( + options: Partial = {} + ) { + const { id = '', title = null, action = null } = options; + this.id = id; + this.title = title; + this.action = action; + } + + toDict(): IterableEmbeddedMessageElementsButtonDict { + return { + id: this.id, + title: this.title ?? null, + action: this.action ?? null, + }; + } + + static fromDict( + jsonObject: IterableEmbeddedMessageElementsButtonDict + ): IterableEmbeddedMessageElementsButton { + if (!jsonObject?.id) { + throw new Error( + 'id is required when calling IterableEmbeddedMessageElementsButton.fromDict' + ); + } + + return new IterableEmbeddedMessageElementsButton({ + id: jsonObject.id, + title: jsonObject.title ?? null, + action: jsonObject.action + ? IterableEmbeddedMessageElementsButtonAction.fromDict( + jsonObject.action + ) + : null, + }); + } +} diff --git a/src/embedded/classes/IterableEmbeddedMessageElementsButtonAction.ts b/src/embedded/classes/IterableEmbeddedMessageElementsButtonAction.ts new file mode 100644 index 000000000..1af8d0d5f --- /dev/null +++ b/src/embedded/classes/IterableEmbeddedMessageElementsButtonAction.ts @@ -0,0 +1,35 @@ +export interface IterableEmbeddedMessageElementsButtonActionDict { + /** The type. */ + type: string; + /** The data. */ + data?: string; +} + +export class IterableEmbeddedMessageElementsButtonAction { + public type: string; + public data?: string; + + constructor( + options: Partial = {} + ) { + const { type = '', data = '' } = options; + this.type = type; + this.data = data; + } + + toDict(): IterableEmbeddedMessageElementsButtonActionDict { + return { + type: this.type, + data: this.data, + }; + } + + static fromDict( + jsonObject: IterableEmbeddedMessageElementsButtonActionDict + ): IterableEmbeddedMessageElementsButtonAction { + return new IterableEmbeddedMessageElementsButtonAction({ + type: jsonObject.type, + data: jsonObject.data, + }); + } +} diff --git a/src/embedded/classes/IterableEmbeddedMessageElementsDefaultAction.ts b/src/embedded/classes/IterableEmbeddedMessageElementsDefaultAction.ts new file mode 100644 index 000000000..da16e1afe --- /dev/null +++ b/src/embedded/classes/IterableEmbeddedMessageElementsDefaultAction.ts @@ -0,0 +1,35 @@ +export interface IterableEmbeddedMessageElementsDefaultActionDict { + /** The type. */ + type: string; + /** The data. */ + data?: string; +} + +export class IterableEmbeddedMessageElementsDefaultAction { + public type: string; + public data?: string; + + constructor( + options: Partial = {} + ) { + const { type = '', data = '' } = options; + this.type = type; + this.data = data; + } + + toDict(): IterableEmbeddedMessageElementsDefaultActionDict { + return { + type: this.type, + data: this.data, + }; + } + + static fromDict( + jsonObject: IterableEmbeddedMessageElementsDefaultActionDict + ): IterableEmbeddedMessageElementsDefaultAction { + return new IterableEmbeddedMessageElementsDefaultAction({ + type: jsonObject.type, + data: jsonObject.data, + }); + } +} diff --git a/src/embedded/classes/IterableEmbeddedMessageElementsText.ts b/src/embedded/classes/IterableEmbeddedMessageElementsText.ts new file mode 100644 index 000000000..78d5cdf16 --- /dev/null +++ b/src/embedded/classes/IterableEmbeddedMessageElementsText.ts @@ -0,0 +1,39 @@ +export interface IterableEmbeddedMessageElementsTextDict { + /** The ID. */ + id: string; + /** The text. */ + text?: string | null; + /** The label. */ + label?: string | null; +} + +export class IterableEmbeddedMessageElementsText { + public id: string; + public text?: string | null; + public label?: string | null; + + constructor(options: Partial = {}) { + const { id = '', text = null, label = null } = options; + this.id = id; + this.text = text; + this.label = label; + } + + toDict(): IterableEmbeddedMessageElementsTextDict { + return { + id: this.id, + text: this.text, + label: this.label, + }; + } + + static fromDict( + jsonObject: IterableEmbeddedMessageElementsTextDict + ): IterableEmbeddedMessageElementsText { + return new IterableEmbeddedMessageElementsText({ + id: jsonObject.id, + text: jsonObject.text, + label: jsonObject.label, + }); + } +} diff --git a/src/embedded/classes/IterableEmbeddedMessageMetadata.ts b/src/embedded/classes/IterableEmbeddedMessageMetadata.ts new file mode 100644 index 000000000..bdd33a089 --- /dev/null +++ b/src/embedded/classes/IterableEmbeddedMessageMetadata.ts @@ -0,0 +1,47 @@ +export interface IterableEmbeddedMessageMetadataDict { + messageId: string; + placementId: number; + campaignId?: number | null; + isProof?: boolean; +} + +export class IterableEmbeddedMessageMetadata { + public messageId: string; + public placementId: number; + public campaignId?: number | null = null; + public isProof: boolean = false; + + constructor(options: Partial = {}) { + const { + messageId = '', + placementId = 0, + campaignId = null, + isProof = false, + } = options; + + this.messageId = messageId; + this.placementId = placementId; + this.campaignId = campaignId; + this.isProof = isProof; + } + + toDict(): IterableEmbeddedMessageMetadataDict { + return { + messageId: this.messageId, + placementId: this.placementId, + campaignId: this.campaignId ?? null, + isProof: this.isProof, + }; + } + + static fromDict( + jsonObject: IterableEmbeddedMessageMetadataDict + ): IterableEmbeddedMessageMetadata { + return new IterableEmbeddedMessageMetadata({ + messageId: jsonObject.messageId, + placementId: jsonObject.placementId, + campaignId: jsonObject.campaignId ?? null, + isProof: jsonObject.isProof ?? false, + }); + } +} diff --git a/src/embedded/classes/IterableEmbeddedPlacement.ts b/src/embedded/classes/IterableEmbeddedPlacement.ts new file mode 100644 index 000000000..760da7d87 --- /dev/null +++ b/src/embedded/classes/IterableEmbeddedPlacement.ts @@ -0,0 +1,35 @@ +import { IterableEmbeddedMessage } from './IterableEmbeddedMessage'; +import type { IterableEmbeddedMessageDict } from './IterableEmbeddedMessage'; + +export interface IterableEmbeddedPlacementDict { + placementId: number; + messages: IterableEmbeddedMessageDict[]; +} + +export class IterableEmbeddedPlacement { + public placementId: number; + public messages: IterableEmbeddedMessage[]; + + constructor(placementId: number, messages: IterableEmbeddedMessage[]) { + this.placementId = placementId; + this.messages = messages; + } + + toDict(): IterableEmbeddedPlacementDict { + return { + placementId: this.placementId, + messages: this.messages.map((m) => m.toDict()), + }; + } + + static fromDict( + jsonObject: IterableEmbeddedPlacementDict + ): IterableEmbeddedPlacement { + const placementId = jsonObject.placementId; + const messages = (jsonObject.messages ?? []).map((m) => + IterableEmbeddedMessage.fromDict(m) + ); + + return new IterableEmbeddedPlacement(placementId, messages); + } +} diff --git a/src/embedded/classes/IterableEmbeddedSessionManager.ts b/src/embedded/classes/IterableEmbeddedSessionManager.ts index fd77f7ba4..f96d9f113 100644 --- a/src/embedded/classes/IterableEmbeddedSessionManager.ts +++ b/src/embedded/classes/IterableEmbeddedSessionManager.ts @@ -12,6 +12,7 @@ export class IterableEmbeddedSessionManager { private logger: IterableLogger = new IterableLogger(new IterableConfig()); private impressions: Record = {}; public session?: IterableEmbeddedSession; + constructor(logger: IterableLogger) { this.logger = logger; this.impressions = {}; diff --git a/src/embedded/classes/IterableEmbeddedViewConfig.ts b/src/embedded/classes/IterableEmbeddedViewConfig.ts new file mode 100644 index 000000000..cea8a9a97 --- /dev/null +++ b/src/embedded/classes/IterableEmbeddedViewConfig.ts @@ -0,0 +1,64 @@ +export interface IterableEmbeddedViewConfigDict { + /** Background color hex (e.g., 0xFF0000) */ + backgroundColor?: number | null; + /** Border color hex */ + borderColor?: number | null; + /** Border width in pixels */ + borderWidth?: number | null; + /** Corner radius in points */ + borderCornerRadius?: number | null; + /** Primary button background color hex */ + primaryBtnBackgroundColor?: number | null; + /** Primary button text color hex */ + primaryBtnTextColor?: number | null; + /** Secondary button background color hex */ + secondaryBtnBackgroundColor?: number | null; + /** Secondary button text color hex */ + secondaryBtnTextColor?: number | null; + /** Title text color hex */ + titleTextColor?: number | null; + /** Body text color hex */ + bodyTextColor?: number | null; +} + +/** + * Represents view-level styling configuration for an embedded view. + */ +export class IterableEmbeddedViewConfig { + public backgroundColor?: IterableEmbeddedViewConfigDict['backgroundColor']; + public borderColor?: IterableEmbeddedViewConfigDict['borderColor']; + public borderWidth?: IterableEmbeddedViewConfigDict['borderWidth']; + public borderCornerRadius?: IterableEmbeddedViewConfigDict['borderCornerRadius']; + public primaryBtnBackgroundColor?: IterableEmbeddedViewConfigDict['primaryBtnBackgroundColor']; + public primaryBtnTextColor?: IterableEmbeddedViewConfigDict['primaryBtnTextColor']; + public secondaryBtnBackgroundColor?: IterableEmbeddedViewConfigDict['secondaryBtnBackgroundColor']; + public secondaryBtnTextColor?: IterableEmbeddedViewConfigDict['secondaryBtnTextColor']; + public titleTextColor?: IterableEmbeddedViewConfigDict['titleTextColor']; + public bodyTextColor?: IterableEmbeddedViewConfigDict['bodyTextColor']; + + constructor(options: IterableEmbeddedViewConfigDict = {}) { + const { + backgroundColor = null, + borderColor = null, + borderWidth = null, + borderCornerRadius = null, + primaryBtnBackgroundColor = null, + primaryBtnTextColor = null, + secondaryBtnBackgroundColor = null, + secondaryBtnTextColor = null, + titleTextColor = null, + bodyTextColor = null, + } = options; + + this.backgroundColor = backgroundColor; + this.borderColor = borderColor; + this.borderWidth = borderWidth; + this.borderCornerRadius = borderCornerRadius; + this.primaryBtnBackgroundColor = primaryBtnBackgroundColor; + this.primaryBtnTextColor = primaryBtnTextColor; + this.secondaryBtnBackgroundColor = secondaryBtnBackgroundColor; + this.secondaryBtnTextColor = secondaryBtnTextColor; + this.titleTextColor = titleTextColor; + this.bodyTextColor = bodyTextColor; + } +} diff --git a/src/embedded/enums/IterableEmbeddedView.ts b/src/embedded/enums/IterableEmbeddedView.ts new file mode 100644 index 000000000..8e8e811be --- /dev/null +++ b/src/embedded/enums/IterableEmbeddedView.ts @@ -0,0 +1,11 @@ +/** + * The view type for an embedded message. + */ +export enum IterableEmbeddedView { + /** The embedded view is a banner */ + Banner = 0, + /** The embedded view is a card */ + Card = 1, + /** The embedded view is a notification */ + Notification = 2, +} From 2106df07a19897d31aa96b2073984df4ce660af8 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 7 Oct 2025 12:36:55 -0700 Subject: [PATCH 10/45] feat: add getEmbeddedMessages method to IterableApi for retrieving embedded messages --- src/core/classes/IterableApi.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/core/classes/IterableApi.ts b/src/core/classes/IterableApi.ts index 9daed568e..79a74af39 100644 --- a/src/core/classes/IterableApi.ts +++ b/src/core/classes/IterableApi.ts @@ -494,14 +494,16 @@ export class IterableApi { // ======================= EMBEDDED ======================= // // ====================================================== // - /** - * Start an embedded session. - * - * @param visibleRows - The visible rows. - */ - static startEmbeddedSession(visibleRows: IterableInboxImpressionRowInfo[]) { - IterableApi.logger.log('startEmbeddedSession: ', visibleRows); - return RNIterableAPI.startEmbeddedSession(visibleRows); + // static getEmbeddedMessages(placementIds: number[] = []) { + // IterableApi.logger.log('startEmbeddedSession: ', visibleRows); + // return RNIterableAPI.startEmbeddedSession(visibleRows); + // } + + static getEmbeddedMessages(): Promise { + IterableApi.logger.log('getEmbeddedMessages'); + return RNIterableAPI.getEmbeddedMessages() as unknown as Promise< + IterableInAppMessage[] + >; } // ---- End EMBEDDED ---- // From 09f371ea78de392857e6ac57c209813ca59d2957 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 7 Oct 2025 12:52:10 -0700 Subject: [PATCH 11/45] feat: add support for enabling embedded messaging and update IterableApi methods --- .../reactnative/RNIterableAPIModuleImpl.java | 4 +++ src/core/classes/IterableApi.ts | 27 ++++++++++++++----- src/core/classes/IterableConfig.ts | 6 +++++ 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java index 3207bb5dc..4d1145e6f 100644 --- a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java +++ b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java @@ -88,6 +88,10 @@ public void initializeWithApiKey(String apiKey, ReadableMap configReadableMap, S configBuilder.setAuthHandler(this); } + if (configReadableMap.hasKey("enableEmbeddedMessaging") && configReadableMap.getBoolean("enableEmbeddedMessaging") == true) { + configBuilder.setEnableEmbeddedMessaging(this); + } + IterableApi.initialize(reactContext, apiKey, configBuilder.build()); IterableApi.getInstance().setDeviceAttribute("reactNativeSDKVersion", version); diff --git a/src/core/classes/IterableApi.ts b/src/core/classes/IterableApi.ts index 79a74af39..6676eb80f 100644 --- a/src/core/classes/IterableApi.ts +++ b/src/core/classes/IterableApi.ts @@ -14,6 +14,8 @@ import type { IterableHtmlInAppContent } from '../../inApp/classes/IterableHtmlI import type { IterableInAppShowResponse } from '../../inApp/enums/IterableInAppShowResponse'; import type { IterableInboxImpressionRowInfo } from '../../inbox/types/IterableInboxImpressionRowInfo'; import type { IterableEmbeddedSession } from '../../embedded/classes/IterableEmbeddedSession'; +import type { IterableEmbeddedMessage } from '../../embedded/classes/IterableEmbeddedMessage'; +import type { IterableEmbeddedPlacement } from '../../embedded/classes/IterableEmbeddedPlacement'; export class IterableApi { static logger: IterableLogger = defaultLogger; @@ -494,18 +496,29 @@ export class IterableApi { // ======================= EMBEDDED ======================= // // ====================================================== // - // static getEmbeddedMessages(placementIds: number[] = []) { - // IterableApi.logger.log('startEmbeddedSession: ', visibleRows); - // return RNIterableAPI.startEmbeddedSession(visibleRows); - // } - - static getEmbeddedMessages(): Promise { + /** + * Get the embedded messages. + * + * @returns A Promise that resolves to an array of embedded messages. + */ + static getEmbeddedMessages(): Promise { IterableApi.logger.log('getEmbeddedMessages'); return RNIterableAPI.getEmbeddedMessages() as unknown as Promise< - IterableInAppMessage[] + IterableEmbeddedPlacement[] >; } + static trackEmbeddedMessageReceived(message: IterableEmbeddedMessage) { + IterableApi.logger.log('trackEmbeddedMessageReceived: ', message); + + if (message == null) { + IterableApi.logger.log('trackEmbeddedMessageReceived: message is null'); + return; + } + + return RNIterableAPI.trackEmbeddedMessageReceived(message); + } + // ---- End EMBEDDED ---- // // ====================================================== // diff --git a/src/core/classes/IterableConfig.ts b/src/core/classes/IterableConfig.ts index c8ee67400..94b4ba705 100644 --- a/src/core/classes/IterableConfig.ts +++ b/src/core/classes/IterableConfig.ts @@ -319,6 +319,11 @@ export class IterableConfig { */ encryptionEnforced = false; + /** + * This specifies whether the SDK should enable embedded messaging. + */ + enableEmbeddedMessaging = false; + /** * Converts the IterableConfig instance to a dictionary object. * @@ -368,6 +373,7 @@ export class IterableConfig { pushPlatform: this.pushPlatform, encryptionEnforced: this.encryptionEnforced, retryPolicy: this.retryPolicy, + enableEmbeddedMessaging: this.enableEmbeddedMessaging, }; } } From 1846e74cbfe4a1533e601d9134a77e45d89bc1f3 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 7 Oct 2025 13:11:06 -0700 Subject: [PATCH 12/45] feat: implement IterableEmbeddedView and related types for embedded messaging --- src/embedded/classes/IterableEmbeddedView.ts | 181 ++++++++++++++++++ src/embedded/constants/embeddedColors.ts | 47 +++++ ...dedView.ts => IterableEmbeddedViewType.ts} | 2 +- src/embedded/enums/index.ts | 1 + .../types/IterableEmbeddedViewButtonInfo.ts | 5 + .../types/IterableEmbeddedViewStyles.ts | 12 ++ 6 files changed, 247 insertions(+), 1 deletion(-) create mode 100644 src/embedded/classes/IterableEmbeddedView.ts create mode 100644 src/embedded/constants/embeddedColors.ts rename src/embedded/enums/{IterableEmbeddedView.ts => IterableEmbeddedViewType.ts} (84%) create mode 100644 src/embedded/types/IterableEmbeddedViewButtonInfo.ts create mode 100644 src/embedded/types/IterableEmbeddedViewStyles.ts diff --git a/src/embedded/classes/IterableEmbeddedView.ts b/src/embedded/classes/IterableEmbeddedView.ts new file mode 100644 index 000000000..f6b998993 --- /dev/null +++ b/src/embedded/classes/IterableEmbeddedView.ts @@ -0,0 +1,181 @@ +import { IterableEmbeddedViewType } from '../enums/IterableEmbeddedViewType'; +import { IterableEmbeddedMessage } from './IterableEmbeddedMessage'; +import { IterableEmbeddedViewConfig } from './IterableEmbeddedViewConfig'; +import type { IterableEmbeddedMessageElementsButton } from './IterableEmbeddedMessageElementsButton'; +import type { IterableEmbeddedViewButtonInfo } from '../types/IterableEmbeddedViewButtonInfo'; +import type { IterableEmbeddedViewStyles } from '../types/IterableEmbeddedViewStyles'; +import { + embeddedBackgroundColors, + embeddedBodyTextColors, + embeddedBorderColors, + embeddedPrimaryBtnBackgroundColors, + embeddedPrimaryBtnTextColors, + embeddedSecondaryBtnBackgroundColors, + embeddedSecondaryBtnTextColors, + embeddedTitleTextColors, +} from '../constants/embeddedColors'; + +export class IterableEmbeddedView { + private viewType: IterableEmbeddedViewType; + private message: IterableEmbeddedMessage; + private config?: IterableEmbeddedViewConfig | null; + + private readonly defaultBackgroundColor: number; + private readonly defaultBorderColor: number; + private readonly defaultPrimaryBtnBackgroundColor: number; + private readonly defaultPrimaryBtnTextColor: number; + private readonly defaultSecondaryBtnBackgroundColor: number; + private readonly defaultSecondaryBtnTextColor: number; + private readonly defaultTitleTextColor: number; + private readonly defaultBodyTextColor: number; + private readonly defaultBorderWidth: number = 1; + private readonly defaultBorderCornerRadius: number = 8.0; + + constructor( + viewType: IterableEmbeddedViewType, + message: IterableEmbeddedMessage, + config?: IterableEmbeddedViewConfig | null + ) { + this.viewType = viewType; + this.message = message; + this.config = config ?? null; + + // Default color values are placeholders; caller can map them to theme values if needed. + // These numeric values mirror the Kotlin use of Int colors. + this.defaultBackgroundColor = this.getDefaultColor( + viewType, + embeddedBackgroundColors.notification, + embeddedBackgroundColors.card, + embeddedBackgroundColors.banner + ); + this.defaultBorderColor = this.getDefaultColor( + viewType, + embeddedBorderColors.notification, + embeddedBorderColors.card, + embeddedBorderColors.banner + ); + this.defaultPrimaryBtnBackgroundColor = this.getDefaultColor( + viewType, + embeddedPrimaryBtnBackgroundColors.notification, + embeddedPrimaryBtnBackgroundColors.card, + embeddedPrimaryBtnBackgroundColors.banner + ); + this.defaultPrimaryBtnTextColor = this.getDefaultColor( + viewType, + embeddedPrimaryBtnTextColors.notification, + embeddedPrimaryBtnTextColors.card, + embeddedPrimaryBtnTextColors.banner + ); + this.defaultSecondaryBtnBackgroundColor = this.getDefaultColor( + viewType, + embeddedSecondaryBtnBackgroundColors.notification, + embeddedSecondaryBtnBackgroundColors.card, + embeddedSecondaryBtnBackgroundColors.banner + ); + this.defaultSecondaryBtnTextColor = this.getDefaultColor( + viewType, + embeddedSecondaryBtnTextColors.notification, + embeddedSecondaryBtnTextColors.card, + embeddedSecondaryBtnTextColors.banner + ); + this.defaultTitleTextColor = this.getDefaultColor( + viewType, + embeddedTitleTextColors.notification, + embeddedTitleTextColors.card, + embeddedTitleTextColors.banner + ); + this.defaultBodyTextColor = this.getDefaultColor( + viewType, + embeddedBodyTextColors.notification, + embeddedBodyTextColors.card, + embeddedBodyTextColors.banner + ); + } + + getStyles(): IterableEmbeddedViewStyles { + const c = this.config; + return { + backgroundColor: c?.backgroundColor ?? this.defaultBackgroundColor, + borderColor: c?.borderColor ?? this.defaultBorderColor, + borderWidth: c?.borderWidth ?? this.defaultBorderWidth, + borderCornerRadius: + c?.borderCornerRadius ?? this.defaultBorderCornerRadius, + primaryBtnBackgroundColor: + c?.primaryBtnBackgroundColor ?? this.defaultPrimaryBtnBackgroundColor, + primaryBtnTextColor: + c?.primaryBtnTextColor ?? this.defaultPrimaryBtnTextColor, + secondaryBtnBackgroundColor: + c?.secondaryBtnBackgroundColor ?? + this.defaultSecondaryBtnBackgroundColor, + secondaryBtnTextColor: + c?.secondaryBtnTextColor ?? this.defaultSecondaryBtnTextColor, + titleTextColor: c?.titleTextColor ?? this.defaultTitleTextColor, + bodyTextColor: c?.bodyTextColor ?? this.defaultBodyTextColor, + }; + } + + getTitle(): string | null | undefined { + return this.message.elements?.title ?? null; + } + + getBody(): string | null | undefined { + return this.message.elements?.body ?? null; + } + + getMedia(): { + url?: string | null; + caption?: string | null; + shouldShow: boolean; + } { + const url = this.message.elements?.mediaURL ?? null; + const caption = this.message.elements?.mediaUrlCaption ?? null; + const shouldShow = + !!url && + url.length > 0 && + this.viewType !== IterableEmbeddedViewType.Notification; + return { url, caption, shouldShow }; + } + + getButtons(): IterableEmbeddedViewButtonInfo[] { + const buttons = this.message.elements?.buttons ?? null; + if (!buttons || buttons.length === 0) return []; + + const mapOne = ( + b?: IterableEmbeddedMessageElementsButton | null + ): IterableEmbeddedViewButtonInfo => { + if (!b) return { id: null, title: null, clickedUrl: null }; + const clickedUrl = + (b.action?.data && b.action?.data?.length > 0 + ? b.action.data + : b.action?.type) ?? null; + return { id: b.id ?? null, title: b.title ?? null, clickedUrl }; + }; + + const first = mapOne(buttons[0] ?? null); + const second = mapOne(buttons.length > 1 ? buttons[1] : null); + + return [first, second].filter((bi) => bi.title && bi.title.length > 0); + } + + getDefaultActionUrl(): string | null { + const da = this.message.elements?.defaultAction ?? null; + if (!da) return null; + return (da.data && da.data.length > 0 ? da.data : da.type) ?? null; + } + + private getDefaultColor( + viewType: IterableEmbeddedViewType, + notificationColor: number, + cardColor: number, + bannerColor: number + ): number { + switch (viewType) { + case IterableEmbeddedViewType.Notification: + return notificationColor; + case IterableEmbeddedViewType.Card: + return cardColor; + default: + return bannerColor; + } + } +} diff --git a/src/embedded/constants/embeddedColors.ts b/src/embedded/constants/embeddedColors.ts new file mode 100644 index 000000000..b366c216a --- /dev/null +++ b/src/embedded/constants/embeddedColors.ts @@ -0,0 +1,47 @@ +export const embeddedBackgroundColors = { + notification: 0xff121212, + card: 0xffffffff, + banner: 0xffffffff, +}; + +export const embeddedBorderColors = { + notification: 0xff2a2a2a, + card: 0xffe0e0e0, + banner: 0xffe0e0e0, +}; + +export const embeddedPrimaryBtnBackgroundColors = { + notification: 0xffffffff, + card: 0xffffffff, + banner: 0xff1a73e8, +}; + +export const embeddedPrimaryBtnTextColors = { + notification: 0xff121212, + card: 0xff1a73e8, + banner: 0xffffffff, +}; + +export const embeddedSecondaryBtnBackgroundColors = { + notification: 0xff121212, + card: 0xffffffff, + banner: 0xffffffff, +}; + +export const embeddedSecondaryBtnTextColors = { + notification: 0xff121212, + card: 0xff1a73e8, + banner: 0xff1a73e8, +}; + +export const embeddedTitleTextColors = { + notification: 0xffffffff, + card: 0xff111111, + banner: 0xff111111, +}; + +export const embeddedBodyTextColors = { + notification: 0xffffffff, + card: 0xff444444, + banner: 0xff444444, +}; diff --git a/src/embedded/enums/IterableEmbeddedView.ts b/src/embedded/enums/IterableEmbeddedViewType.ts similarity index 84% rename from src/embedded/enums/IterableEmbeddedView.ts rename to src/embedded/enums/IterableEmbeddedViewType.ts index 8e8e811be..90a0b5d7e 100644 --- a/src/embedded/enums/IterableEmbeddedView.ts +++ b/src/embedded/enums/IterableEmbeddedViewType.ts @@ -1,7 +1,7 @@ /** * The view type for an embedded message. */ -export enum IterableEmbeddedView { +export enum IterableEmbeddedViewType { /** The embedded view is a banner */ Banner = 0, /** The embedded view is a card */ diff --git a/src/embedded/enums/index.ts b/src/embedded/enums/index.ts index e69de29bb..511ad021b 100644 --- a/src/embedded/enums/index.ts +++ b/src/embedded/enums/index.ts @@ -0,0 +1 @@ +export * from './IterableEmbeddedViewType'; diff --git a/src/embedded/types/IterableEmbeddedViewButtonInfo.ts b/src/embedded/types/IterableEmbeddedViewButtonInfo.ts new file mode 100644 index 000000000..54326aea9 --- /dev/null +++ b/src/embedded/types/IterableEmbeddedViewButtonInfo.ts @@ -0,0 +1,5 @@ +export interface IterableEmbeddedViewButtonInfo { + id?: string | null; + title?: string | null; + clickedUrl?: string | null; +} diff --git a/src/embedded/types/IterableEmbeddedViewStyles.ts b/src/embedded/types/IterableEmbeddedViewStyles.ts new file mode 100644 index 000000000..60ef929db --- /dev/null +++ b/src/embedded/types/IterableEmbeddedViewStyles.ts @@ -0,0 +1,12 @@ +export interface IterableEmbeddedViewStyles { + backgroundColor: number; + borderColor: number; + borderWidth: number; + borderCornerRadius: number; + primaryBtnBackgroundColor: number; + primaryBtnTextColor: number; + secondaryBtnBackgroundColor: number; + secondaryBtnTextColor: number; + titleTextColor: number; + bodyTextColor: number; +} From 1408854657a6ed1857214e8ab01d896278a22af3 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 7 Oct 2025 13:42:49 -0700 Subject: [PATCH 13/45] feat: enhance IterableApi with new tracking methods for embedded sessions and clicks --- src/core/classes/IterableApi.ts | 77 ++++++++++++------- .../classes/IterableEmbeddedSessionManager.ts | 46 +++++++---- 2 files changed, 81 insertions(+), 42 deletions(-) diff --git a/src/core/classes/IterableApi.ts b/src/core/classes/IterableApi.ts index 6676eb80f..d5129d8ed 100644 --- a/src/core/classes/IterableApi.ts +++ b/src/core/classes/IterableApi.ts @@ -1,21 +1,20 @@ import { Platform } from 'react-native'; import RNIterableAPI from '../../api'; -import { IterableConfig } from './IterableConfig'; -import type { IterableLogger } from './IterableLogger'; -import { defaultLogger } from '../constants/defaults'; -import { IterableAttributionInfo } from './IterableAttributionInfo'; -import type { IterableCommerceItem } from './IterableCommerceItem'; +import type { IterableEmbeddedMessage } from '../../embedded/classes/IterableEmbeddedMessage'; +import type { IterableEmbeddedSession } from '../../embedded/classes/IterableEmbeddedSession'; +import type { IterableHtmlInAppContent } from '../../inApp/classes/IterableHtmlInAppContent'; import type { IterableInAppMessage } from '../../inApp/classes/IterableInAppMessage'; -import type { IterableInAppLocation } from '../../inApp/enums/IterableInAppLocation'; import type { IterableInAppCloseSource } from '../../inApp/enums/IterableInAppCloseSource'; import type { IterableInAppDeleteSource } from '../../inApp/enums/IterableInAppDeleteSource'; -import type { IterableHtmlInAppContent } from '../../inApp/classes/IterableHtmlInAppContent'; +import type { IterableInAppLocation } from '../../inApp/enums/IterableInAppLocation'; import type { IterableInAppShowResponse } from '../../inApp/enums/IterableInAppShowResponse'; import type { IterableInboxImpressionRowInfo } from '../../inbox/types/IterableInboxImpressionRowInfo'; -import type { IterableEmbeddedSession } from '../../embedded/classes/IterableEmbeddedSession'; -import type { IterableEmbeddedMessage } from '../../embedded/classes/IterableEmbeddedMessage'; -import type { IterableEmbeddedPlacement } from '../../embedded/classes/IterableEmbeddedPlacement'; +import { defaultLogger } from '../constants/defaults'; +import { IterableAttributionInfo } from './IterableAttributionInfo'; +import type { IterableCommerceItem } from './IterableCommerceItem'; +import { IterableConfig } from './IterableConfig'; +import type { IterableLogger } from './IterableLogger'; export class IterableApi { static logger: IterableLogger = defaultLogger; @@ -308,9 +307,48 @@ export class IterableApi { */ static trackEmbeddedSession(session: IterableEmbeddedSession) { IterableApi.logger.log('trackEmbeddedSession: ', session); + + if (session == null) { + IterableApi.logger.log('trackEmbeddedSession: session is null'); + return; + } + + if (!session.start || !session.end) { + IterableApi.logger.log( + 'trackEmbeddedSession: sessionStartTime and sessionEndTime must be set', + session + ); + return; + } + return RNIterableAPI.trackEmbeddedSession(session); } + static trackEmbeddedClick( + message: IterableEmbeddedMessage, + buttonId: string | null | undefined, + clickedUrl: string | null | undefined + ) { + IterableApi.logger.log( + 'trackEmbeddedClick: ', + message, + buttonId, + clickedUrl + ); + return RNIterableAPI.trackEmbeddedClick(message, buttonId, clickedUrl); + } + + static trackEmbeddedMessageReceived(message: IterableEmbeddedMessage) { + IterableApi.logger.log('trackEmbeddedMessageReceived: ', message); + + if (message == null) { + IterableApi.logger.log('trackEmbeddedMessageReceived: message is null'); + return; + } + + return RNIterableAPI.trackEmbeddedMessageReceived(message); + } + // ---- End TRACKING ---- // // ====================================================== // @@ -501,22 +539,9 @@ export class IterableApi { * * @returns A Promise that resolves to an array of embedded messages. */ - static getEmbeddedMessages(): Promise { - IterableApi.logger.log('getEmbeddedMessages'); - return RNIterableAPI.getEmbeddedMessages() as unknown as Promise< - IterableEmbeddedPlacement[] - >; - } - - static trackEmbeddedMessageReceived(message: IterableEmbeddedMessage) { - IterableApi.logger.log('trackEmbeddedMessageReceived: ', message); - - if (message == null) { - IterableApi.logger.log('trackEmbeddedMessageReceived: message is null'); - return; - } - - return RNIterableAPI.trackEmbeddedMessageReceived(message); + static getEmbeddedMessages(placementIds: number[] | null) { + IterableApi.logger.log('getEmbeddedMessages: ', placementIds); + return RNIterableAPI.getEmbeddedMessages(placementIds); } // ---- End EMBEDDED ---- // diff --git a/src/embedded/classes/IterableEmbeddedSessionManager.ts b/src/embedded/classes/IterableEmbeddedSessionManager.ts index f96d9f113..4a8ee5f9e 100644 --- a/src/embedded/classes/IterableEmbeddedSessionManager.ts +++ b/src/embedded/classes/IterableEmbeddedSessionManager.ts @@ -11,7 +11,11 @@ import { IterableEmbeddedSession } from './IterableEmbeddedSession'; export class IterableEmbeddedSessionManager { private logger: IterableLogger = new IterableLogger(new IterableConfig()); private impressions: Record = {}; - public session?: IterableEmbeddedSession; + public session: IterableEmbeddedSession = new IterableEmbeddedSession({ + start: null, + end: null, + impressions: [], + }); constructor(logger: IterableLogger) { this.logger = logger; @@ -31,6 +35,8 @@ export class IterableEmbeddedSessionManager { // TODO: figure out how to get a unique ID for the session this.session = new IterableEmbeddedSession({ start: new Date(), + end: null, + impressions: [], }); } @@ -40,7 +46,7 @@ export class IterableEmbeddedSessionManager { return; } - if (this.session?.impressions?.length) { + if (Object.keys(this.impressions).length > 0) { this.endAllImpressions(); const sessionToTrack = new IterableEmbeddedSession({ @@ -64,6 +70,7 @@ export class IterableEmbeddedSessionManager { public startImpression(messageId: string, placementId: number) { let impressionData = this.impressions[messageId]; + if (!impressionData) { impressionData = new IterableEmbeddedImpressionData( messageId, @@ -71,17 +78,19 @@ export class IterableEmbeddedSessionManager { ); this.impressions[messageId] = impressionData; } + impressionData.start = new Date(); } public pauseImpression(messageId: string) { const impressionData = this.impressions[messageId]; + if (!impressionData) { this.logger.log('onMessageImpressionEnded: impressionData not found'); return; } - if (impressionData.start == null) { + if (!impressionData.start) { this.logger.log('onMessageImpressionEnded: impressionStarted is null'); return; } @@ -89,6 +98,23 @@ export class IterableEmbeddedSessionManager { this.updateDisplayCountAndDuration(impressionData); } + private endAllImpressions() { + Object.values(this.impressions).forEach((impressionData) => { + this.updateDisplayCountAndDuration(impressionData); + }); + } + + private getImpressionList(): IterableEmbeddedImpression[] { + return Object.values(this.impressions).map((impression) => { + return new IterableEmbeddedImpression({ + messageId: impression.messageId || '', + placementId: impression.placementId || 0, + displayCount: impression.displayCount || 0, + duration: impression.duration || 0, + }); + }); + } + private updateDisplayCountAndDuration( impressionData: IterableEmbeddedImpressionData ): IterableEmbeddedImpressionData { @@ -96,21 +122,9 @@ export class IterableEmbeddedSessionManager { impressionData.displayCount = (impressionData.displayCount || 0) + 1; impressionData.duration = (impressionData.duration || 0) + - (new Date().getTime() - impressionData.start.getTime()) / 1000; + (new Date().getTime() - impressionData.start.getTime()) / 1000.0; impressionData.start = null; } return impressionData; } - - private endAllImpressions() { - Object.values(this.impressions).forEach((impression) => { - this.updateDisplayCountAndDuration(impression); - }); - } - - private getImpressionList(): IterableEmbeddedImpressionData[] { - return Object.values(this.impressions).map((impression) => { - return new IterableEmbeddedImpression(impression); - }); - } } From 452444a272586012569eebf126de1983d3460f42 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 7 Oct 2025 13:48:41 -0700 Subject: [PATCH 14/45] feat: update Iterable class tracker with new embedded tracking methods --- src/core/classes/Iterable.ts | 14 +++++++++----- src/core/utils/trackingUtils.ts | 15 +++++++++++++++ 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/core/classes/Iterable.ts b/src/core/classes/Iterable.ts index 1d24bb582..60d3b347b 100644 --- a/src/core/classes/Iterable.ts +++ b/src/core/classes/Iterable.ts @@ -19,6 +19,8 @@ import { IterableAuthResponseResult } from '../enums/IterableAuthResponseResult' import { IterableEventName } from '../enums/IterableEventName'; import type { IterableAuthFailure } from '../types/IterableAuthFailure'; import { + trackEmbeddedClick, + trackEmbeddedMessageReceived, trackEmbeddedSession, trackEvent, trackInAppClick, @@ -113,13 +115,15 @@ export class Iterable { * ``` */ static tracker = { - trackPushOpenWithCampaignId, - trackPurchase, - trackInAppOpen, + trackEmbeddedClick, + trackEmbeddedMessageReceived, + trackEmbeddedSession, + trackEvent, trackInAppClick, trackInAppClose, - trackEvent, - trackEmbeddedSession, + trackInAppOpen, + trackPurchase, + trackPushOpenWithCampaignId, }; /** * Initializes the Iterable React Native SDK in your app's Javascript or Typescript code. diff --git a/src/core/utils/trackingUtils.ts b/src/core/utils/trackingUtils.ts index 27d710cec..2c0c4f5f0 100644 --- a/src/core/utils/trackingUtils.ts +++ b/src/core/utils/trackingUtils.ts @@ -4,6 +4,7 @@ import type { IterableCommerceItem } from '../classes/IterableCommerceItem'; import type { IterableInAppMessage } from '../../inApp/classes/IterableInAppMessage'; import type { IterableInAppLocation } from '../../inApp/enums/IterableInAppLocation'; import type { IterableInAppCloseSource } from '../../inApp/enums/IterableInAppCloseSource'; +import type { IterableEmbeddedMessage } from '../../embedded/classes/IterableEmbeddedMessage'; /** * Create a `pushOpen` event on the current user's Iterable profile, populating @@ -211,3 +212,17 @@ export const trackEvent = (name: string, dataFields?: unknown) => { export const trackEmbeddedSession = (session: IterableEmbeddedSession) => { return IterableApi.trackEmbeddedSession(session); }; + +export const trackEmbeddedClick = ( + message: IterableEmbeddedMessage, + buttonId: string, + clickedUrl: string +) => { + return IterableApi.trackEmbeddedClick(message, buttonId, clickedUrl); +}; + +export const trackEmbeddedMessageReceived = ( + message: IterableEmbeddedMessage +) => { + return IterableApi.trackEmbeddedMessageReceived(message); +}; From a6a45eda70c80924014734492c69cf93a4d97504 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 7 Oct 2025 14:28:31 -0700 Subject: [PATCH 15/45] feat: implement getEmbeddedMessages method and enhance serialization for embedded messages --- .../reactnative/RNIterableAPIModuleImpl.java | 9 +++++---- .../iterable/reactnative/Serialization.java | 18 +++++++++++++++++- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java index 4d1145e6f..e71aacff0 100644 --- a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java +++ b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java @@ -23,10 +23,11 @@ import com.iterable.iterableapi.IterableAction; import com.iterable.iterableapi.IterableActionContext; import com.iterable.iterableapi.IterableApi; +import com.iterable.iterableapi.IterableAttributionInfo; import com.iterable.iterableapi.IterableAuthHandler; import com.iterable.iterableapi.IterableConfig; import com.iterable.iterableapi.IterableCustomActionHandler; -import com.iterable.iterableapi.IterableAttributionInfo; +import com.iterable.iterableapi.IterableEmbeddedMessage; import com.iterable.iterableapi.IterableHelper; import com.iterable.iterableapi.IterableInAppCloseAction; import com.iterable.iterableapi.IterableInAppHandler; @@ -88,9 +89,9 @@ public void initializeWithApiKey(String apiKey, ReadableMap configReadableMap, S configBuilder.setAuthHandler(this); } - if (configReadableMap.hasKey("enableEmbeddedMessaging") && configReadableMap.getBoolean("enableEmbeddedMessaging") == true) { - configBuilder.setEnableEmbeddedMessaging(this); - } + if (iterableContextJSON.has("enableEmbeddedMessaging")) { + configBuilder.setEnableEmbeddedMessaging(iterableContextJSON.optBoolean("enableEmbeddedMessaging")); + } IterableApi.initialize(reactContext, apiKey, configBuilder.build()); IterableApi.getInstance().setDeviceAttribute("reactNativeSDKVersion", version); diff --git a/android/src/main/java/com/iterable/reactnative/Serialization.java b/android/src/main/java/com/iterable/reactnative/Serialization.java index 92c549554..2f0c4e1ca 100644 --- a/android/src/main/java/com/iterable/reactnative/Serialization.java +++ b/android/src/main/java/com/iterable/reactnative/Serialization.java @@ -16,6 +16,7 @@ import com.iterable.iterableapi.IterableActionContext; import com.iterable.iterableapi.IterableConfig; import com.iterable.iterableapi.IterableDataRegion; +import com.iterable.iterableapi.IterableEmbeddedMessage; import com.iterable.iterableapi.IterableInAppCloseAction; import com.iterable.iterableapi.IterableInAppDeleteActionType; import com.iterable.iterableapi.IterableInAppHandler; @@ -23,8 +24,8 @@ import com.iterable.iterableapi.IterableInAppMessage; import com.iterable.iterableapi.IterableInboxSession; import com.iterable.iterableapi.IterableLogger; -import com.iterable.iterableapi.RNIterableInternal; import com.iterable.iterableapi.RetryPolicy; +import com.iterable.iterableapi.RNIterableInternal; import org.json.JSONArray; import org.json.JSONException; @@ -137,6 +138,17 @@ static JSONArray serializeInAppMessages(List inAppMessages return inAppMessagesJson; } + static JSONArray serializeEmbeddedMessages(List embeddedMessages) { + JSONArray embeddedMessagesJson = new JSONArray(); + if (embeddedMessages != null) { + for (IterableEmbeddedMessage message : embeddedMessages) { + JSONObject messageJson = IterableEmbeddedMessage.Companion.toJSONObject(message); + embeddedMessagesJson.put(messageJson); + } + } + return embeddedMessagesJson; + } + static IterableConfig.Builder getConfigFromReadableMap(ReadableMap iterableContextMap) { try { JSONObject iterableContextJSON = convertMapToJson(iterableContextMap); @@ -218,6 +230,10 @@ static IterableConfig.Builder getConfigFromReadableMap(ReadableMap iterableConte configBuilder.setDataRegion(iterableDataRegion); } + if (iterableContextJSON.has("enableEmbeddedMessaging")) { + configBuilder.setEnableEmbeddedMessaging(iterableContextJSON.optBoolean("enableEmbeddedMessaging")); + } + if (iterableContextJSON.has("retryPolicy")) { JSONObject retryPolicyJson = iterableContextJSON.getJSONObject("retryPolicy"); int maxRetry = retryPolicyJson.getInt("maxRetry"); From d67c1ed87b6c715619ed5c1763a550d69e3600e9 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 7 Oct 2025 14:30:27 -0700 Subject: [PATCH 16/45] feat: add getEmbeddedMessages method to RNIterableAPIModule for embedded messaging support --- .../reactnative/RNIterableAPIModuleImpl.java | 31 +++++++++++++++++++ .../newarch/java/com/RNIterableAPIModule.java | 5 +++ .../oldarch/java/com/RNIterableAPIModule.java | 4 +++ example/android/gradle.properties | 4 +-- src/api/NativeRNIterableAPI.ts | 11 +++++++ 5 files changed, 53 insertions(+), 2 deletions(-) diff --git a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java index e71aacff0..eb09ea263 100644 --- a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java +++ b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java @@ -634,6 +634,37 @@ public void sendEvent(@NonNull String eventName, @Nullable Object eventData) { public void onInboxUpdated() { sendEvent(EventName.receivedIterableInboxChanged.name(), null); } + + // --------------------------------------------------------------------------------------- + // endregion + + // --------------------------------------------------------------------------------------- + // region Embedded messaging + + + public void getEmbeddedMessages(Integer placementId, Promise promise) { + IterableLogger.d(TAG, "getEmbeddedMessages for placement: " + placementId); + + try { + JSONArray embeddedMessageJsonArray = Serialization.serializeEmbeddedMessages(IterableApi.getInstance().getEmbeddedManager().getMessages(placementId)); + IterableLogger.d(TAG, "Messages for placement: " + embeddedMessageJsonArray); + + promise.resolve(Serialization.convertJsonToArray(embeddedMessageJsonArray)); + } catch (JSONException e) { + IterableLogger.e(TAG, e.getLocalizedMessage()); + promise.reject("", "Failed to fetch messages with error " + e.getLocalizedMessage()); + } + } + + private JSONObject createTestPlacement(int placementId) throws JSONException { + JSONObject placement = new JSONObject(); + placement.put("placementId", placementId); + return placement; + } + + + // --------------------------------------------------------------------------------------- + // endregion } enum EventName { diff --git a/android/src/newarch/java/com/RNIterableAPIModule.java b/android/src/newarch/java/com/RNIterableAPIModule.java index f145bab10..4063cd067 100644 --- a/android/src/newarch/java/com/RNIterableAPIModule.java +++ b/android/src/newarch/java/com/RNIterableAPIModule.java @@ -231,4 +231,9 @@ public void sendEvent(@NonNull String eventName, @Nullable Object eventData) { public void onInboxUpdated() { moduleImpl.onInboxUpdated(); } + + @Override + public void getEmbeddedMessages(Integer placementId, Promise promise) { + moduleImpl.getEmbeddedMessages(placementId, promise); + } } diff --git a/android/src/oldarch/java/com/RNIterableAPIModule.java b/android/src/oldarch/java/com/RNIterableAPIModule.java index c3a72339b..69544a9f0 100644 --- a/android/src/oldarch/java/com/RNIterableAPIModule.java +++ b/android/src/oldarch/java/com/RNIterableAPIModule.java @@ -228,6 +228,10 @@ public void pauseAuthRetries(boolean pauseRetry) { moduleImpl.pauseAuthRetries(pauseRetry); } + @ReactMethod + public void getEmbeddedMessages(Integer placementId, Promise promise) { + moduleImpl.getEmbeddedMessages(placementId, promise); + } public void sendEvent(@NonNull String eventName, @Nullable Object eventData) { moduleImpl.sendEvent(eventName, eventData); diff --git a/example/android/gradle.properties b/example/android/gradle.properties index c26b81501..136b86960 100644 --- a/example/android/gradle.properties +++ b/example/android/gradle.properties @@ -32,7 +32,7 @@ reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64 # your application. You should enable this flag either if you want # to write custom TurboModules/Fabric components OR use libraries that # are providing them. -newArchEnabled=true +newArchEnabled=false # Use this property to enable or disable the Hermes JS engine. # If set to false, you will be using JSC instead. @@ -40,4 +40,4 @@ hermesEnabled=true # Needed for react-native-webview # See: https://github.com/react-native-webview/react-native-webview/blob/HEAD/docs/Getting-Started.md -android.enableJetifier=true \ No newline at end of file +android.enableJetifier=true diff --git a/src/api/NativeRNIterableAPI.ts b/src/api/NativeRNIterableAPI.ts index 391fadbb7..36ca2fedd 100644 --- a/src/api/NativeRNIterableAPI.ts +++ b/src/api/NativeRNIterableAPI.ts @@ -118,6 +118,17 @@ export interface Spec extends TurboModule { passAlongAuthToken(authToken?: string | null): void; pauseAuthRetries(pauseRetry: boolean): void; + // Embedded messaging + getEmbeddedMessages(placementIds: number[] | null): Promise< + Promise< + { + metadata: { [key: string]: string | number | boolean }; + elements: { [key: string]: string | number | boolean }; + payload: { [key: string]: string | number | boolean }; + }[] + > + >; + // Wake app -- android only wakeApp(): void; From a4fe2acb9370119b1b4ca771c32c1943682755c1 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 7 Oct 2025 14:38:29 -0700 Subject: [PATCH 17/45] fix: update enableEmbeddedMessaging configuration to use configReadableMap for better integration --- .../com/iterable/reactnative/RNIterableAPIModuleImpl.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java index eb09ea263..a5c8df036 100644 --- a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java +++ b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java @@ -89,8 +89,8 @@ public void initializeWithApiKey(String apiKey, ReadableMap configReadableMap, S configBuilder.setAuthHandler(this); } - if (iterableContextJSON.has("enableEmbeddedMessaging")) { - configBuilder.setEnableEmbeddedMessaging(iterableContextJSON.optBoolean("enableEmbeddedMessaging")); + if (configReadableMap.hasKey("enableEmbeddedMessaging")) { + configBuilder.setEnableEmbeddedMessaging(configReadableMap.getBoolean("enableEmbeddedMessaging")); } IterableApi.initialize(reactContext, apiKey, configBuilder.build()); @@ -123,6 +123,10 @@ public void initialize2WithApiKey(String apiKey, ReadableMap configReadableMap, configBuilder.setAuthHandler(this); } + if (configReadableMap.hasKey("enableEmbeddedMessaging")) { + configBuilder.setEnableEmbeddedMessaging(configReadableMap.getBoolean("enableEmbeddedMessaging")); + } + // 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. From bae7dbf7b9b9961faecda143504e3391cd204c29 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 7 Oct 2025 14:49:51 -0700 Subject: [PATCH 18/45] feat: update getEmbeddedMessages method to accept multiple placement IDs --- .../reactnative/RNIterableAPIModuleImpl.java | 30 ++++++++++++++++--- .../newarch/java/com/RNIterableAPIModule.java | 4 +-- .../oldarch/java/com/RNIterableAPIModule.java | 4 +-- example/src/components/User/User.tsx | 11 ++++++- src/core/classes/Iterable.ts | 4 +++ 5 files changed, 44 insertions(+), 9 deletions(-) diff --git a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java index a5c8df036..5e931b952 100644 --- a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java +++ b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java @@ -46,6 +46,7 @@ import java.util.Map; import java.util.HashMap; import java.util.List; +import java.util.ArrayList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -646,12 +647,33 @@ public void onInboxUpdated() { // region Embedded messaging - public void getEmbeddedMessages(Integer placementId, Promise promise) { - IterableLogger.d(TAG, "getEmbeddedMessages for placement: " + placementId); + public void getEmbeddedMessages(@Nullable ReadableArray placementIds, Promise promise) { + IterableLogger.d(TAG, "getEmbeddedMessages for placements: " + placementIds); try { - JSONArray embeddedMessageJsonArray = Serialization.serializeEmbeddedMessages(IterableApi.getInstance().getEmbeddedManager().getMessages(placementId)); - IterableLogger.d(TAG, "Messages for placement: " + embeddedMessageJsonArray); + List allMessages = new ArrayList<>(); + + if (placementIds == null || placementIds.size() == 0) { + // If no placement IDs provided, we need to get messages for all possible placements + // Since the Android SDK requires a placement ID, we'll use 0 as a default + // This might need to be adjusted based on the actual SDK behavior + List messages = IterableApi.getInstance().getEmbeddedManager().getMessages(0L); + if (messages != null) { + allMessages.addAll(messages); + } + } else { + // Convert ReadableArray to individual placement IDs and get messages for each + for (int i = 0; i < placementIds.size(); i++) { + long placementId = placementIds.getInt(i); + List messages = IterableApi.getInstance().getEmbeddedManager().getMessages(placementId); + if (messages != null) { + allMessages.addAll(messages); + } + } + } + + JSONArray embeddedMessageJsonArray = Serialization.serializeEmbeddedMessages(allMessages); + IterableLogger.d(TAG, "Messages for placements: " + embeddedMessageJsonArray); promise.resolve(Serialization.convertJsonToArray(embeddedMessageJsonArray)); } catch (JSONException e) { diff --git a/android/src/newarch/java/com/RNIterableAPIModule.java b/android/src/newarch/java/com/RNIterableAPIModule.java index 4063cd067..bfc78ef26 100644 --- a/android/src/newarch/java/com/RNIterableAPIModule.java +++ b/android/src/newarch/java/com/RNIterableAPIModule.java @@ -233,7 +233,7 @@ public void onInboxUpdated() { } @Override - public void getEmbeddedMessages(Integer placementId, Promise promise) { - moduleImpl.getEmbeddedMessages(placementId, promise); + public void getEmbeddedMessages(@Nullable ReadableArray placementIds, Promise promise) { + moduleImpl.getEmbeddedMessages(placementIds, promise); } } diff --git a/android/src/oldarch/java/com/RNIterableAPIModule.java b/android/src/oldarch/java/com/RNIterableAPIModule.java index 69544a9f0..9c09c10e4 100644 --- a/android/src/oldarch/java/com/RNIterableAPIModule.java +++ b/android/src/oldarch/java/com/RNIterableAPIModule.java @@ -229,8 +229,8 @@ public void pauseAuthRetries(boolean pauseRetry) { } @ReactMethod - public void getEmbeddedMessages(Integer placementId, Promise promise) { - moduleImpl.getEmbeddedMessages(placementId, promise); + public void getEmbeddedMessages(@Nullable ReadableArray placementIds, Promise promise) { + moduleImpl.getEmbeddedMessages(placementIds, promise); } public void sendEvent(@NonNull String eventName, @Nullable Object eventData) { diff --git a/example/src/components/User/User.tsx b/example/src/components/User/User.tsx index 23f8361a5..43cb78b36 100644 --- a/example/src/components/User/User.tsx +++ b/example/src/components/User/User.tsx @@ -1,5 +1,5 @@ import { Iterable } from '@iterable/react-native-sdk'; -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { Text, TouchableOpacity, View } from 'react-native'; import { useIterableApp } from '../../hooks'; @@ -17,6 +17,12 @@ export const User = () => { } }, [isLoggedIn]); + const getEmbeddedMessages = useCallback(() => { + Iterable.getEmbeddedMessages().then((messages: unknown) => { + console.log(messages); + }); + }, []); + return ( Welcome Iterator @@ -24,6 +30,9 @@ export const User = () => { Logout + + Get embedded messages + ); }; diff --git a/src/core/classes/Iterable.ts b/src/core/classes/Iterable.ts index 60d3b347b..671deb8ec 100644 --- a/src/core/classes/Iterable.ts +++ b/src/core/classes/Iterable.ts @@ -918,6 +918,10 @@ export class Iterable { ); } + static getEmbeddedMessages(placementIds?: number[] | null) { + return IterableApi.getEmbeddedMessages(placementIds ?? null); + } + /** * Sets up event handlers for various Iterable events. * From ec940a9aab7fc03b14f22ede31c3b00150e73c92 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 7 Oct 2025 15:29:30 -0700 Subject: [PATCH 19/45] feat: synchronize embedded messages on initialization --- .../reactnative/RNIterableAPIModuleImpl.java | 5 ++++ example/src/components/User/User.tsx | 2 +- example/src/hooks/useIterableApp.tsx | 2 ++ src/api/NativeRNIterableAPI.ts | 30 +++++++++++++------ src/core/classes/Iterable.ts | 6 ++-- 5 files changed, 33 insertions(+), 12 deletions(-) diff --git a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java index 5e931b952..7605c6cb9 100644 --- a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java +++ b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java @@ -19,6 +19,7 @@ import com.facebook.react.modules.core.DeviceEventManagerModule; import com.iterable.iterableapi.AuthFailure; +import com.iterable.iterableapi.EmbeddedMessageElementsButton; import com.iterable.iterableapi.InboxSessionManager; import com.iterable.iterableapi.IterableAction; import com.iterable.iterableapi.IterableActionContext; @@ -27,7 +28,9 @@ import com.iterable.iterableapi.IterableAuthHandler; import com.iterable.iterableapi.IterableConfig; import com.iterable.iterableapi.IterableCustomActionHandler; +import com.iterable.iterableapi.IterableEmbeddedManager; import com.iterable.iterableapi.IterableEmbeddedMessage; +import com.iterable.iterableapi.IterableEmbeddedSession; import com.iterable.iterableapi.IterableHelper; import com.iterable.iterableapi.IterableInAppCloseAction; import com.iterable.iterableapi.IterableInAppHandler; @@ -98,6 +101,7 @@ public void initializeWithApiKey(String apiKey, ReadableMap configReadableMap, S IterableApi.getInstance().setDeviceAttribute("reactNativeSDKVersion", version); IterableApi.getInstance().getInAppManager().addListener(this); + IterableApi.getInstance().getEmbeddedManager().syncMessages(); // MOB-10421: Figure out what the error cases are and handle them appropriately // This is just here to match the TS types and let the JS thread know when we are done initializing @@ -136,6 +140,7 @@ public void initialize2WithApiKey(String apiKey, ReadableMap configReadableMap, IterableApi.getInstance().setDeviceAttribute("reactNativeSDKVersion", version); IterableApi.getInstance().getInAppManager().addListener(this); + IterableApi.getInstance().getEmbeddedManager().syncMessages(); // MOB-10421: Figure out what the error cases are and handle them appropriately // This is just here to match the TS types and let the JS thread know when we are done initializing diff --git a/example/src/components/User/User.tsx b/example/src/components/User/User.tsx index 43cb78b36..65f4751d2 100644 --- a/example/src/components/User/User.tsx +++ b/example/src/components/User/User.tsx @@ -18,7 +18,7 @@ export const User = () => { }, [isLoggedIn]); const getEmbeddedMessages = useCallback(() => { - Iterable.getEmbeddedMessages().then((messages: unknown) => { + Iterable.getEmbeddedMessages([1641]).then((messages: unknown) => { console.log(messages); }); }, []); diff --git a/example/src/hooks/useIterableApp.tsx b/example/src/hooks/useIterableApp.tsx index d648dd25c..ed365372c 100644 --- a/example/src/hooks/useIterableApp.tsx +++ b/example/src/hooks/useIterableApp.tsx @@ -128,6 +128,8 @@ export const IterableAppProvider: FunctionComponent< config.inAppDisplayInterval = 1.0; // Min gap between in-apps. No need to set this in production. + config.enableEmbeddedMessaging = true; + config.retryPolicy = { maxRetry: 5, retryInterval: 10, diff --git a/src/api/NativeRNIterableAPI.ts b/src/api/NativeRNIterableAPI.ts index 36ca2fedd..4ad52b4fc 100644 --- a/src/api/NativeRNIterableAPI.ts +++ b/src/api/NativeRNIterableAPI.ts @@ -1,6 +1,24 @@ import type { TurboModule } from 'react-native'; import { TurboModuleRegistry } from 'react-native'; +interface EmbeddedMessage { + metadata: { [key: string]: string | number | boolean }; + elements: { + buttons: { + id: string; + title: string | null; + action: { [key: string]: string | number | boolean } | null; + }[]; + body: string | null; + mediaURL: string | null; + mediaUrlCaption: string | null; + defaultAction: { [key: string]: string | number | boolean } | null; + text: { [key: string]: string | number | boolean }[] | null; + title: string | null; + }; + payload: { [key: string]: string | number | boolean }; +} + export interface Spec extends TurboModule { // Initialization initializeWithApiKey( @@ -119,15 +137,9 @@ export interface Spec extends TurboModule { pauseAuthRetries(pauseRetry: boolean): void; // Embedded messaging - getEmbeddedMessages(placementIds: number[] | null): Promise< - Promise< - { - metadata: { [key: string]: string | number | boolean }; - elements: { [key: string]: string | number | boolean }; - payload: { [key: string]: string | number | boolean }; - }[] - > - >; + getEmbeddedMessages( + placementIds: number[] | null + ): Promise; // Wake app -- android only wakeApp(): void; diff --git a/src/core/classes/Iterable.ts b/src/core/classes/Iterable.ts index 671deb8ec..cc43bec57 100644 --- a/src/core/classes/Iterable.ts +++ b/src/core/classes/Iterable.ts @@ -918,8 +918,10 @@ export class Iterable { ); } - static getEmbeddedMessages(placementIds?: number[] | null) { - return IterableApi.getEmbeddedMessages(placementIds ?? null); + static getEmbeddedMessages(placementIds?: number[] | null): Promise { + return IterableApi.getEmbeddedMessages( + placementIds ?? null + ) as unknown as Promise; } /** From b24ca2ea14277877019cbb844c8cde6434d36d2b Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 7 Oct 2025 16:11:15 -0700 Subject: [PATCH 20/45] feat: add syncEmbeddedMessages and getEmbeddedPlacementIds --- .../reactnative/RNIterableAPIModuleImpl.java | 23 +++++++++++++++ .../newarch/java/com/RNIterableAPIModule.java | 8 +++++ .../oldarch/java/com/RNIterableAPIModule.java | 10 +++++++ example/src/components/User/User.tsx | 11 ++++++- src/api/NativeRNIterableAPI.ts | 4 +++ src/core/classes/Iterable.ts | 5 ++++ src/core/classes/IterableApi.ts | 10 +++++++ src/core/constants/defaults.ts | 4 +++ src/core/utils/generateUUID.ts | 29 +++++++++++++++++++ .../classes/IterableEmbeddedManager.ts | 28 ++++++++++++++++++ .../classes/IterableEmbeddedSession.ts | 11 ++----- src/inApp/classes/IterableInAppManager.ts | 2 +- 12 files changed, 135 insertions(+), 10 deletions(-) create mode 100644 src/core/utils/generateUUID.ts create mode 100644 src/embedded/classes/IterableEmbeddedManager.ts diff --git a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java index 7605c6cb9..9abbb80d1 100644 --- a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java +++ b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java @@ -15,6 +15,7 @@ import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.UiThreadUtil; +import com.facebook.react.bridge.WritableArray; import com.facebook.react.bridge.WritableMap; import com.facebook.react.modules.core.DeviceEventManagerModule; @@ -687,6 +688,28 @@ public void getEmbeddedMessages(@Nullable ReadableArray placementIds, Promise pr } } + public void syncEmbeddedMessages() { + IterableLogger.d(TAG, "syncEmbeddedMessages"); + IterableApi.getInstance().getEmbeddedManager().syncMessages(); + } + + public void getEmbeddedPlacementIds(Promise promise) { + IterableLogger.d(TAG, "getEmbeddedPlacementIds"); + try { + List placementIds = IterableApi.getInstance().getEmbeddedManager().getPlacementIds(); + WritableArray writableArray = Arguments.createArray(); + if (placementIds != null) { + for (Long placementId : placementIds) { + writableArray.pushDouble(placementId.doubleValue()); + } + } + promise.resolve(writableArray); + } catch (Exception e) { + IterableLogger.e(TAG, "Error getting placement IDs: " + e.getLocalizedMessage()); + promise.reject("", "Failed to get placement IDs: " + e.getLocalizedMessage()); + } + } + private JSONObject createTestPlacement(int placementId) throws JSONException { JSONObject placement = new JSONObject(); placement.put("placementId", placementId); diff --git a/android/src/newarch/java/com/RNIterableAPIModule.java b/android/src/newarch/java/com/RNIterableAPIModule.java index bfc78ef26..cb44f97fa 100644 --- a/android/src/newarch/java/com/RNIterableAPIModule.java +++ b/android/src/newarch/java/com/RNIterableAPIModule.java @@ -236,4 +236,12 @@ public void onInboxUpdated() { public void getEmbeddedMessages(@Nullable ReadableArray placementIds, Promise promise) { moduleImpl.getEmbeddedMessages(placementIds, promise); } + + public void syncEmbeddedMessages() { + moduleImpl.syncEmbeddedMessages(); + } + + public void getEmbeddedPlacementIds(Promise promise) { + moduleImpl.getEmbeddedPlacementIds(promise); + } } diff --git a/android/src/oldarch/java/com/RNIterableAPIModule.java b/android/src/oldarch/java/com/RNIterableAPIModule.java index 9c09c10e4..d2257f878 100644 --- a/android/src/oldarch/java/com/RNIterableAPIModule.java +++ b/android/src/oldarch/java/com/RNIterableAPIModule.java @@ -233,6 +233,16 @@ public void getEmbeddedMessages(@Nullable ReadableArray placementIds, Promise pr moduleImpl.getEmbeddedMessages(placementIds, promise); } + @ReactMethod + public void syncEmbeddedMessages() { + moduleImpl.syncEmbeddedMessages(); + } + + @ReactMethod + public void getEmbeddedPlacementIds(Promise promise) { + moduleImpl.getEmbeddedPlacementIds(promise); + } + public void sendEvent(@NonNull String eventName, @Nullable Object eventData) { moduleImpl.sendEvent(eventName, eventData); } diff --git a/example/src/components/User/User.tsx b/example/src/components/User/User.tsx index 65f4751d2..2bfe8f045 100644 --- a/example/src/components/User/User.tsx +++ b/example/src/components/User/User.tsx @@ -18,11 +18,17 @@ export const User = () => { }, [isLoggedIn]); const getEmbeddedMessages = useCallback(() => { - Iterable.getEmbeddedMessages([1641]).then((messages: unknown) => { + Iterable.embeddedManager.getMessages([1641]).then((messages: unknown) => { console.log(messages); }); }, []); + const getPlacementIds = useCallback(() => { + Iterable.embeddedManager.getPlacementIds().then((ids: unknown) => { + console.log(ids); + }); + }, []); + return ( Welcome Iterator @@ -33,6 +39,9 @@ export const User = () => { Get embedded messages + + Get placement ids + ); }; diff --git a/src/api/NativeRNIterableAPI.ts b/src/api/NativeRNIterableAPI.ts index 4ad52b4fc..42a6bae85 100644 --- a/src/api/NativeRNIterableAPI.ts +++ b/src/api/NativeRNIterableAPI.ts @@ -141,6 +141,10 @@ export interface Spec extends TurboModule { placementIds: number[] | null ): Promise; + syncEmbeddedMessages(): void; + + getEmbeddedPlacementIds(): Promise; + // Wake app -- android only wakeApp(): void; diff --git a/src/core/classes/Iterable.ts b/src/core/classes/Iterable.ts index cc43bec57..62422a6c1 100644 --- a/src/core/classes/Iterable.ts +++ b/src/core/classes/Iterable.ts @@ -12,6 +12,7 @@ import { IterableInAppLocation } from '../../inApp/enums/IterableInAppLocation'; import { defaultAuthManager, defaultConfig, + defaultEmbeddedManager, defaultInAppManager, defaultLogger, } from '../constants/defaults'; @@ -38,6 +39,7 @@ import { IterableAuthResponse } from './IterableAuthResponse'; import type { IterableCommerceItem } from './IterableCommerceItem'; import { IterableConfig } from './IterableConfig'; import { IterableLogger } from './IterableLogger'; +import { IterableEmbeddedManager } from '../../embedded/classes/IterableEmbeddedManager'; const RNEventEmitter = new NativeEventEmitter(RNIterableAPI); @@ -103,6 +105,8 @@ export class Iterable { */ static authManager: IterableAuthManager = defaultAuthManager; + static embeddedManager: IterableEmbeddedManager = defaultEmbeddedManager; + /** * Tracking manager for the current user. * @@ -203,6 +207,7 @@ export class Iterable { Iterable.logger = logger; Iterable.inAppManager = new IterableInAppManager(logger); Iterable.authManager = new IterableAuthManager(logger); + Iterable.embeddedManager = new IterableEmbeddedManager(logger); IterableApi.setLogger(logger); this.setupEventHandlers(); diff --git a/src/core/classes/IterableApi.ts b/src/core/classes/IterableApi.ts index d5129d8ed..26a4eeb42 100644 --- a/src/core/classes/IterableApi.ts +++ b/src/core/classes/IterableApi.ts @@ -544,6 +544,16 @@ export class IterableApi { return RNIterableAPI.getEmbeddedMessages(placementIds); } + static syncEmbeddedMessages() { + IterableApi.logger.log('syncEmbeddedMessages'); + return RNIterableAPI.syncEmbeddedMessages(); + } + + static getEmbeddedPlacementIds() { + IterableApi.logger.log('getEmbeddedPlacementIds'); + return RNIterableAPI.getEmbeddedPlacementIds(); + } + // ---- End EMBEDDED ---- // // ====================================================== // diff --git a/src/core/constants/defaults.ts b/src/core/constants/defaults.ts index 0351fdc9f..5db65970d 100644 --- a/src/core/constants/defaults.ts +++ b/src/core/constants/defaults.ts @@ -1,3 +1,4 @@ +import { IterableEmbeddedManager } from '../../embedded/classes/IterableEmbeddedManager'; import { IterableInAppManager } from '../../inApp/classes/IterableInAppManager'; import { IterableAuthManager } from '../classes/IterableAuthManager'; import { IterableConfig } from '../classes/IterableConfig'; @@ -7,3 +8,6 @@ export const defaultConfig = new IterableConfig(); export const defaultLogger = new IterableLogger(defaultConfig); export const defaultInAppManager = new IterableInAppManager(defaultLogger); export const defaultAuthManager = new IterableAuthManager(defaultLogger); +export const defaultEmbeddedManager = new IterableEmbeddedManager( + defaultLogger +); diff --git a/src/core/utils/generateUUID.ts b/src/core/utils/generateUUID.ts new file mode 100644 index 000000000..6d3f653dc --- /dev/null +++ b/src/core/utils/generateUUID.ts @@ -0,0 +1,29 @@ +export const generateUUID = () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const g: any = typeof global !== 'undefined' ? (global as any) : undefined; + + if (g?.crypto?.getRandomValues) { + const bytes = new Uint8Array(16); + g.crypto.getRandomValues(bytes); + + // RFC 4122 compliance + bytes[6] = (bytes[6] ?? 0 & 0x0f) | 0x40; // version 4 + bytes[8] = (bytes[8] ?? 0 & 0x3f) | 0x80; // variant 10 + + const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join( + '' + ); + return `${hex.substring(0, 8)}-${hex.substring(8, 12)}-${hex.substring(12, 16)}-${hex.substring(16, 20)}-${hex.substring(20)}`; + } + + // Fallback using Math.random (not cryptographically strong) + const hexDigits = '0123456789abcdef'; + const s: string[] = Array(36); + for (let i = 0; i < 36; i++) { + s[i] = hexDigits.charAt(Math.floor(Math.random() * 16)); + } + s[14] = '4'; + s[19] = hexDigits.charAt((parseInt(s[19] ?? '0', 16) & 0x3) | 0x8); + s[8] = s[13] = s[18] = s[23] = '-'; + return s.join(''); +}; diff --git a/src/embedded/classes/IterableEmbeddedManager.ts b/src/embedded/classes/IterableEmbeddedManager.ts new file mode 100644 index 000000000..488b45db7 --- /dev/null +++ b/src/embedded/classes/IterableEmbeddedManager.ts @@ -0,0 +1,28 @@ +import { IterableApi } from '../../core/classes/IterableApi'; +import type { IterableLogger } from '../../core/classes/IterableLogger'; +import { defaultLogger } from '../../core/constants/defaults'; +import { IterableEmbeddedSessionManager } from './IterableEmbeddedSessionManager'; + +export class IterableEmbeddedManager { + static logger: IterableLogger = defaultLogger; + + sessionManager: IterableEmbeddedSessionManager = + new IterableEmbeddedSessionManager(defaultLogger); + + constructor(logger: IterableLogger) { + IterableEmbeddedManager.logger = logger; + this.sessionManager = new IterableEmbeddedSessionManager(logger); + } + + syncMessages() { + IterableApi.syncEmbeddedMessages(); + } + + getMessages(placementIds: number[] | null) { + return IterableApi.getEmbeddedMessages(placementIds); + } + + getPlacementIds() { + return IterableApi.getEmbeddedPlacementIds(); + } +} diff --git a/src/embedded/classes/IterableEmbeddedSession.ts b/src/embedded/classes/IterableEmbeddedSession.ts index 5d81f7f19..2c9bf83d6 100644 --- a/src/embedded/classes/IterableEmbeddedSession.ts +++ b/src/embedded/classes/IterableEmbeddedSession.ts @@ -1,5 +1,5 @@ import type { IterableEmbeddedImpressionData } from './IterableEmbeddedImpressionData'; -import { IterableUtil } from '../../core/classes/IterableUtil'; +import { generateUUID } from '../../core/utils/generateUUID'; export interface IterableEmbeddedSessionDict { /** The ID of the session. */ @@ -19,13 +19,8 @@ export class IterableEmbeddedSession { public impressions: IterableEmbeddedSessionDict['impressions'] = []; constructor(options: Partial = {}) { - const { - id = IterableUtil.generateUUID(), - start = new Date(), - end = null, - impressions = [], - } = options; - this.id = id; + const { start = null, end = null, impressions = [] } = options; + this.id = generateUUID(); this.start = start; this.end = end; this.impressions = impressions; diff --git a/src/inApp/classes/IterableInAppManager.ts b/src/inApp/classes/IterableInAppManager.ts index c14867c62..7785a4749 100644 --- a/src/inApp/classes/IterableInAppManager.ts +++ b/src/inApp/classes/IterableInAppManager.ts @@ -1,5 +1,5 @@ import { IterableApi } from '../../core/classes/IterableApi'; -import { IterableLogger } from '../../core/classes/IterableLogger'; +import type { IterableLogger } from '../../core/classes/IterableLogger'; import { defaultLogger } from '../../core/constants/defaults'; import type { IterableInAppDeleteSource } from '../enums/IterableInAppDeleteSource'; import type { IterableInAppLocation } from '../enums/IterableInAppLocation'; From 33a94988a23662f06cce227b9241394be8a34c61 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 7 Oct 2025 18:19:34 -0700 Subject: [PATCH 21/45] feat: add embedded update listener support to RNIterableAPIModule and related classes --- .../reactnative/RNIterableAPIModuleImpl.java | 47 +++++++++++++-- .../newarch/java/com/RNIterableAPIModule.java | 14 +++++ .../oldarch/java/com/RNIterableAPIModule.java | 14 +++++ example/src/components/User/User.tsx | 18 +++++- src/api/NativeRNIterableAPI.ts | 4 ++ src/core/classes/IterableApi.ts | 9 +++ src/core/enums/IterableEventName.ts | 2 + .../classes/IterableEmbeddedManager.ts | 58 +++++++++++++++++++ src/index.tsx | 1 + 9 files changed, 160 insertions(+), 7 deletions(-) diff --git a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java index 9abbb80d1..6e0602ce0 100644 --- a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java +++ b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java @@ -42,6 +42,7 @@ import com.iterable.iterableapi.IterableLogger; import com.iterable.iterableapi.IterableUrlHandler; import com.iterable.iterableapi.RNIterableInternal; +import com.iterable.iterableapi.IterableEmbeddedUpdateHandler; import org.json.JSONArray; import org.json.JSONException; @@ -54,13 +55,14 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; -public class RNIterableAPIModuleImpl implements IterableUrlHandler, IterableCustomActionHandler, IterableInAppHandler, IterableAuthHandler, IterableInAppManager.Listener { +public class RNIterableAPIModuleImpl implements IterableUrlHandler, IterableCustomActionHandler, IterableInAppHandler, IterableAuthHandler, IterableInAppManager.Listener, IterableEmbeddedUpdateHandler { public static final String NAME = "RNIterableAPI"; private static String TAG = "RNIterableAPIModule"; private final ReactApplicationContext reactContext; private IterableInAppHandler.InAppResponse inAppResponse = IterableInAppHandler.InAppResponse.SHOW; + private IterableEmbeddedUpdateHandler embeddedUpdateHandler; //A CountDownLatch. This helps decide whether to handle the in-app in Default way by waiting for JS to respond in runtime. private CountDownLatch jsCallBackLatch; @@ -646,6 +648,7 @@ public void onInboxUpdated() { sendEvent(EventName.receivedIterableInboxChanged.name(), null); } + // --------------------------------------------------------------------------------------- // endregion @@ -710,6 +713,37 @@ public void getEmbeddedPlacementIds(Promise promise) { } } + public void addEmbeddedUpdateListener(@Nullable IterableEmbeddedUpdateHandler handler) { + if (embeddedUpdateHandler == null) { + // If handler is null, use this (the module implementation) as the default handler + embeddedUpdateHandler = (handler != null) ? handler : this; + IterableApi.getInstance().getEmbeddedManager().addUpdateListener(embeddedUpdateHandler); + } + } + + public void removeEmbeddedUpdateListener(@Nullable IterableEmbeddedUpdateHandler handler) { + if (embeddedUpdateHandler != null) { + // If handler is null, remove the current handler regardless of what it is + // If handler is provided, only remove if it matches + if (handler == null || embeddedUpdateHandler.equals(handler)) { + IterableApi.getInstance().getEmbeddedManager().removeUpdateListener(embeddedUpdateHandler); + embeddedUpdateHandler = null; + } + } + } + + @Override + public void onMessagesUpdated() { + IterableLogger.d(TAG, "onMessagesUpdated"); + sendEvent(EventName.receivedIterableEmbeddedMessagesChanged.name(), null); + } + + @Override + public void onEmbeddedMessagingDisabled() { + IterableLogger.d(TAG, "onEmbeddedMessagingDisabled"); + sendEvent(EventName.receivedIterableEmbeddedMessagesChanged.name(), null); + } + private JSONObject createTestPlacement(int placementId) throws JSONException { JSONObject placement = new JSONObject(); placement.put("placementId", placementId); @@ -722,11 +756,12 @@ private JSONObject createTestPlacement(int placementId) throws JSONException { } enum EventName { - handleUrlCalled, - handleCustomActionCalled, - handleInAppCalled, handleAuthCalled, - receivedIterableInboxChanged, + handleAuthFailureCalled, handleAuthSuccessCalled, - handleAuthFailureCalled + handleCustomActionCalled, + handleInAppCalled, + handleUrlCalled, + receivedIterableEmbeddedMessagesChanged, + receivedIterableInboxChanged } diff --git a/android/src/newarch/java/com/RNIterableAPIModule.java b/android/src/newarch/java/com/RNIterableAPIModule.java index cb44f97fa..7e382bba0 100644 --- a/android/src/newarch/java/com/RNIterableAPIModule.java +++ b/android/src/newarch/java/com/RNIterableAPIModule.java @@ -244,4 +244,18 @@ public void syncEmbeddedMessages() { public void getEmbeddedPlacementIds(Promise promise) { moduleImpl.getEmbeddedPlacementIds(promise); } + + @Override + public void addEmbeddedUpdateListener(@Nullable ReadableMap handler) { + // Convert ReadableMap to IterableEmbeddedUpdateHandler if needed + // For now, we'll use null to use the default handler + moduleImpl.addEmbeddedUpdateListener(null); + } + + @Override + public void removeEmbeddedUpdateListener(@Nullable ReadableMap handler) { + // Convert ReadableMap to IterableEmbeddedUpdateHandler if needed + // For now, we'll use null to remove the current handler + moduleImpl.removeEmbeddedUpdateListener(null); + } } diff --git a/android/src/oldarch/java/com/RNIterableAPIModule.java b/android/src/oldarch/java/com/RNIterableAPIModule.java index d2257f878..641930908 100644 --- a/android/src/oldarch/java/com/RNIterableAPIModule.java +++ b/android/src/oldarch/java/com/RNIterableAPIModule.java @@ -243,6 +243,20 @@ public void getEmbeddedPlacementIds(Promise promise) { moduleImpl.getEmbeddedPlacementIds(promise); } + @ReactMethod + public void addEmbeddedUpdateListener(@Nullable ReadableMap handler) { + // Convert ReadableMap to IterableEmbeddedUpdateHandler if needed + // For now, we'll use null to use the default handler + moduleImpl.addEmbeddedUpdateListener(null); + } + + @ReactMethod + public void removeEmbeddedUpdateListener(@Nullable ReadableMap handler) { + // Convert ReadableMap to IterableEmbeddedUpdateHandler if needed + // For now, we'll use null to remove the current handler + moduleImpl.removeEmbeddedUpdateListener(null); + } + public void sendEvent(@NonNull String eventName, @Nullable Object eventData) { moduleImpl.sendEvent(eventName, eventData); } diff --git a/example/src/components/User/User.tsx b/example/src/components/User/User.tsx index 2bfe8f045..ad1353708 100644 --- a/example/src/components/User/User.tsx +++ b/example/src/components/User/User.tsx @@ -1,4 +1,4 @@ -import { Iterable } from '@iterable/react-native-sdk'; +import { Iterable, IterableEmbeddedMessage } from '@iterable/react-native-sdk'; import { useCallback, useEffect, useState } from 'react'; import { Text, TouchableOpacity, View } from 'react-native'; @@ -8,13 +8,29 @@ import styles from './User.styles'; export const User = () => { const { logout, isLoggedIn } = useIterableApp(); const [loggedInAs, setLoggedInAs] = useState(''); + const [hasListener, setHasListener] = useState(false); useEffect(() => { + console.log(`🚀 > User > isLoggedIn:`, isLoggedIn); + const embeddedUpdateListener = (messages: IterableEmbeddedMessage[]) => { + console.log('UPDATE', messages); + }; + if (isLoggedIn) { Iterable.getEmail().then((email) => setLoggedInAs(email || '')); + + Iterable.embeddedManager.addUpdateListener(embeddedUpdateListener); + setHasListener(true); } else { setLoggedInAs(''); } + + return () => { + if (hasListener) { + Iterable.embeddedManager.removeUpdateListener(embeddedUpdateListener); + } + }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, [isLoggedIn]); const getEmbeddedMessages = useCallback(() => { diff --git a/src/api/NativeRNIterableAPI.ts b/src/api/NativeRNIterableAPI.ts index 42a6bae85..5ca247d2b 100644 --- a/src/api/NativeRNIterableAPI.ts +++ b/src/api/NativeRNIterableAPI.ts @@ -145,6 +145,10 @@ export interface Spec extends TurboModule { getEmbeddedPlacementIds(): Promise; + addEmbeddedUpdateListener(handler?: unknown): void; + + removeEmbeddedUpdateListener(handler?: unknown): void; + // Wake app -- android only wakeApp(): void; diff --git a/src/core/classes/IterableApi.ts b/src/core/classes/IterableApi.ts index 26a4eeb42..50674f2af 100644 --- a/src/core/classes/IterableApi.ts +++ b/src/core/classes/IterableApi.ts @@ -554,6 +554,15 @@ export class IterableApi { return RNIterableAPI.getEmbeddedPlacementIds(); } + static addEmbeddedUpdateListener(handler?: unknown) { + IterableApi.logger.log('addEmbeddedUpdateListener'); + return RNIterableAPI.addEmbeddedUpdateListener(handler); + } + + static removeEmbeddedUpdateListener(handler?: unknown) { + IterableApi.logger.log('removeEmbeddedUpdateListener'); + return RNIterableAPI.removeEmbeddedUpdateListener(handler); + } // ---- End EMBEDDED ---- // // ====================================================== // diff --git a/src/core/enums/IterableEventName.ts b/src/core/enums/IterableEventName.ts index 4a44cbb40..dfa2bcc11 100644 --- a/src/core/enums/IterableEventName.ts +++ b/src/core/enums/IterableEventName.ts @@ -15,6 +15,8 @@ export enum IterableEventName { handleAuthCalled = 'handleAuthCalled', /** Event that fires when the Iterable inbox is updated */ receivedIterableInboxChanged = 'receivedIterableInboxChanged', + /** Event that fires when embedded messages are updated */ + receivedIterableEmbeddedMessagesChanged = 'receivedIterableEmbeddedMessagesChanged', /** Event that fires when authentication with Iterable succeeds */ handleAuthSuccessCalled = 'handleAuthSuccessCalled', /** Event that fires when authentication with Iterable fails */ diff --git a/src/embedded/classes/IterableEmbeddedManager.ts b/src/embedded/classes/IterableEmbeddedManager.ts index 488b45db7..bc19ee484 100644 --- a/src/embedded/classes/IterableEmbeddedManager.ts +++ b/src/embedded/classes/IterableEmbeddedManager.ts @@ -1,14 +1,23 @@ +import { NativeEventEmitter } from 'react-native'; import { IterableApi } from '../../core/classes/IterableApi'; import type { IterableLogger } from '../../core/classes/IterableLogger'; import { defaultLogger } from '../../core/constants/defaults'; +import { IterableEventName } from '../../core/enums/IterableEventName'; +import { RNIterableAPI } from '../../api'; +import type { IterableEmbeddedMessage } from './IterableEmbeddedMessage'; import { IterableEmbeddedSessionManager } from './IterableEmbeddedSessionManager'; +const RNEventEmitter = new NativeEventEmitter(RNIterableAPI); + export class IterableEmbeddedManager { static logger: IterableLogger = defaultLogger; sessionManager: IterableEmbeddedSessionManager = new IterableEmbeddedSessionManager(defaultLogger); + private listeners: ((messages: IterableEmbeddedMessage[]) => void)[] = []; + private isNativeListenerAdded = false; + constructor(logger: IterableLogger) { IterableEmbeddedManager.logger = logger; this.sessionManager = new IterableEmbeddedSessionManager(logger); @@ -25,4 +34,53 @@ export class IterableEmbeddedManager { getPlacementIds() { return IterableApi.getEmbeddedPlacementIds(); } + + addUpdateListener(listener: (messages: IterableEmbeddedMessage[]) => void) { + this.listeners.push(listener); + + // Add native listener only once + if (!this.isNativeListenerAdded) { + this.isNativeListenerAdded = true; + // Pass null to use the default handler (this in Java implementation) + IterableApi.addEmbeddedUpdateListener(null); + + // Listen for native events + RNEventEmitter.addListener( + IterableEventName.receivedIterableEmbeddedMessagesChanged, + () => { + // When native side notifies of changes, get fresh messages and notify all listeners + this.getMessages(null) + .then((messages: IterableEmbeddedMessage[]) => { + this.listeners.forEach((listenerCallback) => + listenerCallback(messages) + ); + }) + .catch((error: unknown) => { + IterableEmbeddedManager.logger.log( + 'Error getting embedded messages: ' + String(error) + ); + }); + } + ); + } + } + + removeUpdateListener( + listener: (messages: IterableEmbeddedMessage[]) => void + ) { + const index = this.listeners.indexOf(listener); + if (index > -1) { + this.listeners.splice(index, 1); + } + + // Remove native listener if no more listeners + if (this.listeners.length === 0 && this.isNativeListenerAdded) { + this.isNativeListenerAdded = false; + // Pass null to remove the current handler + IterableApi.removeEmbeddedUpdateListener(null); + RNEventEmitter.removeAllListeners( + IterableEventName.receivedIterableEmbeddedMessagesChanged + ); + } + } } diff --git a/src/index.tsx b/src/index.tsx index 240ac51f5..d87876d7d 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -59,3 +59,4 @@ export { type IterableInboxProps, type IterableInboxRowViewModel, } from './inbox'; +export { IterableEmbeddedMessage } from './embedded/classes/IterableEmbeddedMessage'; From f31dd7249ab4127d52da131c02d12c4f8cd972cc Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 7 Oct 2025 18:36:24 -0700 Subject: [PATCH 22/45] feat: add start and end session methods for embedded messaging --- .../reactnative/RNIterableAPIModuleImpl.java | 8 +++++ .../newarch/java/com/RNIterableAPIModule.java | 10 +++++++ .../oldarch/java/com/RNIterableAPIModule.java | 10 +++++++ example/src/components/User/User.tsx | 30 +++++++++++++------ src/api/NativeRNIterableAPI.ts | 4 +++ src/core/classes/IterableApi.ts | 11 +++++++ .../classes/IterableEmbeddedManager.ts | 8 +++++ 7 files changed, 72 insertions(+), 9 deletions(-) diff --git a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java index 6e0602ce0..bd866600a 100644 --- a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java +++ b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java @@ -732,6 +732,14 @@ public void removeEmbeddedUpdateListener(@Nullable IterableEmbeddedUpdateHandler } } + public void startEmbeddedSession() { + IterableApi.getInstance().getEmbeddedManager().getEmbeddedSessionManager().startSession(); + } + + public void endEmbeddedSession() { + IterableApi.getInstance().getEmbeddedManager().getEmbeddedSessionManager().endSession(); + } + @Override public void onMessagesUpdated() { IterableLogger.d(TAG, "onMessagesUpdated"); diff --git a/android/src/newarch/java/com/RNIterableAPIModule.java b/android/src/newarch/java/com/RNIterableAPIModule.java index 7e382bba0..295db4c30 100644 --- a/android/src/newarch/java/com/RNIterableAPIModule.java +++ b/android/src/newarch/java/com/RNIterableAPIModule.java @@ -258,4 +258,14 @@ public void removeEmbeddedUpdateListener(@Nullable ReadableMap handler) { // For now, we'll use null to remove the current handler moduleImpl.removeEmbeddedUpdateListener(null); } + + @Override + public void startEmbeddedSession() { + moduleImpl.startEmbeddedSession(); + } + + @Override + public void endEmbeddedSession() { + moduleImpl.endEmbeddedSession(); + } } diff --git a/android/src/oldarch/java/com/RNIterableAPIModule.java b/android/src/oldarch/java/com/RNIterableAPIModule.java index 641930908..401143193 100644 --- a/android/src/oldarch/java/com/RNIterableAPIModule.java +++ b/android/src/oldarch/java/com/RNIterableAPIModule.java @@ -257,6 +257,16 @@ public void removeEmbeddedUpdateListener(@Nullable ReadableMap handler) { moduleImpl.removeEmbeddedUpdateListener(null); } + @ReactMethod + public void startEmbeddedSession() { + moduleImpl.startEmbeddedSession(); + } + + @ReactMethod + public void endEmbeddedSession() { + moduleImpl.endEmbeddedSession(); + } + public void sendEvent(@NonNull String eventName, @Nullable Object eventData) { moduleImpl.sendEvent(eventName, eventData); } diff --git a/example/src/components/User/User.tsx b/example/src/components/User/User.tsx index ad1353708..ba998ef95 100644 --- a/example/src/components/User/User.tsx +++ b/example/src/components/User/User.tsx @@ -1,4 +1,5 @@ import { Iterable, IterableEmbeddedMessage } from '@iterable/react-native-sdk'; +import { useIsFocused } from '@react-navigation/native'; import { useCallback, useEffect, useState } from 'react'; import { Text, TouchableOpacity, View } from 'react-native'; @@ -7,30 +8,41 @@ import styles from './User.styles'; export const User = () => { const { logout, isLoggedIn } = useIterableApp(); + const isFocused = useIsFocused(); const [loggedInAs, setLoggedInAs] = useState(''); const [hasListener, setHasListener] = useState(false); + const [hasSession, setHasSession] = useState(false); useEffect(() => { - console.log(`🚀 > User > isLoggedIn:`, isLoggedIn); const embeddedUpdateListener = (messages: IterableEmbeddedMessage[]) => { console.log('UPDATE', messages); }; - if (isLoggedIn) { - Iterable.getEmail().then((email) => setLoggedInAs(email || '')); - + if (isFocused) { Iterable.embeddedManager.addUpdateListener(embeddedUpdateListener); setHasListener(true); - } else { - setLoggedInAs(''); - } - return () => { + Iterable.embeddedManager.startSession(); + setHasSession(true); + } else { if (hasListener) { Iterable.embeddedManager.removeUpdateListener(embeddedUpdateListener); + setHasListener(false); } - }; + if (hasSession) { + Iterable.embeddedManager.endSession(); + setHasSession(false); + } + } // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isFocused]); + + useEffect(() => { + if (isLoggedIn) { + Iterable.getEmail().then((email) => setLoggedInAs(email || '')); + } else { + setLoggedInAs(''); + } }, [isLoggedIn]); const getEmbeddedMessages = useCallback(() => { diff --git a/src/api/NativeRNIterableAPI.ts b/src/api/NativeRNIterableAPI.ts index 5ca247d2b..c405edb85 100644 --- a/src/api/NativeRNIterableAPI.ts +++ b/src/api/NativeRNIterableAPI.ts @@ -149,6 +149,10 @@ export interface Spec extends TurboModule { removeEmbeddedUpdateListener(handler?: unknown): void; + startEmbeddedSession(): void; + + endEmbeddedSession(): void; + // Wake app -- android only wakeApp(): void; diff --git a/src/core/classes/IterableApi.ts b/src/core/classes/IterableApi.ts index 50674f2af..da6c70316 100644 --- a/src/core/classes/IterableApi.ts +++ b/src/core/classes/IterableApi.ts @@ -563,6 +563,17 @@ export class IterableApi { IterableApi.logger.log('removeEmbeddedUpdateListener'); return RNIterableAPI.removeEmbeddedUpdateListener(handler); } + + static startEmbeddedSession() { + IterableApi.logger.log('startEmbeddedSession'); + return RNIterableAPI.startEmbeddedSession(); + } + + static endEmbeddedSession() { + IterableApi.logger.log('endEmbeddedSession'); + return RNIterableAPI.endEmbeddedSession(); + } + // ---- End EMBEDDED ---- // // ====================================================== // diff --git a/src/embedded/classes/IterableEmbeddedManager.ts b/src/embedded/classes/IterableEmbeddedManager.ts index bc19ee484..b74bb12f2 100644 --- a/src/embedded/classes/IterableEmbeddedManager.ts +++ b/src/embedded/classes/IterableEmbeddedManager.ts @@ -83,4 +83,12 @@ export class IterableEmbeddedManager { ); } } + + startSession() { + IterableApi.startEmbeddedSession(); + } + + endSession() { + IterableApi.endEmbeddedSession(); + } } From 76a1ca344cb2a43c17ee7ba8f6f4083ebf2b67ec Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 7 Oct 2025 18:39:51 -0700 Subject: [PATCH 23/45] feat: add start and pause impression methods for embedded messaging --- .../iterable/reactnative/RNIterableAPIModuleImpl.java | 8 ++++++++ android/src/newarch/java/com/RNIterableAPIModule.java | 10 ++++++++++ android/src/oldarch/java/com/RNIterableAPIModule.java | 10 ++++++++++ src/api/NativeRNIterableAPI.ts | 4 ++++ src/core/classes/IterableApi.ts | 10 ++++++++++ 5 files changed, 42 insertions(+) diff --git a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java index bd866600a..34a06c6f2 100644 --- a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java +++ b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java @@ -740,6 +740,14 @@ public void endEmbeddedSession() { IterableApi.getInstance().getEmbeddedManager().getEmbeddedSessionManager().endSession(); } + public void startEmbeddedImpression(String messageId, int placementId) { + IterableApi.getInstance().getEmbeddedManager().getEmbeddedSessionManager().startImpression(messageId, placementId); + } + + public void pauseEmbeddedImpression(String messageId) { + IterableApi.getInstance().getEmbeddedManager().getEmbeddedSessionManager().pauseImpression(messageId); + } + @Override public void onMessagesUpdated() { IterableLogger.d(TAG, "onMessagesUpdated"); diff --git a/android/src/newarch/java/com/RNIterableAPIModule.java b/android/src/newarch/java/com/RNIterableAPIModule.java index 295db4c30..63e5ee1be 100644 --- a/android/src/newarch/java/com/RNIterableAPIModule.java +++ b/android/src/newarch/java/com/RNIterableAPIModule.java @@ -259,6 +259,16 @@ public void removeEmbeddedUpdateListener(@Nullable ReadableMap handler) { moduleImpl.removeEmbeddedUpdateListener(null); } + @Override + public void startEmbeddedImpression(String messageId, int placementId) { + moduleImpl.startEmbeddedImpression(messageId, placementId); + } + + @Override + public void pauseEmbeddedImpression(String messageId) { + moduleImpl.pauseEmbeddedImpression(messageId); + } + @Override public void startEmbeddedSession() { moduleImpl.startEmbeddedSession(); diff --git a/android/src/oldarch/java/com/RNIterableAPIModule.java b/android/src/oldarch/java/com/RNIterableAPIModule.java index 401143193..9b33988b6 100644 --- a/android/src/oldarch/java/com/RNIterableAPIModule.java +++ b/android/src/oldarch/java/com/RNIterableAPIModule.java @@ -267,6 +267,16 @@ public void endEmbeddedSession() { moduleImpl.endEmbeddedSession(); } + @ReactMethod + public void startEmbeddedImpression(String messageId, int placementId) { + moduleImpl.startEmbeddedImpression(messageId, placementId); + } + + @ReactMethod + public void pauseEmbeddedImpression(String messageId) { + moduleImpl.pauseEmbeddedImpression(messageId); + } + public void sendEvent(@NonNull String eventName, @Nullable Object eventData) { moduleImpl.sendEvent(eventName, eventData); } diff --git a/src/api/NativeRNIterableAPI.ts b/src/api/NativeRNIterableAPI.ts index c405edb85..9d4ea9239 100644 --- a/src/api/NativeRNIterableAPI.ts +++ b/src/api/NativeRNIterableAPI.ts @@ -153,6 +153,10 @@ export interface Spec extends TurboModule { endEmbeddedSession(): void; + startEmbeddedImpression(messageId: string, placementId: number): void; + + pauseEmbeddedImpression(messageId: string): void; + // Wake app -- android only wakeApp(): void; diff --git a/src/core/classes/IterableApi.ts b/src/core/classes/IterableApi.ts index da6c70316..ad3d6642c 100644 --- a/src/core/classes/IterableApi.ts +++ b/src/core/classes/IterableApi.ts @@ -574,6 +574,16 @@ export class IterableApi { return RNIterableAPI.endEmbeddedSession(); } + static startEmbeddedImpression(messageId: string, placementId: number) { + IterableApi.logger.log('startEmbeddedImpression: ', messageId, placementId); + return RNIterableAPI.startEmbeddedImpression(messageId, placementId); + } + + static pauseEmbeddedImpression(messageId: string) { + IterableApi.logger.log('pauseEmbeddedImpression: ', messageId); + return RNIterableAPI.pauseEmbeddedImpression(messageId); + } + // ---- End EMBEDDED ---- // // ====================================================== // From 1e349df8dd2aba6aaff1e8f2c17c87dfa36cb8d7 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 7 Oct 2025 19:14:56 -0700 Subject: [PATCH 24/45] feat: add handleEmbeddedClick and trackEmbeddedClick methods --- .../reactnative/RNIterableAPIModuleImpl.java | 8 +++++++ .../newarch/java/com/RNIterableAPIModule.java | 10 ++++++++ .../oldarch/java/com/RNIterableAPIModule.java | 10 ++++++++ src/api/NativeRNIterableAPI.ts | 20 ++++++++++++++++ src/core/classes/IterableApi.ts | 18 ++++++++++++-- .../classes/IterableEmbeddedManager.ts | 24 +++++++++++++++++++ 6 files changed, 88 insertions(+), 2 deletions(-) diff --git a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java index 34a06c6f2..01b513962 100644 --- a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java +++ b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java @@ -748,6 +748,14 @@ public void pauseEmbeddedImpression(String messageId) { IterableApi.getInstance().getEmbeddedManager().getEmbeddedSessionManager().pauseImpression(messageId); } + public void handleEmbeddedClick(IterableEmbeddedMessage message, String buttonId, String clickedUrl) { + IterableApi.getInstance().getEmbeddedManager().handleEmbeddedClick(message, buttonId, clickedUrl); + } + + public void trackEmbeddedClick(IterableEmbeddedMessage message, String buttonId, String clickedUrl) { + IterableApi.getInstance().trackEmbeddedClick(message, buttonId, clickedUrl); + } + @Override public void onMessagesUpdated() { IterableLogger.d(TAG, "onMessagesUpdated"); diff --git a/android/src/newarch/java/com/RNIterableAPIModule.java b/android/src/newarch/java/com/RNIterableAPIModule.java index 63e5ee1be..679ea8a20 100644 --- a/android/src/newarch/java/com/RNIterableAPIModule.java +++ b/android/src/newarch/java/com/RNIterableAPIModule.java @@ -269,6 +269,16 @@ public void pauseEmbeddedImpression(String messageId) { moduleImpl.pauseEmbeddedImpression(messageId); } + @Override + public void handleEmbeddedClick(IterableEmbeddedMessage message, String buttonId, String clickedUrl) { + moduleImpl.handleEmbeddedClick(message, buttonId, clickedUrl); + } + + @Override + public void trackEmbeddedClick(IterableEmbeddedMessage message, String buttonId, String clickedUrl) { + moduleImpl.trackEmbeddedClick(message, buttonId, clickedUrl); + } + @Override public void startEmbeddedSession() { moduleImpl.startEmbeddedSession(); diff --git a/android/src/oldarch/java/com/RNIterableAPIModule.java b/android/src/oldarch/java/com/RNIterableAPIModule.java index 9b33988b6..9540f5e39 100644 --- a/android/src/oldarch/java/com/RNIterableAPIModule.java +++ b/android/src/oldarch/java/com/RNIterableAPIModule.java @@ -277,6 +277,16 @@ public void pauseEmbeddedImpression(String messageId) { moduleImpl.pauseEmbeddedImpression(messageId); } + @ReactMethod + public void handleEmbeddedClick(IterableEmbeddedMessage message, String buttonId, String clickedUrl) { + moduleImpl.handleEmbeddedClick(message, buttonId, clickedUrl); + } + + @ReactMethod + public void trackEmbeddedClick(IterableEmbeddedMessage message, String buttonId, String clickedUrl) { + moduleImpl.trackEmbeddedClick(message, buttonId, clickedUrl); + } + public void sendEvent(@NonNull String eventName, @Nullable Object eventData) { moduleImpl.sendEvent(eventName, eventData); } diff --git a/src/api/NativeRNIterableAPI.ts b/src/api/NativeRNIterableAPI.ts index 9d4ea9239..dfdcad50a 100644 --- a/src/api/NativeRNIterableAPI.ts +++ b/src/api/NativeRNIterableAPI.ts @@ -157,6 +157,26 @@ export interface Spec extends TurboModule { pauseEmbeddedImpression(messageId: string): void; + handleEmbeddedClick( + message: EmbeddedMessage, + buttonId: string | null, + clickedUrl: string | null + ): void; + + trackEmbeddedClick( + message: EmbeddedMessage, + buttonId: string | null, + clickedUrl: string | null + ): void; + + trackEmbeddedSession(session: { + [key: string]: string | number | boolean; + }): void; + + trackEmbeddedMessageReceived(message: { + [key: string]: string | number | boolean; + }): void; + // Wake app -- android only wakeApp(): void; diff --git a/src/core/classes/IterableApi.ts b/src/core/classes/IterableApi.ts index ad3d6642c..a6d90865b 100644 --- a/src/core/classes/IterableApi.ts +++ b/src/core/classes/IterableApi.ts @@ -326,8 +326,8 @@ export class IterableApi { static trackEmbeddedClick( message: IterableEmbeddedMessage, - buttonId: string | null | undefined, - clickedUrl: string | null | undefined + buttonId: string | null, + clickedUrl: string | null ) { IterableApi.logger.log( 'trackEmbeddedClick: ', @@ -584,6 +584,20 @@ export class IterableApi { return RNIterableAPI.pauseEmbeddedImpression(messageId); } + static handleEmbeddedClick( + message: IterableEmbeddedMessage, + buttonId: string | null, + clickedUrl: string | null + ) { + IterableApi.logger.log( + 'handleEmbeddedClick: ', + message, + buttonId, + clickedUrl + ); + return RNIterableAPI.handleEmbeddedClick(message, buttonId, clickedUrl); + } + // ---- End EMBEDDED ---- // // ====================================================== // diff --git a/src/embedded/classes/IterableEmbeddedManager.ts b/src/embedded/classes/IterableEmbeddedManager.ts index b74bb12f2..659762f0a 100644 --- a/src/embedded/classes/IterableEmbeddedManager.ts +++ b/src/embedded/classes/IterableEmbeddedManager.ts @@ -91,4 +91,28 @@ export class IterableEmbeddedManager { endSession() { IterableApi.endEmbeddedSession(); } + + startImpression(messageId: string, placementId: number) { + IterableApi.startEmbeddedImpression(messageId, placementId); + } + + pauseImpression(messageId: string) { + IterableApi.pauseEmbeddedImpression(messageId); + } + + handleClick( + message: IterableEmbeddedMessage, + buttonId: string | null, + clickedUrl: string | null + ) { + IterableApi.handleEmbeddedClick(message, buttonId, clickedUrl); + } + + trackClick( + message: IterableEmbeddedMessage, + buttonId: string | null, + clickedUrl: string | null + ) { + IterableApi.trackEmbeddedClick(message, buttonId, clickedUrl); + } } From 0e5367442fe846fe95bf572a3d2d3d4bbc6abe3b Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 7 Oct 2025 19:15:24 -0700 Subject: [PATCH 25/45] fix: import missing IterableEmbeddedMessage --- android/src/newarch/java/com/RNIterableAPIModule.java | 1 + android/src/oldarch/java/com/RNIterableAPIModule.java | 1 + 2 files changed, 2 insertions(+) diff --git a/android/src/newarch/java/com/RNIterableAPIModule.java b/android/src/newarch/java/com/RNIterableAPIModule.java index 679ea8a20..d521bca9a 100644 --- a/android/src/newarch/java/com/RNIterableAPIModule.java +++ b/android/src/newarch/java/com/RNIterableAPIModule.java @@ -8,6 +8,7 @@ import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; import com.iterable.iterableapi.AuthFailure; +import com.iterable.iterableapi.IterableEmbeddedMessage; import com.iterable.iterableapi.IterableLogger; public class RNIterableAPIModule extends NativeRNIterableAPISpec { diff --git a/android/src/oldarch/java/com/RNIterableAPIModule.java b/android/src/oldarch/java/com/RNIterableAPIModule.java index 9540f5e39..a1259d75e 100644 --- a/android/src/oldarch/java/com/RNIterableAPIModule.java +++ b/android/src/oldarch/java/com/RNIterableAPIModule.java @@ -9,6 +9,7 @@ import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; +import com.iterable.iterableapi.IterableEmbeddedMessage; public class RNIterableAPIModule extends ReactContextBaseJavaModule { private final ReactApplicationContext reactContext; From 34666612efad9994483ebc422ed41d418f0bd581 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 7 Oct 2025 20:45:28 -0700 Subject: [PATCH 26/45] refactor: giving up on the callbacks --- .../reactnative/RNIterableAPIModuleImpl.java | 21 ++-- .../newarch/java/com/RNIterableAPIModule.java | 16 +-- .../oldarch/java/com/RNIterableAPIModule.java | 16 +-- example/src/components/User/User.tsx | 31 ++++-- src/api/NativeRNIterableAPI.ts | 9 +- src/core/classes/IterableApi.ts | 11 +- .../classes/IterableEmbeddedManager.ts | 100 ++++++++---------- .../types/IterableEmbeddedUpdateHandler.ts | 4 + 8 files changed, 112 insertions(+), 96 deletions(-) create mode 100644 src/inApp/types/IterableEmbeddedUpdateHandler.ts diff --git a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java index 01b513962..782d7d697 100644 --- a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java +++ b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java @@ -42,6 +42,7 @@ import com.iterable.iterableapi.IterableLogger; import com.iterable.iterableapi.IterableUrlHandler; import com.iterable.iterableapi.RNIterableInternal; + import com.iterable.iterableapi.IterableEmbeddedUpdateHandler; import org.json.JSONArray; @@ -55,6 +56,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; + public class RNIterableAPIModuleImpl implements IterableUrlHandler, IterableCustomActionHandler, IterableInAppHandler, IterableAuthHandler, IterableInAppManager.Listener, IterableEmbeddedUpdateHandler { public static final String NAME = "RNIterableAPI"; @@ -62,7 +64,6 @@ public class RNIterableAPIModuleImpl implements IterableUrlHandler, IterableCust private final ReactApplicationContext reactContext; private IterableInAppHandler.InAppResponse inAppResponse = IterableInAppHandler.InAppResponse.SHOW; - private IterableEmbeddedUpdateHandler embeddedUpdateHandler; //A CountDownLatch. This helps decide whether to handle the in-app in Default way by waiting for JS to respond in runtime. private CountDownLatch jsCallBackLatch; @@ -714,22 +715,14 @@ public void getEmbeddedPlacementIds(Promise promise) { } public void addEmbeddedUpdateListener(@Nullable IterableEmbeddedUpdateHandler handler) { - if (embeddedUpdateHandler == null) { - // If handler is null, use this (the module implementation) as the default handler - embeddedUpdateHandler = (handler != null) ? handler : this; - IterableApi.getInstance().getEmbeddedManager().addUpdateListener(embeddedUpdateHandler); - } + // For React Native bridge, we'll use this module as the handler + // The JavaScript side will receive events through the event emitter + IterableApi.getInstance().getEmbeddedManager().addUpdateListener(handler); } public void removeEmbeddedUpdateListener(@Nullable IterableEmbeddedUpdateHandler handler) { - if (embeddedUpdateHandler != null) { - // If handler is null, remove the current handler regardless of what it is - // If handler is provided, only remove if it matches - if (handler == null || embeddedUpdateHandler.equals(handler)) { - IterableApi.getInstance().getEmbeddedManager().removeUpdateListener(embeddedUpdateHandler); - embeddedUpdateHandler = null; - } - } + // Remove this module as the handler + IterableApi.getInstance().getEmbeddedManager().removeUpdateListener(handler); } public void startEmbeddedSession() { diff --git a/android/src/newarch/java/com/RNIterableAPIModule.java b/android/src/newarch/java/com/RNIterableAPIModule.java index d521bca9a..61201049f 100644 --- a/android/src/newarch/java/com/RNIterableAPIModule.java +++ b/android/src/newarch/java/com/RNIterableAPIModule.java @@ -10,6 +10,10 @@ import com.iterable.iterableapi.AuthFailure; import com.iterable.iterableapi.IterableEmbeddedMessage; import com.iterable.iterableapi.IterableLogger; +import com.iterable.iterableapi.IterableEmbeddedManager; + +import com.iterable.iterableapi.IterableEmbeddedUpdateHandler; + public class RNIterableAPIModule extends NativeRNIterableAPISpec { private final ReactApplicationContext reactContext; @@ -247,17 +251,13 @@ public void getEmbeddedPlacementIds(Promise promise) { } @Override - public void addEmbeddedUpdateListener(@Nullable ReadableMap handler) { - // Convert ReadableMap to IterableEmbeddedUpdateHandler if needed - // For now, we'll use null to use the default handler - moduleImpl.addEmbeddedUpdateListener(null); + public void addEmbeddedUpdateListener(@Nullable IterableEmbeddedUpdateHandler handler) { + moduleImpl.addEmbeddedUpdateListener(handler); } @Override - public void removeEmbeddedUpdateListener(@Nullable ReadableMap handler) { - // Convert ReadableMap to IterableEmbeddedUpdateHandler if needed - // For now, we'll use null to remove the current handler - moduleImpl.removeEmbeddedUpdateListener(null); + public void removeEmbeddedUpdateListener(@Nullable IterableEmbeddedUpdateHandler handler) { + moduleImpl.removeEmbeddedUpdateListener(handler); } @Override diff --git a/android/src/oldarch/java/com/RNIterableAPIModule.java b/android/src/oldarch/java/com/RNIterableAPIModule.java index a1259d75e..262674079 100644 --- a/android/src/oldarch/java/com/RNIterableAPIModule.java +++ b/android/src/oldarch/java/com/RNIterableAPIModule.java @@ -10,6 +10,10 @@ import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; import com.iterable.iterableapi.IterableEmbeddedMessage; +import com.iterable.iterableapi.IterableEmbeddedManager; + +import com.iterable.iterableapi.IterableEmbeddedUpdateHandler; + public class RNIterableAPIModule extends ReactContextBaseJavaModule { private final ReactApplicationContext reactContext; @@ -245,17 +249,13 @@ public void getEmbeddedPlacementIds(Promise promise) { } @ReactMethod - public void addEmbeddedUpdateListener(@Nullable ReadableMap handler) { - // Convert ReadableMap to IterableEmbeddedUpdateHandler if needed - // For now, we'll use null to use the default handler - moduleImpl.addEmbeddedUpdateListener(null); + public void addEmbeddedUpdateListener(@Nullable IterableEmbeddedUpdateHandler handler) { + moduleImpl.addEmbeddedUpdateListener(handler); } @ReactMethod - public void removeEmbeddedUpdateListener(@Nullable ReadableMap handler) { - // Convert ReadableMap to IterableEmbeddedUpdateHandler if needed - // For now, we'll use null to remove the current handler - moduleImpl.removeEmbeddedUpdateListener(null); + public void removeEmbeddedUpdateListener(@Nullable IterableEmbeddedUpdateHandler handler) { + moduleImpl.removeEmbeddedUpdateListener(handler); } @ReactMethod diff --git a/example/src/components/User/User.tsx b/example/src/components/User/User.tsx index ba998ef95..95cc8f34d 100644 --- a/example/src/components/User/User.tsx +++ b/example/src/components/User/User.tsx @@ -1,4 +1,4 @@ -import { Iterable, IterableEmbeddedMessage } from '@iterable/react-native-sdk'; +import { Iterable } from '@iterable/react-native-sdk'; import { useIsFocused } from '@react-navigation/native'; import { useCallback, useEffect, useState } from 'react'; import { Text, TouchableOpacity, View } from 'react-native'; @@ -12,10 +12,16 @@ export const User = () => { const [loggedInAs, setLoggedInAs] = useState(''); const [hasListener, setHasListener] = useState(false); const [hasSession, setHasSession] = useState(false); + const [placementIds, setPlacementIds] = useState([]); useEffect(() => { - const embeddedUpdateListener = (messages: IterableEmbeddedMessage[]) => { - console.log('UPDATE', messages); + const embeddedUpdateListener = { + onMessagesUpdated: () => { + console.log('UPDATE'); + }, + onEmbeddedMessagingDisabled: () => { + console.log('DISABLED'); + }, }; if (isFocused) { @@ -40,16 +46,22 @@ export const User = () => { useEffect(() => { if (isLoggedIn) { Iterable.getEmail().then((email) => setLoggedInAs(email || '')); + Iterable.embeddedManager.getPlacementIds().then((ids: unknown) => { + console.log(`🚀 > User > ids:`, ids); + setPlacementIds(ids as number[]); + }); } else { setLoggedInAs(''); } }, [isLoggedIn]); const getEmbeddedMessages = useCallback(() => { - Iterable.embeddedManager.getMessages([1641]).then((messages: unknown) => { - console.log(messages); - }); - }, []); + Iterable.embeddedManager + .getMessages(placementIds) + .then((messages: unknown) => { + console.log(messages); + }); + }, [placementIds]); const getPlacementIds = useCallback(() => { Iterable.embeddedManager.getPlacementIds().then((ids: unknown) => { @@ -61,6 +73,11 @@ export const User = () => { Welcome Iterator Logged in as {loggedInAs} + Has listener: {hasListener.toString()} + Has session: {hasSession.toString()} + + Placement ids: [{placementIds.join(', ')}] + Logout diff --git a/src/api/NativeRNIterableAPI.ts b/src/api/NativeRNIterableAPI.ts index dfdcad50a..c2397c25f 100644 --- a/src/api/NativeRNIterableAPI.ts +++ b/src/api/NativeRNIterableAPI.ts @@ -19,6 +19,11 @@ interface EmbeddedMessage { payload: { [key: string]: string | number | boolean }; } +export interface EmbeddedUpdateListener { + onMessagesUpdated: () => void; + onEmbeddedMessagingDisabled: () => void; +} + export interface Spec extends TurboModule { // Initialization initializeWithApiKey( @@ -145,9 +150,9 @@ export interface Spec extends TurboModule { getEmbeddedPlacementIds(): Promise; - addEmbeddedUpdateListener(handler?: unknown): void; + addEmbeddedUpdateListener(handler: EmbeddedUpdateListener | null): void; - removeEmbeddedUpdateListener(handler?: unknown): void; + removeEmbeddedUpdateListener(handler: EmbeddedUpdateListener | null): void; startEmbeddedSession(): void; diff --git a/src/core/classes/IterableApi.ts b/src/core/classes/IterableApi.ts index a6d90865b..530e2c0f7 100644 --- a/src/core/classes/IterableApi.ts +++ b/src/core/classes/IterableApi.ts @@ -15,6 +15,7 @@ import { IterableAttributionInfo } from './IterableAttributionInfo'; import type { IterableCommerceItem } from './IterableCommerceItem'; import { IterableConfig } from './IterableConfig'; import type { IterableLogger } from './IterableLogger'; +import type { IterableEmbeddedUpdateHandler } from '../../inApp/types/IterableEmbeddedUpdateHandler'; export class IterableApi { static logger: IterableLogger = defaultLogger; @@ -554,13 +555,19 @@ export class IterableApi { return RNIterableAPI.getEmbeddedPlacementIds(); } - static addEmbeddedUpdateListener(handler?: unknown) { + static addEmbeddedUpdateListener(handler?: IterableEmbeddedUpdateHandler) { IterableApi.logger.log('addEmbeddedUpdateListener'); + // Note: The native implementation uses the module itself as the handler + // The handler parameter is kept for API compatibility + void handler; // Suppress unused parameter warning return RNIterableAPI.addEmbeddedUpdateListener(handler); } - static removeEmbeddedUpdateListener(handler?: unknown) { + static removeEmbeddedUpdateListener(handler?: IterableEmbeddedUpdateHandler) { IterableApi.logger.log('removeEmbeddedUpdateListener'); + // Note: The native implementation uses the module itself as the handler + // The handler parameter is kept for API compatibility + void handler; // Suppress unused parameter warning return RNIterableAPI.removeEmbeddedUpdateListener(handler); } diff --git a/src/embedded/classes/IterableEmbeddedManager.ts b/src/embedded/classes/IterableEmbeddedManager.ts index 659762f0a..71ab246cc 100644 --- a/src/embedded/classes/IterableEmbeddedManager.ts +++ b/src/embedded/classes/IterableEmbeddedManager.ts @@ -1,13 +1,9 @@ -import { NativeEventEmitter } from 'react-native'; import { IterableApi } from '../../core/classes/IterableApi'; import type { IterableLogger } from '../../core/classes/IterableLogger'; import { defaultLogger } from '../../core/constants/defaults'; -import { IterableEventName } from '../../core/enums/IterableEventName'; -import { RNIterableAPI } from '../../api'; import type { IterableEmbeddedMessage } from './IterableEmbeddedMessage'; import { IterableEmbeddedSessionManager } from './IterableEmbeddedSessionManager'; - -const RNEventEmitter = new NativeEventEmitter(RNIterableAPI); +import type { IterableEmbeddedUpdateHandler } from '../../inApp/types/IterableEmbeddedUpdateHandler'; export class IterableEmbeddedManager { static logger: IterableLogger = defaultLogger; @@ -15,9 +11,6 @@ export class IterableEmbeddedManager { sessionManager: IterableEmbeddedSessionManager = new IterableEmbeddedSessionManager(defaultLogger); - private listeners: ((messages: IterableEmbeddedMessage[]) => void)[] = []; - private isNativeListenerAdded = false; - constructor(logger: IterableLogger) { IterableEmbeddedManager.logger = logger; this.sessionManager = new IterableEmbeddedSessionManager(logger); @@ -27,61 +20,58 @@ export class IterableEmbeddedManager { IterableApi.syncEmbeddedMessages(); } - getMessages(placementIds: number[] | null) { - return IterableApi.getEmbeddedMessages(placementIds); + getMessages(placementIds?: number[] | null) { + return IterableApi.getEmbeddedMessages(placementIds ?? null); } getPlacementIds() { return IterableApi.getEmbeddedPlacementIds(); } - addUpdateListener(listener: (messages: IterableEmbeddedMessage[]) => void) { - this.listeners.push(listener); - - // Add native listener only once - if (!this.isNativeListenerAdded) { - this.isNativeListenerAdded = true; - // Pass null to use the default handler (this in Java implementation) - IterableApi.addEmbeddedUpdateListener(null); - - // Listen for native events - RNEventEmitter.addListener( - IterableEventName.receivedIterableEmbeddedMessagesChanged, - () => { - // When native side notifies of changes, get fresh messages and notify all listeners - this.getMessages(null) - .then((messages: IterableEmbeddedMessage[]) => { - this.listeners.forEach((listenerCallback) => - listenerCallback(messages) - ); - }) - .catch((error: unknown) => { - IterableEmbeddedManager.logger.log( - 'Error getting embedded messages: ' + String(error) - ); - }); - } - ); - } + addUpdateListener(listener: IterableEmbeddedUpdateHandler) { + return IterableApi.addEmbeddedUpdateListener(listener); + // this.listeners.push(listener); + // // Add native listener only once + // if (!this.isNativeListenerAdded) { + // this.isNativeListenerAdded = true; + // // Pass null to use the default handler (this in Java implementation) + // // IterableApi.addEmbeddedUpdateListener(null); + // // Listen for native events + // RNEventEmitter.addListener( + // IterableEventName.receivedIterableEmbeddedMessagesChanged, + // () => { + // // When native side notifies of changes, get fresh messages and notify all listeners + // this.getMessages(null) + // .then((messages: IterableEmbeddedMessage[]) => { + // this.listeners.forEach((listenerCallback) => + // listenerCallback(messages) + // ); + // }) + // .catch((error: unknown) => { + // IterableEmbeddedManager.logger.log( + // 'Error getting embedded messages: ' + String(error) + // ); + // }); + // } + // ); + // } } - removeUpdateListener( - listener: (messages: IterableEmbeddedMessage[]) => void - ) { - const index = this.listeners.indexOf(listener); - if (index > -1) { - this.listeners.splice(index, 1); - } - - // Remove native listener if no more listeners - if (this.listeners.length === 0 && this.isNativeListenerAdded) { - this.isNativeListenerAdded = false; - // Pass null to remove the current handler - IterableApi.removeEmbeddedUpdateListener(null); - RNEventEmitter.removeAllListeners( - IterableEventName.receivedIterableEmbeddedMessagesChanged - ); - } + removeUpdateListener(listener: IterableEmbeddedUpdateHandler) { + return IterableApi.removeEmbeddedUpdateListener(listener); + // const index = this.listeners.indexOf(listener); + // if (index > -1) { + // this.listeners.splice(index, 1); + // } + // // Remove native listener if no more listeners + // if (this.listeners.length === 0 && this.isNativeListenerAdded) { + // this.isNativeListenerAdded = false; + // // Pass null to remove the current handler + // IterableApi.removeEmbeddedUpdateListener(null); + // RNEventEmitter.removeAllListeners( + // IterableEventName.receivedIterableEmbeddedMessagesChanged + // ); + // } } startSession() { diff --git a/src/inApp/types/IterableEmbeddedUpdateHandler.ts b/src/inApp/types/IterableEmbeddedUpdateHandler.ts new file mode 100644 index 000000000..26dbd037e --- /dev/null +++ b/src/inApp/types/IterableEmbeddedUpdateHandler.ts @@ -0,0 +1,4 @@ +export interface IterableEmbeddedUpdateHandler { + onMessagesUpdated: () => void; + onEmbeddedMessagingDisabled: () => void; +} From 95b8085b46898aeb03f377cd665e24c4ad979887 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Wed, 8 Oct 2025 10:11:08 -0700 Subject: [PATCH 27/45] refactor: remove embedded update listener methods from RNIterableAPIModule and related classes --- .../reactnative/RNIterableAPIModuleImpl.java | 11 ----- .../newarch/java/com/RNIterableAPIModule.java | 10 ---- .../oldarch/java/com/RNIterableAPIModule.java | 10 ---- src/api/NativeRNIterableAPI.ts | 4 -- src/core/classes/IterableApi.ts | 17 ------- .../classes/IterableEmbeddedManager.ts | 49 +------------------ 6 files changed, 1 insertion(+), 100 deletions(-) diff --git a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java index 782d7d697..8eb0e83b1 100644 --- a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java +++ b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java @@ -714,17 +714,6 @@ public void getEmbeddedPlacementIds(Promise promise) { } } - public void addEmbeddedUpdateListener(@Nullable IterableEmbeddedUpdateHandler handler) { - // For React Native bridge, we'll use this module as the handler - // The JavaScript side will receive events through the event emitter - IterableApi.getInstance().getEmbeddedManager().addUpdateListener(handler); - } - - public void removeEmbeddedUpdateListener(@Nullable IterableEmbeddedUpdateHandler handler) { - // Remove this module as the handler - IterableApi.getInstance().getEmbeddedManager().removeUpdateListener(handler); - } - public void startEmbeddedSession() { IterableApi.getInstance().getEmbeddedManager().getEmbeddedSessionManager().startSession(); } diff --git a/android/src/newarch/java/com/RNIterableAPIModule.java b/android/src/newarch/java/com/RNIterableAPIModule.java index 61201049f..043db01f6 100644 --- a/android/src/newarch/java/com/RNIterableAPIModule.java +++ b/android/src/newarch/java/com/RNIterableAPIModule.java @@ -250,16 +250,6 @@ public void getEmbeddedPlacementIds(Promise promise) { moduleImpl.getEmbeddedPlacementIds(promise); } - @Override - public void addEmbeddedUpdateListener(@Nullable IterableEmbeddedUpdateHandler handler) { - moduleImpl.addEmbeddedUpdateListener(handler); - } - - @Override - public void removeEmbeddedUpdateListener(@Nullable IterableEmbeddedUpdateHandler handler) { - moduleImpl.removeEmbeddedUpdateListener(handler); - } - @Override public void startEmbeddedImpression(String messageId, int placementId) { moduleImpl.startEmbeddedImpression(messageId, placementId); diff --git a/android/src/oldarch/java/com/RNIterableAPIModule.java b/android/src/oldarch/java/com/RNIterableAPIModule.java index 262674079..e05a09619 100644 --- a/android/src/oldarch/java/com/RNIterableAPIModule.java +++ b/android/src/oldarch/java/com/RNIterableAPIModule.java @@ -248,16 +248,6 @@ public void getEmbeddedPlacementIds(Promise promise) { moduleImpl.getEmbeddedPlacementIds(promise); } - @ReactMethod - public void addEmbeddedUpdateListener(@Nullable IterableEmbeddedUpdateHandler handler) { - moduleImpl.addEmbeddedUpdateListener(handler); - } - - @ReactMethod - public void removeEmbeddedUpdateListener(@Nullable IterableEmbeddedUpdateHandler handler) { - moduleImpl.removeEmbeddedUpdateListener(handler); - } - @ReactMethod public void startEmbeddedSession() { moduleImpl.startEmbeddedSession(); diff --git a/src/api/NativeRNIterableAPI.ts b/src/api/NativeRNIterableAPI.ts index c2397c25f..9f5bb5171 100644 --- a/src/api/NativeRNIterableAPI.ts +++ b/src/api/NativeRNIterableAPI.ts @@ -150,10 +150,6 @@ export interface Spec extends TurboModule { getEmbeddedPlacementIds(): Promise; - addEmbeddedUpdateListener(handler: EmbeddedUpdateListener | null): void; - - removeEmbeddedUpdateListener(handler: EmbeddedUpdateListener | null): void; - startEmbeddedSession(): void; endEmbeddedSession(): void; diff --git a/src/core/classes/IterableApi.ts b/src/core/classes/IterableApi.ts index 530e2c0f7..ff170ef03 100644 --- a/src/core/classes/IterableApi.ts +++ b/src/core/classes/IterableApi.ts @@ -15,7 +15,6 @@ import { IterableAttributionInfo } from './IterableAttributionInfo'; import type { IterableCommerceItem } from './IterableCommerceItem'; import { IterableConfig } from './IterableConfig'; import type { IterableLogger } from './IterableLogger'; -import type { IterableEmbeddedUpdateHandler } from '../../inApp/types/IterableEmbeddedUpdateHandler'; export class IterableApi { static logger: IterableLogger = defaultLogger; @@ -555,22 +554,6 @@ export class IterableApi { return RNIterableAPI.getEmbeddedPlacementIds(); } - static addEmbeddedUpdateListener(handler?: IterableEmbeddedUpdateHandler) { - IterableApi.logger.log('addEmbeddedUpdateListener'); - // Note: The native implementation uses the module itself as the handler - // The handler parameter is kept for API compatibility - void handler; // Suppress unused parameter warning - return RNIterableAPI.addEmbeddedUpdateListener(handler); - } - - static removeEmbeddedUpdateListener(handler?: IterableEmbeddedUpdateHandler) { - IterableApi.logger.log('removeEmbeddedUpdateListener'); - // Note: The native implementation uses the module itself as the handler - // The handler parameter is kept for API compatibility - void handler; // Suppress unused parameter warning - return RNIterableAPI.removeEmbeddedUpdateListener(handler); - } - static startEmbeddedSession() { IterableApi.logger.log('startEmbeddedSession'); return RNIterableAPI.startEmbeddedSession(); diff --git a/src/embedded/classes/IterableEmbeddedManager.ts b/src/embedded/classes/IterableEmbeddedManager.ts index 71ab246cc..b3e51963a 100644 --- a/src/embedded/classes/IterableEmbeddedManager.ts +++ b/src/embedded/classes/IterableEmbeddedManager.ts @@ -3,7 +3,6 @@ import type { IterableLogger } from '../../core/classes/IterableLogger'; import { defaultLogger } from '../../core/constants/defaults'; import type { IterableEmbeddedMessage } from './IterableEmbeddedMessage'; import { IterableEmbeddedSessionManager } from './IterableEmbeddedSessionManager'; -import type { IterableEmbeddedUpdateHandler } from '../../inApp/types/IterableEmbeddedUpdateHandler'; export class IterableEmbeddedManager { static logger: IterableLogger = defaultLogger; @@ -17,7 +16,7 @@ export class IterableEmbeddedManager { } syncMessages() { - IterableApi.syncEmbeddedMessages(); + return IterableApi.syncEmbeddedMessages(); } getMessages(placementIds?: number[] | null) { @@ -28,52 +27,6 @@ export class IterableEmbeddedManager { return IterableApi.getEmbeddedPlacementIds(); } - addUpdateListener(listener: IterableEmbeddedUpdateHandler) { - return IterableApi.addEmbeddedUpdateListener(listener); - // this.listeners.push(listener); - // // Add native listener only once - // if (!this.isNativeListenerAdded) { - // this.isNativeListenerAdded = true; - // // Pass null to use the default handler (this in Java implementation) - // // IterableApi.addEmbeddedUpdateListener(null); - // // Listen for native events - // RNEventEmitter.addListener( - // IterableEventName.receivedIterableEmbeddedMessagesChanged, - // () => { - // // When native side notifies of changes, get fresh messages and notify all listeners - // this.getMessages(null) - // .then((messages: IterableEmbeddedMessage[]) => { - // this.listeners.forEach((listenerCallback) => - // listenerCallback(messages) - // ); - // }) - // .catch((error: unknown) => { - // IterableEmbeddedManager.logger.log( - // 'Error getting embedded messages: ' + String(error) - // ); - // }); - // } - // ); - // } - } - - removeUpdateListener(listener: IterableEmbeddedUpdateHandler) { - return IterableApi.removeEmbeddedUpdateListener(listener); - // const index = this.listeners.indexOf(listener); - // if (index > -1) { - // this.listeners.splice(index, 1); - // } - // // Remove native listener if no more listeners - // if (this.listeners.length === 0 && this.isNativeListenerAdded) { - // this.isNativeListenerAdded = false; - // // Pass null to remove the current handler - // IterableApi.removeEmbeddedUpdateListener(null); - // RNEventEmitter.removeAllListeners( - // IterableEventName.receivedIterableEmbeddedMessagesChanged - // ); - // } - } - startSession() { IterableApi.startEmbeddedSession(); } From 74219388c5a9e8953164ab772cfd800ad2dfaeed Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Wed, 8 Oct 2025 11:20:01 -0700 Subject: [PATCH 28/45] feat: add Embedded component and integrate into navigation --- example/src/components/App/App.constants.ts | 1 + example/src/components/App/Main.tsx | 8 ++ .../components/Embedded/Embedded.styles.ts | 17 ++++ example/src/components/Embedded/Embedded.tsx | 80 +++++++++++++++++++ example/src/components/Embedded/index.ts | 2 + example/src/components/User/User.tsx | 18 ----- example/src/constants/routes.ts | 1 + example/src/types/navigation.ts | 1 + 8 files changed, 110 insertions(+), 18 deletions(-) create mode 100644 example/src/components/Embedded/Embedded.styles.ts create mode 100644 example/src/components/Embedded/Embedded.tsx create mode 100644 example/src/components/Embedded/index.ts diff --git a/example/src/components/App/App.constants.ts b/example/src/components/App/App.constants.ts index f84c390cb..4710a6ba9 100644 --- a/example/src/components/App/App.constants.ts +++ b/example/src/components/App/App.constants.ts @@ -2,6 +2,7 @@ import { Route } from '../../constants'; export const routeIcon = { [Route.Commerce]: 'cash-outline', + [Route.Embedded]: 'chatbubble-outline', [Route.Inbox]: 'mail-outline', [Route.User]: 'person-outline', }; diff --git a/example/src/components/App/Main.tsx b/example/src/components/App/Main.tsx index 55b0d74e2..244bfb1c2 100644 --- a/example/src/components/App/Main.tsx +++ b/example/src/components/App/Main.tsx @@ -8,6 +8,7 @@ import { User } from '../User'; import { Inbox } from '../Inbox'; import { useIterableApp } from '../../hooks'; import { Commerce } from '../Commerce'; +import Embedded from '../Embedded'; const Tab = createBottomTabNavigator(); @@ -44,6 +45,13 @@ export const Main = () => { }, })} /> + ({ + tabPress: () => setIsInboxTab(false), + })} + /> { + const { isLoggedIn } = useIterableApp(); + const isFocused = useIsFocused(); + const [loggedInAs, setLoggedInAs] = useState(''); + const [hasSession, setHasSession] = useState(false); + const [placementIds, setPlacementIds] = useState([]); + + useEffect(() => { + if (isFocused) { + Iterable.embeddedManager.startSession(); + setHasSession(true); + } else { + if (hasSession) { + Iterable.embeddedManager.endSession(); + setHasSession(false); + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isFocused]); + + useEffect(() => { + if (isLoggedIn) { + Iterable.getEmail().then((email) => setLoggedInAs(email || '')); + Iterable.embeddedManager.getPlacementIds().then((ids: unknown) => { + console.log(`🚀 > User > ids:`, ids); + setPlacementIds(ids as number[]); + }); + } else { + setLoggedInAs(''); + } + }, [isLoggedIn]); + + const getEmbeddedMessages = useCallback(() => { + Iterable.embeddedManager + .getMessages(placementIds) + .then((messages: unknown) => { + console.log(messages); + }); + }, [placementIds]); + + const getPlacementIds = useCallback(() => { + Iterable.embeddedManager.getPlacementIds().then((ids: unknown) => { + console.log(ids); + }); + }, []); + + const sync = useCallback(() => { + Iterable.embeddedManager.syncMessages(); + }, []); + + return ( + + Welcome Iterator + Logged in as {loggedInAs} + Has session: {hasSession.toString()} + + Placement ids: [{placementIds.join(', ')}] + + + Get embedded messages + + + Get placement ids + + + Sync + + + ); +}; + +export default Embedded; diff --git a/example/src/components/Embedded/index.ts b/example/src/components/Embedded/index.ts new file mode 100644 index 000000000..908767962 --- /dev/null +++ b/example/src/components/Embedded/index.ts @@ -0,0 +1,2 @@ +export * from './Embedded'; +export { default } from './Embedded'; diff --git a/example/src/components/User/User.tsx b/example/src/components/User/User.tsx index 95cc8f34d..549d17ced 100644 --- a/example/src/components/User/User.tsx +++ b/example/src/components/User/User.tsx @@ -10,31 +10,14 @@ export const User = () => { const { logout, isLoggedIn } = useIterableApp(); const isFocused = useIsFocused(); const [loggedInAs, setLoggedInAs] = useState(''); - const [hasListener, setHasListener] = useState(false); const [hasSession, setHasSession] = useState(false); const [placementIds, setPlacementIds] = useState([]); useEffect(() => { - const embeddedUpdateListener = { - onMessagesUpdated: () => { - console.log('UPDATE'); - }, - onEmbeddedMessagingDisabled: () => { - console.log('DISABLED'); - }, - }; - if (isFocused) { - Iterable.embeddedManager.addUpdateListener(embeddedUpdateListener); - setHasListener(true); - Iterable.embeddedManager.startSession(); setHasSession(true); } else { - if (hasListener) { - Iterable.embeddedManager.removeUpdateListener(embeddedUpdateListener); - setHasListener(false); - } if (hasSession) { Iterable.embeddedManager.endSession(); setHasSession(false); @@ -73,7 +56,6 @@ export const User = () => { Welcome Iterator Logged in as {loggedInAs} - Has listener: {hasListener.toString()} Has session: {hasSession.toString()} Placement ids: [{placementIds.join(', ')}] diff --git a/example/src/constants/routes.ts b/example/src/constants/routes.ts index 4af27c548..c2087cacf 100644 --- a/example/src/constants/routes.ts +++ b/example/src/constants/routes.ts @@ -1,5 +1,6 @@ export enum Route { Commerce = 'Commerce', + Embedded = 'Embedded', Inbox = 'Inbox', Login = 'Login', Main = 'Main', diff --git a/example/src/types/navigation.ts b/example/src/types/navigation.ts index 5b5ad8a50..e3038d088 100644 --- a/example/src/types/navigation.ts +++ b/example/src/types/navigation.ts @@ -10,6 +10,7 @@ import { Route } from '../constants/routes'; export type MainScreenParamList = { [Route.Commerce]: undefined; [Route.Inbox]: undefined; + [Route.Embedded]: undefined; [Route.User]: undefined; }; From 595560c80fc8eb71a1ed827e019f4b983a544931 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Wed, 8 Oct 2025 12:54:46 -0700 Subject: [PATCH 29/45] feat: enhance Embedded component with new styles and message handling --- .../components/Embedded/Embedded.styles.ts | 13 +--- example/src/components/Embedded/Embedded.tsx | 33 ++++++--- example/src/constants/styles/index.ts | 1 + example/src/constants/styles/miscElements.ts | 9 +++ src/core/classes/IterableApi.ts | 4 +- src/embedded/classes/IterableEmbeddedView.ts | 2 +- .../IterableEmbeddedBanner.tsx | 14 ++++ .../IterableEmbeddedBanner/index.ts | 1 + .../IterableEmbeddedCard.tsx | 12 +++ .../components/IterableEmbeddedCard/index.ts | 1 + .../IterableEmbeddedNotification.tsx | 14 ++++ .../IterableEmbeddedNotification/index.ts | 1 + .../IterableEmbeddedView.styles.ts | 0 .../IterableEmbeddedView.tsx | 66 +++++++++++++++++ .../components/IterableEmbeddedView/index.ts | 1 + .../components/IterableEmbeddedViewProps.ts | 7 ++ src/embedded/components/index.ts | 1 + src/embedded/components/utils/getButtons.ts | 24 ++++++ .../components/utils/getDefaultActionUrl.ts | 7 ++ .../components/utils/getDefaultColor.ts | 19 +++++ src/embedded/components/utils/getMedia.ts | 12 +++ src/embedded/components/utils/getStyles.ts | 74 +++++++++++++++++++ ...eddedColors.ts => embeddedViewDefaults.ts} | 14 ++++ src/embedded/index.ts | 1 + .../types/IterableEmbeddedViewStyles.ts | 16 ++-- src/index.tsx | 3 + 26 files changed, 319 insertions(+), 31 deletions(-) create mode 100644 example/src/constants/styles/miscElements.ts create mode 100644 src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx create mode 100644 src/embedded/components/IterableEmbeddedBanner/index.ts create mode 100644 src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx create mode 100644 src/embedded/components/IterableEmbeddedCard/index.ts create mode 100644 src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.tsx create mode 100644 src/embedded/components/IterableEmbeddedNotification/index.ts create mode 100644 src/embedded/components/IterableEmbeddedView/IterableEmbeddedView.styles.ts create mode 100644 src/embedded/components/IterableEmbeddedView/IterableEmbeddedView.tsx create mode 100644 src/embedded/components/IterableEmbeddedView/index.ts create mode 100644 src/embedded/components/IterableEmbeddedViewProps.ts create mode 100644 src/embedded/components/utils/getButtons.ts create mode 100644 src/embedded/components/utils/getDefaultActionUrl.ts create mode 100644 src/embedded/components/utils/getDefaultColor.ts create mode 100644 src/embedded/components/utils/getMedia.ts create mode 100644 src/embedded/components/utils/getStyles.ts rename src/embedded/constants/{embeddedColors.ts => embeddedViewDefaults.ts} (66%) diff --git a/example/src/components/Embedded/Embedded.styles.ts b/example/src/components/Embedded/Embedded.styles.ts index 4ab5d0452..3441caf53 100644 --- a/example/src/components/Embedded/Embedded.styles.ts +++ b/example/src/components/Embedded/Embedded.styles.ts @@ -1,17 +1,12 @@ -import { StyleSheet, type TextStyle } from 'react-native'; -import { appNameSmall, button, buttonText, container } from '../../constants'; - -const text: TextStyle = { - textAlign: 'center', - marginBottom: 20, -}; +import { StyleSheet } from 'react-native'; +import { button, buttonText, container, hr } from '../../constants'; const styles = StyleSheet.create({ - appName: appNameSmall, button, buttonText, container, - text, + hr, + text: { textAlign: 'center' }, }); export default styles; diff --git a/example/src/components/Embedded/Embedded.tsx b/example/src/components/Embedded/Embedded.tsx index d35384cc8..20dc818a7 100644 --- a/example/src/components/Embedded/Embedded.tsx +++ b/example/src/components/Embedded/Embedded.tsx @@ -1,4 +1,9 @@ -import { Iterable } from '@iterable/react-native-sdk'; +import { + Iterable, + IterableEmbeddedView, + IterableEmbeddedViewType, + IterableEmbeddedMessage, +} from '@iterable/react-native-sdk'; import { useIsFocused } from '@react-navigation/native'; import { useCallback, useEffect, useState } from 'react'; import { Text, TouchableOpacity, View } from 'react-native'; @@ -9,9 +14,9 @@ import styles from './Embedded.styles'; export const Embedded = () => { const { isLoggedIn } = useIterableApp(); const isFocused = useIsFocused(); - const [loggedInAs, setLoggedInAs] = useState(''); const [hasSession, setHasSession] = useState(false); const [placementIds, setPlacementIds] = useState([]); + const [messages, setMessages] = useState([]); useEffect(() => { if (isFocused) { @@ -28,22 +33,18 @@ export const Embedded = () => { useEffect(() => { if (isLoggedIn) { - Iterable.getEmail().then((email) => setLoggedInAs(email || '')); Iterable.embeddedManager.getPlacementIds().then((ids: unknown) => { console.log(`🚀 > User > ids:`, ids); setPlacementIds(ids as number[]); }); - } else { - setLoggedInAs(''); } }, [isLoggedIn]); const getEmbeddedMessages = useCallback(() => { - Iterable.embeddedManager - .getMessages(placementIds) - .then((messages: unknown) => { - console.log(messages); - }); + Iterable.embeddedManager.getMessages(placementIds).then((messageList) => { + console.log(messageList); + setMessages(messageList as IterableEmbeddedMessage[]); + }); }, [placementIds]); const getPlacementIds = useCallback(() => { @@ -58,8 +59,6 @@ export const Embedded = () => { return ( - Welcome Iterator - Logged in as {loggedInAs} Has session: {hasSession.toString()} Placement ids: [{placementIds.join(', ')}] @@ -73,6 +72,16 @@ export const Embedded = () => { Sync + + {messages.map((message) => { + return ( + + ); + })} ); }; diff --git a/example/src/constants/styles/index.ts b/example/src/constants/styles/index.ts index 794f9680c..b8c3bac5e 100644 --- a/example/src/constants/styles/index.ts +++ b/example/src/constants/styles/index.ts @@ -1,5 +1,6 @@ export * from './colors'; export * from './containers'; export * from './formElements'; +export * from './miscElements'; export * from './shadows'; export * from './typography'; diff --git a/example/src/constants/styles/miscElements.ts b/example/src/constants/styles/miscElements.ts new file mode 100644 index 000000000..470605ea7 --- /dev/null +++ b/example/src/constants/styles/miscElements.ts @@ -0,0 +1,9 @@ +import { type ViewStyle } from 'react-native'; +import { colors } from './colors'; + +export const hr: ViewStyle = { + backgroundColor: colors.borderPrimary, + height: 1, + marginVertical: 20, + marginHorizontal: 0, +}; diff --git a/src/core/classes/IterableApi.ts b/src/core/classes/IterableApi.ts index ff170ef03..205244d9f 100644 --- a/src/core/classes/IterableApi.ts +++ b/src/core/classes/IterableApi.ts @@ -539,7 +539,9 @@ export class IterableApi { * * @returns A Promise that resolves to an array of embedded messages. */ - static getEmbeddedMessages(placementIds: number[] | null) { + static getEmbeddedMessages( + placementIds: number[] | null + ): Promise { IterableApi.logger.log('getEmbeddedMessages: ', placementIds); return RNIterableAPI.getEmbeddedMessages(placementIds); } diff --git a/src/embedded/classes/IterableEmbeddedView.ts b/src/embedded/classes/IterableEmbeddedView.ts index f6b998993..2e24c9745 100644 --- a/src/embedded/classes/IterableEmbeddedView.ts +++ b/src/embedded/classes/IterableEmbeddedView.ts @@ -13,7 +13,7 @@ import { embeddedSecondaryBtnBackgroundColors, embeddedSecondaryBtnTextColors, embeddedTitleTextColors, -} from '../constants/embeddedColors'; +} from '../constants/embeddedViewDefaults'; export class IterableEmbeddedView { private viewType: IterableEmbeddedViewType; diff --git a/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx b/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx new file mode 100644 index 000000000..f5071e6cd --- /dev/null +++ b/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx @@ -0,0 +1,14 @@ +import { Text, View } from 'react-native'; + +import type { IterableEmbeddedComponentProps } from '../IterableEmbeddedViewProps'; + +export const IterableEmbeddedBanner = ( + props: IterableEmbeddedComponentProps +) => { + console.log(`🚀 > IterableEmbeddedBanner > props:`, props); + return ( + + IterableEmbeddedBanner + + ); +}; diff --git a/src/embedded/components/IterableEmbeddedBanner/index.ts b/src/embedded/components/IterableEmbeddedBanner/index.ts new file mode 100644 index 000000000..bd574a288 --- /dev/null +++ b/src/embedded/components/IterableEmbeddedBanner/index.ts @@ -0,0 +1 @@ +export * from './IterableEmbeddedBanner'; diff --git a/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx b/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx new file mode 100644 index 000000000..4a3f87639 --- /dev/null +++ b/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx @@ -0,0 +1,12 @@ +import { Text, View } from 'react-native'; + +import type { IterableEmbeddedComponentProps } from '../IterableEmbeddedViewProps'; + +export const IterableEmbeddedCard = (props: IterableEmbeddedComponentProps) => { + console.log(`🚀 > IterableEmbeddedCard > props:`, props); + return ( + + IterableEmbeddedCard + + ); +}; diff --git a/src/embedded/components/IterableEmbeddedCard/index.ts b/src/embedded/components/IterableEmbeddedCard/index.ts new file mode 100644 index 000000000..748f2064f --- /dev/null +++ b/src/embedded/components/IterableEmbeddedCard/index.ts @@ -0,0 +1 @@ +export * from './IterableEmbeddedCard'; diff --git a/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.tsx b/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.tsx new file mode 100644 index 000000000..b68c08ee2 --- /dev/null +++ b/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.tsx @@ -0,0 +1,14 @@ +import { Text, View } from 'react-native'; + +import type { IterableEmbeddedComponentProps } from '../IterableEmbeddedViewProps'; + +export const IterableEmbeddedNotification = ( + props: IterableEmbeddedComponentProps +) => { + console.log(`🚀 > IterableEmbeddedNotification > props:`, props); + return ( + + IterableEmbeddedNotification + + ); +}; diff --git a/src/embedded/components/IterableEmbeddedNotification/index.ts b/src/embedded/components/IterableEmbeddedNotification/index.ts new file mode 100644 index 000000000..23c458a11 --- /dev/null +++ b/src/embedded/components/IterableEmbeddedNotification/index.ts @@ -0,0 +1 @@ +export * from './IterableEmbeddedNotification'; diff --git a/src/embedded/components/IterableEmbeddedView/IterableEmbeddedView.styles.ts b/src/embedded/components/IterableEmbeddedView/IterableEmbeddedView.styles.ts new file mode 100644 index 000000000..e69de29bb diff --git a/src/embedded/components/IterableEmbeddedView/IterableEmbeddedView.tsx b/src/embedded/components/IterableEmbeddedView/IterableEmbeddedView.tsx new file mode 100644 index 000000000..c2e2e761b --- /dev/null +++ b/src/embedded/components/IterableEmbeddedView/IterableEmbeddedView.tsx @@ -0,0 +1,66 @@ +import { useMemo } from 'react'; +import { Text, View } from 'react-native'; + +import { IterableEmbeddedViewType } from '../../enums'; +import { getMedia } from '../utils/getMedia'; +import { getStyles } from '../utils/getStyles'; + +import { IterableEmbeddedBanner } from '../IterableEmbeddedBanner'; +import { IterableEmbeddedCard } from '../IterableEmbeddedCard'; +import { IterableEmbeddedNotification } from '../IterableEmbeddedNotification'; +import type { IterableEmbeddedComponentProps } from '../IterableEmbeddedViewProps'; + +interface IterableEmbeddedViewProps extends IterableEmbeddedComponentProps { + viewType: IterableEmbeddedViewType; +} + +export const IterableEmbeddedView = ({ + viewType, + message, + config, +}: IterableEmbeddedViewProps) => { + console.log(`🚀 > IterableEmbeddedView > config:`, config); + console.log(`🚀 > IterableEmbeddedView > message:`, message); + console.log(`🚀 > IterableEmbeddedView > viewType:`, viewType); + + const parsedStyles = useMemo(() => { + return getStyles(viewType, config); + }, [viewType, config]); + console.log(`🚀 > IterableEmbeddedView > parsedStyles:`, parsedStyles); + const media = useMemo(() => { + return getMedia(viewType, message); + }, [viewType, message]); + console.log(`🚀 > IterableEmbeddedView > media:`, media); + + const Cmp = useMemo(() => { + switch (viewType) { + case IterableEmbeddedViewType.Card: + return IterableEmbeddedCard; + case IterableEmbeddedViewType.Notification: + return IterableEmbeddedNotification; + case IterableEmbeddedViewType.Banner: + return IterableEmbeddedBanner; + default: + return null; + } + }, [viewType]); + + return ( + + EMBEDDED MESSAGE!!! + {Cmp && } + viewType: {viewType} + title: {message.elements?.title} + body: {message.elements?.body} + mediaURL: {message.elements?.mediaURL} + mediaUrlCaption: {message.elements?.mediaUrlCaption} + config: {config?.backgroundColor} + config: {config?.borderColor} + config: {config?.borderWidth} + config: {config?.borderCornerRadius} + config: {config?.primaryBtnBackgroundColor} + config: {config?.primaryBtnTextColor} + config: {config?.secondaryBtnBackgroundColor} + + ); +}; diff --git a/src/embedded/components/IterableEmbeddedView/index.ts b/src/embedded/components/IterableEmbeddedView/index.ts new file mode 100644 index 000000000..e8c383bfb --- /dev/null +++ b/src/embedded/components/IterableEmbeddedView/index.ts @@ -0,0 +1 @@ +export * from './IterableEmbeddedView'; diff --git a/src/embedded/components/IterableEmbeddedViewProps.ts b/src/embedded/components/IterableEmbeddedViewProps.ts new file mode 100644 index 000000000..f7c8bc8c3 --- /dev/null +++ b/src/embedded/components/IterableEmbeddedViewProps.ts @@ -0,0 +1,7 @@ +import type { IterableEmbeddedMessage } from '../classes/IterableEmbeddedMessage'; +import type { IterableEmbeddedViewConfig } from '../classes/IterableEmbeddedViewConfig'; + +export interface IterableEmbeddedComponentProps { + message: IterableEmbeddedMessage; + config?: IterableEmbeddedViewConfig | null; +} diff --git a/src/embedded/components/index.ts b/src/embedded/components/index.ts index e69de29bb..59c1b84f4 100644 --- a/src/embedded/components/index.ts +++ b/src/embedded/components/index.ts @@ -0,0 +1 @@ +export * from './IterableEmbeddedView/IterableEmbeddedView'; diff --git a/src/embedded/components/utils/getButtons.ts b/src/embedded/components/utils/getButtons.ts new file mode 100644 index 000000000..f8370eafb --- /dev/null +++ b/src/embedded/components/utils/getButtons.ts @@ -0,0 +1,24 @@ +import type { IterableEmbeddedMessage } from '../../classes/IterableEmbeddedMessage'; +import type { IterableEmbeddedMessageElementsButton } from '../../classes/IterableEmbeddedMessageElementsButton'; +import type { IterableEmbeddedViewButtonInfo } from '../../types/IterableEmbeddedViewButtonInfo'; + +export const getButtons = (message: IterableEmbeddedMessage) => { + const buttons = message.elements?.buttons ?? null; + if (!buttons || buttons.length === 0) return []; + + const mapOne = ( + b?: IterableEmbeddedMessageElementsButton | null + ): IterableEmbeddedViewButtonInfo => { + if (!b) return { id: null, title: null, clickedUrl: null }; + const clickedUrl = + (b.action?.data && b.action?.data?.length > 0 + ? b.action.data + : b.action?.type) ?? null; + return { id: b.id ?? null, title: b.title ?? null, clickedUrl }; + }; + + const first = mapOne(buttons[0] ?? null); + const second = mapOne(buttons.length > 1 ? buttons[1] : null); + + return [first, second].filter((bi) => bi.title && bi.title.length > 0); +}; diff --git a/src/embedded/components/utils/getDefaultActionUrl.ts b/src/embedded/components/utils/getDefaultActionUrl.ts new file mode 100644 index 000000000..225106616 --- /dev/null +++ b/src/embedded/components/utils/getDefaultActionUrl.ts @@ -0,0 +1,7 @@ +import type { IterableEmbeddedMessage } from '../../classes/IterableEmbeddedMessage'; + +export const getDefaultActionUrl = (message: IterableEmbeddedMessage) => { + const defaultAction = message.elements?.defaultAction ?? null; + if (!defaultAction) return null; + return defaultAction.data ?? defaultAction.type; +}; diff --git a/src/embedded/components/utils/getDefaultColor.ts b/src/embedded/components/utils/getDefaultColor.ts new file mode 100644 index 000000000..4c5d8d6a9 --- /dev/null +++ b/src/embedded/components/utils/getDefaultColor.ts @@ -0,0 +1,19 @@ +import { IterableEmbeddedViewType } from '../../enums/IterableEmbeddedViewType'; + +export const getDefaultColor = ( + viewType: IterableEmbeddedViewType, + colors: { + banner: number | string; + card: number | string; + notification: number | string; + } +) => { + switch (viewType) { + case IterableEmbeddedViewType.Notification: + return colors.notification; + case IterableEmbeddedViewType.Card: + return colors.card; + default: + return colors.banner; + } +}; diff --git a/src/embedded/components/utils/getMedia.ts b/src/embedded/components/utils/getMedia.ts new file mode 100644 index 000000000..2d417a409 --- /dev/null +++ b/src/embedded/components/utils/getMedia.ts @@ -0,0 +1,12 @@ +import type { IterableEmbeddedMessage } from "../../classes/IterableEmbeddedMessage"; +import { IterableEmbeddedViewType } from "../../enums/IterableEmbeddedViewType"; + +export const getMedia = (viewType: IterableEmbeddedViewType, message: IterableEmbeddedMessage) => { + const url = message.elements?.mediaURL ?? null; + const caption = message.elements?.mediaUrlCaption ?? null; + const shouldShow = + !!url && + url.length > 0 && + viewType !== IterableEmbeddedViewType.Notification; + return { url, caption, shouldShow }; +}; diff --git a/src/embedded/components/utils/getStyles.ts b/src/embedded/components/utils/getStyles.ts new file mode 100644 index 000000000..4a4ca8c6b --- /dev/null +++ b/src/embedded/components/utils/getStyles.ts @@ -0,0 +1,74 @@ +import type { IterableEmbeddedViewConfig } from '../../classes/IterableEmbeddedViewConfig'; +import { + defaultBorderWidth, + defaultBorderCornerRadius, + embeddedColors, +} from '../../constants/embeddedViewDefaults'; +import type { IterableEmbeddedViewType } from '../../enums/IterableEmbeddedViewType'; +import { getDefaultColor } from './getDefaultColor'; + +export const getStyles = ( + viewType: IterableEmbeddedViewType, + c?: IterableEmbeddedViewConfig | null +) => { + return { + backgroundColor: + c?.backgroundColor ?? + getDefaultColor(viewType, { + banner: embeddedColors.background.banner, + card: embeddedColors.background.card, + notification: embeddedColors.background.notification, + }), + borderColor: + c?.borderColor ?? + getDefaultColor(viewType, { + banner: embeddedColors.border.banner, + card: embeddedColors.border.card, + notification: embeddedColors.border.notification, + }), + borderWidth: c?.borderWidth ?? defaultBorderWidth, + borderCornerRadius: c?.borderCornerRadius ?? defaultBorderCornerRadius, + primaryBtnBackgroundColor: + c?.primaryBtnBackgroundColor ?? + getDefaultColor(viewType, { + banner: embeddedColors.primaryBtnBackground.banner, + card: embeddedColors.primaryBtnBackground.card, + notification: embeddedColors.primaryBtnBackground.notification, + }), + primaryBtnTextColor: + c?.primaryBtnTextColor ?? + getDefaultColor(viewType, { + banner: embeddedColors.primaryBtnText.banner, + card: embeddedColors.primaryBtnText.card, + notification: embeddedColors.primaryBtnText.notification, + }), + secondaryBtnBackgroundColor: + c?.secondaryBtnBackgroundColor ?? + getDefaultColor(viewType, { + banner: embeddedColors.secondaryBtnBackground.banner, + card: embeddedColors.secondaryBtnBackground.card, + notification: embeddedColors.secondaryBtnBackground.notification, + }), + secondaryBtnTextColor: + c?.secondaryBtnTextColor ?? + getDefaultColor(viewType, { + banner: embeddedColors.secondaryBtnText.banner, + card: embeddedColors.secondaryBtnText.card, + notification: embeddedColors.secondaryBtnText.notification, + }), + titleTextColor: + c?.titleTextColor ?? + getDefaultColor(viewType, { + banner: embeddedColors.titleText.banner, + card: embeddedColors.titleText.card, + notification: embeddedColors.titleText.notification, + }), + bodyTextColor: + c?.bodyTextColor ?? + getDefaultColor(viewType, { + banner: embeddedColors.bodyText.banner, + card: embeddedColors.bodyText.card, + notification: embeddedColors.bodyText.notification, + }), + }; +}; diff --git a/src/embedded/constants/embeddedColors.ts b/src/embedded/constants/embeddedViewDefaults.ts similarity index 66% rename from src/embedded/constants/embeddedColors.ts rename to src/embedded/constants/embeddedViewDefaults.ts index b366c216a..ca5279ccf 100644 --- a/src/embedded/constants/embeddedColors.ts +++ b/src/embedded/constants/embeddedViewDefaults.ts @@ -45,3 +45,17 @@ export const embeddedBodyTextColors = { card: 0xff444444, banner: 0xff444444, }; + +export const defaultBorderWidth = 1; +export const defaultBorderCornerRadius = 8.0; + +export const embeddedColors = { + background: embeddedBackgroundColors, + border: embeddedBorderColors, + primaryBtnBackground: embeddedPrimaryBtnBackgroundColors, + primaryBtnText: embeddedPrimaryBtnTextColors, + secondaryBtnBackground: embeddedSecondaryBtnBackgroundColors, + secondaryBtnText: embeddedSecondaryBtnTextColors, + titleText: embeddedTitleTextColors, + bodyText: embeddedBodyTextColors, +}; diff --git a/src/embedded/index.ts b/src/embedded/index.ts index e69de29bb..07635cbbc 100644 --- a/src/embedded/index.ts +++ b/src/embedded/index.ts @@ -0,0 +1 @@ +export * from './components'; diff --git a/src/embedded/types/IterableEmbeddedViewStyles.ts b/src/embedded/types/IterableEmbeddedViewStyles.ts index 60ef929db..d92a54f15 100644 --- a/src/embedded/types/IterableEmbeddedViewStyles.ts +++ b/src/embedded/types/IterableEmbeddedViewStyles.ts @@ -1,12 +1,12 @@ export interface IterableEmbeddedViewStyles { - backgroundColor: number; - borderColor: number; + backgroundColor: number | string; + borderColor: number | string; borderWidth: number; borderCornerRadius: number; - primaryBtnBackgroundColor: number; - primaryBtnTextColor: number; - secondaryBtnBackgroundColor: number; - secondaryBtnTextColor: number; - titleTextColor: number; - bodyTextColor: number; + primaryBtnBackgroundColor: number | string; + primaryBtnTextColor: number | string; + secondaryBtnBackgroundColor: number | string; + secondaryBtnTextColor: number | string; + titleTextColor: number | string; + bodyTextColor: number | string; } diff --git a/src/index.tsx b/src/index.tsx index d87876d7d..e74779e6a 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -60,3 +60,6 @@ export { type IterableInboxRowViewModel, } from './inbox'; export { IterableEmbeddedMessage } from './embedded/classes/IterableEmbeddedMessage'; +export { IterableEmbeddedView } from './embedded/components'; +export { IterableEmbeddedViewType } from './embedded/enums'; +export { IterableEmbeddedViewConfig } from './embedded/classes/IterableEmbeddedViewConfig'; From 22fa618bc9c8d4a2f743ef09d5b850b71797fe12 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Wed, 8 Oct 2025 13:32:46 -0700 Subject: [PATCH 30/45] feat: initial implementation of IterableEmbeddedBanner and IterableEmbeddedCard --- example/src/components/Embedded/Embedded.tsx | 7 +- src/embedded/classes/IterableEmbeddedView.ts | 334 +++++++++--------- .../IterableEmbeddedBanner.styles.tsx | 11 + .../IterableEmbeddedBanner.tsx | 26 +- .../IterableEmbeddedCard.styles.ts | 10 + .../IterableEmbeddedCard.tsx | 24 +- .../constants/embeddedViewDefaults.ts | 48 +-- 7 files changed, 260 insertions(+), 200 deletions(-) create mode 100644 src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.styles.tsx create mode 100644 src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.styles.ts diff --git a/example/src/components/Embedded/Embedded.tsx b/example/src/components/Embedded/Embedded.tsx index 20dc818a7..b16eb176d 100644 --- a/example/src/components/Embedded/Embedded.tsx +++ b/example/src/components/Embedded/Embedded.tsx @@ -21,6 +21,11 @@ export const Embedded = () => { useEffect(() => { if (isFocused) { Iterable.embeddedManager.startSession(); + Iterable.embeddedManager.syncMessages(); + Iterable.embeddedManager.getMessages(placementIds).then((messageList) => { + console.log(messageList); + setMessages(messageList as IterableEmbeddedMessage[]); + }); setHasSession(true); } else { if (hasSession) { @@ -77,7 +82,7 @@ export const Embedded = () => { return ( ); diff --git a/src/embedded/classes/IterableEmbeddedView.ts b/src/embedded/classes/IterableEmbeddedView.ts index 2e24c9745..191e44f4a 100644 --- a/src/embedded/classes/IterableEmbeddedView.ts +++ b/src/embedded/classes/IterableEmbeddedView.ts @@ -1,181 +1,181 @@ -import { IterableEmbeddedViewType } from '../enums/IterableEmbeddedViewType'; -import { IterableEmbeddedMessage } from './IterableEmbeddedMessage'; -import { IterableEmbeddedViewConfig } from './IterableEmbeddedViewConfig'; -import type { IterableEmbeddedMessageElementsButton } from './IterableEmbeddedMessageElementsButton'; -import type { IterableEmbeddedViewButtonInfo } from '../types/IterableEmbeddedViewButtonInfo'; -import type { IterableEmbeddedViewStyles } from '../types/IterableEmbeddedViewStyles'; -import { - embeddedBackgroundColors, - embeddedBodyTextColors, - embeddedBorderColors, - embeddedPrimaryBtnBackgroundColors, - embeddedPrimaryBtnTextColors, - embeddedSecondaryBtnBackgroundColors, - embeddedSecondaryBtnTextColors, - embeddedTitleTextColors, -} from '../constants/embeddedViewDefaults'; +// import { IterableEmbeddedViewType } from '../enums/IterableEmbeddedViewType'; +// import { IterableEmbeddedMessage } from './IterableEmbeddedMessage'; +// import { IterableEmbeddedViewConfig } from './IterableEmbeddedViewConfig'; +// import type { IterableEmbeddedMessageElementsButton } from './IterableEmbeddedMessageElementsButton'; +// import type { IterableEmbeddedViewButtonInfo } from '../types/IterableEmbeddedViewButtonInfo'; +// import type { IterableEmbeddedViewStyles } from '../types/IterableEmbeddedViewStyles'; +// import { +// embeddedBackgroundColors, +// embeddedBodyTextColors, +// embeddedBorderColors, +// embeddedPrimaryBtnBackgroundColors, +// embeddedPrimaryBtnTextColors, +// embeddedSecondaryBtnBackgroundColors, +// embeddedSecondaryBtnTextColors, +// embeddedTitleTextColors, +// } from '../constants/embeddedViewDefaults'; -export class IterableEmbeddedView { - private viewType: IterableEmbeddedViewType; - private message: IterableEmbeddedMessage; - private config?: IterableEmbeddedViewConfig | null; +// export class IterableEmbeddedView { +// private viewType: IterableEmbeddedViewType; +// private message: IterableEmbeddedMessage; +// private config?: IterableEmbeddedViewConfig | null; - private readonly defaultBackgroundColor: number; - private readonly defaultBorderColor: number; - private readonly defaultPrimaryBtnBackgroundColor: number; - private readonly defaultPrimaryBtnTextColor: number; - private readonly defaultSecondaryBtnBackgroundColor: number; - private readonly defaultSecondaryBtnTextColor: number; - private readonly defaultTitleTextColor: number; - private readonly defaultBodyTextColor: number; - private readonly defaultBorderWidth: number = 1; - private readonly defaultBorderCornerRadius: number = 8.0; +// private readonly defaultBackgroundColor: number; +// private readonly defaultBorderColor: number; +// private readonly defaultPrimaryBtnBackgroundColor: number; +// private readonly defaultPrimaryBtnTextColor: number; +// private readonly defaultSecondaryBtnBackgroundColor: number; +// private readonly defaultSecondaryBtnTextColor: number; +// private readonly defaultTitleTextColor: number; +// private readonly defaultBodyTextColor: number; +// private readonly defaultBorderWidth: number = 1; +// private readonly defaultBorderCornerRadius: number = 8.0; - constructor( - viewType: IterableEmbeddedViewType, - message: IterableEmbeddedMessage, - config?: IterableEmbeddedViewConfig | null - ) { - this.viewType = viewType; - this.message = message; - this.config = config ?? null; +// constructor( +// viewType: IterableEmbeddedViewType, +// message: IterableEmbeddedMessage, +// config?: IterableEmbeddedViewConfig | null +// ) { +// this.viewType = viewType; +// this.message = message; +// this.config = config ?? null; - // Default color values are placeholders; caller can map them to theme values if needed. - // These numeric values mirror the Kotlin use of Int colors. - this.defaultBackgroundColor = this.getDefaultColor( - viewType, - embeddedBackgroundColors.notification, - embeddedBackgroundColors.card, - embeddedBackgroundColors.banner - ); - this.defaultBorderColor = this.getDefaultColor( - viewType, - embeddedBorderColors.notification, - embeddedBorderColors.card, - embeddedBorderColors.banner - ); - this.defaultPrimaryBtnBackgroundColor = this.getDefaultColor( - viewType, - embeddedPrimaryBtnBackgroundColors.notification, - embeddedPrimaryBtnBackgroundColors.card, - embeddedPrimaryBtnBackgroundColors.banner - ); - this.defaultPrimaryBtnTextColor = this.getDefaultColor( - viewType, - embeddedPrimaryBtnTextColors.notification, - embeddedPrimaryBtnTextColors.card, - embeddedPrimaryBtnTextColors.banner - ); - this.defaultSecondaryBtnBackgroundColor = this.getDefaultColor( - viewType, - embeddedSecondaryBtnBackgroundColors.notification, - embeddedSecondaryBtnBackgroundColors.card, - embeddedSecondaryBtnBackgroundColors.banner - ); - this.defaultSecondaryBtnTextColor = this.getDefaultColor( - viewType, - embeddedSecondaryBtnTextColors.notification, - embeddedSecondaryBtnTextColors.card, - embeddedSecondaryBtnTextColors.banner - ); - this.defaultTitleTextColor = this.getDefaultColor( - viewType, - embeddedTitleTextColors.notification, - embeddedTitleTextColors.card, - embeddedTitleTextColors.banner - ); - this.defaultBodyTextColor = this.getDefaultColor( - viewType, - embeddedBodyTextColors.notification, - embeddedBodyTextColors.card, - embeddedBodyTextColors.banner - ); - } +// // Default color values are placeholders; caller can map them to theme values if needed. +// // These numeric values mirror the Kotlin use of Int colors. +// this.defaultBackgroundColor = this.getDefaultColor( +// viewType, +// embeddedBackgroundColors.notification, +// embeddedBackgroundColors.card, +// embeddedBackgroundColors.banner +// ); +// this.defaultBorderColor = this.getDefaultColor( +// viewType, +// embeddedBorderColors.notification, +// embeddedBorderColors.card, +// embeddedBorderColors.banner +// ); +// this.defaultPrimaryBtnBackgroundColor = this.getDefaultColor( +// viewType, +// embeddedPrimaryBtnBackgroundColors.notification, +// embeddedPrimaryBtnBackgroundColors.card, +// embeddedPrimaryBtnBackgroundColors.banner +// ); +// this.defaultPrimaryBtnTextColor = this.getDefaultColor( +// viewType, +// embeddedPrimaryBtnTextColors.notification, +// embeddedPrimaryBtnTextColors.card, +// embeddedPrimaryBtnTextColors.banner +// ); +// this.defaultSecondaryBtnBackgroundColor = this.getDefaultColor( +// viewType, +// embeddedSecondaryBtnBackgroundColors.notification, +// embeddedSecondaryBtnBackgroundColors.card, +// embeddedSecondaryBtnBackgroundColors.banner +// ); +// this.defaultSecondaryBtnTextColor = this.getDefaultColor( +// viewType, +// embeddedSecondaryBtnTextColors.notification, +// embeddedSecondaryBtnTextColors.card, +// embeddedSecondaryBtnTextColors.banner +// ); +// this.defaultTitleTextColor = this.getDefaultColor( +// viewType, +// embeddedTitleTextColors.notification, +// embeddedTitleTextColors.card, +// embeddedTitleTextColors.banner +// ); +// this.defaultBodyTextColor = this.getDefaultColor( +// viewType, +// embeddedBodyTextColors.notification, +// embeddedBodyTextColors.card, +// embeddedBodyTextColors.banner +// ); +// } - getStyles(): IterableEmbeddedViewStyles { - const c = this.config; - return { - backgroundColor: c?.backgroundColor ?? this.defaultBackgroundColor, - borderColor: c?.borderColor ?? this.defaultBorderColor, - borderWidth: c?.borderWidth ?? this.defaultBorderWidth, - borderCornerRadius: - c?.borderCornerRadius ?? this.defaultBorderCornerRadius, - primaryBtnBackgroundColor: - c?.primaryBtnBackgroundColor ?? this.defaultPrimaryBtnBackgroundColor, - primaryBtnTextColor: - c?.primaryBtnTextColor ?? this.defaultPrimaryBtnTextColor, - secondaryBtnBackgroundColor: - c?.secondaryBtnBackgroundColor ?? - this.defaultSecondaryBtnBackgroundColor, - secondaryBtnTextColor: - c?.secondaryBtnTextColor ?? this.defaultSecondaryBtnTextColor, - titleTextColor: c?.titleTextColor ?? this.defaultTitleTextColor, - bodyTextColor: c?.bodyTextColor ?? this.defaultBodyTextColor, - }; - } +// getStyles(): IterableEmbeddedViewStyles { +// const c = this.config; +// return { +// backgroundColor: c?.backgroundColor ?? this.defaultBackgroundColor, +// borderColor: c?.borderColor ?? this.defaultBorderColor, +// borderWidth: c?.borderWidth ?? this.defaultBorderWidth, +// borderCornerRadius: +// c?.borderCornerRadius ?? this.defaultBorderCornerRadius, +// primaryBtnBackgroundColor: +// c?.primaryBtnBackgroundColor ?? this.defaultPrimaryBtnBackgroundColor, +// primaryBtnTextColor: +// c?.primaryBtnTextColor ?? this.defaultPrimaryBtnTextColor, +// secondaryBtnBackgroundColor: +// c?.secondaryBtnBackgroundColor ?? +// this.defaultSecondaryBtnBackgroundColor, +// secondaryBtnTextColor: +// c?.secondaryBtnTextColor ?? this.defaultSecondaryBtnTextColor, +// titleTextColor: c?.titleTextColor ?? this.defaultTitleTextColor, +// bodyTextColor: c?.bodyTextColor ?? this.defaultBodyTextColor, +// }; +// } - getTitle(): string | null | undefined { - return this.message.elements?.title ?? null; - } +// getTitle(): string | null | undefined { +// return this.message.elements?.title ?? null; +// } - getBody(): string | null | undefined { - return this.message.elements?.body ?? null; - } +// getBody(): string | null | undefined { +// return this.message.elements?.body ?? null; +// } - getMedia(): { - url?: string | null; - caption?: string | null; - shouldShow: boolean; - } { - const url = this.message.elements?.mediaURL ?? null; - const caption = this.message.elements?.mediaUrlCaption ?? null; - const shouldShow = - !!url && - url.length > 0 && - this.viewType !== IterableEmbeddedViewType.Notification; - return { url, caption, shouldShow }; - } +// getMedia(): { +// url?: string | null; +// caption?: string | null; +// shouldShow: boolean; +// } { +// const url = this.message.elements?.mediaURL ?? null; +// const caption = this.message.elements?.mediaUrlCaption ?? null; +// const shouldShow = +// !!url && +// url.length > 0 && +// this.viewType !== IterableEmbeddedViewType.Notification; +// return { url, caption, shouldShow }; +// } - getButtons(): IterableEmbeddedViewButtonInfo[] { - const buttons = this.message.elements?.buttons ?? null; - if (!buttons || buttons.length === 0) return []; +// getButtons(): IterableEmbeddedViewButtonInfo[] { +// const buttons = this.message.elements?.buttons ?? null; +// if (!buttons || buttons.length === 0) return []; - const mapOne = ( - b?: IterableEmbeddedMessageElementsButton | null - ): IterableEmbeddedViewButtonInfo => { - if (!b) return { id: null, title: null, clickedUrl: null }; - const clickedUrl = - (b.action?.data && b.action?.data?.length > 0 - ? b.action.data - : b.action?.type) ?? null; - return { id: b.id ?? null, title: b.title ?? null, clickedUrl }; - }; +// const mapOne = ( +// b?: IterableEmbeddedMessageElementsButton | null +// ): IterableEmbeddedViewButtonInfo => { +// if (!b) return { id: null, title: null, clickedUrl: null }; +// const clickedUrl = +// (b.action?.data && b.action?.data?.length > 0 +// ? b.action.data +// : b.action?.type) ?? null; +// return { id: b.id ?? null, title: b.title ?? null, clickedUrl }; +// }; - const first = mapOne(buttons[0] ?? null); - const second = mapOne(buttons.length > 1 ? buttons[1] : null); +// const first = mapOne(buttons[0] ?? null); +// const second = mapOne(buttons.length > 1 ? buttons[1] : null); - return [first, second].filter((bi) => bi.title && bi.title.length > 0); - } +// return [first, second].filter((bi) => bi.title && bi.title.length > 0); +// } - getDefaultActionUrl(): string | null { - const da = this.message.elements?.defaultAction ?? null; - if (!da) return null; - return (da.data && da.data.length > 0 ? da.data : da.type) ?? null; - } +// getDefaultActionUrl(): string | null { +// const da = this.message.elements?.defaultAction ?? null; +// if (!da) return null; +// return (da.data && da.data.length > 0 ? da.data : da.type) ?? null; +// } - private getDefaultColor( - viewType: IterableEmbeddedViewType, - notificationColor: number, - cardColor: number, - bannerColor: number - ): number { - switch (viewType) { - case IterableEmbeddedViewType.Notification: - return notificationColor; - case IterableEmbeddedViewType.Card: - return cardColor; - default: - return bannerColor; - } - } -} +// private getDefaultColor( +// viewType: IterableEmbeddedViewType, +// notificationColor: number, +// cardColor: number, +// bannerColor: number +// ): number { +// switch (viewType) { +// case IterableEmbeddedViewType.Notification: +// return notificationColor; +// case IterableEmbeddedViewType.Card: +// return cardColor; +// default: +// return bannerColor; +// } +// } +// } diff --git a/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.styles.tsx b/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.styles.tsx new file mode 100644 index 000000000..983e77a72 --- /dev/null +++ b/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.styles.tsx @@ -0,0 +1,11 @@ +import { StyleSheet } from 'react-native'; + +export const styles = StyleSheet.create({ + container: { + borderStyle: 'solid', + boxShadow: + '0 1px 1px 0 rgba(0, 0, 0, 0.06), 0 0 2px 0 rgba(0, 0, 0, 0.06), 0 0 1px 0 rgba(0, 0, 0, 0.08)', + padding: 16, + width: '100%', + }, +}); diff --git a/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx b/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx index f5071e6cd..b82eff4c5 100644 --- a/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx +++ b/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx @@ -1,13 +1,29 @@ import { Text, View } from 'react-native'; +import { useMemo } from 'react'; import type { IterableEmbeddedComponentProps } from '../IterableEmbeddedViewProps'; +import { IterableEmbeddedViewType } from '../../enums'; +import { getStyles } from '../utils/getStyles'; +import { getMedia } from '../utils/getMedia'; +import { styles } from './IterableEmbeddedBanner.styles'; + +export const IterableEmbeddedBanner = ({ + config, + message, +}: IterableEmbeddedComponentProps) => { + const parsedStyles = useMemo(() => { + return getStyles(IterableEmbeddedViewType.Banner, config); + }, [config]); + console.log(`🚀 > IterableEmbeddedView > parsedStyles:`, parsedStyles); + const media = useMemo(() => { + return getMedia(IterableEmbeddedViewType.Banner, message); + }, [message]); + console.log(`🚀 > IterableEmbeddedView > media:`, media); -export const IterableEmbeddedBanner = ( - props: IterableEmbeddedComponentProps -) => { - console.log(`🚀 > IterableEmbeddedBanner > props:`, props); return ( - + IterableEmbeddedBanner ); diff --git a/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.styles.ts b/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.styles.ts new file mode 100644 index 000000000..066075bbb --- /dev/null +++ b/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.styles.ts @@ -0,0 +1,10 @@ +import { StyleSheet } from 'react-native'; + +export const styles = StyleSheet.create({ + container: { + borderStyle: 'solid', + boxShadow: + '0 1px 1px 0 rgba(0, 0, 0, 0.06), 0 0 2px 0 rgba(0, 0, 0, 0.06), 0 0 1px 0 rgba(0, 0, 0, 0.08)', + width: '100%', + }, +}); diff --git a/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx b/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx index 4a3f87639..0912f5a10 100644 --- a/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx +++ b/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx @@ -1,11 +1,29 @@ import { Text, View } from 'react-native'; +import { useMemo } from 'react'; import type { IterableEmbeddedComponentProps } from '../IterableEmbeddedViewProps'; +import { IterableEmbeddedViewType } from '../../enums'; +import { getStyles } from '../utils/getStyles'; +import { getMedia } from '../utils/getMedia'; +import { styles } from './IterableEmbeddedCard.styles'; + +export const IterableEmbeddedCard = ({ + config, + message, +}: IterableEmbeddedComponentProps) => { + const parsedStyles = useMemo(() => { + return getStyles(IterableEmbeddedViewType.Card, config); + }, [config]); + console.log(`🚀 > IterableEmbeddedView > parsedStyles:`, parsedStyles); + const media = useMemo(() => { + return getMedia(IterableEmbeddedViewType.Card, message); + }, [message]); + console.log(`🚀 > IterableEmbeddedView > media:`, media); -export const IterableEmbeddedCard = (props: IterableEmbeddedComponentProps) => { - console.log(`🚀 > IterableEmbeddedCard > props:`, props); return ( - + IterableEmbeddedCard ); diff --git a/src/embedded/constants/embeddedViewDefaults.ts b/src/embedded/constants/embeddedViewDefaults.ts index ca5279ccf..d13504591 100644 --- a/src/embedded/constants/embeddedViewDefaults.ts +++ b/src/embedded/constants/embeddedViewDefaults.ts @@ -1,49 +1,49 @@ export const embeddedBackgroundColors = { - notification: 0xff121212, - card: 0xffffffff, - banner: 0xffffffff, + notification: '#ff121212', + card: '#ffffffff', + banner: '#ffffffff', }; export const embeddedBorderColors = { - notification: 0xff2a2a2a, - card: 0xffe0e0e0, - banner: 0xffe0e0e0, + notification: '#ff2a2a2a', + card: '#ffe0e0e0', + banner: '#ffe0e0e0', }; export const embeddedPrimaryBtnBackgroundColors = { - notification: 0xffffffff, - card: 0xffffffff, - banner: 0xff1a73e8, + notification: '#ffffffff', + card: '#ffffffff', + banner: '#ff1a73e8', }; export const embeddedPrimaryBtnTextColors = { - notification: 0xff121212, - card: 0xff1a73e8, - banner: 0xffffffff, + notification: '#ff121212', + card: '#ff1a73e8', + banner: '#ffffffff', }; export const embeddedSecondaryBtnBackgroundColors = { - notification: 0xff121212, - card: 0xffffffff, - banner: 0xffffffff, + notification: '#ff121212', + card: '#ffffffff', + banner: '#ffffffff', }; export const embeddedSecondaryBtnTextColors = { - notification: 0xff121212, - card: 0xff1a73e8, - banner: 0xff1a73e8, + notification: '#ff121212', + card: '#ff1a73e8', + banner: '#ff1a73e8', }; export const embeddedTitleTextColors = { - notification: 0xffffffff, - card: 0xff111111, - banner: 0xff111111, + notification: '#ffffffff', + card: '#ff111111', + banner: '#ff111111', }; export const embeddedBodyTextColors = { - notification: 0xffffffff, - card: 0xff444444, - banner: 0xff444444, + notification: '#ffffffff', + card: '#ff444444', + banner: '#ff444444', }; export const defaultBorderWidth = 1; From 371fad943fb2790180da52d4ded03a45205a334c Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Wed, 8 Oct 2025 14:56:52 -0700 Subject: [PATCH 31/45] feat: added buttons to banner --- .../classes/IterableEmbeddedViewConfig.ts | 42 ++++----- .../IterableEmbeddedBanner.styles.tsx | 63 ++++++++++++++ .../IterableEmbeddedBanner.tsx | 85 +++++++++++++++++-- .../IterableEmbeddedCard.tsx | 12 ++- .../IterableEmbeddedView.tsx | 31 +------ .../constants/embeddedViewDefaults.ts | 16 ++-- 6 files changed, 182 insertions(+), 67 deletions(-) diff --git a/src/embedded/classes/IterableEmbeddedViewConfig.ts b/src/embedded/classes/IterableEmbeddedViewConfig.ts index cea8a9a97..a431ea04e 100644 --- a/src/embedded/classes/IterableEmbeddedViewConfig.ts +++ b/src/embedded/classes/IterableEmbeddedViewConfig.ts @@ -1,24 +1,26 @@ +import type { ColorValue } from 'react-native'; + export interface IterableEmbeddedViewConfigDict { /** Background color hex (e.g., 0xFF0000) */ - backgroundColor?: number | null; + backgroundColor?: ColorValue; /** Border color hex */ - borderColor?: number | null; + borderColor?: ColorValue; /** Border width in pixels */ - borderWidth?: number | null; + borderWidth?: number; /** Corner radius in points */ - borderCornerRadius?: number | null; + borderCornerRadius?: number; /** Primary button background color hex */ - primaryBtnBackgroundColor?: number | null; + primaryBtnBackgroundColor?: ColorValue; /** Primary button text color hex */ - primaryBtnTextColor?: number | null; + primaryBtnTextColor?: ColorValue; /** Secondary button background color hex */ - secondaryBtnBackgroundColor?: number | null; + secondaryBtnBackgroundColor?: ColorValue; /** Secondary button text color hex */ - secondaryBtnTextColor?: number | null; + secondaryBtnTextColor?: ColorValue; /** Title text color hex */ - titleTextColor?: number | null; + titleTextColor?: ColorValue; /** Body text color hex */ - bodyTextColor?: number | null; + bodyTextColor?: ColorValue; } /** @@ -38,16 +40,16 @@ export class IterableEmbeddedViewConfig { constructor(options: IterableEmbeddedViewConfigDict = {}) { const { - backgroundColor = null, - borderColor = null, - borderWidth = null, - borderCornerRadius = null, - primaryBtnBackgroundColor = null, - primaryBtnTextColor = null, - secondaryBtnBackgroundColor = null, - secondaryBtnTextColor = null, - titleTextColor = null, - bodyTextColor = null, + backgroundColor, + borderColor, + borderWidth, + borderCornerRadius, + primaryBtnBackgroundColor, + primaryBtnTextColor, + secondaryBtnBackgroundColor, + secondaryBtnTextColor, + titleTextColor, + bodyTextColor, } = options; this.backgroundColor = backgroundColor; diff --git a/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.styles.tsx b/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.styles.tsx index 983e77a72..458ce7a7b 100644 --- a/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.styles.tsx +++ b/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.styles.tsx @@ -1,11 +1,74 @@ import { StyleSheet } from 'react-native'; export const styles = StyleSheet.create({ + body: { + alignSelf: 'stretch', + fontSize: 14, + fontWeight: '400', + lineHeight: 20, + }, + bodyContainer: { + alignItems: 'center', + alignSelf: 'stretch', + // backgroundColor: 'green', + display: 'flex', + flexDirection: 'row', + // gap: 16, + }, + button: { + // backgroundColor: 'blue', + borderRadius: 12, + gap: 8, + // height: 32, + // display: 'flex', + }, + buttonContainer: { + alignItems: 'flex-start', + alignSelf: 'stretch', + // backgroundColor: 'red', + display: 'flex', + flexDirection: 'row', + gap: 12, + width: '100%', + }, + buttonText: { + fontSize: 14, + fontWeight: '400', + lineHeight: 20, + paddingHorizontal: 12, + paddingVertical: 8, + }, container: { + alignItems: 'flex-start', borderStyle: 'solid', boxShadow: '0 1px 1px 0 rgba(0, 0, 0, 0.06), 0 0 2px 0 rgba(0, 0, 0, 0.06), 0 0 1px 0 rgba(0, 0, 0, 0.08)', + display: 'flex', + flexDirection: 'column', + gap: 16, + justifyContent: 'center', padding: 16, width: '100%', }, + mediaContainer: { + display: 'flex', + flexDirection: 'column', + gap: 16, + }, + textContainer: { + alignSelf: 'center', + // alignItems: 'flex-start', + // backgroundColor: 'red', + display: 'flex', + flexDirection: 'column', + flexGrow: 1, + flexShrink: 1, + gap: 4, + width: '100%', + }, + title: { + fontSize: 16, + fontWeight: '700', + lineHeight: 24, + }, }); diff --git a/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx b/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx index b82eff4c5..d8b2434da 100644 --- a/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx +++ b/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx @@ -1,10 +1,17 @@ -import { Text, View } from 'react-native'; -import { useMemo } from 'react'; +import { useCallback, useMemo } from 'react'; +import { + Text, + TouchableOpacity, + View, + type TextStyle, + type ViewStyle, +} from 'react-native'; -import type { IterableEmbeddedComponentProps } from '../IterableEmbeddedViewProps'; +import type { IterableEmbeddedMessageElementsButton } from '../../classes/IterableEmbeddedMessageElementsButton'; import { IterableEmbeddedViewType } from '../../enums'; -import { getStyles } from '../utils/getStyles'; +import type { IterableEmbeddedComponentProps } from '../IterableEmbeddedViewProps'; import { getMedia } from '../utils/getMedia'; +import { getStyles } from '../utils/getStyles'; import { styles } from './IterableEmbeddedBanner.styles'; export const IterableEmbeddedBanner = ({ @@ -14,17 +21,81 @@ export const IterableEmbeddedBanner = ({ const parsedStyles = useMemo(() => { return getStyles(IterableEmbeddedViewType.Banner, config); }, [config]); - console.log(`🚀 > IterableEmbeddedView > parsedStyles:`, parsedStyles); const media = useMemo(() => { return getMedia(IterableEmbeddedViewType.Banner, message); }, [message]); console.log(`🚀 > IterableEmbeddedView > media:`, media); + const onButtonPress = useCallback( + (button: IterableEmbeddedMessageElementsButton) => { + console.log('CLICK', button); + }, + [] + ); + + const buttons = message.elements?.buttons ?? []; return ( - IterableEmbeddedBanner + + + + {message.elements?.title} + + + {message.elements?.body} + + + + {/* */} + + + {buttons.length > 0 && ( + + {buttons.map((button, index) => { + const backgroundColor = + index === 0 + ? parsedStyles.primaryBtnBackgroundColor + : parsedStyles.secondaryBtnBackgroundColor; + const textColor = + index === 0 + ? parsedStyles.primaryBtnTextColor + : parsedStyles.secondaryBtnTextColor; + return ( + onButtonPress(button)} + key={button.id} + > + + {button.title} + + + ); + })} + + )} ); }; diff --git a/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx b/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx index 0912f5a10..8efed5c03 100644 --- a/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx +++ b/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx @@ -1,4 +1,4 @@ -import { Text, View } from 'react-native'; +import { Text, View, type ViewStyle } from 'react-native'; import { useMemo } from 'react'; import type { IterableEmbeddedComponentProps } from '../IterableEmbeddedViewProps'; @@ -22,7 +22,15 @@ export const IterableEmbeddedCard = ({ return ( IterableEmbeddedCard diff --git a/src/embedded/components/IterableEmbeddedView/IterableEmbeddedView.tsx b/src/embedded/components/IterableEmbeddedView/IterableEmbeddedView.tsx index c2e2e761b..9891afb20 100644 --- a/src/embedded/components/IterableEmbeddedView/IterableEmbeddedView.tsx +++ b/src/embedded/components/IterableEmbeddedView/IterableEmbeddedView.tsx @@ -1,9 +1,6 @@ import { useMemo } from 'react'; -import { Text, View } from 'react-native'; import { IterableEmbeddedViewType } from '../../enums'; -import { getMedia } from '../utils/getMedia'; -import { getStyles } from '../utils/getStyles'; import { IterableEmbeddedBanner } from '../IterableEmbeddedBanner'; import { IterableEmbeddedCard } from '../IterableEmbeddedCard'; @@ -23,15 +20,6 @@ export const IterableEmbeddedView = ({ console.log(`🚀 > IterableEmbeddedView > message:`, message); console.log(`🚀 > IterableEmbeddedView > viewType:`, viewType); - const parsedStyles = useMemo(() => { - return getStyles(viewType, config); - }, [viewType, config]); - console.log(`🚀 > IterableEmbeddedView > parsedStyles:`, parsedStyles); - const media = useMemo(() => { - return getMedia(viewType, message); - }, [viewType, message]); - console.log(`🚀 > IterableEmbeddedView > media:`, media); - const Cmp = useMemo(() => { switch (viewType) { case IterableEmbeddedViewType.Card: @@ -45,22 +33,5 @@ export const IterableEmbeddedView = ({ } }, [viewType]); - return ( - - EMBEDDED MESSAGE!!! - {Cmp && } - viewType: {viewType} - title: {message.elements?.title} - body: {message.elements?.body} - mediaURL: {message.elements?.mediaURL} - mediaUrlCaption: {message.elements?.mediaUrlCaption} - config: {config?.backgroundColor} - config: {config?.borderColor} - config: {config?.borderWidth} - config: {config?.borderCornerRadius} - config: {config?.primaryBtnBackgroundColor} - config: {config?.primaryBtnTextColor} - config: {config?.secondaryBtnBackgroundColor} - - ); + return Cmp ? : null; }; diff --git a/src/embedded/constants/embeddedViewDefaults.ts b/src/embedded/constants/embeddedViewDefaults.ts index d13504591..051968422 100644 --- a/src/embedded/constants/embeddedViewDefaults.ts +++ b/src/embedded/constants/embeddedViewDefaults.ts @@ -1,19 +1,19 @@ export const embeddedBackgroundColors = { notification: '#ff121212', - card: '#ffffffff', - banner: '#ffffffff', + card: '#ffffff', + banner: '#ffffff', }; export const embeddedBorderColors = { notification: '#ff2a2a2a', card: '#ffe0e0e0', - banner: '#ffe0e0e0', + banner: '#E0DEDF', }; export const embeddedPrimaryBtnBackgroundColors = { notification: '#ffffffff', card: '#ffffffff', - banner: '#ff1a73e8', + banner: '#6A266D', }; export const embeddedPrimaryBtnTextColors = { @@ -25,25 +25,25 @@ export const embeddedPrimaryBtnTextColors = { export const embeddedSecondaryBtnBackgroundColors = { notification: '#ff121212', card: '#ffffffff', - banner: '#ffffffff', + banner: 'transparent', }; export const embeddedSecondaryBtnTextColors = { notification: '#ff121212', card: '#ff1a73e8', - banner: '#ff1a73e8', + banner: '#79347F', }; export const embeddedTitleTextColors = { notification: '#ffffffff', card: '#ff111111', - banner: '#ff111111', + banner: '#3D3A3B', }; export const embeddedBodyTextColors = { notification: '#ffffffff', card: '#ff444444', - banner: '#ff444444', + banner: '#787174', }; export const defaultBorderWidth = 1; From e6c113283201cfdfbd1d8d043029294de9e27751 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Wed, 8 Oct 2025 16:09:25 -0700 Subject: [PATCH 32/45] feat: enhance IterableEmbedded components with button click handling and logging --- .../reactnative/RNIterableAPIModuleImpl.java | 1 + example/src/components/Embedded/Embedded.tsx | 2 +- example/src/hooks/useIterableApp.tsx | 1 + src/api/NativeRNIterableAPI.ts | 35 +++-- src/core/enums/IterableActionType.ts | 4 + src/core/enums/index.ts | 1 + src/core/utils/index.ts | 2 + src/core/utils/isIterableAction.ts | 2 + .../classes/IterableEmbeddedMessage.ts | 82 ++++++------ .../IterableEmbeddedMessageElements.ts | 124 ++++++++++-------- .../IterableEmbeddedMessageElementsButton.ts | 87 ++++++------ ...ableEmbeddedMessageElementsButtonAction.ts | 57 ++++---- ...bleEmbeddedMessageElementsDefaultAction.ts | 57 ++++---- .../IterableEmbeddedMessageElementsText.ts | 63 +++++---- .../classes/IterableEmbeddedPlacement.ts | 52 ++++---- .../IterableEmbeddedBanner.tsx | 17 ++- .../IterableEmbeddedView.tsx | 7 +- .../components/IterableEmbeddedViewProps.ts | 2 + .../components/utils/runButtonClick.ts | 43 ++++++ src/index.tsx | 3 +- 20 files changed, 381 insertions(+), 261 deletions(-) create mode 100644 src/core/enums/IterableActionType.ts create mode 100644 src/core/utils/isIterableAction.ts create mode 100644 src/embedded/components/utils/runButtonClick.ts diff --git a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java index 8eb0e83b1..41c1aea9b 100644 --- a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java +++ b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java @@ -735,6 +735,7 @@ public void handleEmbeddedClick(IterableEmbeddedMessage message, String buttonId } public void trackEmbeddedClick(IterableEmbeddedMessage message, String buttonId, String clickedUrl) { + IterableLogger.d(TAG, "trackEmbeddedClick: buttonId: " + buttonId + " clickedUrl: " + clickedUrl); IterableApi.getInstance().trackEmbeddedClick(message, buttonId, clickedUrl); } diff --git a/example/src/components/Embedded/Embedded.tsx b/example/src/components/Embedded/Embedded.tsx index b16eb176d..058f1f126 100644 --- a/example/src/components/Embedded/Embedded.tsx +++ b/example/src/components/Embedded/Embedded.tsx @@ -2,7 +2,7 @@ import { Iterable, IterableEmbeddedView, IterableEmbeddedViewType, - IterableEmbeddedMessage, + type IterableEmbeddedMessage, } from '@iterable/react-native-sdk'; import { useIsFocused } from '@react-navigation/native'; import { useCallback, useEffect, useState } from 'react'; diff --git a/example/src/hooks/useIterableApp.tsx b/example/src/hooks/useIterableApp.tsx index ed365372c..7141cb950 100644 --- a/example/src/hooks/useIterableApp.tsx +++ b/example/src/hooks/useIterableApp.tsx @@ -151,6 +151,7 @@ export const IterableAppProvider: FunctionComponent< }; config.urlHandler = (url: string) => { + console.log('urlHandler', url); const routeNames = [Route.Commerce, Route.Inbox, Route.User]; for (const route of routeNames) { if (url.includes(route.toLowerCase())) { diff --git a/src/api/NativeRNIterableAPI.ts b/src/api/NativeRNIterableAPI.ts index 9f5bb5171..005831903 100644 --- a/src/api/NativeRNIterableAPI.ts +++ b/src/api/NativeRNIterableAPI.ts @@ -2,21 +2,28 @@ import type { TurboModule } from 'react-native'; import { TurboModuleRegistry } from 'react-native'; interface EmbeddedMessage { - metadata: { [key: string]: string | number | boolean }; - elements: { - buttons: { - id: string; - title: string | null; - action: { [key: string]: string | number | boolean } | null; - }[]; - body: string | null; - mediaURL: string | null; - mediaUrlCaption: string | null; - defaultAction: { [key: string]: string | number | boolean } | null; - text: { [key: string]: string | number | boolean }[] | null; - title: string | null; + metadata: { + messageId: string; + placementId: number; + campaignId?: number | null; + isProof?: boolean; }; - payload: { [key: string]: string | number | boolean }; + elements: { + buttons?: + | { + id: string; + title?: string | null; + action: { type: string; data?: string } | null; + }[] + | null; + body?: string | null; + mediaURL?: string | null; + mediaUrlCaption?: string | null; + defaultAction?: { type: string; data?: string } | null; + text?: { id: string; text?: string | null; label?: string | null }[] | null; + title?: string | null; + } | null; + payload?: { [key: string]: string | number | boolean | null } | null; } export interface EmbeddedUpdateListener { diff --git a/src/core/enums/IterableActionType.ts b/src/core/enums/IterableActionType.ts new file mode 100644 index 000000000..ee6f60aa7 --- /dev/null +++ b/src/core/enums/IterableActionType.ts @@ -0,0 +1,4 @@ +export enum IterableActionType { + openUrl = 'openUrl', + action = 'action', +} diff --git a/src/core/enums/index.ts b/src/core/enums/index.ts index 52f4eb20d..77725bbc9 100644 --- a/src/core/enums/index.ts +++ b/src/core/enums/index.ts @@ -1,4 +1,5 @@ export * from './IterableActionSource'; +export * from './IterableActionType'; export * from './IterableAuthFailureReason'; export * from './IterableAuthResponseResult'; export * from './IterableDataRegion'; diff --git a/src/core/utils/index.ts b/src/core/utils/index.ts index 8059f89c9..84aea443b 100644 --- a/src/core/utils/index.ts +++ b/src/core/utils/index.ts @@ -1 +1,3 @@ export * from './trackingUtils'; +export * from './isIterableAction'; +export * from './generateUUID'; diff --git a/src/core/utils/isIterableAction.ts b/src/core/utils/isIterableAction.ts new file mode 100644 index 000000000..f7f015aaa --- /dev/null +++ b/src/core/utils/isIterableAction.ts @@ -0,0 +1,2 @@ +export const isIterableAction = (str: string = '') => + str.slice(0, 9) === 'action://'; diff --git a/src/embedded/classes/IterableEmbeddedMessage.ts b/src/embedded/classes/IterableEmbeddedMessage.ts index 418569b49..39533cbbb 100644 --- a/src/embedded/classes/IterableEmbeddedMessage.ts +++ b/src/embedded/classes/IterableEmbeddedMessage.ts @@ -1,7 +1,5 @@ -import { IterableEmbeddedMessageMetadata } from './IterableEmbeddedMessageMetadata'; -import type { IterableEmbeddedMessageMetadataDict } from './IterableEmbeddedMessageMetadata'; -import { IterableEmbeddedMessageElements } from './IterableEmbeddedMessageElements'; import type { IterableEmbeddedMessageElementsDict } from './IterableEmbeddedMessageElements'; +import type { IterableEmbeddedMessageMetadataDict } from './IterableEmbeddedMessageMetadata'; export interface IterableEmbeddedMessageDict { metadata: IterableEmbeddedMessageMetadataDict; @@ -9,43 +7,49 @@ export interface IterableEmbeddedMessageDict { payload?: Record | null; } -export class IterableEmbeddedMessage { - public metadata: IterableEmbeddedMessageMetadata; - public elements?: IterableEmbeddedMessageElements | null = null; - public payload?: Record | null = null; +export interface IterableEmbeddedMessage { + metadata: IterableEmbeddedMessageMetadataDict; + elements?: IterableEmbeddedMessageElementsDict | null; + payload?: Record | null; +} + +// export class IterableEmbeddedMessage { +// public metadata: IterableEmbeddedMessageMetadata; +// public elements?: IterableEmbeddedMessageElements | null = null; +// public payload?: Record | null = null; - constructor( - metadata: IterableEmbeddedMessageMetadata, - options: { - elements?: IterableEmbeddedMessageElements | null; - payload?: Record | null; - } = {} - ) { - const { elements = null, payload = null } = options; - this.metadata = metadata; - this.elements = elements; - this.payload = payload; - } +// constructor( +// metadata: IterableEmbeddedMessageMetadata, +// options: { +// elements?: IterableEmbeddedMessageElements | null; +// payload?: Record | null; +// } = {} +// ) { +// const { elements = null, payload = null } = options; +// this.metadata = metadata; +// this.elements = elements; +// this.payload = payload; +// } - toDict(): IterableEmbeddedMessageDict { - return { - metadata: this.metadata.toDict(), - elements: this.elements ? this.elements.toDict() : null, - payload: this.payload ?? null, - }; - } +// toDict(): IterableEmbeddedMessageDict { +// return { +// metadata: this.metadata.toDict(), +// elements: this.elements ? this.elements.toDict() : null, +// payload: this.payload ?? null, +// }; +// } - static fromDict( - jsonObject: IterableEmbeddedMessageDict - ): IterableEmbeddedMessage { - const metadata = IterableEmbeddedMessageMetadata.fromDict( - jsonObject.metadata - ); - const elements = IterableEmbeddedMessageElements.fromDict( - jsonObject.elements ?? null - ); - const payload = jsonObject.payload ?? null; +// static fromDict( +// jsonObject: IterableEmbeddedMessageDict +// ): IterableEmbeddedMessage { +// const metadata = IterableEmbeddedMessageMetadata.fromDict( +// jsonObject.metadata +// ); +// const elements = IterableEmbeddedMessageElements.fromDict( +// jsonObject.elements ?? null +// ); +// const payload = jsonObject.payload ?? null; - return new IterableEmbeddedMessage(metadata, { elements, payload }); - } -} +// return new IterableEmbeddedMessage(metadata, { elements, payload }); +// } +// } diff --git a/src/embedded/classes/IterableEmbeddedMessageElements.ts b/src/embedded/classes/IterableEmbeddedMessageElements.ts index 2fe4c7159..5e175b43c 100644 --- a/src/embedded/classes/IterableEmbeddedMessageElements.ts +++ b/src/embedded/classes/IterableEmbeddedMessageElements.ts @@ -1,6 +1,6 @@ -import { IterableEmbeddedMessageElementsButton } from './IterableEmbeddedMessageElementsButton'; -import { IterableEmbeddedMessageElementsDefaultAction } from './IterableEmbeddedMessageElementsDefaultAction'; -import { IterableEmbeddedMessageElementsText } from './IterableEmbeddedMessageElementsText'; +import type { IterableEmbeddedMessageElementsButton } from './IterableEmbeddedMessageElementsButton'; +import type { IterableEmbeddedMessageElementsDefaultAction } from './IterableEmbeddedMessageElementsDefaultAction'; +import type { IterableEmbeddedMessageElementsText } from './IterableEmbeddedMessageElementsText'; export interface IterableEmbeddedMessageElementsDict { title?: string | null; @@ -12,62 +12,72 @@ export interface IterableEmbeddedMessageElementsDict { text?: IterableEmbeddedMessageElementsText[] | null; } -export class IterableEmbeddedMessageElements { - public title?: IterableEmbeddedMessageElementsDict['title'] = null; - public body?: IterableEmbeddedMessageElementsDict['body'] = null; - public mediaURL?: IterableEmbeddedMessageElementsDict['mediaURL'] = null; - public mediaUrlCaption?: IterableEmbeddedMessageElementsDict['mediaUrlCaption'] = - null; - public defaultAction?: IterableEmbeddedMessageElementsDict['defaultAction'] = - null; - public buttons?: IterableEmbeddedMessageElementsDict['buttons'] = null; - public text?: IterableEmbeddedMessageElementsDict['text'] = null; +export interface IterableEmbeddedMessageElements { + title?: string | null; + body?: string | null; + mediaURL?: string | null; + mediaUrlCaption?: string | null; + defaultAction?: IterableEmbeddedMessageElementsDefaultAction | null; + buttons?: IterableEmbeddedMessageElementsButton[] | null; + text?: IterableEmbeddedMessageElementsText[] | null; +} - constructor(options: Partial = {}) { - const { - title = null, - body = null, - mediaURL = null, - mediaUrlCaption = null, - defaultAction = null, - buttons = null, - text = null, - } = options; +// export class IterableEmbeddedMessageElements { +// public title?: IterableEmbeddedMessageElementsDict['title'] = null; +// public body?: IterableEmbeddedMessageElementsDict['body'] = null; +// public mediaURL?: IterableEmbeddedMessageElementsDict['mediaURL'] = null; +// public mediaUrlCaption?: IterableEmbeddedMessageElementsDict['mediaUrlCaption'] = +// null; +// public defaultAction?: IterableEmbeddedMessageElementsDict['defaultAction'] = +// null; +// public buttons?: IterableEmbeddedMessageElementsDict['buttons'] = null; +// public text?: IterableEmbeddedMessageElementsDict['text'] = null; - this.title = title; - this.body = body; - this.mediaURL = mediaURL; - this.mediaUrlCaption = mediaUrlCaption; - this.defaultAction = defaultAction; - this.buttons = buttons; - this.text = text; - } +// constructor(options: Partial = {}) { +// const { +// title = null, +// body = null, +// mediaURL = null, +// mediaUrlCaption = null, +// defaultAction = null, +// buttons = null, +// text = null, +// } = options; - toDict(): IterableEmbeddedMessageElementsDict { - return { - title: this.title, - body: this.body, - mediaURL: this.mediaURL, - mediaUrlCaption: this.mediaUrlCaption, - defaultAction: this.defaultAction, - buttons: this.buttons, - text: this.text, - }; - } +// this.title = title; +// this.body = body; +// this.mediaURL = mediaURL; +// this.mediaUrlCaption = mediaUrlCaption; +// this.defaultAction = defaultAction; +// this.buttons = buttons; +// this.text = text; +// } - static fromDict( - jsonObject?: IterableEmbeddedMessageElementsDict | null - ): IterableEmbeddedMessageElements | null { - if (!jsonObject) return null; +// toDict(): IterableEmbeddedMessageElementsDict { +// return { +// title: this.title, +// body: this.body, +// mediaURL: this.mediaURL, +// mediaUrlCaption: this.mediaUrlCaption, +// defaultAction: this.defaultAction, +// buttons: this.buttons, +// text: this.text, +// }; +// } - return new IterableEmbeddedMessageElements({ - title: jsonObject.title ?? null, - body: jsonObject.body ?? null, - mediaURL: jsonObject.mediaURL ?? null, - mediaUrlCaption: jsonObject.mediaUrlCaption ?? null, - defaultAction: jsonObject.defaultAction ?? null, - buttons: jsonObject.buttons ?? null, - text: jsonObject.text ?? null, - }); - } -} +// static fromDict( +// jsonObject?: IterableEmbeddedMessageElementsDict | null +// ): IterableEmbeddedMessageElements | null { +// if (!jsonObject) return null; + +// return new IterableEmbeddedMessageElements({ +// title: jsonObject.title ?? null, +// body: jsonObject.body ?? null, +// mediaURL: jsonObject.mediaURL ?? null, +// mediaUrlCaption: jsonObject.mediaUrlCaption ?? null, +// defaultAction: jsonObject.defaultAction ?? null, +// buttons: jsonObject.buttons ?? null, +// text: jsonObject.text ?? null, +// }); +// } +// } diff --git a/src/embedded/classes/IterableEmbeddedMessageElementsButton.ts b/src/embedded/classes/IterableEmbeddedMessageElementsButton.ts index a79be30a1..d7cd4da85 100644 --- a/src/embedded/classes/IterableEmbeddedMessageElementsButton.ts +++ b/src/embedded/classes/IterableEmbeddedMessageElementsButton.ts @@ -1,4 +1,4 @@ -import { IterableEmbeddedMessageElementsButtonAction } from './IterableEmbeddedMessageElementsButtonAction'; +import type { IterableEmbeddedMessageElementsButtonAction } from './IterableEmbeddedMessageElementsButtonAction'; export interface IterableEmbeddedMessageElementsButtonDict { /** The ID. */ @@ -9,45 +9,54 @@ export interface IterableEmbeddedMessageElementsButtonDict { action?: IterableEmbeddedMessageElementsButtonAction | null; } -export class IterableEmbeddedMessageElementsButton { - public id: string; - public title?: string | null; - public action?: IterableEmbeddedMessageElementsButtonAction | null; +export interface IterableEmbeddedMessageElementsButton { + /** The ID. */ + id: string; + /** The title. */ + title?: string | null; + /** The action. */ + action?: IterableEmbeddedMessageElementsButtonAction | null; +} - constructor( - options: Partial = {} - ) { - const { id = '', title = null, action = null } = options; - this.id = id; - this.title = title; - this.action = action; - } +// export class IterableEmbeddedMessageElementsButton { +// public id: string; +// public title?: string | null; +// public action?: IterableEmbeddedMessageElementsButtonAction | null; - toDict(): IterableEmbeddedMessageElementsButtonDict { - return { - id: this.id, - title: this.title ?? null, - action: this.action ?? null, - }; - } +// constructor( +// options: Partial = {} +// ) { +// const { id = '', title = null, action = null } = options; +// this.id = id; +// this.title = title; +// this.action = action; +// } - static fromDict( - jsonObject: IterableEmbeddedMessageElementsButtonDict - ): IterableEmbeddedMessageElementsButton { - if (!jsonObject?.id) { - throw new Error( - 'id is required when calling IterableEmbeddedMessageElementsButton.fromDict' - ); - } +// toDict(): IterableEmbeddedMessageElementsButtonDict { +// return { +// id: this.id, +// title: this.title ?? null, +// action: this.action ?? null, +// }; +// } - return new IterableEmbeddedMessageElementsButton({ - id: jsonObject.id, - title: jsonObject.title ?? null, - action: jsonObject.action - ? IterableEmbeddedMessageElementsButtonAction.fromDict( - jsonObject.action - ) - : null, - }); - } -} +// static fromDict( +// jsonObject: IterableEmbeddedMessageElementsButtonDict +// ): IterableEmbeddedMessageElementsButton { +// if (!jsonObject?.id) { +// throw new Error( +// 'id is required when calling IterableEmbeddedMessageElementsButton.fromDict' +// ); +// } + +// return new IterableEmbeddedMessageElementsButton({ +// id: jsonObject.id, +// title: jsonObject.title ?? null, +// action: jsonObject.action +// ? IterableEmbeddedMessageElementsButtonAction.fromDict( +// jsonObject.action +// ) +// : null, +// }); +// } +// } diff --git a/src/embedded/classes/IterableEmbeddedMessageElementsButtonAction.ts b/src/embedded/classes/IterableEmbeddedMessageElementsButtonAction.ts index 1af8d0d5f..d708681af 100644 --- a/src/embedded/classes/IterableEmbeddedMessageElementsButtonAction.ts +++ b/src/embedded/classes/IterableEmbeddedMessageElementsButtonAction.ts @@ -5,31 +5,38 @@ export interface IterableEmbeddedMessageElementsButtonActionDict { data?: string; } -export class IterableEmbeddedMessageElementsButtonAction { - public type: string; - public data?: string; +export interface IterableEmbeddedMessageElementsButtonAction { + /** The type. */ + type: string; + /** The data. */ + data?: string; +} - constructor( - options: Partial = {} - ) { - const { type = '', data = '' } = options; - this.type = type; - this.data = data; - } +// export class IterableEmbeddedMessageElementsButtonAction { +// public type: string; +// public data?: string; - toDict(): IterableEmbeddedMessageElementsButtonActionDict { - return { - type: this.type, - data: this.data, - }; - } +// constructor( +// options: Partial = {} +// ) { +// const { type = '', data = '' } = options; +// this.type = type; +// this.data = data; +// } - static fromDict( - jsonObject: IterableEmbeddedMessageElementsButtonActionDict - ): IterableEmbeddedMessageElementsButtonAction { - return new IterableEmbeddedMessageElementsButtonAction({ - type: jsonObject.type, - data: jsonObject.data, - }); - } -} +// toDict(): IterableEmbeddedMessageElementsButtonActionDict { +// return { +// type: this.type, +// data: this.data, +// }; +// } + +// static fromDict( +// jsonObject: IterableEmbeddedMessageElementsButtonActionDict +// ): IterableEmbeddedMessageElementsButtonAction { +// return new IterableEmbeddedMessageElementsButtonAction({ +// type: jsonObject.type, +// data: jsonObject.data, +// }); +// } +// } diff --git a/src/embedded/classes/IterableEmbeddedMessageElementsDefaultAction.ts b/src/embedded/classes/IterableEmbeddedMessageElementsDefaultAction.ts index da16e1afe..adfefad2c 100644 --- a/src/embedded/classes/IterableEmbeddedMessageElementsDefaultAction.ts +++ b/src/embedded/classes/IterableEmbeddedMessageElementsDefaultAction.ts @@ -5,31 +5,38 @@ export interface IterableEmbeddedMessageElementsDefaultActionDict { data?: string; } -export class IterableEmbeddedMessageElementsDefaultAction { - public type: string; - public data?: string; +export interface IterableEmbeddedMessageElementsDefaultAction { + /** The type. */ + type: string; + /** The data. */ + data?: string; +} - constructor( - options: Partial = {} - ) { - const { type = '', data = '' } = options; - this.type = type; - this.data = data; - } +// export class IterableEmbeddedMessageElementsDefaultAction { +// public type: string; +// public data?: string; - toDict(): IterableEmbeddedMessageElementsDefaultActionDict { - return { - type: this.type, - data: this.data, - }; - } +// constructor( +// options: Partial = {} +// ) { +// const { type = '', data = '' } = options; +// this.type = type; +// this.data = data; +// } - static fromDict( - jsonObject: IterableEmbeddedMessageElementsDefaultActionDict - ): IterableEmbeddedMessageElementsDefaultAction { - return new IterableEmbeddedMessageElementsDefaultAction({ - type: jsonObject.type, - data: jsonObject.data, - }); - } -} +// toDict(): IterableEmbeddedMessageElementsDefaultActionDict { +// return { +// type: this.type, +// data: this.data, +// }; +// } + +// static fromDict( +// jsonObject: IterableEmbeddedMessageElementsDefaultActionDict +// ): IterableEmbeddedMessageElementsDefaultAction { +// return new IterableEmbeddedMessageElementsDefaultAction({ +// type: jsonObject.type, +// data: jsonObject.data, +// }); +// } +// } diff --git a/src/embedded/classes/IterableEmbeddedMessageElementsText.ts b/src/embedded/classes/IterableEmbeddedMessageElementsText.ts index 78d5cdf16..54537959a 100644 --- a/src/embedded/classes/IterableEmbeddedMessageElementsText.ts +++ b/src/embedded/classes/IterableEmbeddedMessageElementsText.ts @@ -7,33 +7,42 @@ export interface IterableEmbeddedMessageElementsTextDict { label?: string | null; } -export class IterableEmbeddedMessageElementsText { - public id: string; - public text?: string | null; - public label?: string | null; +export interface IterableEmbeddedMessageElementsText { + /** The ID. */ + id: string; + /** The text. */ + text?: string | null; + /** The label. */ + label?: string | null; +} - constructor(options: Partial = {}) { - const { id = '', text = null, label = null } = options; - this.id = id; - this.text = text; - this.label = label; - } +// export class IterableEmbeddedMessageElementsText { +// public id: string; +// public text?: string | null; +// public label?: string | null; - toDict(): IterableEmbeddedMessageElementsTextDict { - return { - id: this.id, - text: this.text, - label: this.label, - }; - } +// constructor(options: Partial = {}) { +// const { id = '', text = null, label = null } = options; +// this.id = id; +// this.text = text; +// this.label = label; +// } - static fromDict( - jsonObject: IterableEmbeddedMessageElementsTextDict - ): IterableEmbeddedMessageElementsText { - return new IterableEmbeddedMessageElementsText({ - id: jsonObject.id, - text: jsonObject.text, - label: jsonObject.label, - }); - } -} +// toDict(): IterableEmbeddedMessageElementsTextDict { +// return { +// id: this.id, +// text: this.text, +// label: this.label, +// }; +// } + +// static fromDict( +// jsonObject: IterableEmbeddedMessageElementsTextDict +// ): IterableEmbeddedMessageElementsText { +// return new IterableEmbeddedMessageElementsText({ +// id: jsonObject.id, +// text: jsonObject.text, +// label: jsonObject.label, +// }); +// } +// } diff --git a/src/embedded/classes/IterableEmbeddedPlacement.ts b/src/embedded/classes/IterableEmbeddedPlacement.ts index 760da7d87..a1ada3e53 100644 --- a/src/embedded/classes/IterableEmbeddedPlacement.ts +++ b/src/embedded/classes/IterableEmbeddedPlacement.ts @@ -1,4 +1,3 @@ -import { IterableEmbeddedMessage } from './IterableEmbeddedMessage'; import type { IterableEmbeddedMessageDict } from './IterableEmbeddedMessage'; export interface IterableEmbeddedPlacementDict { @@ -6,30 +5,35 @@ export interface IterableEmbeddedPlacementDict { messages: IterableEmbeddedMessageDict[]; } -export class IterableEmbeddedPlacement { - public placementId: number; - public messages: IterableEmbeddedMessage[]; +export interface IterableEmbeddedPlacement { + placementId: number; + messages: IterableEmbeddedMessageDict[]; +} - constructor(placementId: number, messages: IterableEmbeddedMessage[]) { - this.placementId = placementId; - this.messages = messages; - } +// export class IterableEmbeddedPlacement { +// public placementId: number; +// public messages: IterableEmbeddedMessage[]; - toDict(): IterableEmbeddedPlacementDict { - return { - placementId: this.placementId, - messages: this.messages.map((m) => m.toDict()), - }; - } +// constructor(placementId: number, messages: IterableEmbeddedMessage[]) { +// this.placementId = placementId; +// this.messages = messages; +// } - static fromDict( - jsonObject: IterableEmbeddedPlacementDict - ): IterableEmbeddedPlacement { - const placementId = jsonObject.placementId; - const messages = (jsonObject.messages ?? []).map((m) => - IterableEmbeddedMessage.fromDict(m) - ); +// toDict(): IterableEmbeddedPlacementDict { +// return { +// placementId: this.placementId, +// messages: this.messages.map((m) => m.toDict()), +// }; +// } - return new IterableEmbeddedPlacement(placementId, messages); - } -} +// static fromDict( +// jsonObject: IterableEmbeddedPlacementDict +// ): IterableEmbeddedPlacement { +// const placementId = jsonObject.placementId; +// const messages = (jsonObject.messages ?? []).map((m) => +// IterableEmbeddedMessage.fromDict(m) +// ); + +// return new IterableEmbeddedPlacement(placementId, messages); +// } +// } diff --git a/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx b/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx index d8b2434da..08bc1a778 100644 --- a/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx +++ b/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx @@ -13,10 +13,12 @@ import type { IterableEmbeddedComponentProps } from '../IterableEmbeddedViewProp import { getMedia } from '../utils/getMedia'; import { getStyles } from '../utils/getStyles'; import { styles } from './IterableEmbeddedBanner.styles'; +import { runButtonClick } from '../utils/runButtonClick'; export const IterableEmbeddedBanner = ({ config, message, + onButtonClick = () => {}, }: IterableEmbeddedComponentProps) => { const parsedStyles = useMemo(() => { return getStyles(IterableEmbeddedViewType.Banner, config); @@ -25,11 +27,18 @@ export const IterableEmbeddedBanner = ({ return getMedia(IterableEmbeddedViewType.Banner, message); }, [message]); console.log(`🚀 > IterableEmbeddedView > media:`, media); - const onButtonPress = useCallback( + const handleButtonClick = useCallback( (button: IterableEmbeddedMessageElementsButton) => { - console.log('CLICK', button); + console.group('handleButtonClick'); + console.log('message', message); + console.log('button', button); + console.log('button.action', button.action); + + onButtonClick(button); + runButtonClick(button, message); + console.groupEnd(); }, - [] + [onButtonClick, message] ); const buttons = message.elements?.buttons ?? []; @@ -83,7 +92,7 @@ export const IterableEmbeddedBanner = ({ return ( onButtonPress(button)} + onPress={() => handleButtonClick(button)} key={button.id} > { - console.log(`🚀 > IterableEmbeddedView > config:`, config); - console.log(`🚀 > IterableEmbeddedView > message:`, message); console.log(`🚀 > IterableEmbeddedView > viewType:`, viewType); const Cmp = useMemo(() => { @@ -33,5 +30,5 @@ export const IterableEmbeddedView = ({ } }, [viewType]); - return Cmp ? : null; + return Cmp ? : null; }; diff --git a/src/embedded/components/IterableEmbeddedViewProps.ts b/src/embedded/components/IterableEmbeddedViewProps.ts index f7c8bc8c3..854f2c8fd 100644 --- a/src/embedded/components/IterableEmbeddedViewProps.ts +++ b/src/embedded/components/IterableEmbeddedViewProps.ts @@ -1,7 +1,9 @@ import type { IterableEmbeddedMessage } from '../classes/IterableEmbeddedMessage'; +import type { IterableEmbeddedMessageElementsButton } from '../classes/IterableEmbeddedMessageElementsButton'; import type { IterableEmbeddedViewConfig } from '../classes/IterableEmbeddedViewConfig'; export interface IterableEmbeddedComponentProps { message: IterableEmbeddedMessage; config?: IterableEmbeddedViewConfig | null; + onButtonClick?: (button: IterableEmbeddedMessageElementsButton) => void; } diff --git a/src/embedded/components/utils/runButtonClick.ts b/src/embedded/components/utils/runButtonClick.ts new file mode 100644 index 000000000..1b227ed0b --- /dev/null +++ b/src/embedded/components/utils/runButtonClick.ts @@ -0,0 +1,43 @@ +import { Iterable } from '../../../core/classes/Iterable'; +import { IterableAction } from '../../../core/classes/IterableAction'; +import { IterableActionContext } from '../../../core/classes/IterableActionContext'; +import { IterableActionSource } from '../../../core/enums'; +import { isIterableAction } from '../../../core/utils'; +import type { IterableEmbeddedMessage } from '../../classes/IterableEmbeddedMessage'; +import type { IterableEmbeddedMessageElementsButton } from '../../classes/IterableEmbeddedMessageElementsButton'; + +const getUrl = (button: IterableEmbeddedMessageElementsButton) => { + const { data, type: actionType } = button.action ?? {}; + return data && data?.length > 0 ? data : actionType; +}; + +export const runButtonClick = ( + button: IterableEmbeddedMessageElementsButton, + message: IterableEmbeddedMessage +) => { + const url = getUrl(button); + const source = IterableActionSource.embedded; + + Iterable.embeddedManager.trackClick(message, button.id, url ?? null); + + if (isIterableAction(url)) { + const action = new IterableAction( + (url ?? '').replace('action://', ''), + button?.action?.data, + '' + ); + + const context = new IterableActionContext(action, source); + + if (Iterable.savedConfig.customActionHandler) { + Iterable.savedConfig.customActionHandler(action, context); + } + } else { + const action = new IterableAction('openUrl', url, ''); + const context = new IterableActionContext(action, source); + + if (Iterable.savedConfig.urlHandler) { + Iterable.savedConfig.urlHandler(url ?? '', context); + } + } +}; diff --git a/src/index.tsx b/src/index.tsx index e74779e6a..aa3a489ca 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -14,6 +14,7 @@ export { } from './core/classes'; export { IterableActionSource, + IterableActionType, IterableAuthFailureReason, IterableAuthResponseResult, IterableDataRegion, @@ -59,7 +60,7 @@ export { type IterableInboxProps, type IterableInboxRowViewModel, } from './inbox'; -export { IterableEmbeddedMessage } from './embedded/classes/IterableEmbeddedMessage'; +export { type IterableEmbeddedMessage } from './embedded/classes/IterableEmbeddedMessage'; export { IterableEmbeddedView } from './embedded/components'; export { IterableEmbeddedViewType } from './embedded/enums'; export { IterableEmbeddedViewConfig } from './embedded/classes/IterableEmbeddedViewConfig'; From 14922e0cc8200fc7a6b214111c37d83e34292054 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Wed, 8 Oct 2025 16:18:03 -0700 Subject: [PATCH 33/45] feat: fix error on embedded button click --- .../reactnative/RNIterableAPIModuleImpl.java | 18 ++++++++++++++---- .../iterable/reactnative/Serialization.java | 10 ++++++++++ .../newarch/java/com/RNIterableAPIModule.java | 4 ++-- .../oldarch/java/com/RNIterableAPIModule.java | 4 ++-- 4 files changed, 28 insertions(+), 8 deletions(-) diff --git a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java index 41c1aea9b..b59fcc13d 100644 --- a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java +++ b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java @@ -730,13 +730,23 @@ public void pauseEmbeddedImpression(String messageId) { IterableApi.getInstance().getEmbeddedManager().getEmbeddedSessionManager().pauseImpression(messageId); } - public void handleEmbeddedClick(IterableEmbeddedMessage message, String buttonId, String clickedUrl) { - IterableApi.getInstance().getEmbeddedManager().handleEmbeddedClick(message, buttonId, clickedUrl); + public void handleEmbeddedClick(ReadableMap messageMap, String buttonId, String clickedUrl) { + IterableEmbeddedMessage message = Serialization.embeddedMessageFromReadableMap(messageMap); + if (message != null) { + IterableApi.getInstance().getEmbeddedManager().handleEmbeddedClick(message, buttonId, clickedUrl); + } else { + IterableLogger.e(TAG, "Failed to convert message map to IterableEmbeddedMessage"); + } } - public void trackEmbeddedClick(IterableEmbeddedMessage message, String buttonId, String clickedUrl) { + public void trackEmbeddedClick(ReadableMap messageMap, String buttonId, String clickedUrl) { IterableLogger.d(TAG, "trackEmbeddedClick: buttonId: " + buttonId + " clickedUrl: " + clickedUrl); - IterableApi.getInstance().trackEmbeddedClick(message, buttonId, clickedUrl); + IterableEmbeddedMessage message = Serialization.embeddedMessageFromReadableMap(messageMap); + if (message != null) { + IterableApi.getInstance().trackEmbeddedClick(message, buttonId, clickedUrl); + } else { + IterableLogger.e(TAG, "Failed to convert message map to IterableEmbeddedMessage"); + } } @Override diff --git a/android/src/main/java/com/iterable/reactnative/Serialization.java b/android/src/main/java/com/iterable/reactnative/Serialization.java index 2f0c4e1ca..61adbd9e6 100644 --- a/android/src/main/java/com/iterable/reactnative/Serialization.java +++ b/android/src/main/java/com/iterable/reactnative/Serialization.java @@ -149,6 +149,16 @@ static JSONArray serializeEmbeddedMessages(List embedde return embeddedMessagesJson; } + static IterableEmbeddedMessage embeddedMessageFromReadableMap(ReadableMap messageMap) { + try { + JSONObject messageJson = convertMapToJson(messageMap); + return IterableEmbeddedMessage.Companion.fromJSONObject(messageJson); + } catch (JSONException e) { + IterableLogger.e(TAG, "Failed to convert ReadableMap to IterableEmbeddedMessage: " + e.getLocalizedMessage()); + return null; + } + } + static IterableConfig.Builder getConfigFromReadableMap(ReadableMap iterableContextMap) { try { JSONObject iterableContextJSON = convertMapToJson(iterableContextMap); diff --git a/android/src/newarch/java/com/RNIterableAPIModule.java b/android/src/newarch/java/com/RNIterableAPIModule.java index 043db01f6..cd2243245 100644 --- a/android/src/newarch/java/com/RNIterableAPIModule.java +++ b/android/src/newarch/java/com/RNIterableAPIModule.java @@ -261,12 +261,12 @@ public void pauseEmbeddedImpression(String messageId) { } @Override - public void handleEmbeddedClick(IterableEmbeddedMessage message, String buttonId, String clickedUrl) { + public void handleEmbeddedClick(ReadableMap message, String buttonId, String clickedUrl) { moduleImpl.handleEmbeddedClick(message, buttonId, clickedUrl); } @Override - public void trackEmbeddedClick(IterableEmbeddedMessage message, String buttonId, String clickedUrl) { + public void trackEmbeddedClick(ReadableMap message, String buttonId, String clickedUrl) { moduleImpl.trackEmbeddedClick(message, buttonId, clickedUrl); } diff --git a/android/src/oldarch/java/com/RNIterableAPIModule.java b/android/src/oldarch/java/com/RNIterableAPIModule.java index e05a09619..5b0a8f6ac 100644 --- a/android/src/oldarch/java/com/RNIterableAPIModule.java +++ b/android/src/oldarch/java/com/RNIterableAPIModule.java @@ -269,12 +269,12 @@ public void pauseEmbeddedImpression(String messageId) { } @ReactMethod - public void handleEmbeddedClick(IterableEmbeddedMessage message, String buttonId, String clickedUrl) { + public void handleEmbeddedClick(ReadableMap message, String buttonId, String clickedUrl) { moduleImpl.handleEmbeddedClick(message, buttonId, clickedUrl); } @ReactMethod - public void trackEmbeddedClick(IterableEmbeddedMessage message, String buttonId, String clickedUrl) { + public void trackEmbeddedClick(ReadableMap message, String buttonId, String clickedUrl) { moduleImpl.trackEmbeddedClick(message, buttonId, clickedUrl); } From 297ae51304c68dc2f8f52de26cf2d6767e685762 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Wed, 8 Oct 2025 17:05:22 -0700 Subject: [PATCH 34/45] feat: improve Embedded component with enhanced message fetching and styling updates --- example/src/components/Embedded/Embedded.tsx | 64 ++++++++++--------- src/api/NativeRNIterableAPI.ts | 2 +- .../IterableEmbeddedMessageElements.ts | 4 +- .../IterableEmbeddedBanner.styles.tsx | 32 ++++++---- .../IterableEmbeddedBanner.tsx | 27 ++++---- src/embedded/components/utils/getMedia.ts | 11 ++-- .../constants/embeddedViewDefaults.ts | 13 ++++ 7 files changed, 94 insertions(+), 59 deletions(-) diff --git a/example/src/components/Embedded/Embedded.tsx b/example/src/components/Embedded/Embedded.tsx index 058f1f126..b9f1e441a 100644 --- a/example/src/components/Embedded/Embedded.tsx +++ b/example/src/components/Embedded/Embedded.tsx @@ -18,15 +18,45 @@ export const Embedded = () => { const [placementIds, setPlacementIds] = useState([]); const [messages, setMessages] = useState([]); + const getPlacementIds = useCallback(() => { + Iterable.embeddedManager.getPlacementIds().then((ids: unknown) => { + console.log(ids); + setPlacementIds(ids as number[]); + }); + }, []); + + const getEmbeddedMessages = useCallback(() => { + Iterable.embeddedManager.getMessages(placementIds).then((messageList) => { + console.log(messageList); + setMessages(messageList as IterableEmbeddedMessage[]); + }); + }, [placementIds]); + + const sync = useCallback(() => { + Iterable.embeddedManager.syncMessages(); + }, []); + + useEffect(() => { + if (isLoggedIn) { + getPlacementIds(); + } + }, [isLoggedIn, getPlacementIds]); + useEffect(() => { if (isFocused) { Iterable.embeddedManager.startSession(); Iterable.embeddedManager.syncMessages(); - Iterable.embeddedManager.getMessages(placementIds).then((messageList) => { - console.log(messageList); - setMessages(messageList as IterableEmbeddedMessage[]); + Iterable.embeddedManager.getPlacementIds().then((ids: unknown) => { + console.log(ids); + setPlacementIds(ids as number[]); + Iterable.embeddedManager + .getMessages(placementIds) + .then((messageList) => { + console.log(messageList); + setMessages(messageList as IterableEmbeddedMessage[]); + setHasSession(true); + }); }); - setHasSession(true); } else { if (hasSession) { Iterable.embeddedManager.endSession(); @@ -36,32 +66,6 @@ export const Embedded = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [isFocused]); - useEffect(() => { - if (isLoggedIn) { - Iterable.embeddedManager.getPlacementIds().then((ids: unknown) => { - console.log(`🚀 > User > ids:`, ids); - setPlacementIds(ids as number[]); - }); - } - }, [isLoggedIn]); - - const getEmbeddedMessages = useCallback(() => { - Iterable.embeddedManager.getMessages(placementIds).then((messageList) => { - console.log(messageList); - setMessages(messageList as IterableEmbeddedMessage[]); - }); - }, [placementIds]); - - const getPlacementIds = useCallback(() => { - Iterable.embeddedManager.getPlacementIds().then((ids: unknown) => { - console.log(ids); - }); - }, []); - - const sync = useCallback(() => { - Iterable.embeddedManager.syncMessages(); - }, []); - return ( Has session: {hasSession.toString()} diff --git a/src/api/NativeRNIterableAPI.ts b/src/api/NativeRNIterableAPI.ts index 005831903..3b0c8835e 100644 --- a/src/api/NativeRNIterableAPI.ts +++ b/src/api/NativeRNIterableAPI.ts @@ -17,7 +17,7 @@ interface EmbeddedMessage { }[] | null; body?: string | null; - mediaURL?: string | null; + mediaUrl?: string | null; mediaUrlCaption?: string | null; defaultAction?: { type: string; data?: string } | null; text?: { id: string; text?: string | null; label?: string | null }[] | null; diff --git a/src/embedded/classes/IterableEmbeddedMessageElements.ts b/src/embedded/classes/IterableEmbeddedMessageElements.ts index 5e175b43c..cfe46fc3e 100644 --- a/src/embedded/classes/IterableEmbeddedMessageElements.ts +++ b/src/embedded/classes/IterableEmbeddedMessageElements.ts @@ -5,7 +5,7 @@ import type { IterableEmbeddedMessageElementsText } from './IterableEmbeddedMess export interface IterableEmbeddedMessageElementsDict { title?: string | null; body?: string | null; - mediaURL?: string | null; + mediaUrl?: string | null; mediaUrlCaption?: string | null; defaultAction?: IterableEmbeddedMessageElementsDefaultAction | null; buttons?: IterableEmbeddedMessageElementsButton[] | null; @@ -15,7 +15,7 @@ export interface IterableEmbeddedMessageElementsDict { export interface IterableEmbeddedMessageElements { title?: string | null; body?: string | null; - mediaURL?: string | null; + mediaUrl?: string | null; mediaUrlCaption?: string | null; defaultAction?: IterableEmbeddedMessageElementsDefaultAction | null; buttons?: IterableEmbeddedMessageElementsButton[] | null; diff --git a/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.styles.tsx b/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.styles.tsx index 458ce7a7b..9f0513fe1 100644 --- a/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.styles.tsx +++ b/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.styles.tsx @@ -1,4 +1,8 @@ import { StyleSheet } from 'react-native'; +import { + embeddedMediaImageBorderColors, + embeddedMediaImageBackgroundColors, +} from '../../constants/embeddedViewDefaults'; export const styles = StyleSheet.create({ body: { @@ -10,22 +14,17 @@ export const styles = StyleSheet.create({ bodyContainer: { alignItems: 'center', alignSelf: 'stretch', - // backgroundColor: 'green', display: 'flex', flexDirection: 'row', - // gap: 16, + paddingTop: 4, }, button: { - // backgroundColor: 'blue', borderRadius: 12, gap: 8, - // height: 32, - // display: 'flex', }, buttonContainer: { alignItems: 'flex-start', alignSelf: 'stretch', - // backgroundColor: 'red', display: 'flex', flexDirection: 'row', gap: 12, @@ -51,14 +50,24 @@ export const styles = StyleSheet.create({ width: '100%', }, mediaContainer: { + alignItems: 'flex-start', + alignSelf: 'stretch', display: 'flex', - flexDirection: 'column', - gap: 16, + flexDirection: 'row', + }, + mediaImage: { + backgroundColor: embeddedMediaImageBackgroundColors.banner, + borderColor: embeddedMediaImageBorderColors.banner, + borderRadius: 6, + borderStyle: 'solid', + borderWidth: 1, + height: 70, + paddingHorizontal: 0, + paddingVertical: 0, + width: 70, }, textContainer: { alignSelf: 'center', - // alignItems: 'flex-start', - // backgroundColor: 'red', display: 'flex', flexDirection: 'column', flexGrow: 1, @@ -69,6 +78,7 @@ export const styles = StyleSheet.create({ title: { fontSize: 16, fontWeight: '700', - lineHeight: 24, + lineHeight: 16, + paddingBottom: 4, }, }); diff --git a/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx b/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx index 08bc1a778..cf9dd4097 100644 --- a/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx +++ b/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx @@ -1,5 +1,6 @@ import { useCallback, useMemo } from 'react'; import { + Image, Text, TouchableOpacity, View, @@ -15,6 +16,10 @@ import { getStyles } from '../utils/getStyles'; import { styles } from './IterableEmbeddedBanner.styles'; import { runButtonClick } from '../utils/runButtonClick'; +/** + * TODO: figure out how default action works. + */ + export const IterableEmbeddedBanner = ({ config, message, @@ -26,17 +31,10 @@ export const IterableEmbeddedBanner = ({ const media = useMemo(() => { return getMedia(IterableEmbeddedViewType.Banner, message); }, [message]); - console.log(`🚀 > IterableEmbeddedView > media:`, media); const handleButtonClick = useCallback( (button: IterableEmbeddedMessageElementsButton) => { - console.group('handleButtonClick'); - console.log('message', message); - console.log('button', button); - console.log('button.action', button.action); - onButtonClick(button); runButtonClick(button, message); - console.groupEnd(); }, [onButtonClick, message] ); @@ -55,7 +53,8 @@ export const IterableEmbeddedBanner = ({ } as ViewStyle, ]} > - + {/* eslint-disable-next-line react-native/no-inline-styles */} + - - {/* */} - + {media.shouldShow && ( + + {media.caption + + )} {buttons.length > 0 && ( diff --git a/src/embedded/components/utils/getMedia.ts b/src/embedded/components/utils/getMedia.ts index 2d417a409..fd4ee1665 100644 --- a/src/embedded/components/utils/getMedia.ts +++ b/src/embedded/components/utils/getMedia.ts @@ -1,8 +1,11 @@ -import type { IterableEmbeddedMessage } from "../../classes/IterableEmbeddedMessage"; -import { IterableEmbeddedViewType } from "../../enums/IterableEmbeddedViewType"; +import type { IterableEmbeddedMessage } from '../../classes/IterableEmbeddedMessage'; +import { IterableEmbeddedViewType } from '../../enums/IterableEmbeddedViewType'; -export const getMedia = (viewType: IterableEmbeddedViewType, message: IterableEmbeddedMessage) => { - const url = message.elements?.mediaURL ?? null; +export const getMedia = ( + viewType: IterableEmbeddedViewType, + message: IterableEmbeddedMessage +) => { + const url = message.elements?.mediaUrl ?? null; const caption = message.elements?.mediaUrlCaption ?? null; const shouldShow = !!url && diff --git a/src/embedded/constants/embeddedViewDefaults.ts b/src/embedded/constants/embeddedViewDefaults.ts index 051968422..95139fb25 100644 --- a/src/embedded/constants/embeddedViewDefaults.ts +++ b/src/embedded/constants/embeddedViewDefaults.ts @@ -49,6 +49,18 @@ export const embeddedBodyTextColors = { export const defaultBorderWidth = 1; export const defaultBorderCornerRadius = 8.0; +export const embeddedMediaImageBorderColors = { + notification: '#E0DEDF', + card: '#E0DEDF', + banner: '#E0DEDF', +}; + +export const embeddedMediaImageBackgroundColors = { + notification: '#F5F4F4', + card: '#F5F4F4', + banner: '#F5F4F4', +}; + export const embeddedColors = { background: embeddedBackgroundColors, border: embeddedBorderColors, @@ -58,4 +70,5 @@ export const embeddedColors = { secondaryBtnText: embeddedSecondaryBtnTextColors, titleText: embeddedTitleTextColors, bodyText: embeddedBodyTextColors, + mediaImageBorder: embeddedMediaImageBorderColors, }; From 13fc4a5e7bd89bc15bf6e1c7ffa4fe3b15ffa1eb Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Wed, 8 Oct 2025 17:26:21 -0700 Subject: [PATCH 35/45] feat: implemented IterableEmbeddedNotification --- example/src/components/Embedded/Embedded.tsx | 2 +- .../IterableEmbeddedBanner.styles.tsx | 2 +- .../IterableEmbeddedNotification.styles.ts | 54 +++++++++++ .../IterableEmbeddedNotification.tsx | 96 +++++++++++++++++-- .../constants/embeddedViewDefaults.ts | 22 ++--- 5 files changed, 156 insertions(+), 20 deletions(-) create mode 100644 src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.styles.ts diff --git a/example/src/components/Embedded/Embedded.tsx b/example/src/components/Embedded/Embedded.tsx index b9f1e441a..dd66f4e0f 100644 --- a/example/src/components/Embedded/Embedded.tsx +++ b/example/src/components/Embedded/Embedded.tsx @@ -86,7 +86,7 @@ export const Embedded = () => { return ( ); diff --git a/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.styles.tsx b/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.styles.tsx index 9f0513fe1..95faf6d73 100644 --- a/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.styles.tsx +++ b/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.styles.tsx @@ -19,7 +19,7 @@ export const styles = StyleSheet.create({ paddingTop: 4, }, button: { - borderRadius: 12, + borderRadius: 32, gap: 8, }, buttonContainer: { diff --git a/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.styles.ts b/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.styles.ts new file mode 100644 index 000000000..923df66fc --- /dev/null +++ b/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.styles.ts @@ -0,0 +1,54 @@ +import { StyleSheet } from 'react-native'; + +export const styles = StyleSheet.create({ + body: { + alignSelf: 'stretch', + fontSize: 14, + fontWeight: '400', + lineHeight: 20, + }, + bodyContainer: { + display: 'flex', + flexDirection: 'column', + flexGrow: 1, + flexShrink: 1, + gap: 4, + width: '100%', + }, + button: { + borderRadius: 32, + gap: 8, + paddingHorizontal: 12, + paddingVertical: 8, + }, + buttonContainer: { + alignItems: 'flex-start', + alignSelf: 'stretch', + display: 'flex', + flexDirection: 'row', + gap: 12, + width: '100%', + }, + buttonText: { + fontSize: 14, + fontWeight: '700', + lineHeight: 20, + }, + container: { + alignItems: 'flex-start', + borderStyle: 'solid', + boxShadow: + '0 1px 1px 0 rgba(0, 0, 0, 0.06), 0 0 2px 0 rgba(0, 0, 0, 0.06), 0 0 1px 0 rgba(0, 0, 0, 0.08)', + display: 'flex', + flexDirection: 'column', + gap: 8, + justifyContent: 'center', + padding: 16, + width: '100%', + }, + title: { + fontSize: 16, + fontWeight: '700', + lineHeight: 24, + }, +}); diff --git a/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.tsx b/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.tsx index b68c08ee2..c2bd18ca4 100644 --- a/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.tsx +++ b/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.tsx @@ -1,14 +1,96 @@ -import { Text, View } from 'react-native'; +import { + Text, + TouchableOpacity, + View, + type TextStyle, + type ViewStyle, +} from 'react-native'; +import { useCallback, useMemo } from 'react'; +import type { IterableEmbeddedMessageElementsButton } from '../../classes/IterableEmbeddedMessageElementsButton'; +import { IterableEmbeddedViewType } from '../../enums'; import type { IterableEmbeddedComponentProps } from '../IterableEmbeddedViewProps'; +import { getStyles } from '../utils/getStyles'; +import { runButtonClick } from '../utils/runButtonClick'; +import { styles } from './IterableEmbeddedNotification.styles'; + +export const IterableEmbeddedNotification = ({ + config, + message, + onButtonClick = () => {}, +}: IterableEmbeddedComponentProps) => { + const parsedStyles = useMemo(() => { + return getStyles(IterableEmbeddedViewType.Notification, config); + }, [config]); + + const handleButtonClick = useCallback( + (button: IterableEmbeddedMessageElementsButton) => { + onButtonClick(button); + runButtonClick(button, message); + }, + [onButtonClick, message] + ); + + const buttons = message.elements?.buttons ?? []; -export const IterableEmbeddedNotification = ( - props: IterableEmbeddedComponentProps -) => { - console.log(`🚀 > IterableEmbeddedNotification > props:`, props); return ( - - IterableEmbeddedNotification + + {} + + + {message.elements?.title} + + + {message.elements?.body} + + + {buttons.length > 0 && ( + + {buttons.map((button, index) => { + const backgroundColor = + index === 0 + ? parsedStyles.primaryBtnBackgroundColor + : parsedStyles.secondaryBtnBackgroundColor; + const textColor = + index === 0 + ? parsedStyles.primaryBtnTextColor + : parsedStyles.secondaryBtnTextColor; + return ( + handleButtonClick(button)} + key={button.id} + > + + {button.title} + + + ); + })} + + )} ); }; diff --git a/src/embedded/constants/embeddedViewDefaults.ts b/src/embedded/constants/embeddedViewDefaults.ts index 95139fb25..e0e1a601a 100644 --- a/src/embedded/constants/embeddedViewDefaults.ts +++ b/src/embedded/constants/embeddedViewDefaults.ts @@ -1,47 +1,47 @@ export const embeddedBackgroundColors = { - notification: '#ff121212', + notification: '#ffffff', card: '#ffffff', banner: '#ffffff', }; export const embeddedBorderColors = { - notification: '#ff2a2a2a', + notification: '#E0DEDF', card: '#ffe0e0e0', banner: '#E0DEDF', }; export const embeddedPrimaryBtnBackgroundColors = { - notification: '#ffffffff', - card: '#ffffffff', + notification: '#6A266D', + card: '#ffffff', banner: '#6A266D', }; export const embeddedPrimaryBtnTextColors = { - notification: '#ff121212', + notification: '#ffffff', card: '#ff1a73e8', - banner: '#ffffffff', + banner: '#ffffff', }; export const embeddedSecondaryBtnBackgroundColors = { - notification: '#ff121212', - card: '#ffffffff', + notification: 'transparent', + card: '#ffffff', banner: 'transparent', }; export const embeddedSecondaryBtnTextColors = { - notification: '#ff121212', + notification: '#79347F', card: '#ff1a73e8', banner: '#79347F', }; export const embeddedTitleTextColors = { - notification: '#ffffffff', + notification: '#3D3A3B', card: '#ff111111', banner: '#3D3A3B', }; export const embeddedBodyTextColors = { - notification: '#ffffffff', + notification: '#787174', card: '#ff444444', banner: '#787174', }; From 56d8dfc6fe642811f421bccf2bc6d6c97958307b Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Wed, 8 Oct 2025 19:30:36 -0700 Subject: [PATCH 36/45] feat: initial card view --- example/src/components/Embedded/Embedded.tsx | 2 +- .../IterableEmbeddedCard.styles.ts | 74 ++++++++++++++++ .../IterableEmbeddedCard.tsx | 85 +++++++++++++++++-- 3 files changed, 155 insertions(+), 6 deletions(-) diff --git a/example/src/components/Embedded/Embedded.tsx b/example/src/components/Embedded/Embedded.tsx index dd66f4e0f..e71c5388c 100644 --- a/example/src/components/Embedded/Embedded.tsx +++ b/example/src/components/Embedded/Embedded.tsx @@ -86,7 +86,7 @@ export const Embedded = () => { return ( ); diff --git a/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.styles.ts b/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.styles.ts index 066075bbb..6742d3294 100644 --- a/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.styles.ts +++ b/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.styles.ts @@ -1,10 +1,84 @@ import { StyleSheet } from 'react-native'; +import { + embeddedMediaImageBorderColors, + embeddedMediaImageBackgroundColors, +} from '../../constants/embeddedViewDefaults'; export const styles = StyleSheet.create({ + body: { + alignSelf: 'stretch', + fontSize: 14, + fontWeight: '400', + lineHeight: 20, + }, + bodyContainer: { + alignItems: 'center', + alignSelf: 'stretch', + display: 'flex', + flexDirection: 'row', + paddingTop: 4, + }, + button: { + borderRadius: 32, + gap: 8, + }, + buttonContainer: { + alignItems: 'flex-start', + alignSelf: 'stretch', + display: 'flex', + flexDirection: 'row', + gap: 12, + width: '100%', + }, + buttonText: { + fontSize: 14, + fontWeight: '400', + lineHeight: 20, + paddingHorizontal: 12, + paddingVertical: 8, + }, container: { + alignItems: 'flex-start', borderStyle: 'solid', boxShadow: '0 1px 1px 0 rgba(0, 0, 0, 0.06), 0 0 2px 0 rgba(0, 0, 0, 0.06), 0 0 1px 0 rgba(0, 0, 0, 0.08)', + display: 'flex', + flexDirection: 'column', + gap: 16, + justifyContent: 'center', + padding: 16, width: '100%', }, + mediaContainer: { + alignItems: 'flex-start', + alignSelf: 'stretch', + display: 'flex', + flexDirection: 'row', + }, + mediaImage: { + backgroundColor: embeddedMediaImageBackgroundColors.card, + borderColor: embeddedMediaImageBorderColors.card, + borderRadius: 6, + borderStyle: 'solid', + borderWidth: 1, + height: 70, + paddingHorizontal: 0, + paddingVertical: 0, + width: 70, + }, + textContainer: { + alignSelf: 'center', + display: 'flex', + flexDirection: 'column', + flexGrow: 1, + flexShrink: 1, + gap: 4, + width: '100%', + }, + title: { + fontSize: 16, + fontWeight: '700', + lineHeight: 16, + paddingBottom: 4, + }, }); diff --git a/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx b/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx index 8efed5c03..f812db630 100644 --- a/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx +++ b/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx @@ -1,24 +1,41 @@ -import { Text, View, type ViewStyle } from 'react-native'; -import { useMemo } from 'react'; +import { + Image, + Text, + TouchableOpacity, + View, + type TextStyle, + type ViewStyle, +} from 'react-native'; +import { useCallback, useMemo } from 'react'; import type { IterableEmbeddedComponentProps } from '../IterableEmbeddedViewProps'; import { IterableEmbeddedViewType } from '../../enums'; import { getStyles } from '../utils/getStyles'; import { getMedia } from '../utils/getMedia'; import { styles } from './IterableEmbeddedCard.styles'; +import type { IterableEmbeddedMessageElementsButton } from '../../classes/IterableEmbeddedMessageElementsButton'; +import { runButtonClick } from '../utils/runButtonClick'; export const IterableEmbeddedCard = ({ config, message, + onButtonClick = () => {}, }: IterableEmbeddedComponentProps) => { const parsedStyles = useMemo(() => { return getStyles(IterableEmbeddedViewType.Card, config); }, [config]); - console.log(`🚀 > IterableEmbeddedView > parsedStyles:`, parsedStyles); const media = useMemo(() => { return getMedia(IterableEmbeddedViewType.Card, message); }, [message]); - console.log(`🚀 > IterableEmbeddedView > media:`, media); + const handleButtonClick = useCallback( + (button: IterableEmbeddedMessageElementsButton) => { + onButtonClick(button); + runButtonClick(button, message); + }, + [onButtonClick, message] + ); + + const buttons = message.elements?.buttons ?? []; return ( - IterableEmbeddedCard + {media.shouldShow && ( + + {media.caption + + )} + + + + {message.elements?.title} + + + {message.elements?.body} + + + {buttons.length > 0 && ( + + {buttons.map((button, index) => { + const backgroundColor = + index === 0 + ? parsedStyles.primaryBtnBackgroundColor + : parsedStyles.secondaryBtnBackgroundColor; + const textColor = + index === 0 + ? parsedStyles.primaryBtnTextColor + : parsedStyles.secondaryBtnTextColor; + return ( + handleButtonClick(button)} + key={button.id} + > + + {button.title} + + + ); + })} + + )} + ); }; From f9b22b781c43531db542754ea2dcba1d4c693403 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Wed, 8 Oct 2025 20:47:04 -0700 Subject: [PATCH 37/45] feat: completed styles for IterableEmbeddedCard --- .../components/Embedded/Embedded.styles.ts | 11 ++- example/src/components/Embedded/Embedded.tsx | 52 ++++++++------ src/core/enums/IterableRetryBackoff.ts | 1 + src/core/images/logo-grey.png | Bin 0 -> 3986 bytes .../IterableEmbeddedCard.styles.ts | 56 ++++++++------- .../IterableEmbeddedCard.tsx | 28 +++++--- ...{getDefaultColor.ts => getDefaultStyle.ts} | 2 +- src/embedded/components/utils/getStyles.ts | 66 ++++-------------- .../constants/embeddedViewDefaults.ts | 31 +++++--- 9 files changed, 125 insertions(+), 122 deletions(-) create mode 100644 src/core/images/logo-grey.png rename src/embedded/components/utils/{getDefaultColor.ts => getDefaultStyle.ts} (93%) diff --git a/example/src/components/Embedded/Embedded.styles.ts b/example/src/components/Embedded/Embedded.styles.ts index 3441caf53..9dc6aea47 100644 --- a/example/src/components/Embedded/Embedded.styles.ts +++ b/example/src/components/Embedded/Embedded.styles.ts @@ -4,9 +4,18 @@ import { button, buttonText, container, hr } from '../../constants'; const styles = StyleSheet.create({ button, buttonText, - container, + container: { ...container, paddingHorizontal: 0 }, + embeddedSection: { + display: 'flex', + flexDirection: 'column', + gap: 16, + paddingHorizontal: 16, + }, hr, text: { textAlign: 'center' }, + utilitySection: { + paddingHorizontal: 16, + }, }); export default styles; diff --git a/example/src/components/Embedded/Embedded.tsx b/example/src/components/Embedded/Embedded.tsx index e71c5388c..e8506111a 100644 --- a/example/src/components/Embedded/Embedded.tsx +++ b/example/src/components/Embedded/Embedded.tsx @@ -6,7 +6,7 @@ import { } from '@iterable/react-native-sdk'; import { useIsFocused } from '@react-navigation/native'; import { useCallback, useEffect, useState } from 'react'; -import { Text, TouchableOpacity, View } from 'react-native'; +import { Text, TouchableOpacity, View, ScrollView } from 'react-native'; import { useIterableApp } from '../../hooks'; import styles from './Embedded.styles'; @@ -68,29 +68,35 @@ export const Embedded = () => { return ( - Has session: {hasSession.toString()} - - Placement ids: [{placementIds.join(', ')}] - - - Get embedded messages - - - Get placement ids - - - Sync - + + Has session: {hasSession.toString()} + + Placement ids: [{placementIds.join(', ')}] + + + Get embedded messages + + + Get placement ids + + + Sync + + - {messages.map((message) => { - return ( - - ); - })} + + + {messages.map((message) => { + return ( + + ); + })} + + ); }; diff --git a/src/core/enums/IterableRetryBackoff.ts b/src/core/enums/IterableRetryBackoff.ts index 4afcf9046..e37dd472d 100644 --- a/src/core/enums/IterableRetryBackoff.ts +++ b/src/core/enums/IterableRetryBackoff.ts @@ -1,3 +1,4 @@ +/* eslint-disable tsdoc/syntax */ /** * The type of backoff to use when retrying a request. */ diff --git a/src/core/images/logo-grey.png b/src/core/images/logo-grey.png new file mode 100644 index 0000000000000000000000000000000000000000..5c0d56a926c317223c1d46fd3ed3c1e63f193fe5 GIT binary patch literal 3986 zcmV;D4{h*?P)Dg|00001b5ch_0Itp) z=>Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91Z=eGJ1ONa40RR91ZvX%Q0PW%XN&o;3{7FPXRCodHoo$F+M;XUw=G?p4 zZ8vG6-8PVyU6UeR1MNbv)R5o?3pRo@ZBgO}X^Mg$ih>}DA0!l^w4ZD#N~ua)`^67x zT5N3##cs{Ml-8OR#1={F&2B=rHpHzPO}cx}8UN3@cP{6ioO|zinVBI zYf1ESCc5;;_U+p@!TsPhfV;hbGMl9z939)abLq_D;-3r1Ddo|=gh${!#sQ9P)sSn0 zka2i)`}PkP&YpckgFI^@`mWlXy#4mEKzOf7bVzjPk`(;l^z`(UZn>u9^%cCmd-qna z-7xq!6X2Q|yo@$*9Btr|sSMXCSLhUGQdR@p(6OZg-czDZZ&JIMJYejq1z?b@|atJPLJgmL1s^Y9cnR1sg2lHFq^*QESUAmw=g zyy!AOr>ji`4!|k^cZU^l6ba{dQk?aAeZu7`)LD2@a3M4)Zwq}J|D@c?gMfx1P6Koj zUK|{zvZ$Ri-PX1JEVvLiH7_^|&^dT&?V_A|yc7sPMln$^ocv-+vPDv?_lW)Ue zdTc&rJf#9G1zuKgl#wV2Z^E-vbxQtBN_Z5kG=SFUHuHc}83}_TnkYV?&W-q-zbplN zC8KN+1i=lq=R&QPydvNzGqG5T62Yki8gbOt!NM#{sf+4iy#!h-c}2mgT=H00ebOYg z{^>H#et=SUO2w7Kw^{TkXdZe}Zo@Lsv*;NXn)gIic`1;=j{)xGsC*5_^-85O50i>Z8e}DkFk13VfuodA z2}FbaDy*Ri-mb*d^|~n8U(t?!7qb8-dj-vL-*^c$R`N`Oi)N${Vna|54hSZXpcJO+ zi6YVO22B1MD+hnT^QKK5jFmtm@N9uo5D6eAugB4DZNr@8&44({nH&x8bACNW_!n^e z0q5cjfc?pr{s%b!hXrs-MI!*U!?O)e<&FY~_N!|rCSU>BS_P+6)DBQ z``qLIQiVkJ6Fl;2XN=lpFayz^(*+R@7}%p0$^Sbkcqv}lELOK zftlg;6F3EBy#mYxuOGp!XMm~U^)tBj3NY35SQK1ERyO&<$8wCkS!D{^l5<|*y<6vX zJ2oaN;-*iX6V=AWZY7@L&7Z-wwY3T8J(w84&c$K8%rcA>4SJ#F5>`pbT1_n5P3L(_&?7XpTLiwptU=! zEn=Q;9w4U~XD{|z59|t$z+E#i_y`1j4Y6c0I{?JvoOzF%gqRnc$}wvIB>`hc0p@C8 zN}0G%wxg2$tTIq}q|<_KM|iaBg710HV|aT@TyENP2tUUJe;ms&+HEe2f>UWQ2@pTv zt=KE(dzc}h5|`8QvvuhooSU1w%C;u4!$SL^?%BR$`)iAfi)~iiSHYta*95n@K!ou}Zv(&;04%)A zkWO$)N!rmpJ$ke_?mFRxlwpUu)+-9)0cFY_&syl;+vErS+(T*g? zYL#{byb_5z^&#fD2GIR~zz{gnW6WP$drB$Psy4%Ip}6>^x?#v4#;>y4wAM;(_+tZ{ zN^jmOl)%A!@HX@izpi8%^2c_Lj=sLIuuyB2JYRUn3`G_fdzkw$3S#MARkjIEDVnte zh9wRg5XgXiQOPjrW4su$YQaNfhfPKpkv+@=yCn_4GQcT4(*jiBu<6cq-6t@VWDPzx zg{A&U2#x>ZG%1keR=O21A3qu?kk+Pb3SITd9(U$9$4co;7fm;p!^S2Psr?C2gRZ6(kp za7yPmz&JPxhdlh43U9;5yNVOhHqS)03Wg1EYW4-H3K;~aM8hgA5;eaZIWc~SYI-?@haxy{Jho^AoWG|| zqu|sv*8nPTnEDDAZv*_u;sB9{7nS}te301z;SU(k~8> zb7^JqS;dcq@u?7Ab4T;#)(yi4lJU82;8gTw1MCD&=@$ivlJ^&N-G=XR*?>opVUYQl zjYv`I-p9+3^Qw*O6=zuhdx2AWM*x0^v|Y7es={gZYg4nc^Q{hx7^pmo2#l9L3}%0k z7Mx13ZGdUPDg6{cykh-Zb4fr*D!0LXI+bVhC2}&P*;mSaS z4UtlOH4iwYkLePa7o5_MN+7lw`615RhC=GT4xc_;rR221!z{wG@cp|{ch^(^js8QZ zE*Fc+GX=0HIF+t&w?J&hqt12ucp>}SU{q5wTh*qzP&<2e`4ii=Jr6sU!??CtT`%ZA z)Yy8epp@q0x~ouK;99AJ*`AP`He21_x>oQ2J@f}>=4Kv_in$6N-BbXj@IH;xozVxf z9HRtd>?0n)mQcUT@?4s*x&Uei*Hj*zms3dp3__s-1BBpPjjOLwQHJj|7XP!N|zSCD=962P)cz81b-Wo%d=3*AqC( zsNs55gf5d)nTE4an~i@HlBUa^VDrz^dUOV4UZmn z9U$gWH=s9n1O9x{*Fl#X9Hk{wn-@>K%WWWm3-{m*}64()3Lq!(=Y6C|}(gu%& zl?kvXI8u;Yc_mOQI7&*P-X;l1a<+S+3Y;JKTQE;JjzrxIy+&Z$c#YBz23BWqyZ|$W zchLL1fGv$L!vD9rZ`jukj`TGGj|AHSNZ??Nb`Q4Z{~XNk$bO^+jPyuy2=^{_djAb3 z`DQHuGy;y&rM23OW|WO8w92Ymf}nm3zrb%G?os?zqdIgtM)>Pde;xq19hjtj55_|u zb_!eYx-oF1moa!G+$6xpveX*ALr<&yxf~!8@cr<$7c`DI4OCT)Q*bybe zq#u4*IQ=GfrGTUKmu8Fd_Sq$TI>0}wMIn{2doq>pm6U57eO@Jk0rI+YHOl;{Oe zNMXE_LV849z*yTxKaqP*fFpgJfJcgS1?Z*QA&}%4cFusKbUFi%l<5f2#}>!sJEy== zb~pu(6iNapc#!EO5|UHYA!DLzea?ZS^f?EQl!^oN@x>Hewh9oPq$Sb?&Pi~jn3M2G zu{c0X7~}OZI}WSyOU)GqXTecUI17)Iivq+uYuZ1oL2)EG3`=NJ1*gH0-cG|K1r@-m zz;cbPs#EZv_xg?x=~lKYWo|QTqG>Ml<0B&%@rpH>IZS4KK3jPHf>-VQ{hn;H%!V#y zRUvPZXf@*6!J$8*GjU5^8Qq`j{N0}J&y{o6`D!zoEmVaHzkV04El00wJNEcDlRmhl zO}=J9eFcxwKJ~^MHJ{0YIMfP|_BlCm;>2^B^Vg`jzN$8(IYD0UmOPaJ z@(=9WcNE)g-3R-Y$>{Y?#~6g;V^6_j*gfy*PGS1t`nrIpGCKU)rVx`DzTSn>xEbGX s7*qGqrp*KH>k?ZsH+S|`xBAxqFNL) - {media.shouldShow && ( - - {media.caption - - )} + + {media.caption + Date: Wed, 8 Oct 2025 21:36:10 -0700 Subject: [PATCH 38/45] feat: refactor embedded click handling and introduce action prefix utilities --- .../reactnative/RNIterableAPIModuleImpl.java | 12 +--- .../newarch/java/com/RNIterableAPIModule.java | 5 -- .../oldarch/java/com/RNIterableAPIModule.java | 6 -- src/api/NativeRNIterableAPI.ts | 6 -- src/core/classes/Iterable.ts | 2 +- src/core/classes/IterableApi.ts | 14 ---- src/core/constants/defaults.ts | 3 +- src/core/enums/IterableCustomActionPrefix.ts | 4 ++ src/core/utils/getActionPrefix.ts | 14 ++++ src/core/utils/isIterableAction.ts | 5 +- .../classes/IterableEmbeddedManager.ts | 68 +++++++++++++++++-- .../IterableEmbeddedBanner.tsx | 9 ++- .../IterableEmbeddedCard.tsx | 17 +++-- .../IterableEmbeddedNotification.tsx | 9 ++- .../components/utils/getUrlFromButton.ts | 8 +++ .../components/utils/runButtonClick.ts | 43 ------------ 16 files changed, 122 insertions(+), 103 deletions(-) create mode 100644 src/core/enums/IterableCustomActionPrefix.ts create mode 100644 src/core/utils/getActionPrefix.ts create mode 100644 src/embedded/components/utils/getUrlFromButton.ts delete mode 100644 src/embedded/components/utils/runButtonClick.ts diff --git a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java index b59fcc13d..d74ee9d09 100644 --- a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java +++ b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java @@ -32,6 +32,7 @@ import com.iterable.iterableapi.IterableEmbeddedManager; import com.iterable.iterableapi.IterableEmbeddedMessage; import com.iterable.iterableapi.IterableEmbeddedSession; +import com.iterable.iterableapi.IterableEmbeddedUpdateHandler; import com.iterable.iterableapi.IterableHelper; import com.iterable.iterableapi.IterableInAppCloseAction; import com.iterable.iterableapi.IterableInAppHandler; @@ -43,8 +44,6 @@ import com.iterable.iterableapi.IterableUrlHandler; import com.iterable.iterableapi.RNIterableInternal; -import com.iterable.iterableapi.IterableEmbeddedUpdateHandler; - import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -730,15 +729,6 @@ public void pauseEmbeddedImpression(String messageId) { IterableApi.getInstance().getEmbeddedManager().getEmbeddedSessionManager().pauseImpression(messageId); } - public void handleEmbeddedClick(ReadableMap messageMap, String buttonId, String clickedUrl) { - IterableEmbeddedMessage message = Serialization.embeddedMessageFromReadableMap(messageMap); - if (message != null) { - IterableApi.getInstance().getEmbeddedManager().handleEmbeddedClick(message, buttonId, clickedUrl); - } else { - IterableLogger.e(TAG, "Failed to convert message map to IterableEmbeddedMessage"); - } - } - public void trackEmbeddedClick(ReadableMap messageMap, String buttonId, String clickedUrl) { IterableLogger.d(TAG, "trackEmbeddedClick: buttonId: " + buttonId + " clickedUrl: " + clickedUrl); IterableEmbeddedMessage message = Serialization.embeddedMessageFromReadableMap(messageMap); diff --git a/android/src/newarch/java/com/RNIterableAPIModule.java b/android/src/newarch/java/com/RNIterableAPIModule.java index cd2243245..d17ef2d6a 100644 --- a/android/src/newarch/java/com/RNIterableAPIModule.java +++ b/android/src/newarch/java/com/RNIterableAPIModule.java @@ -260,11 +260,6 @@ public void pauseEmbeddedImpression(String messageId) { moduleImpl.pauseEmbeddedImpression(messageId); } - @Override - public void handleEmbeddedClick(ReadableMap message, String buttonId, String clickedUrl) { - moduleImpl.handleEmbeddedClick(message, buttonId, clickedUrl); - } - @Override public void trackEmbeddedClick(ReadableMap message, String buttonId, String clickedUrl) { moduleImpl.trackEmbeddedClick(message, buttonId, clickedUrl); diff --git a/android/src/oldarch/java/com/RNIterableAPIModule.java b/android/src/oldarch/java/com/RNIterableAPIModule.java index 5b0a8f6ac..9e0aebc80 100644 --- a/android/src/oldarch/java/com/RNIterableAPIModule.java +++ b/android/src/oldarch/java/com/RNIterableAPIModule.java @@ -11,7 +11,6 @@ import com.facebook.react.bridge.ReadableMap; import com.iterable.iterableapi.IterableEmbeddedMessage; import com.iterable.iterableapi.IterableEmbeddedManager; - import com.iterable.iterableapi.IterableEmbeddedUpdateHandler; @@ -268,11 +267,6 @@ public void pauseEmbeddedImpression(String messageId) { moduleImpl.pauseEmbeddedImpression(messageId); } - @ReactMethod - public void handleEmbeddedClick(ReadableMap message, String buttonId, String clickedUrl) { - moduleImpl.handleEmbeddedClick(message, buttonId, clickedUrl); - } - @ReactMethod public void trackEmbeddedClick(ReadableMap message, String buttonId, String clickedUrl) { moduleImpl.trackEmbeddedClick(message, buttonId, clickedUrl); diff --git a/src/api/NativeRNIterableAPI.ts b/src/api/NativeRNIterableAPI.ts index 3b0c8835e..a034123b8 100644 --- a/src/api/NativeRNIterableAPI.ts +++ b/src/api/NativeRNIterableAPI.ts @@ -165,12 +165,6 @@ export interface Spec extends TurboModule { pauseEmbeddedImpression(messageId: string): void; - handleEmbeddedClick( - message: EmbeddedMessage, - buttonId: string | null, - clickedUrl: string | null - ): void; - trackEmbeddedClick( message: EmbeddedMessage, buttonId: string | null, diff --git a/src/core/classes/Iterable.ts b/src/core/classes/Iterable.ts index 62422a6c1..9ff3e82c9 100644 --- a/src/core/classes/Iterable.ts +++ b/src/core/classes/Iterable.ts @@ -207,7 +207,7 @@ export class Iterable { Iterable.logger = logger; Iterable.inAppManager = new IterableInAppManager(logger); Iterable.authManager = new IterableAuthManager(logger); - Iterable.embeddedManager = new IterableEmbeddedManager(logger); + Iterable.embeddedManager = new IterableEmbeddedManager(logger, config); IterableApi.setLogger(logger); this.setupEventHandlers(); diff --git a/src/core/classes/IterableApi.ts b/src/core/classes/IterableApi.ts index 205244d9f..bee62bcc6 100644 --- a/src/core/classes/IterableApi.ts +++ b/src/core/classes/IterableApi.ts @@ -576,20 +576,6 @@ export class IterableApi { return RNIterableAPI.pauseEmbeddedImpression(messageId); } - static handleEmbeddedClick( - message: IterableEmbeddedMessage, - buttonId: string | null, - clickedUrl: string | null - ) { - IterableApi.logger.log( - 'handleEmbeddedClick: ', - message, - buttonId, - clickedUrl - ); - return RNIterableAPI.handleEmbeddedClick(message, buttonId, clickedUrl); - } - // ---- End EMBEDDED ---- // // ====================================================== // diff --git a/src/core/constants/defaults.ts b/src/core/constants/defaults.ts index 5db65970d..c72a60c23 100644 --- a/src/core/constants/defaults.ts +++ b/src/core/constants/defaults.ts @@ -9,5 +9,6 @@ export const defaultLogger = new IterableLogger(defaultConfig); export const defaultInAppManager = new IterableInAppManager(defaultLogger); export const defaultAuthManager = new IterableAuthManager(defaultLogger); export const defaultEmbeddedManager = new IterableEmbeddedManager( - defaultLogger + defaultLogger, + defaultConfig ); diff --git a/src/core/enums/IterableCustomActionPrefix.ts b/src/core/enums/IterableCustomActionPrefix.ts new file mode 100644 index 000000000..2e989334f --- /dev/null +++ b/src/core/enums/IterableCustomActionPrefix.ts @@ -0,0 +1,4 @@ +export enum IterableCustomActionPrefix { + Action = 'action://', + Itbl = 'itbl://', +} diff --git a/src/core/utils/getActionPrefix.ts b/src/core/utils/getActionPrefix.ts new file mode 100644 index 000000000..664064da9 --- /dev/null +++ b/src/core/utils/getActionPrefix.ts @@ -0,0 +1,14 @@ +import { IterableCustomActionPrefix } from '../enums/IterableCustomActionPrefix'; + +export const getActionPrefix = ( + str?: string | null +): IterableCustomActionPrefix | null => { + if (!str) return null; + if (str.startsWith(IterableCustomActionPrefix.Action)) { + return IterableCustomActionPrefix.Action; + } + if (str.startsWith(IterableCustomActionPrefix.Itbl)) { + return IterableCustomActionPrefix.Itbl; + } + return null; +}; diff --git a/src/core/utils/isIterableAction.ts b/src/core/utils/isIterableAction.ts index f7f015aaa..ea1ccfcd5 100644 --- a/src/core/utils/isIterableAction.ts +++ b/src/core/utils/isIterableAction.ts @@ -1,2 +1,5 @@ +import { IterableCustomActionPrefix } from '../enums/IterableCustomActionPrefix'; + export const isIterableAction = (str: string = '') => - str.slice(0, 9) === 'action://'; + str.startsWith(IterableCustomActionPrefix.Action) || + str.startsWith(IterableCustomActionPrefix.Itbl); diff --git a/src/embedded/classes/IterableEmbeddedManager.ts b/src/embedded/classes/IterableEmbeddedManager.ts index b3e51963a..6919a82b0 100644 --- a/src/embedded/classes/IterableEmbeddedManager.ts +++ b/src/embedded/classes/IterableEmbeddedManager.ts @@ -1,45 +1,63 @@ +import { IterableAction } from '../../core/classes/IterableAction'; +import { IterableActionContext } from '../../core/classes/IterableActionContext'; import { IterableApi } from '../../core/classes/IterableApi'; +import type { IterableConfig } from '../../core/classes/IterableConfig'; import type { IterableLogger } from '../../core/classes/IterableLogger'; -import { defaultLogger } from '../../core/constants/defaults'; +import { defaultConfig, defaultLogger } from '../../core/constants/defaults'; +import { IterableActionSource } from '../../core/enums/IterableActionSource'; +import { getActionPrefix } from '../../core/utils/getActionPrefix'; import type { IterableEmbeddedMessage } from './IterableEmbeddedMessage'; import { IterableEmbeddedSessionManager } from './IterableEmbeddedSessionManager'; export class IterableEmbeddedManager { - static logger: IterableLogger = defaultLogger; + logger: IterableLogger = defaultLogger; + config: IterableConfig = defaultConfig; sessionManager: IterableEmbeddedSessionManager = new IterableEmbeddedSessionManager(defaultLogger); - constructor(logger: IterableLogger) { - IterableEmbeddedManager.logger = logger; + constructor(logger: IterableLogger, config: IterableConfig) { + this.logger = logger; + this.config = config; this.sessionManager = new IterableEmbeddedSessionManager(logger); } syncMessages() { + this.logger.log('IterableEmbeddedManager.syncMessages'); return IterableApi.syncEmbeddedMessages(); } getMessages(placementIds?: number[] | null) { + this.logger.log('IterableEmbeddedManager.getMessages', placementIds); return IterableApi.getEmbeddedMessages(placementIds ?? null); } getPlacementIds() { + this.logger.log('IterableEmbeddedManager.getPlacementIds'); return IterableApi.getEmbeddedPlacementIds(); } startSession() { + this.logger.log('IterableEmbeddedManager.startSession'); IterableApi.startEmbeddedSession(); } endSession() { + this.logger.log('IterableEmbeddedManager.endSession'); IterableApi.endEmbeddedSession(); } startImpression(messageId: string, placementId: number) { + this.logger.log( + 'IterableEmbeddedManager.startImpression', + messageId, + placementId + ); IterableApi.startEmbeddedImpression(messageId, placementId); } pauseImpression(messageId: string) { + this.logger.log('IterableEmbeddedManager.pauseImpression', messageId); IterableApi.pauseEmbeddedImpression(messageId); } @@ -48,7 +66,41 @@ export class IterableEmbeddedManager { buttonId: string | null, clickedUrl: string | null ) { - IterableApi.handleEmbeddedClick(message, buttonId, clickedUrl); + this.logger.log( + 'IterableEmbeddedManager.handleClick', + message, + buttonId, + clickedUrl + ); + + if (!clickedUrl) { + this.logger.log( + 'IterableEmbeddedManager.handleClick:', + 'A url or action is required to handle an embedded click', + clickedUrl + ); + return; + } + + const actionPrefix = getActionPrefix(clickedUrl); + const source = IterableActionSource.embedded; + + this.trackClick(message, buttonId, clickedUrl); + + if (actionPrefix) { + const actionName = clickedUrl?.replace(actionPrefix, ''); + const action = new IterableAction(actionName, '', ''); + const context = new IterableActionContext(action, source); + if (this.config.customActionHandler) { + this.config.customActionHandler(action, context); + } + } else { + const action = new IterableAction('openUrl', clickedUrl, ''); + const context = new IterableActionContext(action, source); + if (this.config.urlHandler) { + this.config.urlHandler(clickedUrl, context); + } + } } trackClick( @@ -56,6 +108,12 @@ export class IterableEmbeddedManager { buttonId: string | null, clickedUrl: string | null ) { + this.logger.log( + 'IterableEmbeddedManager.trackClick', + message, + buttonId, + clickedUrl + ); IterableApi.trackEmbeddedClick(message, buttonId, clickedUrl); } } diff --git a/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx b/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx index cf9dd4097..dea1822fc 100644 --- a/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx +++ b/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx @@ -8,13 +8,14 @@ import { type ViewStyle, } from 'react-native'; +import { Iterable } from '../../../core/classes/Iterable'; import type { IterableEmbeddedMessageElementsButton } from '../../classes/IterableEmbeddedMessageElementsButton'; import { IterableEmbeddedViewType } from '../../enums'; import type { IterableEmbeddedComponentProps } from '../IterableEmbeddedViewProps'; import { getMedia } from '../utils/getMedia'; import { getStyles } from '../utils/getStyles'; +import { getUrlFromButton } from '../utils/getUrlFromButton'; import { styles } from './IterableEmbeddedBanner.styles'; -import { runButtonClick } from '../utils/runButtonClick'; /** * TODO: figure out how default action works. @@ -34,7 +35,11 @@ export const IterableEmbeddedBanner = ({ const handleButtonClick = useCallback( (button: IterableEmbeddedMessageElementsButton) => { onButtonClick(button); - runButtonClick(button, message); + Iterable.embeddedManager.handleClick( + message, + button.id, + getUrlFromButton(button) ?? null + ); }, [onButtonClick, message] ); diff --git a/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx b/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx index 862efb87d..49502a42b 100644 --- a/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx +++ b/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx @@ -1,3 +1,4 @@ +import { useCallback, useMemo } from 'react'; import { Image, Text, @@ -6,15 +7,15 @@ import { type TextStyle, type ViewStyle, } from 'react-native'; -import { useCallback, useMemo } from 'react'; -import type { IterableEmbeddedComponentProps } from '../IterableEmbeddedViewProps'; +import { Iterable } from '../../../core/classes/Iterable'; +import type { IterableEmbeddedMessageElementsButton } from '../../classes/IterableEmbeddedMessageElementsButton'; import { IterableEmbeddedViewType } from '../../enums'; -import { getStyles } from '../utils/getStyles'; +import type { IterableEmbeddedComponentProps } from '../IterableEmbeddedViewProps'; import { getMedia } from '../utils/getMedia'; +import { getStyles } from '../utils/getStyles'; +import { getUrlFromButton } from '../utils/getUrlFromButton'; import { styles } from './IterableEmbeddedCard.styles'; -import type { IterableEmbeddedMessageElementsButton } from '../../classes/IterableEmbeddedMessageElementsButton'; -import { runButtonClick } from '../utils/runButtonClick'; export const IterableEmbeddedCard = ({ config, @@ -30,7 +31,11 @@ export const IterableEmbeddedCard = ({ const handleButtonClick = useCallback( (button: IterableEmbeddedMessageElementsButton) => { onButtonClick(button); - runButtonClick(button, message); + Iterable.embeddedManager.handleClick( + message, + button.id, + getUrlFromButton(button) ?? null + ); }, [onButtonClick, message] ); diff --git a/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.tsx b/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.tsx index c2bd18ca4..4e9eb9dba 100644 --- a/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.tsx +++ b/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.tsx @@ -7,11 +7,12 @@ import { } from 'react-native'; import { useCallback, useMemo } from 'react'; +import { Iterable } from '../../../core/classes/Iterable'; import type { IterableEmbeddedMessageElementsButton } from '../../classes/IterableEmbeddedMessageElementsButton'; import { IterableEmbeddedViewType } from '../../enums'; import type { IterableEmbeddedComponentProps } from '../IterableEmbeddedViewProps'; import { getStyles } from '../utils/getStyles'; -import { runButtonClick } from '../utils/runButtonClick'; +import { getUrlFromButton } from '../utils/getUrlFromButton'; import { styles } from './IterableEmbeddedNotification.styles'; export const IterableEmbeddedNotification = ({ @@ -26,7 +27,11 @@ export const IterableEmbeddedNotification = ({ const handleButtonClick = useCallback( (button: IterableEmbeddedMessageElementsButton) => { onButtonClick(button); - runButtonClick(button, message); + Iterable.embeddedManager.handleClick( + message, + button.id, + getUrlFromButton(button) ?? null + ); }, [onButtonClick, message] ); diff --git a/src/embedded/components/utils/getUrlFromButton.ts b/src/embedded/components/utils/getUrlFromButton.ts new file mode 100644 index 000000000..73ce553de --- /dev/null +++ b/src/embedded/components/utils/getUrlFromButton.ts @@ -0,0 +1,8 @@ +import type { IterableEmbeddedMessageElementsButton } from '../../classes/IterableEmbeddedMessageElementsButton'; + +export const getUrlFromButton = ( + button: IterableEmbeddedMessageElementsButton +) => { + const { data, type: actionType } = button.action ?? {}; + return data && data?.length > 0 ? data : actionType; +}; diff --git a/src/embedded/components/utils/runButtonClick.ts b/src/embedded/components/utils/runButtonClick.ts deleted file mode 100644 index 1b227ed0b..000000000 --- a/src/embedded/components/utils/runButtonClick.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Iterable } from '../../../core/classes/Iterable'; -import { IterableAction } from '../../../core/classes/IterableAction'; -import { IterableActionContext } from '../../../core/classes/IterableActionContext'; -import { IterableActionSource } from '../../../core/enums'; -import { isIterableAction } from '../../../core/utils'; -import type { IterableEmbeddedMessage } from '../../classes/IterableEmbeddedMessage'; -import type { IterableEmbeddedMessageElementsButton } from '../../classes/IterableEmbeddedMessageElementsButton'; - -const getUrl = (button: IterableEmbeddedMessageElementsButton) => { - const { data, type: actionType } = button.action ?? {}; - return data && data?.length > 0 ? data : actionType; -}; - -export const runButtonClick = ( - button: IterableEmbeddedMessageElementsButton, - message: IterableEmbeddedMessage -) => { - const url = getUrl(button); - const source = IterableActionSource.embedded; - - Iterable.embeddedManager.trackClick(message, button.id, url ?? null); - - if (isIterableAction(url)) { - const action = new IterableAction( - (url ?? '').replace('action://', ''), - button?.action?.data, - '' - ); - - const context = new IterableActionContext(action, source); - - if (Iterable.savedConfig.customActionHandler) { - Iterable.savedConfig.customActionHandler(action, context); - } - } else { - const action = new IterableAction('openUrl', url, ''); - const context = new IterableActionContext(action, source); - - if (Iterable.savedConfig.urlHandler) { - Iterable.savedConfig.urlHandler(url ?? '', context); - } - } -}; From 1115268012a86b60284e54fa1727ea88b6dced6f Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Wed, 8 Oct 2025 21:51:35 -0700 Subject: [PATCH 39/45] feat: refactor IterableEmbedded components to utilize useEmbeddedView hook --- .../IterableEmbeddedBanner.tsx | 28 +++---------- .../IterableEmbeddedCard.tsx | 31 +++------------ .../IterableEmbeddedNotification.tsx | 27 +++---------- .../IterableEmbeddedView.tsx | 2 +- src/embedded/components/utils/getMedia.ts | 15 ------- src/embedded/hooks/useEmbeddedView.ts | 39 +++++++++++++++++++ .../IterableEmbeddedViewProps.ts | 0 .../{components => }/utils/getButtons.ts | 6 +-- .../utils/getDefaultActionUrl.ts | 2 +- .../{components => }/utils/getDefaultStyle.ts | 2 +- src/embedded/utils/getMedia.ts | 15 +++++++ .../{components => }/utils/getStyles.ts | 6 +-- .../utils/getUrlFromButton.ts | 2 +- 13 files changed, 81 insertions(+), 94 deletions(-) delete mode 100644 src/embedded/components/utils/getMedia.ts create mode 100644 src/embedded/hooks/useEmbeddedView.ts rename src/embedded/{components => types}/IterableEmbeddedViewProps.ts (100%) rename src/embedded/{components => }/utils/getButtons.ts (71%) rename src/embedded/{components => }/utils/getDefaultActionUrl.ts (72%) rename src/embedded/{components => }/utils/getDefaultStyle.ts (83%) create mode 100644 src/embedded/utils/getMedia.ts rename src/embedded/{components => }/utils/getStyles.ts (84%) rename src/embedded/{components => }/utils/getUrlFromButton.ts (64%) diff --git a/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx b/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx index dea1822fc..3bc261636 100644 --- a/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx +++ b/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx @@ -1,4 +1,3 @@ -import { useCallback, useMemo } from 'react'; import { Image, Text, @@ -8,13 +7,9 @@ import { type ViewStyle, } from 'react-native'; -import { Iterable } from '../../../core/classes/Iterable'; -import type { IterableEmbeddedMessageElementsButton } from '../../classes/IterableEmbeddedMessageElementsButton'; import { IterableEmbeddedViewType } from '../../enums'; -import type { IterableEmbeddedComponentProps } from '../IterableEmbeddedViewProps'; -import { getMedia } from '../utils/getMedia'; -import { getStyles } from '../utils/getStyles'; -import { getUrlFromButton } from '../utils/getUrlFromButton'; +import { useEmbeddedView } from '../../hooks/useEmbeddedView'; +import type { IterableEmbeddedComponentProps } from '../../types/IterableEmbeddedViewProps'; import { styles } from './IterableEmbeddedBanner.styles'; /** @@ -26,22 +21,9 @@ export const IterableEmbeddedBanner = ({ message, onButtonClick = () => {}, }: IterableEmbeddedComponentProps) => { - const parsedStyles = useMemo(() => { - return getStyles(IterableEmbeddedViewType.Banner, config); - }, [config]); - const media = useMemo(() => { - return getMedia(IterableEmbeddedViewType.Banner, message); - }, [message]); - const handleButtonClick = useCallback( - (button: IterableEmbeddedMessageElementsButton) => { - onButtonClick(button); - Iterable.embeddedManager.handleClick( - message, - button.id, - getUrlFromButton(button) ?? null - ); - }, - [onButtonClick, message] + const { parsedStyles, media, handleButtonClick } = useEmbeddedView( + IterableEmbeddedViewType.Banner, + { message, config, onButtonClick } ); const buttons = message.elements?.buttons ?? []; diff --git a/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx b/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx index 49502a42b..ad7d2527a 100644 --- a/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx +++ b/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx @@ -1,4 +1,3 @@ -import { useCallback, useMemo } from 'react'; import { Image, Text, @@ -8,39 +7,21 @@ import { type ViewStyle, } from 'react-native'; -import { Iterable } from '../../../core/classes/Iterable'; -import type { IterableEmbeddedMessageElementsButton } from '../../classes/IterableEmbeddedMessageElementsButton'; import { IterableEmbeddedViewType } from '../../enums'; -import type { IterableEmbeddedComponentProps } from '../IterableEmbeddedViewProps'; -import { getMedia } from '../utils/getMedia'; -import { getStyles } from '../utils/getStyles'; -import { getUrlFromButton } from '../utils/getUrlFromButton'; +import type { IterableEmbeddedComponentProps } from '../../types/IterableEmbeddedViewProps'; import { styles } from './IterableEmbeddedCard.styles'; +import { useEmbeddedView } from '../../hooks/useEmbeddedView'; export const IterableEmbeddedCard = ({ config, message, onButtonClick = () => {}, }: IterableEmbeddedComponentProps) => { - const parsedStyles = useMemo(() => { - return getStyles(IterableEmbeddedViewType.Card, config); - }, [config]); - const media = useMemo(() => { - return getMedia(IterableEmbeddedViewType.Card, message); - }, [message]); - const handleButtonClick = useCallback( - (button: IterableEmbeddedMessageElementsButton) => { - onButtonClick(button); - Iterable.embeddedManager.handleClick( - message, - button.id, - getUrlFromButton(button) ?? null - ); - }, - [onButtonClick, message] + const { parsedStyles, media, handleButtonClick } = useEmbeddedView( + IterableEmbeddedViewType.Card, + { message, config, onButtonClick } ); - - const buttons = message.elements?.buttons ?? []; + const buttons = message?.elements?.buttons ?? []; return ( {}, }: IterableEmbeddedComponentProps) => { - const parsedStyles = useMemo(() => { - return getStyles(IterableEmbeddedViewType.Notification, config); - }, [config]); - - const handleButtonClick = useCallback( - (button: IterableEmbeddedMessageElementsButton) => { - onButtonClick(button); - Iterable.embeddedManager.handleClick( - message, - button.id, - getUrlFromButton(button) ?? null - ); - }, - [onButtonClick, message] + const { parsedStyles, handleButtonClick } = useEmbeddedView( + IterableEmbeddedViewType.Notification, + { message, config, onButtonClick } ); const buttons = message.elements?.buttons ?? []; diff --git a/src/embedded/components/IterableEmbeddedView/IterableEmbeddedView.tsx b/src/embedded/components/IterableEmbeddedView/IterableEmbeddedView.tsx index 5a313532a..b45e2259b 100644 --- a/src/embedded/components/IterableEmbeddedView/IterableEmbeddedView.tsx +++ b/src/embedded/components/IterableEmbeddedView/IterableEmbeddedView.tsx @@ -5,7 +5,7 @@ import { IterableEmbeddedViewType } from '../../enums'; import { IterableEmbeddedBanner } from '../IterableEmbeddedBanner'; import { IterableEmbeddedCard } from '../IterableEmbeddedCard'; import { IterableEmbeddedNotification } from '../IterableEmbeddedNotification'; -import type { IterableEmbeddedComponentProps } from '../IterableEmbeddedViewProps'; +import type { IterableEmbeddedComponentProps } from '../../types/IterableEmbeddedViewProps'; interface IterableEmbeddedViewProps extends IterableEmbeddedComponentProps { viewType: IterableEmbeddedViewType; diff --git a/src/embedded/components/utils/getMedia.ts b/src/embedded/components/utils/getMedia.ts deleted file mode 100644 index fd4ee1665..000000000 --- a/src/embedded/components/utils/getMedia.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { IterableEmbeddedMessage } from '../../classes/IterableEmbeddedMessage'; -import { IterableEmbeddedViewType } from '../../enums/IterableEmbeddedViewType'; - -export const getMedia = ( - viewType: IterableEmbeddedViewType, - message: IterableEmbeddedMessage -) => { - const url = message.elements?.mediaUrl ?? null; - const caption = message.elements?.mediaUrlCaption ?? null; - const shouldShow = - !!url && - url.length > 0 && - viewType !== IterableEmbeddedViewType.Notification; - return { url, caption, shouldShow }; -}; diff --git a/src/embedded/hooks/useEmbeddedView.ts b/src/embedded/hooks/useEmbeddedView.ts new file mode 100644 index 000000000..ad0c2988c --- /dev/null +++ b/src/embedded/hooks/useEmbeddedView.ts @@ -0,0 +1,39 @@ +import { useCallback, useMemo } from 'react'; +import { Iterable } from '../../core/classes/Iterable'; +import type { IterableEmbeddedMessageElementsButton } from '../classes/IterableEmbeddedMessageElementsButton'; +import { IterableEmbeddedViewType } from '../enums'; +import type { IterableEmbeddedComponentProps } from '../types/IterableEmbeddedViewProps'; +import { getMedia } from '../utils/getMedia'; +import { getStyles } from '../utils/getStyles'; +import { getUrlFromButton } from '../utils/getUrlFromButton'; + +export const useEmbeddedView = ( + viewType: IterableEmbeddedViewType, + { message, config, onButtonClick = () => {} }: IterableEmbeddedComponentProps +) => { + const parsedStyles = useMemo(() => { + return getStyles(viewType, config); + }, [viewType, config]); + + const media = useMemo(() => { + return getMedia(viewType, message); + }, [viewType, message]); + + const handleButtonClick = useCallback( + (button: IterableEmbeddedMessageElementsButton) => { + onButtonClick(button); + Iterable.embeddedManager.handleClick( + message, + button.id, + getUrlFromButton(button) ?? null + ); + }, + [onButtonClick, message] + ); + + return { + parsedStyles, + media, + handleButtonClick, + }; +}; diff --git a/src/embedded/components/IterableEmbeddedViewProps.ts b/src/embedded/types/IterableEmbeddedViewProps.ts similarity index 100% rename from src/embedded/components/IterableEmbeddedViewProps.ts rename to src/embedded/types/IterableEmbeddedViewProps.ts diff --git a/src/embedded/components/utils/getButtons.ts b/src/embedded/utils/getButtons.ts similarity index 71% rename from src/embedded/components/utils/getButtons.ts rename to src/embedded/utils/getButtons.ts index f8370eafb..11d9d4970 100644 --- a/src/embedded/components/utils/getButtons.ts +++ b/src/embedded/utils/getButtons.ts @@ -1,6 +1,6 @@ -import type { IterableEmbeddedMessage } from '../../classes/IterableEmbeddedMessage'; -import type { IterableEmbeddedMessageElementsButton } from '../../classes/IterableEmbeddedMessageElementsButton'; -import type { IterableEmbeddedViewButtonInfo } from '../../types/IterableEmbeddedViewButtonInfo'; +import type { IterableEmbeddedMessage } from '../classes/IterableEmbeddedMessage'; +import type { IterableEmbeddedMessageElementsButton } from '../classes/IterableEmbeddedMessageElementsButton'; +import type { IterableEmbeddedViewButtonInfo } from '../types/IterableEmbeddedViewButtonInfo'; export const getButtons = (message: IterableEmbeddedMessage) => { const buttons = message.elements?.buttons ?? null; diff --git a/src/embedded/components/utils/getDefaultActionUrl.ts b/src/embedded/utils/getDefaultActionUrl.ts similarity index 72% rename from src/embedded/components/utils/getDefaultActionUrl.ts rename to src/embedded/utils/getDefaultActionUrl.ts index 225106616..7efb5fbbb 100644 --- a/src/embedded/components/utils/getDefaultActionUrl.ts +++ b/src/embedded/utils/getDefaultActionUrl.ts @@ -1,4 +1,4 @@ -import type { IterableEmbeddedMessage } from '../../classes/IterableEmbeddedMessage'; +import type { IterableEmbeddedMessage } from '../classes/IterableEmbeddedMessage'; export const getDefaultActionUrl = (message: IterableEmbeddedMessage) => { const defaultAction = message.elements?.defaultAction ?? null; diff --git a/src/embedded/components/utils/getDefaultStyle.ts b/src/embedded/utils/getDefaultStyle.ts similarity index 83% rename from src/embedded/components/utils/getDefaultStyle.ts rename to src/embedded/utils/getDefaultStyle.ts index ca00fc3b9..b31ab757e 100644 --- a/src/embedded/components/utils/getDefaultStyle.ts +++ b/src/embedded/utils/getDefaultStyle.ts @@ -1,4 +1,4 @@ -import { IterableEmbeddedViewType } from '../../enums/IterableEmbeddedViewType'; +import { IterableEmbeddedViewType } from '../enums'; export const getDefaultStyle = ( viewType: IterableEmbeddedViewType, diff --git a/src/embedded/utils/getMedia.ts b/src/embedded/utils/getMedia.ts new file mode 100644 index 000000000..079c919a6 --- /dev/null +++ b/src/embedded/utils/getMedia.ts @@ -0,0 +1,15 @@ +import type { IterableEmbeddedMessage } from '../classes/IterableEmbeddedMessage'; +import { IterableEmbeddedViewType } from '../enums'; + +export const getMedia = ( + viewType: IterableEmbeddedViewType, + message: IterableEmbeddedMessage +) => { + if (viewType === IterableEmbeddedViewType.Notification) { + return { url: null, caption: null, shouldShow: false }; + } + const url = message.elements?.mediaUrl ?? null; + const caption = message.elements?.mediaUrlCaption ?? null; + const shouldShow = !!url && url.length > 0; + return { url, caption, shouldShow }; +}; diff --git a/src/embedded/components/utils/getStyles.ts b/src/embedded/utils/getStyles.ts similarity index 84% rename from src/embedded/components/utils/getStyles.ts rename to src/embedded/utils/getStyles.ts index 747ab9cdb..b116142ed 100644 --- a/src/embedded/components/utils/getStyles.ts +++ b/src/embedded/utils/getStyles.ts @@ -1,6 +1,6 @@ -import type { IterableEmbeddedViewConfig } from '../../classes/IterableEmbeddedViewConfig'; -import { embeddedStyles } from '../../constants/embeddedViewDefaults'; -import type { IterableEmbeddedViewType } from '../../enums/IterableEmbeddedViewType'; +import type { IterableEmbeddedViewConfig } from '../classes/IterableEmbeddedViewConfig'; +import { embeddedStyles } from '../constants/embeddedViewDefaults'; +import type { IterableEmbeddedViewType } from '../enums'; import { getDefaultStyle } from './getDefaultStyle'; export const getStyles = ( diff --git a/src/embedded/components/utils/getUrlFromButton.ts b/src/embedded/utils/getUrlFromButton.ts similarity index 64% rename from src/embedded/components/utils/getUrlFromButton.ts rename to src/embedded/utils/getUrlFromButton.ts index 73ce553de..2140ffd05 100644 --- a/src/embedded/components/utils/getUrlFromButton.ts +++ b/src/embedded/utils/getUrlFromButton.ts @@ -1,4 +1,4 @@ -import type { IterableEmbeddedMessageElementsButton } from '../../classes/IterableEmbeddedMessageElementsButton'; +import type { IterableEmbeddedMessageElementsButton } from '../classes/IterableEmbeddedMessageElementsButton'; export const getUrlFromButton = ( button: IterableEmbeddedMessageElementsButton From 47329254b6436211b34b0f0727abb418c8af1236 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Wed, 8 Oct 2025 23:24:57 -0700 Subject: [PATCH 40/45] feat: add visibility tracking and refactor image dimensions in IterableEmbedded components --- .../IterableEmbeddedBanner.styles.tsx | 9 +- .../IterableEmbeddedBanner.tsx | 13 +- .../IterableEmbeddedCard.constants.ts | 0 .../IterableEmbeddedCard.styles.ts | 8 +- .../IterableEmbeddedCard.tsx | 36 +++- src/embedded/hooks/useComponentVisibility.ts | 156 ++++++++++++++++++ 6 files changed, 211 insertions(+), 11 deletions(-) create mode 100644 src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.constants.ts create mode 100644 src/embedded/hooks/useComponentVisibility.ts diff --git a/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.styles.tsx b/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.styles.tsx index 95faf6d73..551f50068 100644 --- a/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.styles.tsx +++ b/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.styles.tsx @@ -1,9 +1,12 @@ import { StyleSheet } from 'react-native'; import { - embeddedMediaImageBorderColors, embeddedMediaImageBackgroundColors, + embeddedMediaImageBorderColors, } from '../../constants/embeddedViewDefaults'; +export const IMAGE_HEIGHT = 70; +export const IMAGE_WIDTH = 70; + export const styles = StyleSheet.create({ body: { alignSelf: 'stretch', @@ -61,10 +64,10 @@ export const styles = StyleSheet.create({ borderRadius: 6, borderStyle: 'solid', borderWidth: 1, - height: 70, + height: IMAGE_HEIGHT, paddingHorizontal: 0, paddingVertical: 0, - width: 70, + width: IMAGE_WIDTH, }, textContainer: { alignSelf: 'center', diff --git a/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx b/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx index 3bc261636..d9628da15 100644 --- a/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx +++ b/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx @@ -5,12 +5,17 @@ import { View, type TextStyle, type ViewStyle, + PixelRatio, } from 'react-native'; import { IterableEmbeddedViewType } from '../../enums'; import { useEmbeddedView } from '../../hooks/useEmbeddedView'; import type { IterableEmbeddedComponentProps } from '../../types/IterableEmbeddedViewProps'; -import { styles } from './IterableEmbeddedBanner.styles'; +import { + styles, + IMAGE_HEIGHT, + IMAGE_WIDTH, +} from './IterableEmbeddedBanner.styles'; /** * TODO: figure out how default action works. @@ -63,7 +68,11 @@ export const IterableEmbeddedBanner = ({ {media.shouldShow && ( {media.caption diff --git a/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.constants.ts b/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.constants.ts new file mode 100644 index 000000000..e69de29bb diff --git a/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.styles.ts b/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.styles.ts index e98e60c49..1a728c657 100644 --- a/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.styles.ts +++ b/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.styles.ts @@ -1,7 +1,9 @@ import { StyleSheet } from 'react-native'; import { embeddedMediaImageBackgroundColors } from '../../constants/embeddedViewDefaults'; -const IMAGE_HEIGHT = 230; +export const IMAGE_HEIGHT = 230; +export const PLACEHOLDER_IMAGE_HEIGHT = 56; +export const PLACEHOLDER_IMAGE_WIDTH = 56; export const styles = StyleSheet.create({ body: { @@ -68,9 +70,9 @@ export const styles = StyleSheet.create({ width: '100%', }, mediaImagePlaceholder: { - height: 56, + height: PLACEHOLDER_IMAGE_HEIGHT, opacity: 0.25, - width: 56, + width: PLACEHOLDER_IMAGE_WIDTH, }, textContainer: { alignItems: 'flex-start', diff --git a/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx b/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx index ad7d2527a..4c63e4222 100644 --- a/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx +++ b/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx @@ -1,5 +1,7 @@ +import { useEffect } from 'react'; import { Image, + PixelRatio, Text, TouchableOpacity, View, @@ -8,9 +10,10 @@ import { } from 'react-native'; import { IterableEmbeddedViewType } from '../../enums'; -import type { IterableEmbeddedComponentProps } from '../../types/IterableEmbeddedViewProps'; -import { styles } from './IterableEmbeddedCard.styles'; import { useEmbeddedView } from '../../hooks/useEmbeddedView'; +import { useComponentVisibility } from '../../hooks/useComponentVisibility'; +import type { IterableEmbeddedComponentProps } from '../../types/IterableEmbeddedViewProps'; +import { IMAGE_HEIGHT, styles } from './IterableEmbeddedCard.styles'; export const IterableEmbeddedCard = ({ config, @@ -23,8 +26,32 @@ export const IterableEmbeddedCard = ({ ); const buttons = message?.elements?.buttons ?? []; + // Use the visibility hook to track if the component is visible + const { isVisible, componentRef, handleLayout } = useComponentVisibility({ + threshold: 0.1, // Component is considered visible if 10% is on screen + checkOnAppState: true, // Consider app state (active/background) + enablePeriodicCheck: true, // Enable periodic checking for navigation changes + checkInterval: 500, // Check every 500ms for navigation changes + }); + + useEffect(() => { + console.log('RENDERED'); + return () => { + console.log('UNMOUNTED'); + }; + }, []); + + // Log visibility changes for debugging + useEffect(() => { + console.log('Card visibility changed:', isVisible); + }, [isVisible]); + return ( { + const { + threshold = 0.1, + checkOnAppState = true, + checkInterval = 0, // Default to only check on layout changes + enablePeriodicCheck = true, // Enable periodic checking by default for navigation + } = options; + + const [isVisible, setIsVisible] = useState(false); + const [appState, setAppState] = useState(AppState.currentState); + const componentRef = useRef(null); + const [layout, setLayout] = useState({ + x: 0, + y: 0, + width: 0, + height: 0, + }); + const intervalRef = useRef(null); + + // Handle layout changes + const handleLayout = useCallback((event: LayoutChangeEvent) => { + const { x, y, width, height } = event.nativeEvent.layout; + setLayout({ x, y, width, height }); + }, []); + + // Check if component is visible on screen using measure + const checkVisibility = useCallback((): Promise => { + if (!componentRef.current || layout.width === 0 || layout.height === 0) { + return Promise.resolve(false); + } + + return new Promise((resolve) => { + componentRef.current?.measure((_x, _y, width, height, pageX, pageY) => { + const screenHeight = Dimensions.get('window').height; + const screenWidth = Dimensions.get('window').width; + + // Calculate visible area using page coordinates + const visibleTop = Math.max(0, pageY); + const visibleBottom = Math.min(screenHeight, pageY + height); + const visibleLeft = Math.max(0, pageX); + const visibleRight = Math.min(screenWidth, pageX + width); + + const visibleHeight = Math.max(0, visibleBottom - visibleTop); + const visibleWidth = Math.max(0, visibleRight - visibleLeft); + + const visibleArea = visibleHeight * visibleWidth; + const totalArea = height * width; + const visibilityRatio = totalArea > 0 ? visibleArea / totalArea : 0; + + resolve(visibilityRatio >= threshold); + }); + }).catch(() => { + // Fallback to layout-based calculation if measure fails + const screenHeight = Dimensions.get('window').height; + const screenWidth = Dimensions.get('window').width; + + const visibleTop = Math.max(0, layout.y); + const visibleBottom = Math.min(screenHeight, layout.y + layout.height); + const visibleLeft = Math.max(0, layout.x); + const visibleRight = Math.min(screenWidth, layout.x + layout.width); + + const visibleHeight = Math.max(0, visibleBottom - visibleTop); + const visibleWidth = Math.max(0, visibleRight - visibleLeft); + + const visibleArea = visibleHeight * visibleWidth; + const totalArea = layout.height * layout.width; + const visibilityRatio = totalArea > 0 ? visibleArea / totalArea : 0; + + return visibilityRatio >= threshold; + }); + }, [layout, threshold]); + + // Update visibility state + const updateVisibility = useCallback(async () => { + const isComponentVisible = await checkVisibility(); + const isAppActive = !checkOnAppState || appState === 'active'; + const newVisibility = isComponentVisible && isAppActive; + + setIsVisible(newVisibility); + }, [checkVisibility, appState, checkOnAppState]); + + // Update visibility when layout or app state changes + useEffect(() => { + updateVisibility(); + }, [updateVisibility]); + + // Set up periodic checking for navigation changes + useEffect(() => { + const interval = + checkInterval > 0 ? checkInterval : enablePeriodicCheck ? 500 : 0; + + if (interval > 0) { + intervalRef.current = setInterval(updateVisibility, interval); + return () => { + if (intervalRef.current) { + clearInterval(intervalRef.current); + } + }; + } + return undefined; + }, [checkInterval, enablePeriodicCheck, updateVisibility]); + + // Listen to app state changes + useEffect(() => { + if (!checkOnAppState) return; + + const handleAppStateChange = (nextAppState: string) => { + setAppState(nextAppState as typeof AppState.currentState); + }; + + const subscription = AppState.addEventListener( + 'change', + handleAppStateChange + ); + return () => subscription?.remove(); + }, [checkOnAppState]); + + // Clean up interval on unmount + useEffect(() => { + return () => { + if (intervalRef.current) { + clearInterval(intervalRef.current); + } + }; + }, []); + + return { + isVisible, + componentRef, + handleLayout, + appState, + layout, + }; +}; From 8149ed063b1d504e68d230f2bf6188f369540b28 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Wed, 8 Oct 2025 23:35:52 -0700 Subject: [PATCH 41/45] feat: request notification permission for Android 13+ in App component --- example/src/components/App/App.tsx | 45 +++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/example/src/components/App/App.tsx b/example/src/components/App/App.tsx index 42769db1d..d71b9fe95 100644 --- a/example/src/components/App/App.tsx +++ b/example/src/components/App/App.tsx @@ -1,16 +1,59 @@ +/* eslint-disable react-native/split-platform-components */ import { createNativeStackNavigator } from '@react-navigation/native-stack'; +import { useEffect } from 'react'; +import { PermissionsAndroid, Platform } from 'react-native'; import { Route } from '../../constants/routes'; import { useIterableApp } from '../../hooks/useIterableApp'; +import type { RootStackParamList } from '../../types'; import { Login } from '../Login'; import { Main } from './Main'; -import type { RootStackParamList } from '../../types'; const Stack = createNativeStackNavigator(); +const requestNotificationPermission = async () => { + if (Platform.OS === 'android') { + const apiLevel = Platform.Version; // Get the Android API level + + if (apiLevel >= 33) { + // Check if Android 13 or higher + try { + const granted = await PermissionsAndroid.request( + PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS, + { + title: 'Notification Permission', + message: + 'This app needs access to your notifications for push, in-app messages, embedded messages and more.', + buttonNeutral: 'Ask Me Later', + buttonNegative: 'Cancel', + buttonPositive: 'OK', + } + ); + if (granted === PermissionsAndroid.RESULTS.GRANTED) { + console.log('Notification permission granted'); + } else { + console.log('Notification permission denied'); + } + } catch (err) { + console.warn(err); + } + } else { + // For Android versions below 13, notification permission is generally not required + // or is automatically granted upon app installation. + console.log( + 'Notification permission not required for this Android version.' + ); + } + } +}; + export const App = () => { const { isLoggedIn } = useIterableApp(); + useEffect(() => { + requestNotificationPermission(); + }, []); + return ( {isLoggedIn ? ( From 0ce8ecba599aaf7b404bf18f09e5e0ed7dbd8419 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Thu, 9 Oct 2025 01:09:32 -0700 Subject: [PATCH 42/45] refactor: trying to get session start and end to work --- example/src/components/Embedded/Embedded.tsx | 53 ++-- example/src/components/User/User.tsx | 21 +- .../classes/IterableEmbeddedManager.ts | 8 +- .../classes/IterableEmbeddedSessionManager.ts | 260 +++++++++--------- .../IterableEmbeddedCard.tsx | 9 +- src/embedded/hooks/useEmbeddedView.ts | 29 +- 6 files changed, 205 insertions(+), 175 deletions(-) diff --git a/example/src/components/Embedded/Embedded.tsx b/example/src/components/Embedded/Embedded.tsx index e8506111a..779e47464 100644 --- a/example/src/components/Embedded/Embedded.tsx +++ b/example/src/components/Embedded/Embedded.tsx @@ -4,17 +4,16 @@ import { IterableEmbeddedViewType, type IterableEmbeddedMessage, } from '@iterable/react-native-sdk'; -import { useIsFocused } from '@react-navigation/native'; import { useCallback, useEffect, useState } from 'react'; -import { Text, TouchableOpacity, View, ScrollView } from 'react-native'; +import { ScrollView, Text, TouchableOpacity, View } from 'react-native'; import { useIterableApp } from '../../hooks'; import styles from './Embedded.styles'; export const Embedded = () => { const { isLoggedIn } = useIterableApp(); - const isFocused = useIsFocused(); - const [hasSession, setHasSession] = useState(false); + // const isFocused = useIsFocused(); + const [hasSession] = useState(false); const [placementIds, setPlacementIds] = useState([]); const [messages, setMessages] = useState([]); @@ -42,29 +41,29 @@ export const Embedded = () => { } }, [isLoggedIn, getPlacementIds]); - useEffect(() => { - if (isFocused) { - Iterable.embeddedManager.startSession(); - Iterable.embeddedManager.syncMessages(); - Iterable.embeddedManager.getPlacementIds().then((ids: unknown) => { - console.log(ids); - setPlacementIds(ids as number[]); - Iterable.embeddedManager - .getMessages(placementIds) - .then((messageList) => { - console.log(messageList); - setMessages(messageList as IterableEmbeddedMessage[]); - setHasSession(true); - }); - }); - } else { - if (hasSession) { - Iterable.embeddedManager.endSession(); - setHasSession(false); - } - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isFocused]); + // useEffect(() => { + // if (isFocused) { + // Iterable.embeddedManager.startSession(); + // Iterable.embeddedManager.syncMessages(); + // Iterable.embeddedManager.getPlacementIds().then((ids: unknown) => { + // console.log(ids); + // setPlacementIds(ids as number[]); + // Iterable.embeddedManager + // .getMessages(placementIds) + // .then((messageList) => { + // console.log(messageList); + // setMessages(messageList as IterableEmbeddedMessage[]); + // setHasSession(true); + // }); + // }); + // } else { + // if (hasSession) { + // Iterable.embeddedManager.endSession(); + // setHasSession(false); + // } + // } + // // eslint-disable-next-line react-hooks/exhaustive-deps + // }, [isFocused]); return ( diff --git a/example/src/components/User/User.tsx b/example/src/components/User/User.tsx index 549d17ced..78105cca9 100644 --- a/example/src/components/User/User.tsx +++ b/example/src/components/User/User.tsx @@ -10,20 +10,19 @@ export const User = () => { const { logout, isLoggedIn } = useIterableApp(); const isFocused = useIsFocused(); const [loggedInAs, setLoggedInAs] = useState(''); - const [hasSession, setHasSession] = useState(false); + const [hasSession] = useState(false); const [placementIds, setPlacementIds] = useState([]); useEffect(() => { - if (isFocused) { - Iterable.embeddedManager.startSession(); - setHasSession(true); - } else { - if (hasSession) { - Iterable.embeddedManager.endSession(); - setHasSession(false); - } - } - // eslint-disable-next-line react-hooks/exhaustive-deps + // if (isFocused) { + // Iterable.embeddedManager.startSession(); + // setHasSession(true); + // } else { + // if (hasSession) { + // Iterable.embeddedManager.endSession(); + // setHasSession(false); + // } + // } }, [isFocused]); useEffect(() => { diff --git a/src/embedded/classes/IterableEmbeddedManager.ts b/src/embedded/classes/IterableEmbeddedManager.ts index 6919a82b0..556b76498 100644 --- a/src/embedded/classes/IterableEmbeddedManager.ts +++ b/src/embedded/classes/IterableEmbeddedManager.ts @@ -7,19 +7,19 @@ import { defaultConfig, defaultLogger } from '../../core/constants/defaults'; import { IterableActionSource } from '../../core/enums/IterableActionSource'; import { getActionPrefix } from '../../core/utils/getActionPrefix'; import type { IterableEmbeddedMessage } from './IterableEmbeddedMessage'; -import { IterableEmbeddedSessionManager } from './IterableEmbeddedSessionManager'; +// import { IterableEmbeddedSessionManager } from './IterableEmbeddedSessionManager'; export class IterableEmbeddedManager { logger: IterableLogger = defaultLogger; config: IterableConfig = defaultConfig; - sessionManager: IterableEmbeddedSessionManager = - new IterableEmbeddedSessionManager(defaultLogger); + // sessionManager: IterableEmbeddedSessionManager = + // new IterableEmbeddedSessionManager(defaultLogger); constructor(logger: IterableLogger, config: IterableConfig) { this.logger = logger; this.config = config; - this.sessionManager = new IterableEmbeddedSessionManager(logger); + // this.sessionManager = new IterableEmbeddedSessionManager(logger); } syncMessages() { diff --git a/src/embedded/classes/IterableEmbeddedSessionManager.ts b/src/embedded/classes/IterableEmbeddedSessionManager.ts index 4a8ee5f9e..27eff6408 100644 --- a/src/embedded/classes/IterableEmbeddedSessionManager.ts +++ b/src/embedded/classes/IterableEmbeddedSessionManager.ts @@ -1,130 +1,130 @@ -import { IterableConfig } from '../../core/classes/IterableConfig'; -import { IterableLogger } from '../../core/classes/IterableLogger'; -import { trackEmbeddedSession } from '../../core/utils/trackingUtils'; -import { IterableEmbeddedImpression } from './IterableEmbeddedImpression'; -import { IterableEmbeddedImpressionData } from './IterableEmbeddedImpressionData'; -import { IterableEmbeddedSession } from './IterableEmbeddedSession'; - -/** - * Manages the embedded session for the current user. - */ -export class IterableEmbeddedSessionManager { - private logger: IterableLogger = new IterableLogger(new IterableConfig()); - private impressions: Record = {}; - public session: IterableEmbeddedSession = new IterableEmbeddedSession({ - start: null, - end: null, - impressions: [], - }); - - constructor(logger: IterableLogger) { - this.logger = logger; - this.impressions = {}; - } - - public isTracking(): boolean { - return !!this.session?.start; - } - - public startSession() { - if (this.isTracking()) { - this.logger.log('Embedded session started twice'); - return; - } - - // TODO: figure out how to get a unique ID for the session - this.session = new IterableEmbeddedSession({ - start: new Date(), - end: null, - impressions: [], - }); - } - - public endSession() { - if (!this.isTracking()) { - this.logger.log('Embedded session ended without start'); - return; - } - - if (Object.keys(this.impressions).length > 0) { - this.endAllImpressions(); - - const sessionToTrack = new IterableEmbeddedSession({ - start: this.session.start, - end: new Date(), - impressions: this.getImpressionList(), - }); - - trackEmbeddedSession(sessionToTrack); - - //reset session for next session start - this.session = new IterableEmbeddedSession({ - start: null, - end: null, - impressions: [], - }); - - this.impressions = {}; - } - } - - public startImpression(messageId: string, placementId: number) { - let impressionData = this.impressions[messageId]; - - if (!impressionData) { - impressionData = new IterableEmbeddedImpressionData( - messageId, - placementId - ); - this.impressions[messageId] = impressionData; - } - - impressionData.start = new Date(); - } - - public pauseImpression(messageId: string) { - const impressionData = this.impressions[messageId]; - - if (!impressionData) { - this.logger.log('onMessageImpressionEnded: impressionData not found'); - return; - } - - if (!impressionData.start) { - this.logger.log('onMessageImpressionEnded: impressionStarted is null'); - return; - } - - this.updateDisplayCountAndDuration(impressionData); - } - - private endAllImpressions() { - Object.values(this.impressions).forEach((impressionData) => { - this.updateDisplayCountAndDuration(impressionData); - }); - } - - private getImpressionList(): IterableEmbeddedImpression[] { - return Object.values(this.impressions).map((impression) => { - return new IterableEmbeddedImpression({ - messageId: impression.messageId || '', - placementId: impression.placementId || 0, - displayCount: impression.displayCount || 0, - duration: impression.duration || 0, - }); - }); - } - - private updateDisplayCountAndDuration( - impressionData: IterableEmbeddedImpressionData - ): IterableEmbeddedImpressionData { - if (impressionData.start) { - impressionData.displayCount = (impressionData.displayCount || 0) + 1; - impressionData.duration = - (impressionData.duration || 0) + - (new Date().getTime() - impressionData.start.getTime()) / 1000.0; - impressionData.start = null; - } - return impressionData; - } -} +// import { IterableConfig } from '../../core/classes/IterableConfig'; +// import { IterableLogger } from '../../core/classes/IterableLogger'; +// import { trackEmbeddedSession } from '../../core/utils/trackingUtils'; +// import { IterableEmbeddedImpression } from './IterableEmbeddedImpression'; +// import { IterableEmbeddedImpressionData } from './IterableEmbeddedImpressionData'; +// import { IterableEmbeddedSession } from './IterableEmbeddedSession'; + +// /** +// * Manages the embedded session for the current user. +// */ +// export class IterableEmbeddedSessionManager { +// private logger: IterableLogger = new IterableLogger(new IterableConfig()); +// private impressions: Record = {}; +// public session: IterableEmbeddedSession = new IterableEmbeddedSession({ +// start: null, +// end: null, +// impressions: [], +// }); + +// constructor(logger: IterableLogger) { +// this.logger = logger; +// this.impressions = {}; +// } + +// public isTracking(): boolean { +// return !!this.session?.start; +// } + +// public startSession() { +// if (this.isTracking()) { +// this.logger.log('Embedded session started twice'); +// return; +// } + +// // TODO: figure out how to get a unique ID for the session +// this.session = new IterableEmbeddedSession({ +// start: new Date(), +// end: null, +// impressions: [], +// }); +// } + +// public endSession() { +// if (!this.isTracking()) { +// this.logger.log('Embedded session ended without start'); +// return; +// } + +// if (Object.keys(this.impressions).length > 0) { +// this.endAllImpressions(); + +// const sessionToTrack = new IterableEmbeddedSession({ +// start: this.session.start, +// end: new Date(), +// impressions: this.getImpressionList(), +// }); + +// trackEmbeddedSession(sessionToTrack); + +// //reset session for next session start +// this.session = new IterableEmbeddedSession({ +// start: null, +// end: null, +// impressions: [], +// }); + +// this.impressions = {}; +// } +// } + +// public startImpression(messageId: string, placementId: number) { +// let impressionData = this.impressions[messageId]; + +// if (!impressionData) { +// impressionData = new IterableEmbeddedImpressionData( +// messageId, +// placementId +// ); +// this.impressions[messageId] = impressionData; +// } + +// impressionData.start = new Date(); +// } + +// public pauseImpression(messageId: string) { +// const impressionData = this.impressions[messageId]; + +// if (!impressionData) { +// this.logger.log('onMessageImpressionEnded: impressionData not found'); +// return; +// } + +// if (!impressionData.start) { +// this.logger.log('onMessageImpressionEnded: impressionStarted is null'); +// return; +// } + +// this.updateDisplayCountAndDuration(impressionData); +// } + +// private endAllImpressions() { +// Object.values(this.impressions).forEach((impressionData) => { +// this.updateDisplayCountAndDuration(impressionData); +// }); +// } + +// private getImpressionList(): IterableEmbeddedImpression[] { +// return Object.values(this.impressions).map((impression) => { +// return new IterableEmbeddedImpression({ +// messageId: impression.messageId || '', +// placementId: impression.placementId || 0, +// displayCount: impression.displayCount || 0, +// duration: impression.duration || 0, +// }); +// }); +// } + +// private updateDisplayCountAndDuration( +// impressionData: IterableEmbeddedImpressionData +// ): IterableEmbeddedImpressionData { +// if (impressionData.start) { +// impressionData.displayCount = (impressionData.displayCount || 0) + 1; +// impressionData.duration = +// (impressionData.duration || 0) + +// (new Date().getTime() - impressionData.start.getTime()) / 1000.0; +// impressionData.start = null; +// } +// return impressionData; +// } +// } diff --git a/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx b/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx index 4c63e4222..a84e228f4 100644 --- a/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx +++ b/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx @@ -10,11 +10,15 @@ import { } from 'react-native'; import { IterableEmbeddedViewType } from '../../enums'; -import { useEmbeddedView } from '../../hooks/useEmbeddedView'; import { useComponentVisibility } from '../../hooks/useComponentVisibility'; +import { useEmbeddedView } from '../../hooks/useEmbeddedView'; import type { IterableEmbeddedComponentProps } from '../../types/IterableEmbeddedViewProps'; import { IMAGE_HEIGHT, styles } from './IterableEmbeddedCard.styles'; +/** + * TODO: Add default action click handler. See IterableEmbeddedView for functionality. + */ + export const IterableEmbeddedCard = ({ config, message, @@ -34,6 +38,9 @@ export const IterableEmbeddedCard = ({ checkInterval: 500, // Check every 500ms for navigation changes }); + // const appVisibility = useAppStateListener(); + // console.log('appVisibility', appVisibility); + useEffect(() => { console.log('RENDERED'); return () => { diff --git a/src/embedded/hooks/useEmbeddedView.ts b/src/embedded/hooks/useEmbeddedView.ts index ad0c2988c..87fc95538 100644 --- a/src/embedded/hooks/useEmbeddedView.ts +++ b/src/embedded/hooks/useEmbeddedView.ts @@ -1,4 +1,4 @@ -import { useCallback, useMemo } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { Iterable } from '../../core/classes/Iterable'; import type { IterableEmbeddedMessageElementsButton } from '../classes/IterableEmbeddedMessageElementsButton'; import { IterableEmbeddedViewType } from '../enums'; @@ -6,19 +6,22 @@ import type { IterableEmbeddedComponentProps } from '../types/IterableEmbeddedVi import { getMedia } from '../utils/getMedia'; import { getStyles } from '../utils/getStyles'; import { getUrlFromButton } from '../utils/getUrlFromButton'; +import { useAppStateListener } from '../../core/hooks/useAppStateListener'; export const useEmbeddedView = ( viewType: IterableEmbeddedViewType, { message, config, onButtonClick = () => {} }: IterableEmbeddedComponentProps ) => { + const appVisibility = useAppStateListener(); const parsedStyles = useMemo(() => { return getStyles(viewType, config); }, [viewType, config]); - const media = useMemo(() => { return getMedia(viewType, message); }, [viewType, message]); + const [lastState, setLastState] = useState('initial'); + const handleButtonClick = useCallback( (button: IterableEmbeddedMessageElementsButton) => { onButtonClick(button); @@ -31,6 +34,28 @@ export const useEmbeddedView = ( [onButtonClick, message] ); + useEffect(() => { + console.group('useEmbeddedView'); + console.log('appVisibility changed'); + console.log('appVisibility', appVisibility); + console.log('lastState', lastState); + if (appVisibility !== lastState) { + console.log('setting lastState'); + setLastState(appVisibility); + if (appVisibility === 'active') { + console.log('appVisibility is active'); + Iterable.embeddedManager.startSession(); + } else if ( + appVisibility === 'background' || + appVisibility === 'inactive' + ) { + console.log('appVisibility is background or inactive'); + Iterable.embeddedManager.endSession(); + } + } + console.groupEnd(); + }, [appVisibility, lastState]); + return { parsedStyles, media, From e0c27e078aed578703f4380f0494ea6a3a195514 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Thu, 9 Oct 2025 01:53:26 -0700 Subject: [PATCH 43/45] refactor: reorganize IterableEmbedded types and interfaces for better structure --- src/core/classes/IterableApi.ts | 2 +- src/core/utils/trackingUtils.ts | 2 +- .../classes/IterableEmbeddedManager.ts | 10 +- .../classes/IterableEmbeddedMessage.ts | 55 ---- .../IterableEmbeddedMessageElements.ts | 83 ------ .../IterableEmbeddedMessageElementsButton.ts | 62 ----- ...ableEmbeddedMessageElementsButtonAction.ts | 42 --- ...bleEmbeddedMessageElementsDefaultAction.ts | 42 --- .../IterableEmbeddedMessageElementsText.ts | 48 ---- .../IterableEmbeddedMessageMetadata.ts | 47 ---- .../classes/IterableEmbeddedPlacement.ts | 39 --- .../classes/IterableEmbeddedSession.ts | 2 +- .../classes/IterableEmbeddedSessionManager.ts | 261 +++++++++--------- src/embedded/classes/IterableEmbeddedView.ts | 181 ------------ .../classes/IterableEmbeddedViewConfig.ts | 66 ----- src/embedded/hooks/useEmbeddedView.ts | 2 +- src/embedded/types/IterableEmbeddedMessage.ts | 8 + .../types/IterableEmbeddedMessageElements.ts | 13 + .../IterableEmbeddedMessageElementsButton.ts | 10 + ...ableEmbeddedMessageElementsButtonAction.ts | 6 + ...bleEmbeddedMessageElementsDefaultAction.ts | 6 + .../IterableEmbeddedMessageElementsText.ts | 8 + .../types/IterableEmbeddedMessageMetadata.ts | 6 + .../types/IterableEmbeddedPlacement.ts | 6 + .../types/IterableEmbeddedViewConfig.ts | 27 ++ .../types/IterableEmbeddedViewProps.ts | 6 +- src/embedded/utils/getButtons.ts | 4 +- src/embedded/utils/getDefaultActionUrl.ts | 2 +- src/embedded/utils/getMedia.ts | 2 +- src/embedded/utils/getStyles.ts | 2 +- src/embedded/utils/getUrlFromButton.ts | 2 +- src/index.tsx | 4 +- 32 files changed, 241 insertions(+), 815 deletions(-) delete mode 100644 src/embedded/classes/IterableEmbeddedMessage.ts delete mode 100644 src/embedded/classes/IterableEmbeddedMessageElements.ts delete mode 100644 src/embedded/classes/IterableEmbeddedMessageElementsButton.ts delete mode 100644 src/embedded/classes/IterableEmbeddedMessageElementsButtonAction.ts delete mode 100644 src/embedded/classes/IterableEmbeddedMessageElementsDefaultAction.ts delete mode 100644 src/embedded/classes/IterableEmbeddedMessageElementsText.ts delete mode 100644 src/embedded/classes/IterableEmbeddedMessageMetadata.ts delete mode 100644 src/embedded/classes/IterableEmbeddedPlacement.ts delete mode 100644 src/embedded/classes/IterableEmbeddedView.ts delete mode 100644 src/embedded/classes/IterableEmbeddedViewConfig.ts create mode 100644 src/embedded/types/IterableEmbeddedMessage.ts create mode 100644 src/embedded/types/IterableEmbeddedMessageElements.ts create mode 100644 src/embedded/types/IterableEmbeddedMessageElementsButton.ts create mode 100644 src/embedded/types/IterableEmbeddedMessageElementsButtonAction.ts create mode 100644 src/embedded/types/IterableEmbeddedMessageElementsDefaultAction.ts create mode 100644 src/embedded/types/IterableEmbeddedMessageElementsText.ts create mode 100644 src/embedded/types/IterableEmbeddedMessageMetadata.ts create mode 100644 src/embedded/types/IterableEmbeddedPlacement.ts create mode 100644 src/embedded/types/IterableEmbeddedViewConfig.ts diff --git a/src/core/classes/IterableApi.ts b/src/core/classes/IterableApi.ts index bee62bcc6..1b3f6813a 100644 --- a/src/core/classes/IterableApi.ts +++ b/src/core/classes/IterableApi.ts @@ -1,7 +1,7 @@ import { Platform } from 'react-native'; import RNIterableAPI from '../../api'; -import type { IterableEmbeddedMessage } from '../../embedded/classes/IterableEmbeddedMessage'; +import type { IterableEmbeddedMessage } from '../../embedded/types/IterableEmbeddedMessage'; import type { IterableEmbeddedSession } from '../../embedded/classes/IterableEmbeddedSession'; import type { IterableHtmlInAppContent } from '../../inApp/classes/IterableHtmlInAppContent'; import type { IterableInAppMessage } from '../../inApp/classes/IterableInAppMessage'; diff --git a/src/core/utils/trackingUtils.ts b/src/core/utils/trackingUtils.ts index 2c0c4f5f0..1f000d809 100644 --- a/src/core/utils/trackingUtils.ts +++ b/src/core/utils/trackingUtils.ts @@ -4,7 +4,7 @@ import type { IterableCommerceItem } from '../classes/IterableCommerceItem'; import type { IterableInAppMessage } from '../../inApp/classes/IterableInAppMessage'; import type { IterableInAppLocation } from '../../inApp/enums/IterableInAppLocation'; import type { IterableInAppCloseSource } from '../../inApp/enums/IterableInAppCloseSource'; -import type { IterableEmbeddedMessage } from '../../embedded/classes/IterableEmbeddedMessage'; +import type { IterableEmbeddedMessage } from '../../embedded/types/IterableEmbeddedMessage'; /** * Create a `pushOpen` event on the current user's Iterable profile, populating diff --git a/src/embedded/classes/IterableEmbeddedManager.ts b/src/embedded/classes/IterableEmbeddedManager.ts index 556b76498..9ac3c8bae 100644 --- a/src/embedded/classes/IterableEmbeddedManager.ts +++ b/src/embedded/classes/IterableEmbeddedManager.ts @@ -6,20 +6,20 @@ import type { IterableLogger } from '../../core/classes/IterableLogger'; import { defaultConfig, defaultLogger } from '../../core/constants/defaults'; import { IterableActionSource } from '../../core/enums/IterableActionSource'; import { getActionPrefix } from '../../core/utils/getActionPrefix'; -import type { IterableEmbeddedMessage } from './IterableEmbeddedMessage'; -// import { IterableEmbeddedSessionManager } from './IterableEmbeddedSessionManager'; +import type { IterableEmbeddedMessage } from '../types/IterableEmbeddedMessage'; +import { IterableEmbeddedSessionManager } from './IterableEmbeddedSessionManager'; export class IterableEmbeddedManager { logger: IterableLogger = defaultLogger; config: IterableConfig = defaultConfig; - // sessionManager: IterableEmbeddedSessionManager = - // new IterableEmbeddedSessionManager(defaultLogger); + sessionManager: IterableEmbeddedSessionManager = + new IterableEmbeddedSessionManager(defaultLogger); constructor(logger: IterableLogger, config: IterableConfig) { this.logger = logger; this.config = config; - // this.sessionManager = new IterableEmbeddedSessionManager(logger); + this.sessionManager = new IterableEmbeddedSessionManager(logger); } syncMessages() { diff --git a/src/embedded/classes/IterableEmbeddedMessage.ts b/src/embedded/classes/IterableEmbeddedMessage.ts deleted file mode 100644 index 39533cbbb..000000000 --- a/src/embedded/classes/IterableEmbeddedMessage.ts +++ /dev/null @@ -1,55 +0,0 @@ -import type { IterableEmbeddedMessageElementsDict } from './IterableEmbeddedMessageElements'; -import type { IterableEmbeddedMessageMetadataDict } from './IterableEmbeddedMessageMetadata'; - -export interface IterableEmbeddedMessageDict { - metadata: IterableEmbeddedMessageMetadataDict; - elements?: IterableEmbeddedMessageElementsDict | null; - payload?: Record | null; -} - -export interface IterableEmbeddedMessage { - metadata: IterableEmbeddedMessageMetadataDict; - elements?: IterableEmbeddedMessageElementsDict | null; - payload?: Record | null; -} - -// export class IterableEmbeddedMessage { -// public metadata: IterableEmbeddedMessageMetadata; -// public elements?: IterableEmbeddedMessageElements | null = null; -// public payload?: Record | null = null; - -// constructor( -// metadata: IterableEmbeddedMessageMetadata, -// options: { -// elements?: IterableEmbeddedMessageElements | null; -// payload?: Record | null; -// } = {} -// ) { -// const { elements = null, payload = null } = options; -// this.metadata = metadata; -// this.elements = elements; -// this.payload = payload; -// } - -// toDict(): IterableEmbeddedMessageDict { -// return { -// metadata: this.metadata.toDict(), -// elements: this.elements ? this.elements.toDict() : null, -// payload: this.payload ?? null, -// }; -// } - -// static fromDict( -// jsonObject: IterableEmbeddedMessageDict -// ): IterableEmbeddedMessage { -// const metadata = IterableEmbeddedMessageMetadata.fromDict( -// jsonObject.metadata -// ); -// const elements = IterableEmbeddedMessageElements.fromDict( -// jsonObject.elements ?? null -// ); -// const payload = jsonObject.payload ?? null; - -// return new IterableEmbeddedMessage(metadata, { elements, payload }); -// } -// } diff --git a/src/embedded/classes/IterableEmbeddedMessageElements.ts b/src/embedded/classes/IterableEmbeddedMessageElements.ts deleted file mode 100644 index cfe46fc3e..000000000 --- a/src/embedded/classes/IterableEmbeddedMessageElements.ts +++ /dev/null @@ -1,83 +0,0 @@ -import type { IterableEmbeddedMessageElementsButton } from './IterableEmbeddedMessageElementsButton'; -import type { IterableEmbeddedMessageElementsDefaultAction } from './IterableEmbeddedMessageElementsDefaultAction'; -import type { IterableEmbeddedMessageElementsText } from './IterableEmbeddedMessageElementsText'; - -export interface IterableEmbeddedMessageElementsDict { - title?: string | null; - body?: string | null; - mediaUrl?: string | null; - mediaUrlCaption?: string | null; - defaultAction?: IterableEmbeddedMessageElementsDefaultAction | null; - buttons?: IterableEmbeddedMessageElementsButton[] | null; - text?: IterableEmbeddedMessageElementsText[] | null; -} - -export interface IterableEmbeddedMessageElements { - title?: string | null; - body?: string | null; - mediaUrl?: string | null; - mediaUrlCaption?: string | null; - defaultAction?: IterableEmbeddedMessageElementsDefaultAction | null; - buttons?: IterableEmbeddedMessageElementsButton[] | null; - text?: IterableEmbeddedMessageElementsText[] | null; -} - -// export class IterableEmbeddedMessageElements { -// public title?: IterableEmbeddedMessageElementsDict['title'] = null; -// public body?: IterableEmbeddedMessageElementsDict['body'] = null; -// public mediaURL?: IterableEmbeddedMessageElementsDict['mediaURL'] = null; -// public mediaUrlCaption?: IterableEmbeddedMessageElementsDict['mediaUrlCaption'] = -// null; -// public defaultAction?: IterableEmbeddedMessageElementsDict['defaultAction'] = -// null; -// public buttons?: IterableEmbeddedMessageElementsDict['buttons'] = null; -// public text?: IterableEmbeddedMessageElementsDict['text'] = null; - -// constructor(options: Partial = {}) { -// const { -// title = null, -// body = null, -// mediaURL = null, -// mediaUrlCaption = null, -// defaultAction = null, -// buttons = null, -// text = null, -// } = options; - -// this.title = title; -// this.body = body; -// this.mediaURL = mediaURL; -// this.mediaUrlCaption = mediaUrlCaption; -// this.defaultAction = defaultAction; -// this.buttons = buttons; -// this.text = text; -// } - -// toDict(): IterableEmbeddedMessageElementsDict { -// return { -// title: this.title, -// body: this.body, -// mediaURL: this.mediaURL, -// mediaUrlCaption: this.mediaUrlCaption, -// defaultAction: this.defaultAction, -// buttons: this.buttons, -// text: this.text, -// }; -// } - -// static fromDict( -// jsonObject?: IterableEmbeddedMessageElementsDict | null -// ): IterableEmbeddedMessageElements | null { -// if (!jsonObject) return null; - -// return new IterableEmbeddedMessageElements({ -// title: jsonObject.title ?? null, -// body: jsonObject.body ?? null, -// mediaURL: jsonObject.mediaURL ?? null, -// mediaUrlCaption: jsonObject.mediaUrlCaption ?? null, -// defaultAction: jsonObject.defaultAction ?? null, -// buttons: jsonObject.buttons ?? null, -// text: jsonObject.text ?? null, -// }); -// } -// } diff --git a/src/embedded/classes/IterableEmbeddedMessageElementsButton.ts b/src/embedded/classes/IterableEmbeddedMessageElementsButton.ts deleted file mode 100644 index d7cd4da85..000000000 --- a/src/embedded/classes/IterableEmbeddedMessageElementsButton.ts +++ /dev/null @@ -1,62 +0,0 @@ -import type { IterableEmbeddedMessageElementsButtonAction } from './IterableEmbeddedMessageElementsButtonAction'; - -export interface IterableEmbeddedMessageElementsButtonDict { - /** The ID. */ - id: string; - /** The title. */ - title?: string | null; - /** The action. */ - action?: IterableEmbeddedMessageElementsButtonAction | null; -} - -export interface IterableEmbeddedMessageElementsButton { - /** The ID. */ - id: string; - /** The title. */ - title?: string | null; - /** The action. */ - action?: IterableEmbeddedMessageElementsButtonAction | null; -} - -// export class IterableEmbeddedMessageElementsButton { -// public id: string; -// public title?: string | null; -// public action?: IterableEmbeddedMessageElementsButtonAction | null; - -// constructor( -// options: Partial = {} -// ) { -// const { id = '', title = null, action = null } = options; -// this.id = id; -// this.title = title; -// this.action = action; -// } - -// toDict(): IterableEmbeddedMessageElementsButtonDict { -// return { -// id: this.id, -// title: this.title ?? null, -// action: this.action ?? null, -// }; -// } - -// static fromDict( -// jsonObject: IterableEmbeddedMessageElementsButtonDict -// ): IterableEmbeddedMessageElementsButton { -// if (!jsonObject?.id) { -// throw new Error( -// 'id is required when calling IterableEmbeddedMessageElementsButton.fromDict' -// ); -// } - -// return new IterableEmbeddedMessageElementsButton({ -// id: jsonObject.id, -// title: jsonObject.title ?? null, -// action: jsonObject.action -// ? IterableEmbeddedMessageElementsButtonAction.fromDict( -// jsonObject.action -// ) -// : null, -// }); -// } -// } diff --git a/src/embedded/classes/IterableEmbeddedMessageElementsButtonAction.ts b/src/embedded/classes/IterableEmbeddedMessageElementsButtonAction.ts deleted file mode 100644 index d708681af..000000000 --- a/src/embedded/classes/IterableEmbeddedMessageElementsButtonAction.ts +++ /dev/null @@ -1,42 +0,0 @@ -export interface IterableEmbeddedMessageElementsButtonActionDict { - /** The type. */ - type: string; - /** The data. */ - data?: string; -} - -export interface IterableEmbeddedMessageElementsButtonAction { - /** The type. */ - type: string; - /** The data. */ - data?: string; -} - -// export class IterableEmbeddedMessageElementsButtonAction { -// public type: string; -// public data?: string; - -// constructor( -// options: Partial = {} -// ) { -// const { type = '', data = '' } = options; -// this.type = type; -// this.data = data; -// } - -// toDict(): IterableEmbeddedMessageElementsButtonActionDict { -// return { -// type: this.type, -// data: this.data, -// }; -// } - -// static fromDict( -// jsonObject: IterableEmbeddedMessageElementsButtonActionDict -// ): IterableEmbeddedMessageElementsButtonAction { -// return new IterableEmbeddedMessageElementsButtonAction({ -// type: jsonObject.type, -// data: jsonObject.data, -// }); -// } -// } diff --git a/src/embedded/classes/IterableEmbeddedMessageElementsDefaultAction.ts b/src/embedded/classes/IterableEmbeddedMessageElementsDefaultAction.ts deleted file mode 100644 index adfefad2c..000000000 --- a/src/embedded/classes/IterableEmbeddedMessageElementsDefaultAction.ts +++ /dev/null @@ -1,42 +0,0 @@ -export interface IterableEmbeddedMessageElementsDefaultActionDict { - /** The type. */ - type: string; - /** The data. */ - data?: string; -} - -export interface IterableEmbeddedMessageElementsDefaultAction { - /** The type. */ - type: string; - /** The data. */ - data?: string; -} - -// export class IterableEmbeddedMessageElementsDefaultAction { -// public type: string; -// public data?: string; - -// constructor( -// options: Partial = {} -// ) { -// const { type = '', data = '' } = options; -// this.type = type; -// this.data = data; -// } - -// toDict(): IterableEmbeddedMessageElementsDefaultActionDict { -// return { -// type: this.type, -// data: this.data, -// }; -// } - -// static fromDict( -// jsonObject: IterableEmbeddedMessageElementsDefaultActionDict -// ): IterableEmbeddedMessageElementsDefaultAction { -// return new IterableEmbeddedMessageElementsDefaultAction({ -// type: jsonObject.type, -// data: jsonObject.data, -// }); -// } -// } diff --git a/src/embedded/classes/IterableEmbeddedMessageElementsText.ts b/src/embedded/classes/IterableEmbeddedMessageElementsText.ts deleted file mode 100644 index 54537959a..000000000 --- a/src/embedded/classes/IterableEmbeddedMessageElementsText.ts +++ /dev/null @@ -1,48 +0,0 @@ -export interface IterableEmbeddedMessageElementsTextDict { - /** The ID. */ - id: string; - /** The text. */ - text?: string | null; - /** The label. */ - label?: string | null; -} - -export interface IterableEmbeddedMessageElementsText { - /** The ID. */ - id: string; - /** The text. */ - text?: string | null; - /** The label. */ - label?: string | null; -} - -// export class IterableEmbeddedMessageElementsText { -// public id: string; -// public text?: string | null; -// public label?: string | null; - -// constructor(options: Partial = {}) { -// const { id = '', text = null, label = null } = options; -// this.id = id; -// this.text = text; -// this.label = label; -// } - -// toDict(): IterableEmbeddedMessageElementsTextDict { -// return { -// id: this.id, -// text: this.text, -// label: this.label, -// }; -// } - -// static fromDict( -// jsonObject: IterableEmbeddedMessageElementsTextDict -// ): IterableEmbeddedMessageElementsText { -// return new IterableEmbeddedMessageElementsText({ -// id: jsonObject.id, -// text: jsonObject.text, -// label: jsonObject.label, -// }); -// } -// } diff --git a/src/embedded/classes/IterableEmbeddedMessageMetadata.ts b/src/embedded/classes/IterableEmbeddedMessageMetadata.ts deleted file mode 100644 index bdd33a089..000000000 --- a/src/embedded/classes/IterableEmbeddedMessageMetadata.ts +++ /dev/null @@ -1,47 +0,0 @@ -export interface IterableEmbeddedMessageMetadataDict { - messageId: string; - placementId: number; - campaignId?: number | null; - isProof?: boolean; -} - -export class IterableEmbeddedMessageMetadata { - public messageId: string; - public placementId: number; - public campaignId?: number | null = null; - public isProof: boolean = false; - - constructor(options: Partial = {}) { - const { - messageId = '', - placementId = 0, - campaignId = null, - isProof = false, - } = options; - - this.messageId = messageId; - this.placementId = placementId; - this.campaignId = campaignId; - this.isProof = isProof; - } - - toDict(): IterableEmbeddedMessageMetadataDict { - return { - messageId: this.messageId, - placementId: this.placementId, - campaignId: this.campaignId ?? null, - isProof: this.isProof, - }; - } - - static fromDict( - jsonObject: IterableEmbeddedMessageMetadataDict - ): IterableEmbeddedMessageMetadata { - return new IterableEmbeddedMessageMetadata({ - messageId: jsonObject.messageId, - placementId: jsonObject.placementId, - campaignId: jsonObject.campaignId ?? null, - isProof: jsonObject.isProof ?? false, - }); - } -} diff --git a/src/embedded/classes/IterableEmbeddedPlacement.ts b/src/embedded/classes/IterableEmbeddedPlacement.ts deleted file mode 100644 index a1ada3e53..000000000 --- a/src/embedded/classes/IterableEmbeddedPlacement.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { IterableEmbeddedMessageDict } from './IterableEmbeddedMessage'; - -export interface IterableEmbeddedPlacementDict { - placementId: number; - messages: IterableEmbeddedMessageDict[]; -} - -export interface IterableEmbeddedPlacement { - placementId: number; - messages: IterableEmbeddedMessageDict[]; -} - -// export class IterableEmbeddedPlacement { -// public placementId: number; -// public messages: IterableEmbeddedMessage[]; - -// constructor(placementId: number, messages: IterableEmbeddedMessage[]) { -// this.placementId = placementId; -// this.messages = messages; -// } - -// toDict(): IterableEmbeddedPlacementDict { -// return { -// placementId: this.placementId, -// messages: this.messages.map((m) => m.toDict()), -// }; -// } - -// static fromDict( -// jsonObject: IterableEmbeddedPlacementDict -// ): IterableEmbeddedPlacement { -// const placementId = jsonObject.placementId; -// const messages = (jsonObject.messages ?? []).map((m) => -// IterableEmbeddedMessage.fromDict(m) -// ); - -// return new IterableEmbeddedPlacement(placementId, messages); -// } -// } diff --git a/src/embedded/classes/IterableEmbeddedSession.ts b/src/embedded/classes/IterableEmbeddedSession.ts index 2c9bf83d6..7cf920dfa 100644 --- a/src/embedded/classes/IterableEmbeddedSession.ts +++ b/src/embedded/classes/IterableEmbeddedSession.ts @@ -1,5 +1,5 @@ -import type { IterableEmbeddedImpressionData } from './IterableEmbeddedImpressionData'; import { generateUUID } from '../../core/utils/generateUUID'; +import type { IterableEmbeddedImpressionData } from './IterableEmbeddedImpressionData'; export interface IterableEmbeddedSessionDict { /** The ID of the session. */ diff --git a/src/embedded/classes/IterableEmbeddedSessionManager.ts b/src/embedded/classes/IterableEmbeddedSessionManager.ts index 27eff6408..66a1d91bb 100644 --- a/src/embedded/classes/IterableEmbeddedSessionManager.ts +++ b/src/embedded/classes/IterableEmbeddedSessionManager.ts @@ -1,130 +1,131 @@ -// import { IterableConfig } from '../../core/classes/IterableConfig'; -// import { IterableLogger } from '../../core/classes/IterableLogger'; -// import { trackEmbeddedSession } from '../../core/utils/trackingUtils'; -// import { IterableEmbeddedImpression } from './IterableEmbeddedImpression'; -// import { IterableEmbeddedImpressionData } from './IterableEmbeddedImpressionData'; -// import { IterableEmbeddedSession } from './IterableEmbeddedSession'; - -// /** -// * Manages the embedded session for the current user. -// */ -// export class IterableEmbeddedSessionManager { -// private logger: IterableLogger = new IterableLogger(new IterableConfig()); -// private impressions: Record = {}; -// public session: IterableEmbeddedSession = new IterableEmbeddedSession({ -// start: null, -// end: null, -// impressions: [], -// }); - -// constructor(logger: IterableLogger) { -// this.logger = logger; -// this.impressions = {}; -// } - -// public isTracking(): boolean { -// return !!this.session?.start; -// } - -// public startSession() { -// if (this.isTracking()) { -// this.logger.log('Embedded session started twice'); -// return; -// } - -// // TODO: figure out how to get a unique ID for the session -// this.session = new IterableEmbeddedSession({ -// start: new Date(), -// end: null, -// impressions: [], -// }); -// } - -// public endSession() { -// if (!this.isTracking()) { -// this.logger.log('Embedded session ended without start'); -// return; -// } - -// if (Object.keys(this.impressions).length > 0) { -// this.endAllImpressions(); - -// const sessionToTrack = new IterableEmbeddedSession({ -// start: this.session.start, -// end: new Date(), -// impressions: this.getImpressionList(), -// }); - -// trackEmbeddedSession(sessionToTrack); - -// //reset session for next session start -// this.session = new IterableEmbeddedSession({ -// start: null, -// end: null, -// impressions: [], -// }); - -// this.impressions = {}; -// } -// } - -// public startImpression(messageId: string, placementId: number) { -// let impressionData = this.impressions[messageId]; - -// if (!impressionData) { -// impressionData = new IterableEmbeddedImpressionData( -// messageId, -// placementId -// ); -// this.impressions[messageId] = impressionData; -// } - -// impressionData.start = new Date(); -// } - -// public pauseImpression(messageId: string) { -// const impressionData = this.impressions[messageId]; - -// if (!impressionData) { -// this.logger.log('onMessageImpressionEnded: impressionData not found'); -// return; -// } - -// if (!impressionData.start) { -// this.logger.log('onMessageImpressionEnded: impressionStarted is null'); -// return; -// } - -// this.updateDisplayCountAndDuration(impressionData); -// } - -// private endAllImpressions() { -// Object.values(this.impressions).forEach((impressionData) => { -// this.updateDisplayCountAndDuration(impressionData); -// }); -// } - -// private getImpressionList(): IterableEmbeddedImpression[] { -// return Object.values(this.impressions).map((impression) => { -// return new IterableEmbeddedImpression({ -// messageId: impression.messageId || '', -// placementId: impression.placementId || 0, -// displayCount: impression.displayCount || 0, -// duration: impression.duration || 0, -// }); -// }); -// } - -// private updateDisplayCountAndDuration( -// impressionData: IterableEmbeddedImpressionData -// ): IterableEmbeddedImpressionData { -// if (impressionData.start) { -// impressionData.displayCount = (impressionData.displayCount || 0) + 1; -// impressionData.duration = -// (impressionData.duration || 0) + -// (new Date().getTime() - impressionData.start.getTime()) / 1000.0; -// impressionData.start = null; -// } -// return impressionData; -// } -// } +import { IterableConfig } from '../../core/classes/IterableConfig'; +import { IterableLogger } from '../../core/classes/IterableLogger'; +import { trackEmbeddedSession } from '../../core/utils/trackingUtils'; +import { IterableEmbeddedImpression } from './IterableEmbeddedImpression'; +import { IterableEmbeddedImpressionData } from './IterableEmbeddedImpressionData'; +import { IterableEmbeddedSession } from './IterableEmbeddedSession'; + +/** + * Manages the embedded session for the current user. + * TODO: figure out if we need this + */ +export class IterableEmbeddedSessionManager { + private logger: IterableLogger = new IterableLogger(new IterableConfig()); + private impressions: Record = {}; + public session: IterableEmbeddedSession = new IterableEmbeddedSession({ + start: null, + end: null, + impressions: [], + }); + + constructor(logger: IterableLogger) { + this.logger = logger; + this.impressions = {}; + } + + public isTracking(): boolean { + return !!this.session?.start; + } + + public startSession() { + if (this.isTracking()) { + this.logger.log('Embedded session started twice'); + return; + } + + this.session = new IterableEmbeddedSession({ + start: new Date(), + end: null, + impressions: [], + }); + } + + public endSession() { + if (!this.isTracking()) { + this.logger.log('Embedded session ended without start'); + return; + } + + if (Object.keys(this.impressions).length > 0) { + this.endAllImpressions(); + + const sessionToTrack = new IterableEmbeddedSession({ + start: this.session.start, + end: new Date(), + impressions: this.getImpressionList(), + }); + + trackEmbeddedSession(sessionToTrack); + + //reset session for next session start + this.session = new IterableEmbeddedSession({ + start: null, + end: null, + impressions: [], + }); + + this.impressions = {}; + } + } + + public startImpression(messageId: string, placementId: number) { + let impressionData: IterableEmbeddedImpressionData | undefined = + this.impressions[messageId]; + + if (!impressionData) { + impressionData = new IterableEmbeddedImpressionData( + messageId, + placementId + ); + this.impressions[messageId] = impressionData; + } + + impressionData.start = new Date(); + } + + public pauseImpression(messageId: string) { + const impressionData = this.impressions[messageId]; + + if (!impressionData) { + this.logger.log('onMessageImpressionEnded: impressionData not found'); + return; + } + + if (!impressionData.start) { + this.logger.log('onMessageImpressionEnded: impressionStarted is null'); + return; + } + + this.updateDisplayCountAndDuration(impressionData); + } + + private endAllImpressions() { + Object.values(this.impressions).forEach((impressionData) => { + this.updateDisplayCountAndDuration(impressionData); + }); + } + + private getImpressionList(): IterableEmbeddedImpression[] { + return Object.values(this.impressions).map((impression) => { + return new IterableEmbeddedImpression({ + messageId: impression.messageId || '', + placementId: impression.placementId || 0, + displayCount: impression.displayCount || 0, + duration: impression.duration || 0, + }); + }); + } + + private updateDisplayCountAndDuration( + impressionData: IterableEmbeddedImpressionData + ): IterableEmbeddedImpressionData { + if (impressionData.start) { + impressionData.displayCount = (impressionData.displayCount || 0) + 1; + impressionData.duration = + (impressionData.duration || 0) + + (new Date().getTime() - impressionData.start.getTime()) / 1000.0; + impressionData.start = null; + } + return impressionData; + } +} diff --git a/src/embedded/classes/IterableEmbeddedView.ts b/src/embedded/classes/IterableEmbeddedView.ts deleted file mode 100644 index 191e44f4a..000000000 --- a/src/embedded/classes/IterableEmbeddedView.ts +++ /dev/null @@ -1,181 +0,0 @@ -// import { IterableEmbeddedViewType } from '../enums/IterableEmbeddedViewType'; -// import { IterableEmbeddedMessage } from './IterableEmbeddedMessage'; -// import { IterableEmbeddedViewConfig } from './IterableEmbeddedViewConfig'; -// import type { IterableEmbeddedMessageElementsButton } from './IterableEmbeddedMessageElementsButton'; -// import type { IterableEmbeddedViewButtonInfo } from '../types/IterableEmbeddedViewButtonInfo'; -// import type { IterableEmbeddedViewStyles } from '../types/IterableEmbeddedViewStyles'; -// import { -// embeddedBackgroundColors, -// embeddedBodyTextColors, -// embeddedBorderColors, -// embeddedPrimaryBtnBackgroundColors, -// embeddedPrimaryBtnTextColors, -// embeddedSecondaryBtnBackgroundColors, -// embeddedSecondaryBtnTextColors, -// embeddedTitleTextColors, -// } from '../constants/embeddedViewDefaults'; - -// export class IterableEmbeddedView { -// private viewType: IterableEmbeddedViewType; -// private message: IterableEmbeddedMessage; -// private config?: IterableEmbeddedViewConfig | null; - -// private readonly defaultBackgroundColor: number; -// private readonly defaultBorderColor: number; -// private readonly defaultPrimaryBtnBackgroundColor: number; -// private readonly defaultPrimaryBtnTextColor: number; -// private readonly defaultSecondaryBtnBackgroundColor: number; -// private readonly defaultSecondaryBtnTextColor: number; -// private readonly defaultTitleTextColor: number; -// private readonly defaultBodyTextColor: number; -// private readonly defaultBorderWidth: number = 1; -// private readonly defaultBorderCornerRadius: number = 8.0; - -// constructor( -// viewType: IterableEmbeddedViewType, -// message: IterableEmbeddedMessage, -// config?: IterableEmbeddedViewConfig | null -// ) { -// this.viewType = viewType; -// this.message = message; -// this.config = config ?? null; - -// // Default color values are placeholders; caller can map them to theme values if needed. -// // These numeric values mirror the Kotlin use of Int colors. -// this.defaultBackgroundColor = this.getDefaultColor( -// viewType, -// embeddedBackgroundColors.notification, -// embeddedBackgroundColors.card, -// embeddedBackgroundColors.banner -// ); -// this.defaultBorderColor = this.getDefaultColor( -// viewType, -// embeddedBorderColors.notification, -// embeddedBorderColors.card, -// embeddedBorderColors.banner -// ); -// this.defaultPrimaryBtnBackgroundColor = this.getDefaultColor( -// viewType, -// embeddedPrimaryBtnBackgroundColors.notification, -// embeddedPrimaryBtnBackgroundColors.card, -// embeddedPrimaryBtnBackgroundColors.banner -// ); -// this.defaultPrimaryBtnTextColor = this.getDefaultColor( -// viewType, -// embeddedPrimaryBtnTextColors.notification, -// embeddedPrimaryBtnTextColors.card, -// embeddedPrimaryBtnTextColors.banner -// ); -// this.defaultSecondaryBtnBackgroundColor = this.getDefaultColor( -// viewType, -// embeddedSecondaryBtnBackgroundColors.notification, -// embeddedSecondaryBtnBackgroundColors.card, -// embeddedSecondaryBtnBackgroundColors.banner -// ); -// this.defaultSecondaryBtnTextColor = this.getDefaultColor( -// viewType, -// embeddedSecondaryBtnTextColors.notification, -// embeddedSecondaryBtnTextColors.card, -// embeddedSecondaryBtnTextColors.banner -// ); -// this.defaultTitleTextColor = this.getDefaultColor( -// viewType, -// embeddedTitleTextColors.notification, -// embeddedTitleTextColors.card, -// embeddedTitleTextColors.banner -// ); -// this.defaultBodyTextColor = this.getDefaultColor( -// viewType, -// embeddedBodyTextColors.notification, -// embeddedBodyTextColors.card, -// embeddedBodyTextColors.banner -// ); -// } - -// getStyles(): IterableEmbeddedViewStyles { -// const c = this.config; -// return { -// backgroundColor: c?.backgroundColor ?? this.defaultBackgroundColor, -// borderColor: c?.borderColor ?? this.defaultBorderColor, -// borderWidth: c?.borderWidth ?? this.defaultBorderWidth, -// borderCornerRadius: -// c?.borderCornerRadius ?? this.defaultBorderCornerRadius, -// primaryBtnBackgroundColor: -// c?.primaryBtnBackgroundColor ?? this.defaultPrimaryBtnBackgroundColor, -// primaryBtnTextColor: -// c?.primaryBtnTextColor ?? this.defaultPrimaryBtnTextColor, -// secondaryBtnBackgroundColor: -// c?.secondaryBtnBackgroundColor ?? -// this.defaultSecondaryBtnBackgroundColor, -// secondaryBtnTextColor: -// c?.secondaryBtnTextColor ?? this.defaultSecondaryBtnTextColor, -// titleTextColor: c?.titleTextColor ?? this.defaultTitleTextColor, -// bodyTextColor: c?.bodyTextColor ?? this.defaultBodyTextColor, -// }; -// } - -// getTitle(): string | null | undefined { -// return this.message.elements?.title ?? null; -// } - -// getBody(): string | null | undefined { -// return this.message.elements?.body ?? null; -// } - -// getMedia(): { -// url?: string | null; -// caption?: string | null; -// shouldShow: boolean; -// } { -// const url = this.message.elements?.mediaURL ?? null; -// const caption = this.message.elements?.mediaUrlCaption ?? null; -// const shouldShow = -// !!url && -// url.length > 0 && -// this.viewType !== IterableEmbeddedViewType.Notification; -// return { url, caption, shouldShow }; -// } - -// getButtons(): IterableEmbeddedViewButtonInfo[] { -// const buttons = this.message.elements?.buttons ?? null; -// if (!buttons || buttons.length === 0) return []; - -// const mapOne = ( -// b?: IterableEmbeddedMessageElementsButton | null -// ): IterableEmbeddedViewButtonInfo => { -// if (!b) return { id: null, title: null, clickedUrl: null }; -// const clickedUrl = -// (b.action?.data && b.action?.data?.length > 0 -// ? b.action.data -// : b.action?.type) ?? null; -// return { id: b.id ?? null, title: b.title ?? null, clickedUrl }; -// }; - -// const first = mapOne(buttons[0] ?? null); -// const second = mapOne(buttons.length > 1 ? buttons[1] : null); - -// return [first, second].filter((bi) => bi.title && bi.title.length > 0); -// } - -// getDefaultActionUrl(): string | null { -// const da = this.message.elements?.defaultAction ?? null; -// if (!da) return null; -// return (da.data && da.data.length > 0 ? da.data : da.type) ?? null; -// } - -// private getDefaultColor( -// viewType: IterableEmbeddedViewType, -// notificationColor: number, -// cardColor: number, -// bannerColor: number -// ): number { -// switch (viewType) { -// case IterableEmbeddedViewType.Notification: -// return notificationColor; -// case IterableEmbeddedViewType.Card: -// return cardColor; -// default: -// return bannerColor; -// } -// } -// } diff --git a/src/embedded/classes/IterableEmbeddedViewConfig.ts b/src/embedded/classes/IterableEmbeddedViewConfig.ts deleted file mode 100644 index a431ea04e..000000000 --- a/src/embedded/classes/IterableEmbeddedViewConfig.ts +++ /dev/null @@ -1,66 +0,0 @@ -import type { ColorValue } from 'react-native'; - -export interface IterableEmbeddedViewConfigDict { - /** Background color hex (e.g., 0xFF0000) */ - backgroundColor?: ColorValue; - /** Border color hex */ - borderColor?: ColorValue; - /** Border width in pixels */ - borderWidth?: number; - /** Corner radius in points */ - borderCornerRadius?: number; - /** Primary button background color hex */ - primaryBtnBackgroundColor?: ColorValue; - /** Primary button text color hex */ - primaryBtnTextColor?: ColorValue; - /** Secondary button background color hex */ - secondaryBtnBackgroundColor?: ColorValue; - /** Secondary button text color hex */ - secondaryBtnTextColor?: ColorValue; - /** Title text color hex */ - titleTextColor?: ColorValue; - /** Body text color hex */ - bodyTextColor?: ColorValue; -} - -/** - * Represents view-level styling configuration for an embedded view. - */ -export class IterableEmbeddedViewConfig { - public backgroundColor?: IterableEmbeddedViewConfigDict['backgroundColor']; - public borderColor?: IterableEmbeddedViewConfigDict['borderColor']; - public borderWidth?: IterableEmbeddedViewConfigDict['borderWidth']; - public borderCornerRadius?: IterableEmbeddedViewConfigDict['borderCornerRadius']; - public primaryBtnBackgroundColor?: IterableEmbeddedViewConfigDict['primaryBtnBackgroundColor']; - public primaryBtnTextColor?: IterableEmbeddedViewConfigDict['primaryBtnTextColor']; - public secondaryBtnBackgroundColor?: IterableEmbeddedViewConfigDict['secondaryBtnBackgroundColor']; - public secondaryBtnTextColor?: IterableEmbeddedViewConfigDict['secondaryBtnTextColor']; - public titleTextColor?: IterableEmbeddedViewConfigDict['titleTextColor']; - public bodyTextColor?: IterableEmbeddedViewConfigDict['bodyTextColor']; - - constructor(options: IterableEmbeddedViewConfigDict = {}) { - const { - backgroundColor, - borderColor, - borderWidth, - borderCornerRadius, - primaryBtnBackgroundColor, - primaryBtnTextColor, - secondaryBtnBackgroundColor, - secondaryBtnTextColor, - titleTextColor, - bodyTextColor, - } = options; - - this.backgroundColor = backgroundColor; - this.borderColor = borderColor; - this.borderWidth = borderWidth; - this.borderCornerRadius = borderCornerRadius; - this.primaryBtnBackgroundColor = primaryBtnBackgroundColor; - this.primaryBtnTextColor = primaryBtnTextColor; - this.secondaryBtnBackgroundColor = secondaryBtnBackgroundColor; - this.secondaryBtnTextColor = secondaryBtnTextColor; - this.titleTextColor = titleTextColor; - this.bodyTextColor = bodyTextColor; - } -} diff --git a/src/embedded/hooks/useEmbeddedView.ts b/src/embedded/hooks/useEmbeddedView.ts index 87fc95538..ba0e070a5 100644 --- a/src/embedded/hooks/useEmbeddedView.ts +++ b/src/embedded/hooks/useEmbeddedView.ts @@ -1,6 +1,6 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { Iterable } from '../../core/classes/Iterable'; -import type { IterableEmbeddedMessageElementsButton } from '../classes/IterableEmbeddedMessageElementsButton'; +import type { IterableEmbeddedMessageElementsButton } from '../types/IterableEmbeddedMessageElementsButton'; import { IterableEmbeddedViewType } from '../enums'; import type { IterableEmbeddedComponentProps } from '../types/IterableEmbeddedViewProps'; import { getMedia } from '../utils/getMedia'; diff --git a/src/embedded/types/IterableEmbeddedMessage.ts b/src/embedded/types/IterableEmbeddedMessage.ts new file mode 100644 index 000000000..35724f2b8 --- /dev/null +++ b/src/embedded/types/IterableEmbeddedMessage.ts @@ -0,0 +1,8 @@ +import type { IterableEmbeddedMessageElements } from './IterableEmbeddedMessageElements'; +import type { IterableEmbeddedMessageMetadata } from './IterableEmbeddedMessageMetadata'; + +export interface IterableEmbeddedMessage { + metadata: IterableEmbeddedMessageMetadata; + elements?: IterableEmbeddedMessageElements | null; + payload?: Record | null; +} diff --git a/src/embedded/types/IterableEmbeddedMessageElements.ts b/src/embedded/types/IterableEmbeddedMessageElements.ts new file mode 100644 index 000000000..ffacc9bff --- /dev/null +++ b/src/embedded/types/IterableEmbeddedMessageElements.ts @@ -0,0 +1,13 @@ +import type { IterableEmbeddedMessageElementsButton } from './IterableEmbeddedMessageElementsButton'; +import type { IterableEmbeddedMessageElementsDefaultAction } from './IterableEmbeddedMessageElementsDefaultAction'; +import type { IterableEmbeddedMessageElementsText } from './IterableEmbeddedMessageElementsText'; + +export interface IterableEmbeddedMessageElements { + title?: string | null; + body?: string | null; + mediaUrl?: string | null; + mediaUrlCaption?: string | null; + defaultAction?: IterableEmbeddedMessageElementsDefaultAction | null; + buttons?: IterableEmbeddedMessageElementsButton[] | null; + text?: IterableEmbeddedMessageElementsText[] | null; +} diff --git a/src/embedded/types/IterableEmbeddedMessageElementsButton.ts b/src/embedded/types/IterableEmbeddedMessageElementsButton.ts new file mode 100644 index 000000000..de287d691 --- /dev/null +++ b/src/embedded/types/IterableEmbeddedMessageElementsButton.ts @@ -0,0 +1,10 @@ +import type { IterableEmbeddedMessageElementsButtonAction } from './IterableEmbeddedMessageElementsButtonAction'; + +export interface IterableEmbeddedMessageElementsButton { + /** The ID. */ + id: string; + /** The title. */ + title?: string | null; + /** The action. */ + action?: IterableEmbeddedMessageElementsButtonAction | null; +} diff --git a/src/embedded/types/IterableEmbeddedMessageElementsButtonAction.ts b/src/embedded/types/IterableEmbeddedMessageElementsButtonAction.ts new file mode 100644 index 000000000..7b5204f4a --- /dev/null +++ b/src/embedded/types/IterableEmbeddedMessageElementsButtonAction.ts @@ -0,0 +1,6 @@ +export interface IterableEmbeddedMessageElementsButtonAction { + /** The type. */ + type: string; + /** The data. */ + data?: string; +} diff --git a/src/embedded/types/IterableEmbeddedMessageElementsDefaultAction.ts b/src/embedded/types/IterableEmbeddedMessageElementsDefaultAction.ts new file mode 100644 index 000000000..4e8a2835c --- /dev/null +++ b/src/embedded/types/IterableEmbeddedMessageElementsDefaultAction.ts @@ -0,0 +1,6 @@ +export interface IterableEmbeddedMessageElementsDefaultAction { + /** The type. */ + type: string; + /** The data. */ + data?: string; +} diff --git a/src/embedded/types/IterableEmbeddedMessageElementsText.ts b/src/embedded/types/IterableEmbeddedMessageElementsText.ts new file mode 100644 index 000000000..00b6a8449 --- /dev/null +++ b/src/embedded/types/IterableEmbeddedMessageElementsText.ts @@ -0,0 +1,8 @@ +export interface IterableEmbeddedMessageElementsText { + /** The ID. */ + id: string; + /** The text. */ + text?: string | null; + /** The label. */ + label?: string | null; +} diff --git a/src/embedded/types/IterableEmbeddedMessageMetadata.ts b/src/embedded/types/IterableEmbeddedMessageMetadata.ts new file mode 100644 index 000000000..c4e1350ad --- /dev/null +++ b/src/embedded/types/IterableEmbeddedMessageMetadata.ts @@ -0,0 +1,6 @@ +export interface IterableEmbeddedMessageMetadata { + messageId: string; + placementId: number; + campaignId?: number | null; + isProof?: boolean; +} diff --git a/src/embedded/types/IterableEmbeddedPlacement.ts b/src/embedded/types/IterableEmbeddedPlacement.ts new file mode 100644 index 000000000..8c0c48bae --- /dev/null +++ b/src/embedded/types/IterableEmbeddedPlacement.ts @@ -0,0 +1,6 @@ +import type { IterableEmbeddedMessage } from './IterableEmbeddedMessage'; + +export interface IterableEmbeddedPlacement { + placementId: number; + messages: IterableEmbeddedMessage[]; +} diff --git a/src/embedded/types/IterableEmbeddedViewConfig.ts b/src/embedded/types/IterableEmbeddedViewConfig.ts new file mode 100644 index 000000000..6a41edd8a --- /dev/null +++ b/src/embedded/types/IterableEmbeddedViewConfig.ts @@ -0,0 +1,27 @@ +import type { ColorValue } from 'react-native'; + +/** + * Represents view-level styling configuration for an embedded view. + */ +export interface IterableEmbeddedViewConfig { + /** Background color hex (e.g., 0xFF0000) */ + backgroundColor?: ColorValue; + /** Border color hex */ + borderColor?: ColorValue; + /** Border width in pixels */ + borderWidth?: number; + /** Corner radius in points */ + borderCornerRadius?: number; + /** Primary button background color hex */ + primaryBtnBackgroundColor?: ColorValue; + /** Primary button text color hex */ + primaryBtnTextColor?: ColorValue; + /** Secondary button background color hex */ + secondaryBtnBackgroundColor?: ColorValue; + /** Secondary button text color hex */ + secondaryBtnTextColor?: ColorValue; + /** Title text color hex */ + titleTextColor?: ColorValue; + /** Body text color hex */ + bodyTextColor?: ColorValue; +} diff --git a/src/embedded/types/IterableEmbeddedViewProps.ts b/src/embedded/types/IterableEmbeddedViewProps.ts index 854f2c8fd..9f2b17670 100644 --- a/src/embedded/types/IterableEmbeddedViewProps.ts +++ b/src/embedded/types/IterableEmbeddedViewProps.ts @@ -1,6 +1,6 @@ -import type { IterableEmbeddedMessage } from '../classes/IterableEmbeddedMessage'; -import type { IterableEmbeddedMessageElementsButton } from '../classes/IterableEmbeddedMessageElementsButton'; -import type { IterableEmbeddedViewConfig } from '../classes/IterableEmbeddedViewConfig'; +import type { IterableEmbeddedMessage } from './IterableEmbeddedMessage'; +import type { IterableEmbeddedMessageElementsButton } from './IterableEmbeddedMessageElementsButton'; +import type { IterableEmbeddedViewConfig } from './IterableEmbeddedViewConfig'; export interface IterableEmbeddedComponentProps { message: IterableEmbeddedMessage; diff --git a/src/embedded/utils/getButtons.ts b/src/embedded/utils/getButtons.ts index 11d9d4970..e2eb52a44 100644 --- a/src/embedded/utils/getButtons.ts +++ b/src/embedded/utils/getButtons.ts @@ -1,5 +1,5 @@ -import type { IterableEmbeddedMessage } from '../classes/IterableEmbeddedMessage'; -import type { IterableEmbeddedMessageElementsButton } from '../classes/IterableEmbeddedMessageElementsButton'; +import type { IterableEmbeddedMessage } from '../types/IterableEmbeddedMessage'; +import type { IterableEmbeddedMessageElementsButton } from '../types/IterableEmbeddedMessageElementsButton'; import type { IterableEmbeddedViewButtonInfo } from '../types/IterableEmbeddedViewButtonInfo'; export const getButtons = (message: IterableEmbeddedMessage) => { diff --git a/src/embedded/utils/getDefaultActionUrl.ts b/src/embedded/utils/getDefaultActionUrl.ts index 7efb5fbbb..be1d00dac 100644 --- a/src/embedded/utils/getDefaultActionUrl.ts +++ b/src/embedded/utils/getDefaultActionUrl.ts @@ -1,4 +1,4 @@ -import type { IterableEmbeddedMessage } from '../classes/IterableEmbeddedMessage'; +import type { IterableEmbeddedMessage } from '../types/IterableEmbeddedMessage'; export const getDefaultActionUrl = (message: IterableEmbeddedMessage) => { const defaultAction = message.elements?.defaultAction ?? null; diff --git a/src/embedded/utils/getMedia.ts b/src/embedded/utils/getMedia.ts index 079c919a6..5063d91f8 100644 --- a/src/embedded/utils/getMedia.ts +++ b/src/embedded/utils/getMedia.ts @@ -1,4 +1,4 @@ -import type { IterableEmbeddedMessage } from '../classes/IterableEmbeddedMessage'; +import type { IterableEmbeddedMessage } from '../types/IterableEmbeddedMessage'; import { IterableEmbeddedViewType } from '../enums'; export const getMedia = ( diff --git a/src/embedded/utils/getStyles.ts b/src/embedded/utils/getStyles.ts index b116142ed..aa2212f81 100644 --- a/src/embedded/utils/getStyles.ts +++ b/src/embedded/utils/getStyles.ts @@ -1,4 +1,4 @@ -import type { IterableEmbeddedViewConfig } from '../classes/IterableEmbeddedViewConfig'; +import type { IterableEmbeddedViewConfig } from '../types/IterableEmbeddedViewConfig'; import { embeddedStyles } from '../constants/embeddedViewDefaults'; import type { IterableEmbeddedViewType } from '../enums'; import { getDefaultStyle } from './getDefaultStyle'; diff --git a/src/embedded/utils/getUrlFromButton.ts b/src/embedded/utils/getUrlFromButton.ts index 2140ffd05..90adb4d33 100644 --- a/src/embedded/utils/getUrlFromButton.ts +++ b/src/embedded/utils/getUrlFromButton.ts @@ -1,4 +1,4 @@ -import type { IterableEmbeddedMessageElementsButton } from '../classes/IterableEmbeddedMessageElementsButton'; +import type { IterableEmbeddedMessageElementsButton } from '../types/IterableEmbeddedMessageElementsButton'; export const getUrlFromButton = ( button: IterableEmbeddedMessageElementsButton diff --git a/src/index.tsx b/src/index.tsx index aa3a489ca..a260c6f7d 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -60,7 +60,7 @@ export { type IterableInboxProps, type IterableInboxRowViewModel, } from './inbox'; -export { type IterableEmbeddedMessage } from './embedded/classes/IterableEmbeddedMessage'; +export { type IterableEmbeddedMessage } from './embedded/types/IterableEmbeddedMessage'; export { IterableEmbeddedView } from './embedded/components'; export { IterableEmbeddedViewType } from './embedded/enums'; -export { IterableEmbeddedViewConfig } from './embedded/classes/IterableEmbeddedViewConfig'; +export type { IterableEmbeddedViewConfig } from './embedded/types/IterableEmbeddedViewConfig'; From 24e55d191954ee38a4dbdabd23ebeeae1f8aeed3 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Thu, 9 Oct 2025 02:26:37 -0700 Subject: [PATCH 44/45] feat: consolidate other hooks into the usembedded hook --- .../hooks/useComponentVisibility.ts | 0 .../IterableEmbeddedCard.tsx | 35 ++++--------------- src/embedded/hooks/useEmbeddedView.ts | 35 +++++++++++++------ 3 files changed, 31 insertions(+), 39 deletions(-) rename src/{embedded => core}/hooks/useComponentVisibility.ts (100%) diff --git a/src/embedded/hooks/useComponentVisibility.ts b/src/core/hooks/useComponentVisibility.ts similarity index 100% rename from src/embedded/hooks/useComponentVisibility.ts rename to src/core/hooks/useComponentVisibility.ts diff --git a/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx b/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx index a84e228f4..17cafac30 100644 --- a/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx +++ b/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx @@ -1,4 +1,3 @@ -import { useEffect } from 'react'; import { Image, PixelRatio, @@ -10,7 +9,6 @@ import { } from 'react-native'; import { IterableEmbeddedViewType } from '../../enums'; -import { useComponentVisibility } from '../../hooks/useComponentVisibility'; import { useEmbeddedView } from '../../hooks/useEmbeddedView'; import type { IterableEmbeddedComponentProps } from '../../types/IterableEmbeddedViewProps'; import { IMAGE_HEIGHT, styles } from './IterableEmbeddedCard.styles'; @@ -24,35 +22,14 @@ export const IterableEmbeddedCard = ({ message, onButtonClick = () => {}, }: IterableEmbeddedComponentProps) => { - const { parsedStyles, media, handleButtonClick } = useEmbeddedView( - IterableEmbeddedViewType.Card, - { message, config, onButtonClick } - ); + const { parsedStyles, media, handleButtonClick, componentRef, handleLayout } = + useEmbeddedView(IterableEmbeddedViewType.Card, { + message, + config, + onButtonClick, + }); const buttons = message?.elements?.buttons ?? []; - // Use the visibility hook to track if the component is visible - const { isVisible, componentRef, handleLayout } = useComponentVisibility({ - threshold: 0.1, // Component is considered visible if 10% is on screen - checkOnAppState: true, // Consider app state (active/background) - enablePeriodicCheck: true, // Enable periodic checking for navigation changes - checkInterval: 500, // Check every 500ms for navigation changes - }); - - // const appVisibility = useAppStateListener(); - // console.log('appVisibility', appVisibility); - - useEffect(() => { - console.log('RENDERED'); - return () => { - console.log('UNMOUNTED'); - }; - }, []); - - // Log visibility changes for debugging - useEffect(() => { - console.log('Card visibility changed:', isVisible); - }, [isVisible]); - return ( {} }: IterableEmbeddedComponentProps ) => { const appVisibility = useAppStateListener(); + const { isVisible, componentRef, handleLayout } = useComponentVisibility({ + threshold: 0.1, // Component is considered visible if 10% is on screen + checkOnAppState: true, // Consider app state (active/background) + enablePeriodicCheck: true, // Enable periodic checking for navigation changes + checkInterval: 500, // Check every 500ms for navigation changes + }); + const parsedStyles = useMemo(() => { return getStyles(viewType, config); }, [viewType, config]); @@ -35,30 +43,37 @@ export const useEmbeddedView = ( ); useEffect(() => { - console.group('useEmbeddedView'); - console.log('appVisibility changed'); - console.log('appVisibility', appVisibility); - console.log('lastState', lastState); if (appVisibility !== lastState) { - console.log('setting lastState'); setLastState(appVisibility); if (appVisibility === 'active') { - console.log('appVisibility is active'); + // App is active, start the session + // TODO: figure out how to only do this once, even if there are multiple embedded views Iterable.embeddedManager.startSession(); } else if ( appVisibility === 'background' || appVisibility === 'inactive' ) { - console.log('appVisibility is background or inactive'); + // App is background or inactive, end the session + // TODO: figure out how to only do this once, even if there are multiple embedded views Iterable.embeddedManager.endSession(); } } - console.groupEnd(); }, [appVisibility, lastState]); + useEffect(() => { + console.log('Card visibility changed:', isVisible); + if (isVisible) { + // TODO: Start impression here + } else { + // TODO: Pause impression here + } + }, [isVisible]); + return { parsedStyles, media, handleButtonClick, + componentRef, + handleLayout, }; }; From 0b147e8f350c17b506d878a20cb3542c48f9433c Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Thu, 9 Oct 2025 03:24:49 -0700 Subject: [PATCH 45/45] feat: add TODOs for tracking session and impression events in IterableEmbeddedManager --- TODO.md | 37 +++++++++++++++++++ .../classes/IterableEmbeddedManager.ts | 4 ++ 2 files changed, 41 insertions(+) create mode 100644 TODO.md diff --git a/TODO.md b/TODO.md new file mode 100644 index 000000000..04efbfc6d --- /dev/null +++ b/TODO.md @@ -0,0 +1,37 @@ +# Embedded + +## TODO +- [ ] Track session start and stop +- [ ] Track pause and start impression +- [ ] Align styles with OOTB view notes + - https://support.iterable.com/hc/en-us/articles/23230946708244-Out-of-the-Box-Views-for-Embedded-Messages +- [ ] Go through [Evans + google doc](https://docs.google.com/document/d/15GNyo2x5QwYBPUliB4JZvXLkb04ZkW96jcbUJ96YrAM/edit?tab=t.0) + and [Slab doc](https://iterable.slab.com/posts/embedded-messaging-rn-sdk-urwffrhx#h8if0-public-methods) + and see if there is anything else to do +- [ ] Add the ability to switch between views in the example app. And the + ability to configure your own JSON. + + +## Resources +- [RN SDK - In-app review](https://iterable.slab.com/posts/rn-sdk-in-app-review-bl2vp1ds) +- [Google doc](https://docs.google.com/document/d/15GNyo2x5QwYBPUliB4JZvXLkb04ZkW96jcbUJ96YrAM/edit?tab=t.0) +- [Embedded Messaging - RN SDK](https://iterable.slab.com/posts/embedded-messaging-rn-sdk-urwffrhx#h8if0-public-methods) +- [New arch customers](https://docs.google.com/spreadsheets/d/1FzoAH5CAcNy92Km5DLqr8yYvPDBPLTuBXa0_CXZEUCA/edit?gid=60700846#gid=60700846) +- [RN Epic](https://iterable.atlassian.net/browse/MOB-7052) +- [Figma](https://www.figma.com/design/rbDozNjEF9MjwbvSqzrVTv/Flex-Messaging?node-id=3804-186809&p=f) +- [Embedded Messaging: Timeline for a Successful GA Release](https://iterable.slab.com/posts/embedded-messaging-timeline-for-a-successful-ga-release-7f762g1c) +- [Embedded Task Prioritization](https://tables.area120.google.com/u/0/workspace/av3wJDN6_I94tIbdapKOzu/table/9jRcY5gDv2OaTnkceKbOA5) +- [Acceptance Criteria](https://iterable.slab.com/posts/embedded-messaging-acceptance-criteria-80gfn857) +- [Bug Bash Test Cases](https://docs.google.com/spreadsheets/d/1ZrM8vMoMjhK4x18uoqtcOyUqDhFqlwTfrUWAM7csibQ/edit?gid=1805677430#gid=1805677430) +- [Stories for original](https://docs.google.com/spreadsheets/d/1ZrM8vMoMjhK4x18uoqtcOyUqDhFqlwTfrUWAM7csibQ/edit?gid=1805677430#gid=1805677430) +- [Datadog story](https://iterable.atlassian.net/browse/MOB-6926) +- [Yellow Brick Road: Embedded Messaging Mobile SDK](https://iterable.slab.com/posts/yellow-brick-road-embedded-messaging-mobile-sdk-4v032ww9?shr=4v032ww9#h69qk-2024-01-19-ootb-style-layout-conversation) +- [Android SDK Embedded OOTB Constraint + finalization](https://iterable.atlassian.net/browse/MOB-7678) +- [iOS SDK Embedded OOTB Constraint finalization](https://iterable.atlassian.net/browse/MOB-7679) +- [OOTB view bugs review](https://iterable.slab.com/posts/ootb-view-bugs-review-7u978hhy?shr=7u978hhy) +- [Non-RN Embedded Epic](https://iterable.atlassian.net/browse/MOB-5235) +- [OOTB Style/Layout Questions](https://iterable.atlassian.net/browse/MOB-5235) +- [Evan PR](https://github.com/Iterable/react-native-sdk/pull/732) +- [My PR](https://github.com/Iterable/react-native-sdk/pull/730) diff --git a/src/embedded/classes/IterableEmbeddedManager.ts b/src/embedded/classes/IterableEmbeddedManager.ts index 9ac3c8bae..a055d633c 100644 --- a/src/embedded/classes/IterableEmbeddedManager.ts +++ b/src/embedded/classes/IterableEmbeddedManager.ts @@ -39,11 +39,13 @@ export class IterableEmbeddedManager { startSession() { this.logger.log('IterableEmbeddedManager.startSession'); + // TODO: Track session start IterableApi.startEmbeddedSession(); } endSession() { this.logger.log('IterableEmbeddedManager.endSession'); + // TODO: Track session end IterableApi.endEmbeddedSession(); } @@ -53,11 +55,13 @@ export class IterableEmbeddedManager { messageId, placementId ); + // TODO: Track impression start IterableApi.startEmbeddedImpression(messageId, placementId); } pauseImpression(messageId: string) { this.logger.log('IterableEmbeddedManager.pauseImpression', messageId); + // TODO: Track impression pause IterableApi.pauseEmbeddedImpression(messageId); }