From 1ac73703f90c9c5b6e8e2d6536ec02e558b31e5b Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Fri, 10 Oct 2025 15:08:27 -0700 Subject: [PATCH 01/49] docs: add Embedded component to example app --- example/src/components/App/App.constants.ts | 1 + example/src/components/App/App.tsx | 45 ++++++++++++++++++- .../components/Embedded/Embedded.styles.ts | 21 +++++++++ example/src/components/Embedded/Embedded.tsx | 13 ++++++ example/src/components/Embedded/index.ts | 2 + example/src/constants/routes.ts | 1 + example/src/constants/styles/index.ts | 1 + example/src/constants/styles/miscElements.ts | 9 ++++ example/src/types/navigation.ts | 1 + 9 files changed, 93 insertions(+), 1 deletion(-) 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 create mode 100644 example/src/constants/styles/miscElements.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/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 ? ( diff --git a/example/src/components/Embedded/Embedded.styles.ts b/example/src/components/Embedded/Embedded.styles.ts new file mode 100644 index 000000000..9dc6aea47 --- /dev/null +++ b/example/src/components/Embedded/Embedded.styles.ts @@ -0,0 +1,21 @@ +import { StyleSheet } from 'react-native'; +import { button, buttonText, container, hr } from '../../constants'; + +const styles = StyleSheet.create({ + button, + buttonText, + 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 new file mode 100644 index 000000000..806bb0c23 --- /dev/null +++ b/example/src/components/Embedded/Embedded.tsx @@ -0,0 +1,13 @@ +import { Text, View } from 'react-native'; + +import styles from './Embedded.styles'; + +export const Embedded = () => { + return ( + + EMBEDDED + + ); +}; + +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/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/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/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 15e18946e72ea8f76fb945d148ccee1c37a1ba13 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Fri, 10 Oct 2025 16:34:18 -0700 Subject: [PATCH 02/49] feat: add IterableEmbeddedManager class and update exports for embedded messages --- .../classes/IterableEmbeddedManager.ts | 66 +++++++++++++++++++ src/embedded/classes/index.ts | 1 + src/embedded/index.ts | 1 + src/index.tsx | 1 + 4 files changed, 69 insertions(+) create mode 100644 src/embedded/classes/IterableEmbeddedManager.ts create mode 100644 src/embedded/classes/index.ts create mode 100644 src/embedded/index.ts diff --git a/src/embedded/classes/IterableEmbeddedManager.ts b/src/embedded/classes/IterableEmbeddedManager.ts new file mode 100644 index 000000000..95d558c10 --- /dev/null +++ b/src/embedded/classes/IterableEmbeddedManager.ts @@ -0,0 +1,66 @@ +import { IterableLogger } from '../../core/classes/IterableLogger'; + +/** + * Manages the embedded messages from Iterable. + */ +export class IterableEmbeddedManager { + /** + * Refreshes the local cache of your embedded manager system so that it aligns + * with the server. + * + * At key points during your app's lifecycle, you may want to manually refresh + * your app's local cache of embedded messages. For example, as users navigate + * around, on pull-to-refresh, etc. + * + * @returns A promise that returns messages that the user is *eligible* to see. + * + * @example + * ```typescript + * IterableEmbeddedManager.syncMessages().then(messages => { + * console.log('Messages:', messages); + * }); + * ``` + */ + syncMessages() { + IterableLogger.log('IterableEmbeddedManager.syncMessages'); + } + + getMessages(placementIds?: number[] | null) { + IterableLogger.log( + 'IterableEmbeddedManager.getMessages with placementIds', + placementIds + ); + } + + getPlacementIds() { + IterableLogger.log('IterableEmbeddedManager.getEmbeddedPlacementIds'); + } + + startSession() { + IterableLogger.log('IterableEmbeddedManager.startSession'); + } + + endSession() { + IterableLogger.log('IterableEmbeddedManager.endSession'); + } + + startImpression(messageId: string, placementId: number) { + IterableLogger.log( + 'IterableEmbeddedManager.startImpression', + messageId, + placementId + ); + } + + pauseImpression(messageId: string) { + IterableLogger.log('IterableEmbeddedManager.pauseImpression', messageId); + } + + handleClick() { + IterableLogger.log('IterableEmbeddedManager.handleClick'); + } + + trackClick() { + IterableLogger.log('IterableEmbeddedManager.trackClick'); + } +} diff --git a/src/embedded/classes/index.ts b/src/embedded/classes/index.ts new file mode 100644 index 000000000..be2af76f0 --- /dev/null +++ b/src/embedded/classes/index.ts @@ -0,0 +1 @@ +export * from './IterableEmbeddedManager'; diff --git a/src/embedded/index.ts b/src/embedded/index.ts new file mode 100644 index 000000000..d7d17c691 --- /dev/null +++ b/src/embedded/index.ts @@ -0,0 +1 @@ +export * from './classes'; diff --git a/src/index.tsx b/src/index.tsx index 240ac51f5..c9a014340 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -59,3 +59,4 @@ export { type IterableInboxProps, type IterableInboxRowViewModel, } from './inbox'; +export { IterableEmbeddedManager } from './embedded'; From 2a62913003144ad1f481cd1249aaa3ae0f9c5f4c Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Fri, 10 Oct 2025 17:05:31 -0700 Subject: [PATCH 03/49] feat: add embeddedManager property to Iterable class for managing embedded messages --- src/core/classes/Iterable.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/core/classes/Iterable.ts b/src/core/classes/Iterable.ts index 9ef784679..ebb928cd6 100644 --- a/src/core/classes/Iterable.ts +++ b/src/core/classes/Iterable.ts @@ -20,6 +20,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); @@ -79,6 +80,27 @@ export class Iterable { */ static authManager: IterableAuthManager = new IterableAuthManager(); + /** + * Embedded message manager for the current user. + * + * This property provides access to embedded message functionality including + * retrieving messages, displaying messages, removing messages, and more. + * + * **Documentation** + * - [Embedded Messaging Overview](https://support.iterable.com/hc/en-us/articles/23060529977364-Embedded-Messaging-Overview) + * - [Android Embedded Messaging](https://support.iterable.com/hc/en-us/articles/23061877893652-Embedded-Messages-with-Iterable-s-Android-SDK) + * - [iOS Embedded Messaging](https://support.iterable.com/hc/en-us/articles/23061840746900-Embedded-Messages-with-Iterable-s-iOS-SDK) + * + * @example + * ```typescript + * Iterable.embeddedManager.getMessages().then(messages => { + * console.log('Messages:', messages); + * }); + * ``` + */ + static embeddedManager: IterableEmbeddedManager = + new IterableEmbeddedManager(); + /** * Initializes the Iterable React Native SDK in your app's Javascript or Typescript code. * From 789ae831cebed8d79dc4917850e9134ea6564350 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Fri, 10 Oct 2025 20:48:10 -0700 Subject: [PATCH 04/49] refactor: remove unused methods and improve documentation in IterableEmbeddedManager --- .../classes/IterableEmbeddedManager.ts | 68 +++---------------- 1 file changed, 8 insertions(+), 60 deletions(-) diff --git a/src/embedded/classes/IterableEmbeddedManager.ts b/src/embedded/classes/IterableEmbeddedManager.ts index 95d558c10..f6c59273b 100644 --- a/src/embedded/classes/IterableEmbeddedManager.ts +++ b/src/embedded/classes/IterableEmbeddedManager.ts @@ -1,66 +1,14 @@ -import { IterableLogger } from '../../core/classes/IterableLogger'; - /** - * Manages the embedded messages from Iterable. + * Manages embedded messages from Iterable. + * + * **Documentation** + * - [Embedded Messaging Overview](https://support.iterable.com/hc/en-us/articles/23060529977364-Embedded-Messaging-Overview) + * - [Android Embedded Messaging](https://support.iterable.com/hc/en-us/articles/23061877893652-Embedded-Messages-with-Iterable-s-Android-SDK) + * - [iOS Embedded Messaging](https://support.iterable.com/hc/en-us/articles/23061840746900-Embedded-Messages-with-Iterable-s-iOS-SDK) */ export class IterableEmbeddedManager { /** - * Refreshes the local cache of your embedded manager system so that it aligns - * with the server. - * - * At key points during your app's lifecycle, you may want to manually refresh - * your app's local cache of embedded messages. For example, as users navigate - * around, on pull-to-refresh, etc. - * - * @returns A promise that returns messages that the user is *eligible* to see. - * - * @example - * ```typescript - * IterableEmbeddedManager.syncMessages().then(messages => { - * console.log('Messages:', messages); - * }); - * ``` + * Whether the embedded manager is enabled. */ - syncMessages() { - IterableLogger.log('IterableEmbeddedManager.syncMessages'); - } - - getMessages(placementIds?: number[] | null) { - IterableLogger.log( - 'IterableEmbeddedManager.getMessages with placementIds', - placementIds - ); - } - - getPlacementIds() { - IterableLogger.log('IterableEmbeddedManager.getEmbeddedPlacementIds'); - } - - startSession() { - IterableLogger.log('IterableEmbeddedManager.startSession'); - } - - endSession() { - IterableLogger.log('IterableEmbeddedManager.endSession'); - } - - startImpression(messageId: string, placementId: number) { - IterableLogger.log( - 'IterableEmbeddedManager.startImpression', - messageId, - placementId - ); - } - - pauseImpression(messageId: string) { - IterableLogger.log('IterableEmbeddedManager.pauseImpression', messageId); - } - - handleClick() { - IterableLogger.log('IterableEmbeddedManager.handleClick'); - } - - trackClick() { - IterableLogger.log('IterableEmbeddedManager.trackClick'); - } + isEnabled = false; } From 9940f46b1f4c8cec2fe6252e1dc5a0a6bb3e07ea Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Fri, 10 Oct 2025 20:54:10 -0700 Subject: [PATCH 05/49] docs: enhance documentation --- src/embedded/classes/IterableEmbeddedManager.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/embedded/classes/IterableEmbeddedManager.ts b/src/embedded/classes/IterableEmbeddedManager.ts index f6c59273b..333dd3af7 100644 --- a/src/embedded/classes/IterableEmbeddedManager.ts +++ b/src/embedded/classes/IterableEmbeddedManager.ts @@ -1,6 +1,9 @@ /** * Manages embedded messages from Iterable. * + * Provides embedded message functionality including retrieving messages, + * displaying messages, removing messages, and more. + * * **Documentation** * - [Embedded Messaging Overview](https://support.iterable.com/hc/en-us/articles/23060529977364-Embedded-Messaging-Overview) * - [Android Embedded Messaging](https://support.iterable.com/hc/en-us/articles/23061877893652-Embedded-Messages-with-Iterable-s-Android-SDK) From e7d4ecbb6e05bffc26f9c8d92bc77abe29de3dd8 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Fri, 10 Oct 2025 21:08:42 -0700 Subject: [PATCH 06/49] feat: add embeddedMessagingEnabled property to IterableConfig --- src/core/classes/IterableConfig.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/core/classes/IterableConfig.ts b/src/core/classes/IterableConfig.ts index c8ee67400..cbe3cb665 100644 --- a/src/core/classes/IterableConfig.ts +++ b/src/core/classes/IterableConfig.ts @@ -319,6 +319,16 @@ export class IterableConfig { */ encryptionEnforced = false; + /** + * Should the SDK enable and use embedded messaging? + * + * **Documentation** + * - [Embedded Messaging Overview](https://support.iterable.com/hc/en-us/articles/23060529977364-Embedded-Messaging-Overview) + * - [Android Embedded Messaging](https://support.iterable.com/hc/en-us/articles/23061877893652-Embedded-Messages-with-Iterable-s-Android-SDK) + * - [iOS Embedded Messaging](https://support.iterable.com/hc/en-us/articles/23061840746900-Embedded-Messages-with-Iterable-s-iOS-SDK) + */ + embeddedMessagingEnabled = false; + /** * Converts the IterableConfig instance to a dictionary object. * @@ -368,6 +378,7 @@ export class IterableConfig { pushPlatform: this.pushPlatform, encryptionEnforced: this.encryptionEnforced, retryPolicy: this.retryPolicy, + embeddedMessagingEnabled: this.embeddedMessagingEnabled, }; } } From 12ee8fd000f23d7c50de5fafbec648d19cf2c504 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Fri, 10 Oct 2025 21:16:04 -0700 Subject: [PATCH 07/49] feat: enable embedded messaging configuration in RNIterableAPIModuleImpl --- .../com/iterable/reactnative/RNIterableAPIModuleImpl.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java index 3207bb5dc..5b15e9808 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")) { + configBuilder.setEnableEmbeddedMessaging(configReadableMap.getBoolean("enableEmbeddedMessaging")); + } + IterableApi.initialize(reactContext, apiKey, configBuilder.build()); IterableApi.getInstance().setDeviceAttribute("reactNativeSDKVersion", version); @@ -118,6 +122,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 93402fd9c1ff24b61f7e902be3886b496181487f Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Fri, 10 Oct 2025 21:40:36 -0700 Subject: [PATCH 08/49] feat: update IterableConfig with prop that matches android name --- example/src/hooks/useIterableApp.tsx | 2 ++ src/core/classes/IterableConfig.ts | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/example/src/hooks/useIterableApp.tsx b/example/src/hooks/useIterableApp.tsx index d648dd25c..136323dbd 100644 --- a/example/src/hooks/useIterableApp.tsx +++ b/example/src/hooks/useIterableApp.tsx @@ -134,6 +134,8 @@ export const IterableAppProvider: FunctionComponent< retryBackoff: IterableRetryBackoff.LINEAR, }; + config.enableEmbeddedMessaging = true; + config.onJWTError = (authFailure) => { console.log('onJWTError', authFailure); diff --git a/src/core/classes/IterableConfig.ts b/src/core/classes/IterableConfig.ts index cbe3cb665..6623108e7 100644 --- a/src/core/classes/IterableConfig.ts +++ b/src/core/classes/IterableConfig.ts @@ -327,7 +327,7 @@ export class IterableConfig { * - [Android Embedded Messaging](https://support.iterable.com/hc/en-us/articles/23061877893652-Embedded-Messages-with-Iterable-s-Android-SDK) * - [iOS Embedded Messaging](https://support.iterable.com/hc/en-us/articles/23061840746900-Embedded-Messages-with-Iterable-s-iOS-SDK) */ - embeddedMessagingEnabled = false; + enableEmbeddedMessaging = false; /** * Converts the IterableConfig instance to a dictionary object. @@ -378,7 +378,7 @@ export class IterableConfig { pushPlatform: this.pushPlatform, encryptionEnforced: this.encryptionEnforced, retryPolicy: this.retryPolicy, - embeddedMessagingEnabled: this.embeddedMessagingEnabled, + enableEmbeddedMessaging: this.enableEmbeddedMessaging, }; } } From b916fb376d8d074adcf1147ccdf5da4b92d93e05 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Fri, 10 Oct 2025 21:59:29 -0700 Subject: [PATCH 09/49] feat: implement getEmbeddedPlacementIds --- .../reactnative/RNIterableAPIModuleImpl.java | 22 +++++++++++++++++++ .../newarch/java/com/RNIterableAPIModule.java | 5 +++++ .../oldarch/java/com/RNIterableAPIModule.java | 4 ++++ src/api/NativeRNIterableAPI.ts | 3 +++ src/core/classes/IterableApi.ts | 16 +++++++++++++- .../classes/IterableEmbeddedManager.ts | 11 ++++++++++ 6 files changed, 60 insertions(+), 1 deletion(-) diff --git a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java index 5b15e9808..3db33bd8a 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; @@ -507,6 +508,27 @@ private static JSONObject optSerializedDataFields(ReadableMap dataFields) { // endregion // --------------------------------------------------------------------------------------- + // region Embedded Messaging + + 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()); + } + } + + // --------------------------------------------------------------------------------------- + // endregion // region IterableSDK callbacks @Override diff --git a/android/src/newarch/java/com/RNIterableAPIModule.java b/android/src/newarch/java/com/RNIterableAPIModule.java index f145bab10..50e814f0b 100644 --- a/android/src/newarch/java/com/RNIterableAPIModule.java +++ b/android/src/newarch/java/com/RNIterableAPIModule.java @@ -224,6 +224,11 @@ public void pauseAuthRetries(boolean pauseRetry) { moduleImpl.pauseAuthRetries(pauseRetry); } + @Override + public void getEmbeddedPlacementIds(Promise promise) { + moduleImpl.getEmbeddedPlacementIds(promise); + } + public void sendEvent(@NonNull String eventName, @Nullable Object eventData) { moduleImpl.sendEvent(eventName, eventData); } diff --git a/android/src/oldarch/java/com/RNIterableAPIModule.java b/android/src/oldarch/java/com/RNIterableAPIModule.java index c3a72339b..4555fd71b 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 getEmbeddedPlacementIds(Promise promise) { + moduleImpl.getEmbeddedPlacementIds(promise); + } 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 391fadbb7..9dd2af2b7 100644 --- a/src/api/NativeRNIterableAPI.ts +++ b/src/api/NativeRNIterableAPI.ts @@ -118,6 +118,9 @@ export interface Spec extends TurboModule { passAlongAuthToken(authToken?: string | null): void; pauseAuthRetries(pauseRetry: boolean): void; + // Embedded Messaging + getEmbeddedPlacementIds(): Promise; + // Wake app -- android only wakeApp(): void; diff --git a/src/core/classes/IterableApi.ts b/src/core/classes/IterableApi.ts index fe2b446a3..9825f42ae 100644 --- a/src/core/classes/IterableApi.ts +++ b/src/core/classes/IterableApi.ts @@ -507,7 +507,21 @@ export class IterableApi { // ---- End IN-APP ---- // // ====================================================== // - // ======================= MOSC ======================= // + // ======================= EMBEDDED ===================== // + // ====================================================== // + + /** + * Get the embedded placement IDs. + */ + static getEmbeddedPlacementIds() { + IterableLogger.log('getEmbeddedPlacementIds'); + return RNIterableAPI.getEmbeddedPlacementIds(); + } + + // ---- End EMBEDDED ---- // + + // ====================================================== // + // ======================= MISCELLANEOUS ================ // // ====================================================== // /** diff --git a/src/embedded/classes/IterableEmbeddedManager.ts b/src/embedded/classes/IterableEmbeddedManager.ts index 333dd3af7..c4f8ae229 100644 --- a/src/embedded/classes/IterableEmbeddedManager.ts +++ b/src/embedded/classes/IterableEmbeddedManager.ts @@ -1,3 +1,5 @@ +import { IterableApi } from '../../core/classes/IterableApi'; + /** * Manages embedded messages from Iterable. * @@ -14,4 +16,13 @@ export class IterableEmbeddedManager { * Whether the embedded manager is enabled. */ isEnabled = false; + + /** + * Retrieves a list of placement IDs for the embedded manager. + * + * [Placement Documentation](https://support.iterable.com/hc/en-us/articles/23060529977364-Embedded-Messaging-Overview#placements-and-prioritization) + */ + getPlacementIds() { + return IterableApi.getEmbeddedPlacementIds(); + } } From f456e2089523469d1ba82b5ddbb75d417db2ce7a Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Fri, 10 Oct 2025 22:02:41 -0700 Subject: [PATCH 10/49] feat: add Embedded tab to Main component in example app --- example/src/components/App/Main.tsx | 8 ++++++++ example/src/components/Embedded/index.ts | 1 - 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/example/src/components/App/Main.tsx b/example/src/components/App/Main.tsx index 55b0d74e2..07b7b2d2c 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), + })} + /> Date: Fri, 10 Oct 2025 22:16:56 -0700 Subject: [PATCH 11/49] feat: display embedded manager status in Embedded component --- example/src/components/Embedded/Embedded.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/example/src/components/Embedded/Embedded.tsx b/example/src/components/Embedded/Embedded.tsx index 806bb0c23..ce1348e48 100644 --- a/example/src/components/Embedded/Embedded.tsx +++ b/example/src/components/Embedded/Embedded.tsx @@ -1,4 +1,5 @@ import { Text, View } from 'react-native'; +import { Iterable } from '@iterable/react-native-sdk'; import styles from './Embedded.styles'; @@ -6,6 +7,13 @@ export const Embedded = () => { return ( EMBEDDED + + Does embedded class exist? {Iterable.embeddedManager ? 'Yes' : 'No'} + + + Is embedded manager enabled? + {Iterable.embeddedManager.isEnabled ? 'Yes' : 'No'} + ); }; From b0e17f99db47c540f2ab66babb7062bcca348bf8 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Fri, 10 Oct 2025 22:25:45 -0700 Subject: [PATCH 12/49] feat: add functionality to retrieve and display placement IDs in Embedded component --- example/src/components/Embedded/Embedded.tsx | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/example/src/components/Embedded/Embedded.tsx b/example/src/components/Embedded/Embedded.tsx index ce1348e48..4381cc2d1 100644 --- a/example/src/components/Embedded/Embedded.tsx +++ b/example/src/components/Embedded/Embedded.tsx @@ -1,9 +1,21 @@ -import { Text, View } from 'react-native'; +import { Text, TouchableOpacity, View } from 'react-native'; +import { useCallback, useState } from 'react'; import { Iterable } from '@iterable/react-native-sdk'; import styles from './Embedded.styles'; export const Embedded = () => { + const [placementIds, setPlacementIds] = useState([]); + const getPlacementIds = useCallback(() => { + Iterable.embeddedManager.getPlacementIds().then((ids: unknown) => { + console.log(ids); + setPlacementIds(ids as number[]); + }); + // .catch((error) => { + // console.error(error); + // }); + }, []); + return ( EMBEDDED @@ -14,6 +26,12 @@ export const Embedded = () => { Is embedded manager enabled? {Iterable.embeddedManager.isEnabled ? 'Yes' : 'No'} + + Placement ids: [{placementIds.join(', ')}] + + + Get placement ids + ); }; From d42dd737e39ed7639cc281c8844983d77d7dc03c Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Fri, 10 Oct 2025 22:47:50 -0700 Subject: [PATCH 13/49] feat: implement methods to retrieve and sync embedded messages in RNIterableAPIModuleImpl --- .../reactnative/RNIterableAPIModuleImpl.java | 110 +++++++++++++----- .../iterable/reactnative/Serialization.java | 18 ++- 2 files changed, 98 insertions(+), 30 deletions(-) diff --git a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java index 3db33bd8a..19bcbdb72 100644 --- a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java +++ b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java @@ -24,10 +24,14 @@ 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.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; @@ -46,6 +50,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; @@ -97,6 +102,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 @@ -124,8 +130,8 @@ public void initialize2WithApiKey(String apiKey, ReadableMap configReadableMap, } if (configReadableMap.hasKey("enableEmbeddedMessaging")) { - configBuilder.setEnableEmbeddedMessaging(configReadableMap.getBoolean("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 @@ -135,6 +141,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 @@ -508,27 +515,6 @@ private static JSONObject optSerializedDataFields(ReadableMap dataFields) { // endregion // --------------------------------------------------------------------------------------- - // region Embedded Messaging - - 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()); - } - } - - // --------------------------------------------------------------------------------------- - // endregion // region IterableSDK callbacks @Override @@ -659,14 +645,80 @@ public void sendEvent(@NonNull String eventName, @Nullable Object eventData) { public void onInboxUpdated() { sendEvent(EventName.receivedIterableInboxChanged.name(), null); } + // --------------------------------------------------------------------------------------- + // endregion + + // --------------------------------------------------------------------------------------- + // region Embedded messaging + + public void getEmbeddedMessages(@Nullable ReadableArray placementIds, Promise promise) { + IterableLogger.d(TAG, "getEmbeddedMessages for placements: " + placementIds); + + try { + 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) { + IterableLogger.e(TAG, e.getLocalizedMessage()); + promise.reject("", "Failed to fetch messages with error " + e.getLocalizedMessage()); + } + } + + 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()); + } + } + + // --------------------------------------------------------------------------------------- + // endregion } enum EventName { - handleUrlCalled, - handleCustomActionCalled, - handleInAppCalled, handleAuthCalled, - receivedIterableInboxChanged, + handleAuthFailureCalled, handleAuthSuccessCalled, - handleAuthFailureCalled + handleCustomActionCalled, + handleInAppCalled, + handleUrlCalled, + receivedIterableEmbeddedMessagesChanged, + receivedIterableInboxChanged } 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 67278b71557021d780e1f5ff1fff6e294ec2b73d Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Fri, 10 Oct 2025 23:06:13 -0700 Subject: [PATCH 14/49] refactor: add sync and get messages --- .../reactnative/RNIterableAPIModuleImpl.java | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java index 19bcbdb72..1517c717f 100644 --- a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java +++ b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java @@ -651,6 +651,28 @@ public void onInboxUpdated() { // --------------------------------------------------------------------------------------- // region Embedded messaging + 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()); + } + } + public void getEmbeddedMessages(@Nullable ReadableArray placementIds, Promise promise) { IterableLogger.d(TAG, "getEmbeddedMessages for placements: " + placementIds); @@ -686,28 +708,6 @@ 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()); - } - } - // --------------------------------------------------------------------------------------- // endregion } From 06a94fc96405ac3d6e1e56c39504d5a19a094e10 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Fri, 10 Oct 2025 23:12:09 -0700 Subject: [PATCH 15/49] refactor: remove unused methodss --- .../reactnative/RNIterableAPIModuleImpl.java | 41 ------------------- .../iterable/reactnative/Serialization.java | 11 ----- 2 files changed, 52 deletions(-) diff --git a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java index 1517c717f..fcbbbb2e1 100644 --- a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java +++ b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java @@ -650,12 +650,6 @@ public void onInboxUpdated() { // --------------------------------------------------------------------------------------- // region Embedded messaging - - public void syncEmbeddedMessages() { - IterableLogger.d(TAG, "syncEmbeddedMessages"); - IterableApi.getInstance().getEmbeddedManager().syncMessages(); - } - public void getEmbeddedPlacementIds(Promise promise) { IterableLogger.d(TAG, "getEmbeddedPlacementIds"); try { @@ -673,41 +667,6 @@ public void getEmbeddedPlacementIds(Promise promise) { } } - public void getEmbeddedMessages(@Nullable ReadableArray placementIds, Promise promise) { - IterableLogger.d(TAG, "getEmbeddedMessages for placements: " + placementIds); - - try { - 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) { - IterableLogger.e(TAG, e.getLocalizedMessage()); - promise.reject("", "Failed to fetch messages with error " + e.getLocalizedMessage()); - } - } - // --------------------------------------------------------------------------------------- // endregion } diff --git a/android/src/main/java/com/iterable/reactnative/Serialization.java b/android/src/main/java/com/iterable/reactnative/Serialization.java index 2f0c4e1ca..bace4c16c 100644 --- a/android/src/main/java/com/iterable/reactnative/Serialization.java +++ b/android/src/main/java/com/iterable/reactnative/Serialization.java @@ -138,17 +138,6 @@ 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); From 80ec4fc37e3e2dfafff0de2dce797d2aa6ba370a Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Fri, 10 Oct 2025 23:14:15 -0700 Subject: [PATCH 16/49] refactor: remove commented-out error handling in Embedded component --- example/src/components/Embedded/Embedded.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/example/src/components/Embedded/Embedded.tsx b/example/src/components/Embedded/Embedded.tsx index 4381cc2d1..39707e2ad 100644 --- a/example/src/components/Embedded/Embedded.tsx +++ b/example/src/components/Embedded/Embedded.tsx @@ -11,9 +11,6 @@ export const Embedded = () => { console.log(ids); setPlacementIds(ids as number[]); }); - // .catch((error) => { - // console.error(error); - // }); }, []); return ( From 1bee2d631af79d553ab7b2d0bb5c8e83e9aec085 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 13 Oct 2025 09:38:58 -0700 Subject: [PATCH 17/49] feat: add session management methods to IterableEmbeddedManager --- .../classes/IterableEmbeddedManager.ts | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/embedded/classes/IterableEmbeddedManager.ts b/src/embedded/classes/IterableEmbeddedManager.ts index c4f8ae229..b485280cf 100644 --- a/src/embedded/classes/IterableEmbeddedManager.ts +++ b/src/embedded/classes/IterableEmbeddedManager.ts @@ -1,4 +1,5 @@ import { IterableApi } from '../../core/classes/IterableApi'; +import { IterableLogger } from '../../core/classes/IterableLogger'; /** * Manages embedded messages from Iterable. @@ -25,4 +26,26 @@ export class IterableEmbeddedManager { getPlacementIds() { return IterableApi.getEmbeddedPlacementIds(); } + + /** + * Starts a session, or a period of time when a user is on a screen or page + * that can display embedded messages. + */ + startSession() { + IterableLogger.log('IterableEmbeddedManager.startSession'); + } + + /** + * Ends a session. + */ + endSession() { + IterableLogger.log('IterableEmbeddedManager.endSession'); + } + + /** + * Tracks an embedded session. + */ + trackSession() { + IterableLogger.log('IterableEmbeddedManager.trackSession'); + } } From 0ba0b7f925ede49b6dcc8277e5c87e698acc0274 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 13 Oct 2025 10:40:22 -0700 Subject: [PATCH 18/49] docs: enhance session management method documentation in IterableEmbeddedManager --- .../classes/IterableEmbeddedManager.ts | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/embedded/classes/IterableEmbeddedManager.ts b/src/embedded/classes/IterableEmbeddedManager.ts index b485280cf..df52ffd02 100644 --- a/src/embedded/classes/IterableEmbeddedManager.ts +++ b/src/embedded/classes/IterableEmbeddedManager.ts @@ -28,8 +28,18 @@ export class IterableEmbeddedManager { } /** - * Starts a session, or a period of time when a user is on a screen or page - * that can display embedded messages. + * Starts a session. + * + * As session is a period of time when a user is on a screen or page that can + * display embedded messages. + * + * When a user comes to a screen or page in your app where embedded messages + * are displayed (in one or more placements), a session should be started. + * + * @example + * ```typescript + * IterableEmbeddedManager.startSession(); + * ``` */ startSession() { IterableLogger.log('IterableEmbeddedManager.startSession'); @@ -37,6 +47,15 @@ export class IterableEmbeddedManager { /** * Ends a session. + * + * When a user leaves a screen in your app where embedded messages are + * displayed, the session should be ended. This causes the SDK to send + * session and impression data back to the server. + * + * @example + * ```typescript + * IterableEmbeddedManager.endSession(); + * ``` */ endSession() { IterableLogger.log('IterableEmbeddedManager.endSession'); @@ -44,6 +63,11 @@ export class IterableEmbeddedManager { /** * Tracks an embedded session. + * + * @example + * ```typescript + * IterableEmbeddedManager.trackSession(); + * ``` */ trackSession() { IterableLogger.log('IterableEmbeddedManager.trackSession'); From 24d5a7042f373fb1873fd68d232c7416a9bf6247 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 13 Oct 2025 10:55:15 -0700 Subject: [PATCH 19/49] feat: add startEmbeddedSession method --- .../iterable/reactnative/RNIterableAPIModuleImpl.java | 5 +++++ android/src/newarch/java/com/RNIterableAPIModule.java | 5 +++++ android/src/oldarch/java/com/RNIterableAPIModule.java | 5 +++++ example/src/components/Embedded/Embedded.tsx | 10 ++++++++++ src/api/NativeRNIterableAPI.ts | 1 + src/core/classes/IterableApi.ts | 8 ++++++++ src/embedded/classes/IterableEmbeddedManager.ts | 2 +- 7 files changed, 35 insertions(+), 1 deletion(-) diff --git a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java index fcbbbb2e1..f9bddf2c5 100644 --- a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java +++ b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java @@ -650,6 +650,11 @@ public void onInboxUpdated() { // --------------------------------------------------------------------------------------- // region Embedded messaging + + public void startEmbeddedSession() { + IterableApi.getInstance().getEmbeddedManager().getEmbeddedSessionManager().startSession(); + } + public void getEmbeddedPlacementIds(Promise promise) { IterableLogger.d(TAG, "getEmbeddedPlacementIds"); try { diff --git a/android/src/newarch/java/com/RNIterableAPIModule.java b/android/src/newarch/java/com/RNIterableAPIModule.java index 50e814f0b..3f649f0c6 100644 --- a/android/src/newarch/java/com/RNIterableAPIModule.java +++ b/android/src/newarch/java/com/RNIterableAPIModule.java @@ -224,6 +224,11 @@ public void pauseAuthRetries(boolean pauseRetry) { moduleImpl.pauseAuthRetries(pauseRetry); } + @Override + public void startEmbeddedSession() { + moduleImpl.startEmbeddedSession(); + } + @Override 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 4555fd71b..9da39c755 100644 --- a/android/src/oldarch/java/com/RNIterableAPIModule.java +++ b/android/src/oldarch/java/com/RNIterableAPIModule.java @@ -228,6 +228,11 @@ public void pauseAuthRetries(boolean pauseRetry) { moduleImpl.pauseAuthRetries(pauseRetry); } + @ReactMethod + public void startEmbeddedSession() { + moduleImpl.startEmbeddedSession(); + } + @ReactMethod public void getEmbeddedPlacementIds(Promise promise) { moduleImpl.getEmbeddedPlacementIds(promise); diff --git a/example/src/components/Embedded/Embedded.tsx b/example/src/components/Embedded/Embedded.tsx index 39707e2ad..71962f0a7 100644 --- a/example/src/components/Embedded/Embedded.tsx +++ b/example/src/components/Embedded/Embedded.tsx @@ -13,6 +13,13 @@ export const Embedded = () => { }); }, []); + const startEmbeddedSession = useCallback(() => { + console.log( + 'startEmbeddedSession --> check android/ios logs to check if it worked' + ); + Iterable.embeddedManager.startSession(); + }, []); + return ( EMBEDDED @@ -29,6 +36,9 @@ export const Embedded = () => { Get placement ids + + Start embedded session + ); }; diff --git a/src/api/NativeRNIterableAPI.ts b/src/api/NativeRNIterableAPI.ts index 9dd2af2b7..e157a403f 100644 --- a/src/api/NativeRNIterableAPI.ts +++ b/src/api/NativeRNIterableAPI.ts @@ -119,6 +119,7 @@ export interface Spec extends TurboModule { pauseAuthRetries(pauseRetry: boolean): void; // Embedded Messaging + startEmbeddedSession(): void; getEmbeddedPlacementIds(): Promise; // Wake app -- android only diff --git a/src/core/classes/IterableApi.ts b/src/core/classes/IterableApi.ts index 9825f42ae..e005d32f1 100644 --- a/src/core/classes/IterableApi.ts +++ b/src/core/classes/IterableApi.ts @@ -510,6 +510,14 @@ export class IterableApi { // ======================= EMBEDDED ===================== // // ====================================================== // + /** + * Starts an embedded session. + */ + static startEmbeddedSession() { + IterableLogger.log('startEmbeddedSession'); + return RNIterableAPI.startEmbeddedSession(); + } + /** * Get the embedded placement IDs. */ diff --git a/src/embedded/classes/IterableEmbeddedManager.ts b/src/embedded/classes/IterableEmbeddedManager.ts index df52ffd02..0983303e1 100644 --- a/src/embedded/classes/IterableEmbeddedManager.ts +++ b/src/embedded/classes/IterableEmbeddedManager.ts @@ -42,7 +42,7 @@ export class IterableEmbeddedManager { * ``` */ startSession() { - IterableLogger.log('IterableEmbeddedManager.startSession'); + return IterableApi.startEmbeddedSession(); } /** From be8998a0630693474df2723b547641ea33ad446a Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 13 Oct 2025 10:59:42 -0700 Subject: [PATCH 20/49] feat: add endEmbeddedSession method and update session management in RNIterableAPIModule --- .../iterable/reactnative/RNIterableAPIModuleImpl.java | 6 ++++++ android/src/newarch/java/com/RNIterableAPIModule.java | 5 +++++ android/src/oldarch/java/com/RNIterableAPIModule.java | 5 +++++ example/src/components/Embedded/Embedded.tsx | 10 ++++++++++ src/api/NativeRNIterableAPI.ts | 1 + src/core/classes/IterableApi.ts | 8 ++++++++ src/embedded/classes/IterableEmbeddedManager.ts | 2 +- 7 files changed, 36 insertions(+), 1 deletion(-) diff --git a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java index f9bddf2c5..ece9d8737 100644 --- a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java +++ b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java @@ -652,9 +652,15 @@ public void onInboxUpdated() { // region Embedded messaging public void startEmbeddedSession() { + IterableLogger.d(TAG, "startEmbeddedSession"); IterableApi.getInstance().getEmbeddedManager().getEmbeddedSessionManager().startSession(); } + public void endEmbeddedSession() { + IterableLogger.d(TAG, "endEmbeddedSession"); + IterableApi.getInstance().getEmbeddedManager().getEmbeddedSessionManager().endSession(); + } + public void getEmbeddedPlacementIds(Promise promise) { IterableLogger.d(TAG, "getEmbeddedPlacementIds"); try { diff --git a/android/src/newarch/java/com/RNIterableAPIModule.java b/android/src/newarch/java/com/RNIterableAPIModule.java index 3f649f0c6..4c67af4e5 100644 --- a/android/src/newarch/java/com/RNIterableAPIModule.java +++ b/android/src/newarch/java/com/RNIterableAPIModule.java @@ -229,6 +229,11 @@ public void startEmbeddedSession() { moduleImpl.startEmbeddedSession(); } + @Override + public void endEmbeddedSession() { + moduleImpl.endEmbeddedSession(); + } + @Override 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 9da39c755..868f4051d 100644 --- a/android/src/oldarch/java/com/RNIterableAPIModule.java +++ b/android/src/oldarch/java/com/RNIterableAPIModule.java @@ -233,6 +233,11 @@ public void startEmbeddedSession() { moduleImpl.startEmbeddedSession(); } + @ReactMethod + public void endEmbeddedSession() { + moduleImpl.endEmbeddedSession(); + } + @ReactMethod public void getEmbeddedPlacementIds(Promise promise) { moduleImpl.getEmbeddedPlacementIds(promise); diff --git a/example/src/components/Embedded/Embedded.tsx b/example/src/components/Embedded/Embedded.tsx index 71962f0a7..efedac721 100644 --- a/example/src/components/Embedded/Embedded.tsx +++ b/example/src/components/Embedded/Embedded.tsx @@ -20,6 +20,13 @@ export const Embedded = () => { Iterable.embeddedManager.startSession(); }, []); + const endEmbeddedSession = useCallback(() => { + console.log( + 'endEmbeddedSession --> check android/ios logs to check if it worked' + ); + Iterable.embeddedManager.endSession(); + }, []); + return ( EMBEDDED @@ -39,6 +46,9 @@ export const Embedded = () => { Start embedded session + + End embedded session + ); }; diff --git a/src/api/NativeRNIterableAPI.ts b/src/api/NativeRNIterableAPI.ts index e157a403f..931be6f29 100644 --- a/src/api/NativeRNIterableAPI.ts +++ b/src/api/NativeRNIterableAPI.ts @@ -120,6 +120,7 @@ export interface Spec extends TurboModule { // Embedded Messaging startEmbeddedSession(): void; + endEmbeddedSession(): void; getEmbeddedPlacementIds(): Promise; // Wake app -- android only diff --git a/src/core/classes/IterableApi.ts b/src/core/classes/IterableApi.ts index e005d32f1..84c8f3a7a 100644 --- a/src/core/classes/IterableApi.ts +++ b/src/core/classes/IterableApi.ts @@ -518,6 +518,14 @@ export class IterableApi { return RNIterableAPI.startEmbeddedSession(); } + /** + * Ends an embedded session. + */ + static endEmbeddedSession() { + IterableLogger.log('endEmbeddedSession'); + return RNIterableAPI.endEmbeddedSession(); + } + /** * Get the embedded placement IDs. */ diff --git a/src/embedded/classes/IterableEmbeddedManager.ts b/src/embedded/classes/IterableEmbeddedManager.ts index 0983303e1..9a5f14487 100644 --- a/src/embedded/classes/IterableEmbeddedManager.ts +++ b/src/embedded/classes/IterableEmbeddedManager.ts @@ -58,7 +58,7 @@ export class IterableEmbeddedManager { * ``` */ endSession() { - IterableLogger.log('IterableEmbeddedManager.endSession'); + return IterableApi.endEmbeddedSession(); } /** From cbb547f84ac6aad853774f1ee5f5207e741bd300 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 13 Oct 2025 11:04:24 -0700 Subject: [PATCH 21/49] docs: update IterableEmbeddedManager documentation to clarify enableEmbeddedMessaging flag --- .../classes/IterableEmbeddedManager.ts | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/src/embedded/classes/IterableEmbeddedManager.ts b/src/embedded/classes/IterableEmbeddedManager.ts index 9a5f14487..5515cb6ab 100644 --- a/src/embedded/classes/IterableEmbeddedManager.ts +++ b/src/embedded/classes/IterableEmbeddedManager.ts @@ -1,5 +1,4 @@ import { IterableApi } from '../../core/classes/IterableApi'; -import { IterableLogger } from '../../core/classes/IterableLogger'; /** * Manages embedded messages from Iterable. @@ -15,6 +14,9 @@ import { IterableLogger } from '../../core/classes/IterableLogger'; export class IterableEmbeddedManager { /** * Whether the embedded manager is enabled. + * + * This is set through the `enableEmbeddedMessaging` flag in the + * `IterableConfig` class. */ isEnabled = false; @@ -52,6 +54,9 @@ export class IterableEmbeddedManager { * displayed, the session should be ended. This causes the SDK to send * session and impression data back to the server. * + * A session is tracked when it is ended, so you should be able to find + * tracking data after this method is called. + * * @example * ```typescript * IterableEmbeddedManager.endSession(); @@ -60,16 +65,4 @@ export class IterableEmbeddedManager { endSession() { return IterableApi.endEmbeddedSession(); } - - /** - * Tracks an embedded session. - * - * @example - * ```typescript - * IterableEmbeddedManager.trackSession(); - * ``` - */ - trackSession() { - IterableLogger.log('IterableEmbeddedManager.trackSession'); - } } From 760a9e38950623cd0c9b6192c10bfa2e472cf0ce Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 13 Oct 2025 11:18:08 -0700 Subject: [PATCH 22/49] feat: add syncEmbeddedMessages method --- .../reactnative/RNIterableAPIModuleImpl.java | 5 ++++ .../newarch/java/com/RNIterableAPIModule.java | 5 ++++ .../oldarch/java/com/RNIterableAPIModule.java | 5 ++++ example/src/components/Embedded/Embedded.tsx | 8 +++++++ src/api/NativeRNIterableAPI.ts | 1 + src/core/classes/IterableApi.ts | 8 +++++++ .../classes/IterableEmbeddedManager.ts | 23 +++++++++++++++++++ 7 files changed, 55 insertions(+) diff --git a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java index ece9d8737..0d332fddf 100644 --- a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java +++ b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java @@ -651,6 +651,11 @@ public void onInboxUpdated() { // --------------------------------------------------------------------------------------- // region Embedded messaging + public void syncEmbeddedMessages() { + IterableLogger.d(TAG, "syncEmbeddedMessages"); + IterableApi.getInstance().getEmbeddedManager().syncMessages(); + } + public void startEmbeddedSession() { IterableLogger.d(TAG, "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 4c67af4e5..ceb653958 100644 --- a/android/src/newarch/java/com/RNIterableAPIModule.java +++ b/android/src/newarch/java/com/RNIterableAPIModule.java @@ -224,6 +224,11 @@ public void pauseAuthRetries(boolean pauseRetry) { moduleImpl.pauseAuthRetries(pauseRetry); } + @Override + public void syncEmbeddedMessages() { + moduleImpl.syncEmbeddedMessages(); + } + @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 868f4051d..6ce874896 100644 --- a/android/src/oldarch/java/com/RNIterableAPIModule.java +++ b/android/src/oldarch/java/com/RNIterableAPIModule.java @@ -228,6 +228,11 @@ public void pauseAuthRetries(boolean pauseRetry) { moduleImpl.pauseAuthRetries(pauseRetry); } + @ReactMethod + public void syncEmbeddedMessages() { + moduleImpl.syncEmbeddedMessages(); + } + @ReactMethod public void startEmbeddedSession() { moduleImpl.startEmbeddedSession(); diff --git a/example/src/components/Embedded/Embedded.tsx b/example/src/components/Embedded/Embedded.tsx index efedac721..7f92c154c 100644 --- a/example/src/components/Embedded/Embedded.tsx +++ b/example/src/components/Embedded/Embedded.tsx @@ -6,6 +6,11 @@ import styles from './Embedded.styles'; export const Embedded = () => { const [placementIds, setPlacementIds] = useState([]); + + const syncEmbeddedMessages = useCallback(() => { + Iterable.embeddedManager.syncMessages(); + }, []); + const getPlacementIds = useCallback(() => { Iterable.embeddedManager.getPlacementIds().then((ids: unknown) => { console.log(ids); @@ -40,6 +45,9 @@ export const Embedded = () => { Placement ids: [{placementIds.join(', ')}] + + Sync embedded messages + Get placement ids diff --git a/src/api/NativeRNIterableAPI.ts b/src/api/NativeRNIterableAPI.ts index 931be6f29..f6c1249e5 100644 --- a/src/api/NativeRNIterableAPI.ts +++ b/src/api/NativeRNIterableAPI.ts @@ -119,6 +119,7 @@ export interface Spec extends TurboModule { pauseAuthRetries(pauseRetry: boolean): void; // Embedded Messaging + syncEmbeddedMessages(): void; startEmbeddedSession(): void; endEmbeddedSession(): void; getEmbeddedPlacementIds(): Promise; diff --git a/src/core/classes/IterableApi.ts b/src/core/classes/IterableApi.ts index 84c8f3a7a..9f6c90ec7 100644 --- a/src/core/classes/IterableApi.ts +++ b/src/core/classes/IterableApi.ts @@ -510,6 +510,14 @@ export class IterableApi { // ======================= EMBEDDED ===================== // // ====================================================== // + /** + * Syncs embedded local cache with the server. + */ + static syncEmbeddedMessages() { + IterableLogger.log('syncEmbeddedMessages'); + return RNIterableAPI.syncEmbeddedMessages(); + } + /** * Starts an embedded session. */ diff --git a/src/embedded/classes/IterableEmbeddedManager.ts b/src/embedded/classes/IterableEmbeddedManager.ts index 5515cb6ab..5a31284ff 100644 --- a/src/embedded/classes/IterableEmbeddedManager.ts +++ b/src/embedded/classes/IterableEmbeddedManager.ts @@ -20,6 +20,29 @@ export class IterableEmbeddedManager { */ isEnabled = false; + /** + * Syncs embedded local cache with the server. + * + * When your app first launches, and each time it comes to the foreground, + * Iterable's iOS SDK automatically refresh a local, on-device cache of + * embedded messages for the signed-in user. These are the messages the + * signed-in user is eligible to see. + * + * At key points during your app's lifecycle, you may want to manually refresh + * your app's local cache of embedded messages. For example, as users navigate + * around, on pull-to-refresh, etc. + * + * However, do not poll for new embedded messages at a regular interval. + * + * @example + * ```typescript + * IterableEmbeddedManager.syncMessages(); + * ``` + */ + syncMessages() { + return IterableApi.syncEmbeddedMessages(); + } + /** * Retrieves a list of placement IDs for the embedded manager. * From 1986caa0442ee36e657e875dce788c81f7763edd Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 13 Oct 2025 12:24:15 -0700 Subject: [PATCH 23/49] feat: implement getEmbeddedMessages method and add example on embedded --- .../reactnative/RNIterableAPIModuleImpl.java | 35 ++++++++ .../iterable/reactnative/Serialization.java | 11 +++ .../newarch/java/com/RNIterableAPIModule.java | 5 ++ .../oldarch/java/com/RNIterableAPIModule.java | 5 ++ example/src/components/Embedded/Embedded.tsx | 81 +++++++++++++------ src/api/NativeRNIterableAPI.ts | 30 +++++++ src/core/classes/IterableApi.ts | 13 +++ .../classes/IterableEmbeddedManager.ts | 13 +++ src/embedded/index.ts | 1 + src/embedded/types/IterableEmbeddedMessage.ts | 14 ++++ .../types/IterableEmbeddedMessageElements.ts | 25 ++++++ .../IterableEmbeddedMessageElementsButton.ts | 10 +++ .../IterableEmbeddedMessageElementsText.ts | 8 ++ .../types/IterableEmbeddedMessageMetadata.ts | 19 +++++ src/embedded/types/index.ts | 5 ++ src/index.tsx | 5 +- 16 files changed, 255 insertions(+), 25 deletions(-) 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/IterableEmbeddedMessageElementsText.ts create mode 100644 src/embedded/types/IterableEmbeddedMessageMetadata.ts create mode 100644 src/embedded/types/index.ts diff --git a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java index 0d332fddf..61781ecd2 100644 --- a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java +++ b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java @@ -683,6 +683,41 @@ public void getEmbeddedPlacementIds(Promise promise) { } } + public void getEmbeddedMessages(@Nullable ReadableArray placementIds, Promise promise) { + IterableLogger.d(TAG, "getEmbeddedMessages for placements: " + placementIds); + + try { + 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) { + IterableLogger.e(TAG, e.getLocalizedMessage()); + promise.reject("", "Failed to fetch messages with error " + e.getLocalizedMessage()); + } + } + // --------------------------------------------------------------------------------------- // endregion } diff --git a/android/src/main/java/com/iterable/reactnative/Serialization.java b/android/src/main/java/com/iterable/reactnative/Serialization.java index bace4c16c..2f0c4e1ca 100644 --- a/android/src/main/java/com/iterable/reactnative/Serialization.java +++ b/android/src/main/java/com/iterable/reactnative/Serialization.java @@ -138,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); diff --git a/android/src/newarch/java/com/RNIterableAPIModule.java b/android/src/newarch/java/com/RNIterableAPIModule.java index ceb653958..ade1996f6 100644 --- a/android/src/newarch/java/com/RNIterableAPIModule.java +++ b/android/src/newarch/java/com/RNIterableAPIModule.java @@ -244,6 +244,11 @@ public void getEmbeddedPlacementIds(Promise promise) { moduleImpl.getEmbeddedPlacementIds(promise); } + @Override + public void getEmbeddedMessages(@Nullable ReadableArray placementIds, Promise promise) { + moduleImpl.getEmbeddedMessages(placementIds, promise); + } + public void sendEvent(@NonNull String eventName, @Nullable Object eventData) { moduleImpl.sendEvent(eventName, eventData); } diff --git a/android/src/oldarch/java/com/RNIterableAPIModule.java b/android/src/oldarch/java/com/RNIterableAPIModule.java index 6ce874896..468c2e4e4 100644 --- a/android/src/oldarch/java/com/RNIterableAPIModule.java +++ b/android/src/oldarch/java/com/RNIterableAPIModule.java @@ -248,6 +248,11 @@ public void getEmbeddedPlacementIds(Promise promise) { moduleImpl.getEmbeddedPlacementIds(promise); } + @ReactMethod + public void getEmbeddedMessages(@Nullable ReadableArray placementIds, Promise promise) { + moduleImpl.getEmbeddedMessages(placementIds, promise); + } + public void sendEvent(@NonNull String eventName, @Nullable Object eventData) { moduleImpl.sendEvent(eventName, eventData); } diff --git a/example/src/components/Embedded/Embedded.tsx b/example/src/components/Embedded/Embedded.tsx index 7f92c154c..a824a3f24 100644 --- a/example/src/components/Embedded/Embedded.tsx +++ b/example/src/components/Embedded/Embedded.tsx @@ -1,20 +1,27 @@ import { Text, TouchableOpacity, View } from 'react-native'; import { useCallback, useState } from 'react'; -import { Iterable } from '@iterable/react-native-sdk'; +import { + Iterable, + type IterableEmbeddedMessage, +} from '@iterable/react-native-sdk'; import styles from './Embedded.styles'; export const Embedded = () => { const [placementIds, setPlacementIds] = useState([]); + const [embeddedMessages, setEmbeddedMessages] = useState< + IterableEmbeddedMessage[] + >([]); const syncEmbeddedMessages = useCallback(() => { Iterable.embeddedManager.syncMessages(); }, []); const getPlacementIds = useCallback(() => { - Iterable.embeddedManager.getPlacementIds().then((ids: unknown) => { + return Iterable.embeddedManager.getPlacementIds().then((ids: unknown) => { console.log(ids); setPlacementIds(ids as number[]); + return ids; }); }, []); @@ -32,31 +39,57 @@ export const Embedded = () => { Iterable.embeddedManager.endSession(); }, []); + const getEmbeddedMessages = useCallback(() => { + getPlacementIds() + .then((ids: number[]) => Iterable.embeddedManager.getMessages(ids)) + .then((messages: IterableEmbeddedMessage[]) => { + setEmbeddedMessages(messages); + console.log(messages); + }); + }, [getPlacementIds]); + return ( EMBEDDED - - Does embedded class exist? {Iterable.embeddedManager ? 'Yes' : 'No'} - - - Is embedded manager enabled? - {Iterable.embeddedManager.isEnabled ? 'Yes' : 'No'} - - - Placement ids: [{placementIds.join(', ')}] - - - Sync embedded messages - - - Get placement ids - - - Start embedded session - - - End embedded session - + + + Does embedded class exist? {Iterable.embeddedManager ? 'Yes' : 'No'} + + + Is embedded manager enabled? + {Iterable.embeddedManager.isEnabled ? 'Yes' : 'No'} + + + Placement ids: [{placementIds.join(', ')}] + + + Sync embedded messages + + + Get placement ids + + + Start embedded session + + + End embedded session + + + Get embedded messages + + + + + {embeddedMessages.map((message) => ( + + Embedded message + metadata.messageId: {message.metadata.messageId} + metadata.placementId: {message.metadata.placementId} + elements.title: {message.elements?.title} + elements.body: {message.elements?.body} + + ))} + ); }; diff --git a/src/api/NativeRNIterableAPI.ts b/src/api/NativeRNIterableAPI.ts index f6c1249e5..a9b32a400 100644 --- a/src/api/NativeRNIterableAPI.ts +++ b/src/api/NativeRNIterableAPI.ts @@ -1,6 +1,33 @@ import type { TurboModule } from 'react-native'; import { TurboModuleRegistry } from 'react-native'; +// NOTE: No types can be imported because of the way new arch works, so we have +// to re-define the types here. +interface EmbeddedMessage { + metadata: { + messageId: string; + placementId: number; + campaignId?: number | null; + isProof?: 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 Spec extends TurboModule { // Initialization initializeWithApiKey( @@ -123,6 +150,9 @@ export interface Spec extends TurboModule { startEmbeddedSession(): void; endEmbeddedSession(): void; getEmbeddedPlacementIds(): Promise; + getEmbeddedMessages( + placementIds: number[] | null + ): Promise; // Wake app -- android only wakeApp(): void; diff --git a/src/core/classes/IterableApi.ts b/src/core/classes/IterableApi.ts index 9f6c90ec7..52f54db80 100644 --- a/src/core/classes/IterableApi.ts +++ b/src/core/classes/IterableApi.ts @@ -12,6 +12,7 @@ import { IterableAttributionInfo } from './IterableAttributionInfo'; import type { IterableCommerceItem } from './IterableCommerceItem'; import { IterableConfig } from './IterableConfig'; import { IterableLogger } from './IterableLogger'; +import type { IterableEmbeddedMessage } from '../../embedded/types/IterableEmbeddedMessage'; /** * Contains functions that directly interact with the native layer. @@ -542,6 +543,18 @@ export class IterableApi { return RNIterableAPI.getEmbeddedPlacementIds(); } + /** + * Get the embedded messages. + * + * @returns A Promise that resolves to an array of embedded messages. + */ + static getEmbeddedMessages( + placementIds: number[] | null + ): Promise { + IterableLogger.log('getEmbeddedMessages: ', placementIds); + return RNIterableAPI.getEmbeddedMessages(placementIds); + } + // ---- End EMBEDDED ---- // // ====================================================== // diff --git a/src/embedded/classes/IterableEmbeddedManager.ts b/src/embedded/classes/IterableEmbeddedManager.ts index 5a31284ff..1e2806e68 100644 --- a/src/embedded/classes/IterableEmbeddedManager.ts +++ b/src/embedded/classes/IterableEmbeddedManager.ts @@ -1,4 +1,5 @@ import { IterableApi } from '../../core/classes/IterableApi'; +import type { IterableEmbeddedMessage } from '../types/IterableEmbeddedMessage'; /** * Manages embedded messages from Iterable. @@ -52,6 +53,18 @@ export class IterableEmbeddedManager { return IterableApi.getEmbeddedPlacementIds(); } + /** + * Retrieves a list of embedded messages the user is eligible to see. + * + * @param placementIds - The placement IDs to retrieve messages for. + * @returns A Promise that resolves to an array of embedded messages. + */ + getMessages( + placementIds: number[] | null + ): Promise { + return IterableApi.getEmbeddedMessages(placementIds); + } + /** * Starts a session. * diff --git a/src/embedded/index.ts b/src/embedded/index.ts index d7d17c691..15eb796c9 100644 --- a/src/embedded/index.ts +++ b/src/embedded/index.ts @@ -1 +1,2 @@ export * from './classes'; +export * from './types'; diff --git a/src/embedded/types/IterableEmbeddedMessage.ts b/src/embedded/types/IterableEmbeddedMessage.ts new file mode 100644 index 000000000..2f0778305 --- /dev/null +++ b/src/embedded/types/IterableEmbeddedMessage.ts @@ -0,0 +1,14 @@ +import type { IterableEmbeddedMessageElements } from './IterableEmbeddedMessageElements'; +import type { IterableEmbeddedMessageMetadata } from './IterableEmbeddedMessageMetadata'; + +/** + * An embedded message. + */ +export interface IterableEmbeddedMessage { + /** Identifying information about the campaign. */ + metadata: IterableEmbeddedMessageMetadata; + /** What to display, and how to handle interaction. */ + elements?: IterableEmbeddedMessageElements | null; + /** Custom JSON data included with the campaign. */ + payload?: Record | null; +} diff --git a/src/embedded/types/IterableEmbeddedMessageElements.ts b/src/embedded/types/IterableEmbeddedMessageElements.ts new file mode 100644 index 000000000..7eb41056f --- /dev/null +++ b/src/embedded/types/IterableEmbeddedMessageElements.ts @@ -0,0 +1,25 @@ +import type { IterableAction } from '../../core/classes/IterableAction'; +import type { IterableEmbeddedMessageElementsButton } from './IterableEmbeddedMessageElementsButton'; +import type { IterableEmbeddedMessageElementsText } from './IterableEmbeddedMessageElementsText'; + +/** + * The elements of an embedded message. + * + * Includes what to display, and how to handle interaction. + */ +export interface IterableEmbeddedMessageElements { + /** The message's title text. */ + title?: string | null; + /** The message's body text. */ + body?: string | null; + /** The URL of an image associated with the message. */ + mediaUrl?: string | null; + /** Text description of the image. */ + mediaUrlCaption?: string | null; + /** What to do when a user clicks on the message (outside of its buttons). */ + defaultAction?: IterableAction | null; + /** Buttons to display. */ + buttons?: IterableEmbeddedMessageElementsButton[] | null; + /** Extra data fields. Not for display. */ + text?: IterableEmbeddedMessageElementsText[] | null; +} diff --git a/src/embedded/types/IterableEmbeddedMessageElementsButton.ts b/src/embedded/types/IterableEmbeddedMessageElementsButton.ts new file mode 100644 index 000000000..785d5932d --- /dev/null +++ b/src/embedded/types/IterableEmbeddedMessageElementsButton.ts @@ -0,0 +1,10 @@ +import type { IterableAction } from '../../core/classes/IterableAction'; + +export interface IterableEmbeddedMessageElementsButton { + /** The ID. */ + id: string; + /** The title. */ + title?: string | null; + /** The action. */ + action?: IterableAction | null; +} 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..a4b5ff953 --- /dev/null +++ b/src/embedded/types/IterableEmbeddedMessageMetadata.ts @@ -0,0 +1,19 @@ +/** + * Metadata for an embedded message. + * + * Consists of identifying information about the campaign. + */ +export interface IterableEmbeddedMessageMetadata { + /** The ID of the message. */ + messageId: string; + /** The ID of the placement associated with the message. */ + placementId: number; + /** The ID of the campaign associated with the message. */ + campaignId?: number | null; + /** + * Whether the message is a proof/test message. + * + * EG: Sent directly from a template or campaign edit page. + */ + isProof?: boolean; +} diff --git a/src/embedded/types/index.ts b/src/embedded/types/index.ts new file mode 100644 index 000000000..29b809ebf --- /dev/null +++ b/src/embedded/types/index.ts @@ -0,0 +1,5 @@ +export * from './IterableEmbeddedMessage'; +export * from './IterableEmbeddedMessageElements'; +export * from './IterableEmbeddedMessageElementsButton'; +export * from './IterableEmbeddedMessageElementsText'; +export * from './IterableEmbeddedMessageMetadata'; diff --git a/src/index.tsx b/src/index.tsx index c9a014340..75c8489ec 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -59,4 +59,7 @@ export { type IterableInboxProps, type IterableInboxRowViewModel, } from './inbox'; -export { IterableEmbeddedManager } from './embedded'; +export { + IterableEmbeddedManager, + type IterableEmbeddedMessage, +} from './embedded'; From 94e46a647c737151e1dc56ed5ffde73090270cb9 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 13 Oct 2025 12:30:35 -0700 Subject: [PATCH 24/49] feat: enhance Embedded component with ScrollView and button details display --- example/src/components/Embedded/Embedded.tsx | 36 +++++++++++++------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/example/src/components/Embedded/Embedded.tsx b/example/src/components/Embedded/Embedded.tsx index a824a3f24..2be959133 100644 --- a/example/src/components/Embedded/Embedded.tsx +++ b/example/src/components/Embedded/Embedded.tsx @@ -1,4 +1,4 @@ -import { Text, TouchableOpacity, View } from 'react-native'; +import { ScrollView, Text, TouchableOpacity, View } from 'react-native'; import { useCallback, useState } from 'react'; import { Iterable, @@ -79,17 +79,29 @@ export const Embedded = () => { - - {embeddedMessages.map((message) => ( - - Embedded message - metadata.messageId: {message.metadata.messageId} - metadata.placementId: {message.metadata.placementId} - elements.title: {message.elements?.title} - elements.body: {message.elements?.body} - - ))} - + + + {embeddedMessages.map((message) => ( + + Embedded message + metadata.messageId: {message.metadata.messageId} + metadata.placementId: {message.metadata.placementId} + elements.title: {message.elements?.title} + elements.body: {message.elements?.body} + {(message.elements?.buttons ?? []).map((button, buttonIndex) => ( + + Button {buttonIndex + 1} + button.id: {button.id} + button.title: {button.title} + button.action?.data: {button.action?.data} + button.action?.type: {button.action?.type} + + ))} + payload: {JSON.stringify(message.payload)} + + ))} + + ); }; From b1e5760303a6111be108ed3664dcfe10b0570ebb Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 13 Oct 2025 13:09:29 -0700 Subject: [PATCH 25/49] feat: add startEmbeddedImpression method to manage embedded message impressions --- .../reactnative/RNIterableAPIModuleImpl.java | 4 +++ .../newarch/java/com/RNIterableAPIModule.java | 5 ++++ .../oldarch/java/com/RNIterableAPIModule.java | 5 ++++ .../components/Embedded/Embedded.styles.ts | 12 +++++++- example/src/components/Embedded/Embedded.tsx | 30 +++++++++++++++---- example/src/constants/styles/typography.ts | 5 ++++ src/api/NativeRNIterableAPI.ts | 1 + src/core/classes/IterableApi.ts | 8 +++++ .../classes/IterableEmbeddedManager.ts | 22 ++++++++++++++ 9 files changed, 86 insertions(+), 6 deletions(-) diff --git a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java index 61781ecd2..df7a8eab5 100644 --- a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java +++ b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java @@ -666,6 +666,10 @@ public void endEmbeddedSession() { IterableApi.getInstance().getEmbeddedManager().getEmbeddedSessionManager().endSession(); } + public void startEmbeddedImpression(String messageId, int placementId) { + IterableApi.getInstance().getEmbeddedManager().getEmbeddedSessionManager().startImpression(messageId, placementId); + } + public void getEmbeddedPlacementIds(Promise promise) { IterableLogger.d(TAG, "getEmbeddedPlacementIds"); try { diff --git a/android/src/newarch/java/com/RNIterableAPIModule.java b/android/src/newarch/java/com/RNIterableAPIModule.java index ade1996f6..180dc2d21 100644 --- a/android/src/newarch/java/com/RNIterableAPIModule.java +++ b/android/src/newarch/java/com/RNIterableAPIModule.java @@ -239,6 +239,11 @@ public void endEmbeddedSession() { moduleImpl.endEmbeddedSession(); } + @Override + public void startEmbeddedImpression(String messageId, double placementId) { + moduleImpl.startEmbeddedImpression(messageId, (int) placementId); + } + @Override 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 468c2e4e4..a149a8079 100644 --- a/android/src/oldarch/java/com/RNIterableAPIModule.java +++ b/android/src/oldarch/java/com/RNIterableAPIModule.java @@ -243,6 +243,11 @@ public void endEmbeddedSession() { moduleImpl.endEmbeddedSession(); } + @ReactMethod + public void startEmbeddedImpression(String messageId, double placementId) { + moduleImpl.startEmbeddedImpression(messageId, (int) placementId); + } + @ReactMethod public void getEmbeddedPlacementIds(Promise promise) { moduleImpl.getEmbeddedPlacementIds(promise); diff --git a/example/src/components/Embedded/Embedded.styles.ts b/example/src/components/Embedded/Embedded.styles.ts index 9dc6aea47..56241c676 100644 --- a/example/src/components/Embedded/Embedded.styles.ts +++ b/example/src/components/Embedded/Embedded.styles.ts @@ -1,5 +1,5 @@ import { StyleSheet } from 'react-native'; -import { button, buttonText, container, hr } from '../../constants'; +import { button, buttonText, container, hr, link } from '../../constants'; const styles = StyleSheet.create({ button, @@ -11,7 +11,17 @@ const styles = StyleSheet.create({ gap: 16, paddingHorizontal: 16, }, + embeddedTitle: { + fontSize: 16, + fontWeight: 'bold', + lineHeight: 20, + }, + embeddedTitleContainer: { + display: 'flex', + flexDirection: 'row', + }, hr, + link, text: { textAlign: 'center' }, utilitySection: { paddingHorizontal: 16, diff --git a/example/src/components/Embedded/Embedded.tsx b/example/src/components/Embedded/Embedded.tsx index 2be959133..4e6f84d03 100644 --- a/example/src/components/Embedded/Embedded.tsx +++ b/example/src/components/Embedded/Embedded.tsx @@ -48,6 +48,18 @@ export const Embedded = () => { }); }, [getPlacementIds]); + const startEmbeddedImpression = useCallback( + (message: IterableEmbeddedMessage) => { + console.log(`🚀 > Embedded > message:`, message); + Iterable.embeddedManager.startImpression( + message.metadata.messageId, + // TODO: check if this should be changed to a number, as per the type + Number(message.metadata.placementId) + ); + }, + [] + ); + return ( EMBEDDED @@ -63,19 +75,19 @@ export const Embedded = () => { Placement ids: [{placementIds.join(', ')}] - Sync embedded messages + Sync messages Get placement ids - Start embedded session + Start session - End embedded session + End session - Get embedded messages + Get messages @@ -83,7 +95,15 @@ export const Embedded = () => { {embeddedMessages.map((message) => ( - Embedded message + + Embedded message | + startEmbeddedImpression(message)} + > + Start impression + + + metadata.messageId: {message.metadata.messageId} metadata.placementId: {message.metadata.placementId} elements.title: {message.elements?.title} diff --git a/example/src/constants/styles/typography.ts b/example/src/constants/styles/typography.ts index 09b6c2405..f419eb45b 100644 --- a/example/src/constants/styles/typography.ts +++ b/example/src/constants/styles/typography.ts @@ -57,3 +57,8 @@ export const requiredStar: TextStyle = { ...label, color: colors.textDestructive, }; + +export const link: TextStyle = { + color: colors.textInteractive, + textDecorationLine: 'underline', +}; diff --git a/src/api/NativeRNIterableAPI.ts b/src/api/NativeRNIterableAPI.ts index a9b32a400..4388f0711 100644 --- a/src/api/NativeRNIterableAPI.ts +++ b/src/api/NativeRNIterableAPI.ts @@ -153,6 +153,7 @@ export interface Spec extends TurboModule { getEmbeddedMessages( placementIds: number[] | null ): Promise; + startEmbeddedImpression(messageId: string, placementId: number): void; // Wake app -- android only wakeApp(): void; diff --git a/src/core/classes/IterableApi.ts b/src/core/classes/IterableApi.ts index 52f54db80..e23b0cd1f 100644 --- a/src/core/classes/IterableApi.ts +++ b/src/core/classes/IterableApi.ts @@ -535,6 +535,14 @@ export class IterableApi { return RNIterableAPI.endEmbeddedSession(); } + /** + * Starts an embedded impression. + */ + static startEmbeddedImpression(messageId: string, placementId: number) { + IterableLogger.log('startEmbeddedImpression: ', messageId, placementId); + return RNIterableAPI.startEmbeddedImpression(messageId, placementId); + } + /** * Get the embedded placement IDs. */ diff --git a/src/embedded/classes/IterableEmbeddedManager.ts b/src/embedded/classes/IterableEmbeddedManager.ts index 1e2806e68..da2b7d0d6 100644 --- a/src/embedded/classes/IterableEmbeddedManager.ts +++ b/src/embedded/classes/IterableEmbeddedManager.ts @@ -101,4 +101,26 @@ export class IterableEmbeddedManager { endSession() { return IterableApi.endEmbeddedSession(); } + /** + * Starts an embedded impression. + * + * An impression represents the on-screen appearances of a given embedded message, + * in context of a session. + * + * Each impression tracks: + * - The total number of times a message appears during a session. + * - The total amount of time that message was visible, across all its + * appearances in the session. + * + * Be sure to start and pause impressions when your app goes to and from the + * background, too. + * + * @example + * ```typescript + * IterableEmbeddedManager.startImpression(messageId, placementId); + * ``` + */ + startImpression(messageId: string, placementId: number) { + return IterableApi.startEmbeddedImpression(messageId, placementId); + } } From ed9b4adf0536c89801e0dc8efb1eb0b7bec219c3 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 13 Oct 2025 13:17:03 -0700 Subject: [PATCH 26/49] feat: add pauseEmbeddedImpression method to manage embedded message impressions --- .../reactnative/RNIterableAPIModuleImpl.java | 4 ++++ .../newarch/java/com/RNIterableAPIModule.java | 5 +++++ .../oldarch/java/com/RNIterableAPIModule.java | 5 +++++ example/src/components/Embedded/Embedded.tsx | 16 +++++++++++++++- src/api/NativeRNIterableAPI.ts | 3 ++- src/core/classes/IterableApi.ts | 8 ++++++++ .../classes/IterableEmbeddedManager.ts | 18 ++++++++++++++++++ 7 files changed, 57 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 df7a8eab5..9a0f84f73 100644 --- a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java +++ b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java @@ -670,6 +670,10 @@ 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); + } + public void getEmbeddedPlacementIds(Promise promise) { IterableLogger.d(TAG, "getEmbeddedPlacementIds"); try { diff --git a/android/src/newarch/java/com/RNIterableAPIModule.java b/android/src/newarch/java/com/RNIterableAPIModule.java index 180dc2d21..457788ba8 100644 --- a/android/src/newarch/java/com/RNIterableAPIModule.java +++ b/android/src/newarch/java/com/RNIterableAPIModule.java @@ -244,6 +244,11 @@ public void startEmbeddedImpression(String messageId, double placementId) { moduleImpl.startEmbeddedImpression(messageId, (int) placementId); } + @Override + public void pauseEmbeddedImpression(String messageId) { + moduleImpl.pauseEmbeddedImpression(messageId); + } + @Override 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 a149a8079..fb6c76d76 100644 --- a/android/src/oldarch/java/com/RNIterableAPIModule.java +++ b/android/src/oldarch/java/com/RNIterableAPIModule.java @@ -248,6 +248,11 @@ public void startEmbeddedImpression(String messageId, double placementId) { moduleImpl.startEmbeddedImpression(messageId, (int) placementId); } + @ReactMethod + public void pauseEmbeddedImpression(String messageId) { + moduleImpl.pauseEmbeddedImpression(messageId); + } + @ReactMethod public void getEmbeddedPlacementIds(Promise promise) { moduleImpl.getEmbeddedPlacementIds(promise); diff --git a/example/src/components/Embedded/Embedded.tsx b/example/src/components/Embedded/Embedded.tsx index 4e6f84d03..97addf49c 100644 --- a/example/src/components/Embedded/Embedded.tsx +++ b/example/src/components/Embedded/Embedded.tsx @@ -50,7 +50,7 @@ export const Embedded = () => { const startEmbeddedImpression = useCallback( (message: IterableEmbeddedMessage) => { - console.log(`🚀 > Embedded > message:`, message); + console.log(`startEmbeddedImpression`, message); Iterable.embeddedManager.startImpression( message.metadata.messageId, // TODO: check if this should be changed to a number, as per the type @@ -60,6 +60,14 @@ export const Embedded = () => { [] ); + const pauseEmbeddedImpression = useCallback( + (message: IterableEmbeddedMessage) => { + console.log(`pauseEmbeddedImpression:`, message); + Iterable.embeddedManager.pauseImpression(message.metadata.messageId); + }, + [] + ); + return ( EMBEDDED @@ -102,6 +110,12 @@ export const Embedded = () => { > Start impression + | + pauseEmbeddedImpression(message)} + > + Pause impression + metadata.messageId: {message.metadata.messageId} diff --git a/src/api/NativeRNIterableAPI.ts b/src/api/NativeRNIterableAPI.ts index 4388f0711..6e03cb2b7 100644 --- a/src/api/NativeRNIterableAPI.ts +++ b/src/api/NativeRNIterableAPI.ts @@ -149,11 +149,12 @@ export interface Spec extends TurboModule { syncEmbeddedMessages(): void; startEmbeddedSession(): void; endEmbeddedSession(): void; + startEmbeddedImpression(messageId: string, placementId: number): void; + pauseEmbeddedImpression(messageId: string): void; getEmbeddedPlacementIds(): Promise; getEmbeddedMessages( placementIds: number[] | null ): Promise; - startEmbeddedImpression(messageId: string, placementId: number): void; // Wake app -- android only wakeApp(): void; diff --git a/src/core/classes/IterableApi.ts b/src/core/classes/IterableApi.ts index e23b0cd1f..bbe687605 100644 --- a/src/core/classes/IterableApi.ts +++ b/src/core/classes/IterableApi.ts @@ -543,6 +543,14 @@ export class IterableApi { return RNIterableAPI.startEmbeddedImpression(messageId, placementId); } + /** + * Pauses an embedded impression. + */ + static pauseEmbeddedImpression(messageId: string) { + IterableLogger.log('pauseEmbeddedImpression: ', messageId); + return RNIterableAPI.pauseEmbeddedImpression(messageId); + } + /** * Get the embedded placement IDs. */ diff --git a/src/embedded/classes/IterableEmbeddedManager.ts b/src/embedded/classes/IterableEmbeddedManager.ts index da2b7d0d6..6e1a6bfbc 100644 --- a/src/embedded/classes/IterableEmbeddedManager.ts +++ b/src/embedded/classes/IterableEmbeddedManager.ts @@ -123,4 +123,22 @@ export class IterableEmbeddedManager { startImpression(messageId: string, placementId: number) { return IterableApi.startEmbeddedImpression(messageId, placementId); } + + /** + * Pauses an embedded impression. + * + * An impression represents the on-screen appearances of a given embedded message, + * in context of a session. + * + * And impression should be paused when the message is no longer visible, + * including when your app goes to the background. + * + * @example + * ```typescript + * IterableEmbeddedManager.pauseImpression(messageId); + * ``` + */ + pauseImpression(messageId: string) { + return IterableApi.pauseEmbeddedImpression(messageId); + } } From 00506f029e17c8eb7a40d10b6f0491ac1daf9318 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 13 Oct 2025 13:39:32 -0700 Subject: [PATCH 27/49] feat: add trackEmbeddedClick method to handle clicks on embedded messages --- .../reactnative/RNIterableAPIModuleImpl.java | 10 ++++++++ .../iterable/reactnative/Serialization.java | 16 +++++++++++++ .../newarch/java/com/RNIterableAPIModule.java | 5 ++++ .../oldarch/java/com/RNIterableAPIModule.java | 5 ++++ src/api/NativeRNIterableAPI.ts | 5 ++++ src/core/classes/IterableApi.ts | 12 ++++++++++ .../classes/IterableEmbeddedManager.ts | 24 +++++++++++++++++++ 7 files changed, 77 insertions(+) diff --git a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java index 9a0f84f73..f67a64ed4 100644 --- a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java +++ b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java @@ -726,6 +726,16 @@ public void getEmbeddedMessages(@Nullable ReadableArray placementIds, Promise pr } } + public void trackEmbeddedClick(ReadableMap messageMap, String buttonId, String clickedUrl) { + IterableLogger.d(TAG, "trackEmbeddedClick: buttonId: " + buttonId + " clickedUrl: " + 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"); + } + } + // --------------------------------------------------------------------------------------- // endregion } diff --git a/android/src/main/java/com/iterable/reactnative/Serialization.java b/android/src/main/java/com/iterable/reactnative/Serialization.java index 2f0c4e1ca..97aa52f49 100644 --- a/android/src/main/java/com/iterable/reactnative/Serialization.java +++ b/android/src/main/java/com/iterable/reactnative/Serialization.java @@ -149,6 +149,22 @@ static JSONArray serializeEmbeddedMessages(List embedde return embeddedMessagesJson; } + /** + * Converts a ReadableMap to an IterableEmbeddedMessage. + * + * This is needed as in new arch you can only pass in basic types, which + * then need to be converted in the native layer. + */ + 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 457788ba8..056a5649d 100644 --- a/android/src/newarch/java/com/RNIterableAPIModule.java +++ b/android/src/newarch/java/com/RNIterableAPIModule.java @@ -259,6 +259,11 @@ public void getEmbeddedMessages(@Nullable ReadableArray placementIds, Promise pr moduleImpl.getEmbeddedMessages(placementIds, promise); } + @Override + public void trackEmbeddedClick(ReadableMap 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/android/src/oldarch/java/com/RNIterableAPIModule.java b/android/src/oldarch/java/com/RNIterableAPIModule.java index fb6c76d76..f387ed681 100644 --- a/android/src/oldarch/java/com/RNIterableAPIModule.java +++ b/android/src/oldarch/java/com/RNIterableAPIModule.java @@ -263,6 +263,11 @@ public void getEmbeddedMessages(@Nullable ReadableArray placementIds, Promise pr moduleImpl.getEmbeddedMessages(placementIds, promise); } + @ReactMethod + public void trackEmbeddedClick(ReadableMap 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 6e03cb2b7..ed1590528 100644 --- a/src/api/NativeRNIterableAPI.ts +++ b/src/api/NativeRNIterableAPI.ts @@ -155,6 +155,11 @@ export interface Spec extends TurboModule { getEmbeddedMessages( placementIds: number[] | null ): Promise; + trackEmbeddedClick( + message: EmbeddedMessage, + buttonId: string | null, + clickedUrl: string | null + ): void; // Wake app -- android only wakeApp(): void; diff --git a/src/core/classes/IterableApi.ts b/src/core/classes/IterableApi.ts index bbe687605..09d0dc44f 100644 --- a/src/core/classes/IterableApi.ts +++ b/src/core/classes/IterableApi.ts @@ -571,6 +571,18 @@ export class IterableApi { return RNIterableAPI.getEmbeddedMessages(placementIds); } + /** + * Track an embedded click. + */ + static trackEmbeddedClick( + message: IterableEmbeddedMessage, + buttonId: string | null, + clickedUrl: string | null + ) { + IterableLogger.log('trackEmbeddedClick: ', message, buttonId, clickedUrl); + return RNIterableAPI.trackEmbeddedClick(message, buttonId, clickedUrl); + } + // ---- End EMBEDDED ---- // // ====================================================== // diff --git a/src/embedded/classes/IterableEmbeddedManager.ts b/src/embedded/classes/IterableEmbeddedManager.ts index 6e1a6bfbc..f43a73c02 100644 --- a/src/embedded/classes/IterableEmbeddedManager.ts +++ b/src/embedded/classes/IterableEmbeddedManager.ts @@ -141,4 +141,28 @@ export class IterableEmbeddedManager { pauseImpression(messageId: string) { return IterableApi.pauseEmbeddedImpression(messageId); } + + /** + * Tracks a click on an embedded message. + * + * This is called internally when `Iterable.embeddedManager.handleClick` is + * called. However, if you want to implement your own click handling, you can + * use this method to track the click you implement. + * + * @param message - The embedded message. + * @param buttonId - The button ID. + * @param clickedUrl - The clicked URL. + * + * @example + * ```typescript + * IterableEmbeddedManager.trackClick(message, buttonId, clickedUrl); + * ``` + */ + trackClick( + message: IterableEmbeddedMessage, + buttonId: string | null, + clickedUrl: string | null + ) { + return IterableApi.trackEmbeddedClick(message, buttonId, clickedUrl); + } } From 45ef4ebfffbbad43bcd88603e693432438df8931 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 13 Oct 2025 14:20:35 -0700 Subject: [PATCH 28/49] feat: implement handleClick method in IterableEmbeddedManager --- example/src/components/Embedded/Embedded.tsx | 46 +++++++++++- src/core/classes/Iterable.ts | 36 ++++----- src/core/enums/IterableActionSource.ts | 2 + src/core/enums/IterableCustomActionPrefix.ts | 9 +++ src/core/enums/index.ts | 1 + src/core/utils/callUrlHandler.ts | 31 ++++++++ src/core/utils/getActionPrefix.ts | 20 +++++ src/core/utils/index.ts | 2 + .../classes/IterableEmbeddedManager.ts | 74 +++++++++++++++++++ 9 files changed, 196 insertions(+), 25 deletions(-) create mode 100644 src/core/enums/IterableCustomActionPrefix.ts create mode 100644 src/core/utils/callUrlHandler.ts create mode 100644 src/core/utils/getActionPrefix.ts create mode 100644 src/core/utils/index.ts diff --git a/example/src/components/Embedded/Embedded.tsx b/example/src/components/Embedded/Embedded.tsx index 97addf49c..68b748048 100644 --- a/example/src/components/Embedded/Embedded.tsx +++ b/example/src/components/Embedded/Embedded.tsx @@ -2,6 +2,7 @@ import { ScrollView, Text, TouchableOpacity, View } from 'react-native'; import { useCallback, useState } from 'react'; import { Iterable, + type IterableAction, type IterableEmbeddedMessage, } from '@iterable/react-native-sdk'; @@ -68,6 +69,18 @@ export const Embedded = () => { [] ); + const handleClick = useCallback( + ( + message: IterableEmbeddedMessage, + buttonId: string | null, + action?: IterableAction | null + ) => { + console.log(`handleClick:`, message); + Iterable.embeddedManager.handleClick(message, buttonId, action); + }, + [] + ); + return ( EMBEDDED @@ -104,7 +117,9 @@ export const Embedded = () => { {embeddedMessages.map((message) => ( - Embedded message | + Embedded message + + startEmbeddedImpression(message)} > @@ -116,15 +131,42 @@ export const Embedded = () => { > Pause impression + | + + handleClick(message, null, message.elements?.defaultAction) + } + > + Handle click + metadata.messageId: {message.metadata.messageId} metadata.placementId: {message.metadata.placementId} elements.title: {message.elements?.title} elements.body: {message.elements?.body} + + elements.defaultAction.data:{' '} + {message.elements?.defaultAction?.data} + + + elements.defaultAction.type:{' '} + {message.elements?.defaultAction?.type} + {(message.elements?.buttons ?? []).map((button, buttonIndex) => ( - Button {buttonIndex + 1} + + Button {buttonIndex + 1} + | + + handleClick(message, button.id, button.action) + } + > + Handle click + + + button.id: {button.id} button.title: {button.title} button.action?.data: {button.action?.data} diff --git a/src/core/classes/Iterable.ts b/src/core/classes/Iterable.ts index ebb928cd6..1f4535d22 100644 --- a/src/core/classes/Iterable.ts +++ b/src/core/classes/Iterable.ts @@ -1,8 +1,9 @@ -import { Linking, NativeEventEmitter, Platform } from 'react-native'; +import { NativeEventEmitter, Platform } from 'react-native'; import { buildInfo } from '../../itblBuildInfo'; import { RNIterableAPI } from '../../api'; +import { IterableEmbeddedManager } from '../../embedded/classes/IterableEmbeddedManager'; import { IterableInAppManager } from '../../inApp/classes/IterableInAppManager'; import { IterableInAppMessage } from '../../inApp/classes/IterableInAppMessage'; import { IterableInAppCloseSource } from '../../inApp/enums/IterableInAppCloseSource'; @@ -11,6 +12,7 @@ import { IterableInAppLocation } from '../../inApp/enums/IterableInAppLocation'; import { IterableAuthResponseResult } from '../enums/IterableAuthResponseResult'; import { IterableEventName } from '../enums/IterableEventName'; import type { IterableAuthFailure } from '../types/IterableAuthFailure'; +import { callUrlHandler } from '../utils/callUrlHandler'; import { IterableAction } from './IterableAction'; import { IterableActionContext } from './IterableActionContext'; import { IterableApi } from './IterableApi'; @@ -20,10 +22,11 @@ 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); +const defaultConfig = new IterableConfig(); + /* eslint-disable tsdoc/syntax */ /** * The main class for the Iterable React Native SDK. @@ -46,7 +49,7 @@ export class Iterable { /** * Current configuration of the Iterable SDK */ - static savedConfig: IterableConfig = new IterableConfig(); + static savedConfig: IterableConfig = defaultConfig; /** * In-app message manager for the current user. @@ -98,8 +101,9 @@ export class Iterable { * }); * ``` */ - static embeddedManager: IterableEmbeddedManager = - new IterableEmbeddedManager(); + static embeddedManager: IterableEmbeddedManager = new IterableEmbeddedManager( + defaultConfig + ); /** * Initializes the Iterable React Native SDK in your app's Javascript or Typescript code. @@ -177,6 +181,8 @@ export class Iterable { IterableLogger.setLoggingEnabled(config.logReactNativeSdkCalls ?? true); IterableLogger.setLogLevel(config.logLevel); + + Iterable.embeddedManager = new IterableEmbeddedManager(config); } this.setupEventHandlers(); @@ -933,10 +939,10 @@ export class Iterable { if (Platform.OS === 'android') { //Give enough time for Activity to wake up. setTimeout(() => { - callUrlHandler(url, context); + callUrlHandler(Iterable.savedConfig, url, context); }, 1000); } else { - callUrlHandler(url, context); + callUrlHandler(Iterable.savedConfig, url, context); } }); } @@ -1031,22 +1037,6 @@ export class Iterable { } ); } - - function callUrlHandler(url: string, context: IterableActionContext) { - // MOB-10424: Figure out if this is purposeful - // eslint-disable-next-line eqeqeq - if (Iterable.savedConfig.urlHandler?.(url, context) == false) { - Linking.canOpenURL(url) - .then((canOpen) => { - if (canOpen) { - Linking.openURL(url); - } - }) - .catch((reason) => { - IterableLogger?.log('could not open url: ' + reason); - }); - } - } } /** 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/core/enums/IterableCustomActionPrefix.ts b/src/core/enums/IterableCustomActionPrefix.ts new file mode 100644 index 000000000..c8135a009 --- /dev/null +++ b/src/core/enums/IterableCustomActionPrefix.ts @@ -0,0 +1,9 @@ +/** + * Enum representing the prefix of build-in custom action URL. + */ +export enum IterableCustomActionPrefix { + /** Current action prefix */ + Action = 'action://', + /** Deprecated action prefix */ + Itbl = 'itbl://', +} diff --git a/src/core/enums/index.ts b/src/core/enums/index.ts index 52f4eb20d..21f06dee7 100644 --- a/src/core/enums/index.ts +++ b/src/core/enums/index.ts @@ -6,3 +6,4 @@ export * from './IterableEventName'; export * from './IterableLogLevel'; export * from './IterablePushPlatform'; export * from './IterableRetryBackoff'; +export * from './IterableCustomActionPrefix'; diff --git a/src/core/utils/callUrlHandler.ts b/src/core/utils/callUrlHandler.ts new file mode 100644 index 000000000..90f469362 --- /dev/null +++ b/src/core/utils/callUrlHandler.ts @@ -0,0 +1,31 @@ +import { Linking } from 'react-native'; +import type { IterableActionContext } from '../classes/IterableActionContext'; +import { IterableLogger } from '../classes/IterableLogger'; +import type { IterableConfig } from '../classes/IterableConfig'; + +/** + * Calls the URL handler and attempts to open the URL if the handler returns false. + * + * @param config - The config to use. + * @param url - The URL to call. + * @param context - The context to use. + */ +export function callUrlHandler( + config: IterableConfig, + url: string, + context: IterableActionContext +) { + // MOB-10424: Figure out if this is purposeful + // eslint-disable-next-line eqeqeq + if (config.urlHandler?.(url, context) == false) { + Linking.canOpenURL(url) + .then((canOpen) => { + if (canOpen) { + Linking.openURL(url); + } + }) + .catch((reason) => { + IterableLogger?.log('could not open url: ' + reason); + }); + } +} diff --git a/src/core/utils/getActionPrefix.ts b/src/core/utils/getActionPrefix.ts new file mode 100644 index 000000000..b4fcf9fbe --- /dev/null +++ b/src/core/utils/getActionPrefix.ts @@ -0,0 +1,20 @@ +import { IterableCustomActionPrefix } from '../enums/IterableCustomActionPrefix'; + +/** + * Gets the action prefix from a string. + * + * @param str - The string to get the action prefix from. + * @returns The action prefix. + */ +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/index.ts b/src/core/utils/index.ts new file mode 100644 index 000000000..b489ab8b4 --- /dev/null +++ b/src/core/utils/index.ts @@ -0,0 +1,2 @@ +export * from './getActionPrefix'; +export * from './callUrlHandler'; diff --git a/src/embedded/classes/IterableEmbeddedManager.ts b/src/embedded/classes/IterableEmbeddedManager.ts index f43a73c02..26720dac8 100644 --- a/src/embedded/classes/IterableEmbeddedManager.ts +++ b/src/embedded/classes/IterableEmbeddedManager.ts @@ -1,4 +1,11 @@ +import { IterableAction } from '../../core/classes/IterableAction'; +import { IterableActionContext } from '../../core/classes/IterableActionContext'; import { IterableApi } from '../../core/classes/IterableApi'; +import { IterableConfig } from '../../core/classes/IterableConfig'; +import { IterableLogger } from '../../core/classes/IterableLogger'; +import { IterableActionSource } from '../../core/enums/IterableActionSource'; +import { callUrlHandler } from '../../core/utils/callUrlHandler'; +import { getActionPrefix } from '../../core/utils/getActionPrefix'; import type { IterableEmbeddedMessage } from '../types/IterableEmbeddedMessage'; /** @@ -21,6 +28,15 @@ export class IterableEmbeddedManager { */ isEnabled = false; + /** + * The config for the Iterable SDK. + */ + config: IterableConfig = new IterableConfig(); + + constructor(config: IterableConfig) { + this.config = config; + } + /** * Syncs embedded local cache with the server. * @@ -165,4 +181,62 @@ export class IterableEmbeddedManager { ) { return IterableApi.trackEmbeddedClick(message, buttonId, clickedUrl); } + + /** + * Handles a click on an embedded message. + * + * This will fire the correct handers set in the config, and will track the + * click. It should be use on either a button click or a click on the message itself. + * + * @param message - The embedded message. + * @param buttonId - The button ID. + * @param clickedUrl - The clicked URL. + * + * @example + * ```typescript + * IterableEmbeddedManager.handleClick(message, buttonId, clickedUrl); + * ``` + */ + handleClick( + message: IterableEmbeddedMessage, + buttonId: string | null, + action?: IterableAction | null + ) { + const { data, type: actionType } = action ?? {}; + const clickedUrl = data && data?.length > 0 ? data : actionType; + + IterableLogger.log( + 'IterableEmbeddedManager.handleClick', + message, + buttonId, + clickedUrl + ); + + if (!clickedUrl) { + IterableLogger.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 actionDetails = new IterableAction(actionName, '', ''); + const context = new IterableActionContext(actionDetails, source); + if (this.config.customActionHandler) { + this.config.customActionHandler(actionDetails, context); + } + } else { + const actionDetails = new IterableAction('openUrl', clickedUrl, ''); + const context = new IterableActionContext(actionDetails, source); + callUrlHandler(this.config, clickedUrl, context); + } + } } From e13756e15412e8af05ac486756777148a85923a3 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 14 Oct 2025 16:04:58 -0700 Subject: [PATCH 29/49] feat: implement setEnabled method for managing embedded manager state --- example/src/components/Embedded/Embedded.tsx | 2 +- src/core/classes/Iterable.ts | 4 ++++ src/embedded/classes/IterableEmbeddedManager.ts | 9 +++++++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/example/src/components/Embedded/Embedded.tsx b/example/src/components/Embedded/Embedded.tsx index ce1348e48..0baae1169 100644 --- a/example/src/components/Embedded/Embedded.tsx +++ b/example/src/components/Embedded/Embedded.tsx @@ -11,7 +11,7 @@ export const Embedded = () => { Does embedded class exist? {Iterable.embeddedManager ? 'Yes' : 'No'} - Is embedded manager enabled? + Is embedded manager enabled?{' '} {Iterable.embeddedManager.isEnabled ? 'Yes' : 'No'} diff --git a/src/core/classes/Iterable.ts b/src/core/classes/Iterable.ts index ebb928cd6..c46c1830b 100644 --- a/src/core/classes/Iterable.ts +++ b/src/core/classes/Iterable.ts @@ -177,6 +177,10 @@ export class Iterable { IterableLogger.setLoggingEnabled(config.logReactNativeSdkCalls ?? true); IterableLogger.setLogLevel(config.logLevel); + + Iterable.embeddedManager.setEnabled( + config.embeddedMessagingEnabled === true + ); } this.setupEventHandlers(); diff --git a/src/embedded/classes/IterableEmbeddedManager.ts b/src/embedded/classes/IterableEmbeddedManager.ts index 333dd3af7..f792a62c5 100644 --- a/src/embedded/classes/IterableEmbeddedManager.ts +++ b/src/embedded/classes/IterableEmbeddedManager.ts @@ -14,4 +14,13 @@ export class IterableEmbeddedManager { * Whether the embedded manager is enabled. */ isEnabled = false; + + /** + * Sets whether the embedded manager is enabled. + * + * @param enabled - Whether the embedded manager is enabled. + */ + setEnabled(enabled: boolean) { + this.isEnabled = enabled; + } } From c115493a16a274ebb40081184ba7f37591190679 Mon Sep 17 00:00:00 2001 From: Loren <3190869+lposen@users.noreply.github.com> Date: Tue, 18 Nov 2025 21:13:19 -0800 Subject: [PATCH 30/49] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../com/iterable/reactnative/RNIterableAPIModuleImpl.java | 7 ++----- 1 file changed, 2 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 1a54d2f16..1dedc7fbc 100644 --- a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java +++ b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java @@ -28,10 +28,7 @@ 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.IterableEmbeddedUpdateHandler; import com.iterable.iterableapi.IterableHelper; import com.iterable.iterableapi.IterableInAppCloseAction; import com.iterable.iterableapi.IterableInAppHandler; @@ -130,8 +127,8 @@ public void initialize2WithApiKey(String apiKey, ReadableMap configReadableMap, } if (configReadableMap.hasKey("enableEmbeddedMessaging")) { - configBuilder.setEnableEmbeddedMessaging(configReadableMap.getBoolean("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 From 25ec86def9edb4b9af27311dc42c1067e097a524 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 18 Nov 2025 21:56:24 -0800 Subject: [PATCH 31/49] feat: add chat icon to route configuration in App.constants.ts --- example/src/components/App/App.constants.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/example/src/components/App/App.constants.ts b/example/src/components/App/App.constants.ts index 25743f261..84f8cf78f 100644 --- a/example/src/components/App/App.constants.ts +++ b/example/src/components/App/App.constants.ts @@ -12,9 +12,11 @@ export const personIcon = export const mailIcon = 'data:image/png;base64,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'; + const chatIcon = 'data:image/png;base64,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' + export const routeIcon = { [Route.Commerce]: cashIcon, - [Route.Embedded]: 'chatbubble-outline', + [Route.Embedded]: chatIcon, [Route.Inbox]: mailIcon, [Route.User]: personIcon, }; From 2b4da8aaaf66637018d4924d6268aa5c382b78fd Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 18 Nov 2025 22:10:17 -0800 Subject: [PATCH 32/49] refactor: wrap components in SafeAreaView for improved layout consistency --- example/src/components/Commerce/Commerce.tsx | 57 ++++++++++---------- example/src/components/Embedded/Embedded.tsx | 7 +-- example/src/components/Login/Login.tsx | 7 +-- example/src/components/User/User.tsx | 7 +-- example/src/constants/styles/typography.ts | 1 - 5 files changed, 42 insertions(+), 37 deletions(-) diff --git a/example/src/components/Commerce/Commerce.tsx b/example/src/components/Commerce/Commerce.tsx index c840f9199..134a37706 100644 --- a/example/src/components/Commerce/Commerce.tsx +++ b/example/src/components/Commerce/Commerce.tsx @@ -1,4 +1,5 @@ import { Alert, Image, Pressable, ScrollView, Text, View } from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; import { Iterable, IterableCommerceItem } from '@iterable/react-native-sdk'; @@ -32,35 +33,37 @@ export const Commerce = () => { }; return ( - - - Commerce - - Purchase will be tracked when "Buy" is clicked. See logs for - output. - - {items.map((item) => ( - - - - - - - {item.name} - {item.subtitle} - ${item.price} - handleClick(item)} - > - Buy - + + + + Commerce + + Purchase will be tracked when "Buy" is clicked. See logs for + output. + + {items.map((item) => ( + + + + + + + {item.name} + {item.subtitle} + ${item.price} + handleClick(item)} + > + Buy + + - - ))} - - + ))} + + + ); }; diff --git a/example/src/components/Embedded/Embedded.tsx b/example/src/components/Embedded/Embedded.tsx index 0baae1169..3306e2cc4 100644 --- a/example/src/components/Embedded/Embedded.tsx +++ b/example/src/components/Embedded/Embedded.tsx @@ -1,11 +1,12 @@ -import { Text, View } from 'react-native'; import { Iterable } from '@iterable/react-native-sdk'; +import { Text } from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; import styles from './Embedded.styles'; export const Embedded = () => { return ( - + EMBEDDED Does embedded class exist? {Iterable.embeddedManager ? 'Yes' : 'No'} @@ -14,7 +15,7 @@ export const Embedded = () => { Is embedded manager enabled?{' '} {Iterable.embeddedManager.isEnabled ? 'Yes' : 'No'} - + ); }; diff --git a/example/src/components/Login/Login.tsx b/example/src/components/Login/Login.tsx index 9712792ed..988e9c8db 100644 --- a/example/src/components/Login/Login.tsx +++ b/example/src/components/Login/Login.tsx @@ -1,3 +1,4 @@ +import { useMemo } from 'react'; import { ActivityIndicator, Pressable, @@ -5,7 +6,7 @@ import { TextInput, View, } from 'react-native'; -import { useMemo } from 'react'; +import { SafeAreaView } from 'react-native-safe-area-context'; import { colors, type Route } from '../../constants'; import { useIterableApp } from '../../hooks'; @@ -18,7 +19,7 @@ export const Login = ({ navigation }: RootStackScreenProps) => { const loginIsEnabled = useMemo(() => apiKey && userId, [apiKey, userId]); return ( - + {loginInProgress ? ( @@ -66,7 +67,7 @@ export const Login = ({ navigation }: RootStackScreenProps) => { )} - + ); }; diff --git a/example/src/components/User/User.tsx b/example/src/components/User/User.tsx index 23f8361a5..4f3ee5be1 100644 --- a/example/src/components/User/User.tsx +++ b/example/src/components/User/User.tsx @@ -1,6 +1,7 @@ import { Iterable } from '@iterable/react-native-sdk'; import { useEffect, useState } from 'react'; -import { Text, TouchableOpacity, View } from 'react-native'; +import { Text, TouchableOpacity } from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; import { useIterableApp } from '../../hooks'; import styles from './User.styles'; @@ -18,13 +19,13 @@ export const User = () => { }, [isLoggedIn]); return ( - + Welcome Iterator Logged in as {loggedInAs} Logout - + ); }; diff --git a/example/src/constants/styles/typography.ts b/example/src/constants/styles/typography.ts index 09b6c2405..af8a3b8e4 100644 --- a/example/src/constants/styles/typography.ts +++ b/example/src/constants/styles/typography.ts @@ -6,7 +6,6 @@ export const appName: TextStyle = { fontWeight: 'bold', fontSize: 14, width: '100%', - marginTop: 41, marginBottom: 64, textTransform: 'uppercase', letterSpacing: 2, From 11f822736742c27c26190c98a11e109af3d8bc74 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 18 Nov 2025 22:23:27 -0800 Subject: [PATCH 33/49] feat: refactor IterableEmbeddedManager to use private variable --- example/src/hooks/useIterableApp.tsx | 2 ++ src/embedded/classes/IterableEmbeddedManager.ts | 11 +++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/example/src/hooks/useIterableApp.tsx b/example/src/hooks/useIterableApp.tsx index 9f72f0cf8..f47f58ba1 100644 --- a/example/src/hooks/useIterableApp.tsx +++ b/example/src/hooks/useIterableApp.tsx @@ -192,6 +192,8 @@ export const IterableAppProvider: FunctionComponent< config.logLevel = IterableLogLevel.debug; + config.embeddedMessagingEnabled = true; + config.inAppHandler = () => IterableInAppShowResponse.show; if ( diff --git a/src/embedded/classes/IterableEmbeddedManager.ts b/src/embedded/classes/IterableEmbeddedManager.ts index f792a62c5..13a59ddda 100644 --- a/src/embedded/classes/IterableEmbeddedManager.ts +++ b/src/embedded/classes/IterableEmbeddedManager.ts @@ -13,7 +13,14 @@ export class IterableEmbeddedManager { /** * Whether the embedded manager is enabled. */ - isEnabled = false; + private _isEnabled = false; + + /** + * Gets whether the embedded manager is enabled. + */ + get isEnabled(): boolean { + return this._isEnabled; + } /** * Sets whether the embedded manager is enabled. @@ -21,6 +28,6 @@ export class IterableEmbeddedManager { * @param enabled - Whether the embedded manager is enabled. */ setEnabled(enabled: boolean) { - this.isEnabled = enabled; + this._isEnabled = enabled; } } From 935cf22ec866cc43217e29406ead2d41f17da236 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 18 Nov 2025 22:28:29 -0800 Subject: [PATCH 34/49] test: enhance Iterable configuration tests with additional assertions --- src/core/classes/Iterable.test.ts | 38 ++++++++++++++++--------------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/src/core/classes/Iterable.test.ts b/src/core/classes/Iterable.test.ts index bfe4c26f6..c4ec2c828 100644 --- a/src/core/classes/Iterable.test.ts +++ b/src/core/classes/Iterable.test.ts @@ -300,37 +300,39 @@ describe('Iterable', () => { // WHEN config is initialized const config = new IterableConfig(); // THEN config has default values - expect(config.pushIntegrationName).toBe(undefined); + expect(config.allowedProtocols).toEqual([]); + expect(config.androidSdkUseInMemoryStorageForInApps).toBe(false); + expect(config.authHandler).toBe(undefined); expect(config.autoPushRegistration).toBe(true); expect(config.checkForDeferredDeeplink).toBe(false); - expect(config.inAppDisplayInterval).toBe(30.0); - expect(config.urlHandler).toBe(undefined); expect(config.customActionHandler).toBe(undefined); + expect(config.dataRegion).toBe(IterableDataRegion.US); + expect(config.embeddedMessagingEnabled).toBe(false); + expect(config.encryptionEnforced).toBe(false); + expect(config.expiringAuthTokenRefreshPeriod).toBe(60.0); + expect(config.inAppDisplayInterval).toBe(30.0); expect(config.inAppHandler).toBe(undefined); - expect(config.authHandler).toBe(undefined); expect(config.logLevel).toBe(IterableLogLevel.debug); expect(config.logReactNativeSdkCalls).toBe(true); - expect(config.expiringAuthTokenRefreshPeriod).toBe(60.0); - expect(config.allowedProtocols).toEqual([]); - expect(config.androidSdkUseInMemoryStorageForInApps).toBe(false); + expect(config.pushIntegrationName).toBe(undefined); + expect(config.urlHandler).toBe(undefined); expect(config.useInMemoryStorageForInApps).toBe(false); - expect(config.dataRegion).toBe(IterableDataRegion.US); - expect(config.encryptionEnforced).toBe(false); const configDict = config.toDict(); - expect(configDict.pushIntegrationName).toBe(undefined); + expect(configDict.allowedProtocols).toEqual([]); + expect(configDict.androidSdkUseInMemoryStorageForInApps).toBe(false); + expect(configDict.authHandlerPresent).toBe(false); expect(configDict.autoPushRegistration).toBe(true); - expect(configDict.inAppDisplayInterval).toBe(30.0); - expect(configDict.urlHandlerPresent).toBe(false); expect(configDict.customActionHandlerPresent).toBe(false); + expect(configDict.dataRegion).toBe(IterableDataRegion.US); + expect(configDict.embeddedMessagingEnabled).toBe(false); + expect(configDict.encryptionEnforced).toBe(false); + expect(configDict.expiringAuthTokenRefreshPeriod).toBe(60.0); + expect(configDict.inAppDisplayInterval).toBe(30.0); expect(configDict.inAppHandlerPresent).toBe(false); - expect(configDict.authHandlerPresent).toBe(false); expect(configDict.logLevel).toBe(IterableLogLevel.debug); - expect(configDict.expiringAuthTokenRefreshPeriod).toBe(60.0); - expect(configDict.allowedProtocols).toEqual([]); - expect(configDict.androidSdkUseInMemoryStorageForInApps).toBe(false); + expect(configDict.pushIntegrationName).toBe(undefined); + expect(configDict.urlHandlerPresent).toBe(false); expect(configDict.useInMemoryStorageForInApps).toBe(false); - expect(configDict.dataRegion).toBe(IterableDataRegion.US); - expect(configDict.encryptionEnforced).toBe(false); }); }); From 047f94bb815f22cdc23aac8ca5ee3bdf779c22d0 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 18 Nov 2025 22:28:52 -0800 Subject: [PATCH 35/49] fix: update embeddedMessagingEnabled configuration to use nullish coalescing --- src/core/classes/Iterable.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/classes/Iterable.ts b/src/core/classes/Iterable.ts index 9985ae41a..fa11f9a16 100644 --- a/src/core/classes/Iterable.ts +++ b/src/core/classes/Iterable.ts @@ -196,7 +196,7 @@ export class Iterable { IterableLogger.setLogLevel(config.logLevel); Iterable.embeddedManager.setEnabled( - config.embeddedMessagingEnabled === true + config.embeddedMessagingEnabled ?? false ); } From 92c8c9ebb4e682d0a0924cbf168616a9945c59f0 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 18 Nov 2025 22:33:45 -0800 Subject: [PATCH 36/49] test: add unit tests for IterableEmbeddedManager and enhance Iterable configuration tests --- src/core/classes/Iterable.test.ts | 15 +++++ .../classes/IterableEmbeddedManager.test.ts | 59 +++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 src/embedded/classes/IterableEmbeddedManager.test.ts diff --git a/src/core/classes/Iterable.test.ts b/src/core/classes/Iterable.test.ts index c4ec2c828..1da4e4e00 100644 --- a/src/core/classes/Iterable.test.ts +++ b/src/core/classes/Iterable.test.ts @@ -1214,4 +1214,19 @@ describe('Iterable', () => { }); }); }); + + describe('embeddedManager', () => { + it('should be disabled by default', () => { + const config = new IterableConfig(); + expect(config.embeddedMessagingEnabled).toBe(false); + expect(Iterable.embeddedManager.isEnabled).toBe(false); + }); + + it('should enable embeddedManager when config is set', async () => { + const config = new IterableConfig(); + config.embeddedMessagingEnabled = true; + await Iterable.initialize('test-key', config); + expect(Iterable.embeddedManager.isEnabled).toBe(true); + }); + }); }); diff --git a/src/embedded/classes/IterableEmbeddedManager.test.ts b/src/embedded/classes/IterableEmbeddedManager.test.ts new file mode 100644 index 000000000..96a09ddf4 --- /dev/null +++ b/src/embedded/classes/IterableEmbeddedManager.test.ts @@ -0,0 +1,59 @@ +import { IterableEmbeddedManager } from './IterableEmbeddedManager'; + +describe('IterableEmbeddedManager', () => { + let embeddedManager: IterableEmbeddedManager; + + beforeEach(() => { + embeddedManager = new IterableEmbeddedManager(); + }); + + describe('isEnabled', () => { + it('should be false by default', () => { + expect(embeddedManager.isEnabled).toBe(false); + }); + + it('should return true after being enabled', () => { + embeddedManager.setEnabled(true); + expect(embeddedManager.isEnabled).toBe(true); + }); + + it('should return false after being disabled', () => { + embeddedManager.setEnabled(false); + expect(embeddedManager.isEnabled).toBe(false); + }); + }); + + describe('setEnabled', () => { + it('should enable the embedded manager', () => { + embeddedManager.setEnabled(true); + expect(embeddedManager.isEnabled).toBe(true); + }); + + it('should disable the embedded manager', () => { + embeddedManager.setEnabled(false); + expect(embeddedManager.isEnabled).toBe(false); + }); + + it('should toggle enabled state multiple times', () => { + embeddedManager.setEnabled(true); + expect(embeddedManager.isEnabled).toBe(true); + + embeddedManager.setEnabled(false); + expect(embeddedManager.isEnabled).toBe(false); + + embeddedManager.setEnabled(true); + expect(embeddedManager.isEnabled).toBe(true); + }); + + it('should handle setting the same state multiple times', () => { + embeddedManager.setEnabled(true); + embeddedManager.setEnabled(true); + expect(embeddedManager.isEnabled).toBe(true); + + embeddedManager.setEnabled(false); + embeddedManager.setEnabled(false); + expect(embeddedManager.isEnabled).toBe(false); + }); + }); +}); + From efbda573175bd0ca3031d377a2c3e0e5f092dbae Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 18 Nov 2025 23:00:24 -0800 Subject: [PATCH 37/49] feat: update login function to support JWT token retrieval and enhance user identification --- example/src/hooks/useIterableApp.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/example/src/hooks/useIterableApp.tsx b/example/src/hooks/useIterableApp.tsx index 53de3d126..2a392dd0d 100644 --- a/example/src/hooks/useIterableApp.tsx +++ b/example/src/hooks/useIterableApp.tsx @@ -123,7 +123,7 @@ export const IterableAppProvider: FunctionComponent< return jwtToken; }, [userId]); - const login = useCallback(() => { + const login = useCallback(async () => { const id = userId ?? process.env.ITBL_ID; if (!id) return Promise.reject('No User ID or Email set'); @@ -132,12 +132,18 @@ export const IterableAppProvider: FunctionComponent< const fn = getIsEmail(id) ? Iterable.setEmail : Iterable.setUserId; - fn(id); + let token; + + if (process.env.ITBL_IS_JWT_ENABLED === 'true' && process.env.ITBL_JWT_SECRET) { + token = await getJwtToken(); + } + + fn(id, token); setIsLoggedIn(true); setLoginInProgress(false); return Promise.resolve(true); - }, [userId]); + }, [getJwtToken, userId]); const initialize = useCallback( (navigation: Navigation) => { From 67e116b94e19790c06675442c41a0484b93dc486 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 18 Nov 2025 23:04:06 -0800 Subject: [PATCH 38/49] fix: update example usage of startSession and endSession methods in IterableEmbeddedManager --- src/embedded/classes/IterableEmbeddedManager.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/embedded/classes/IterableEmbeddedManager.ts b/src/embedded/classes/IterableEmbeddedManager.ts index 91cfc9ef5..d36d11885 100644 --- a/src/embedded/classes/IterableEmbeddedManager.ts +++ b/src/embedded/classes/IterableEmbeddedManager.ts @@ -56,7 +56,7 @@ export class IterableEmbeddedManager { * * @example * ```typescript - * IterableEmbeddedManager.startSession(); + * Iterable.embeddedManager.startSession(); * ``` */ startSession() { @@ -75,7 +75,7 @@ export class IterableEmbeddedManager { * * @example * ```typescript - * IterableEmbeddedManager.endSession(); + * Iterable.embeddedManager.endSession(); * ``` */ endSession() { From bab2b356b561541218505e6601b8a27248f6d472 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 18 Nov 2025 23:06:33 -0800 Subject: [PATCH 39/49] test: add unit tests for new embedded session methods in IterableEmbeddedManager --- src/__mocks__/MockRNIterableAPI.ts | 8 ++++ .../classes/IterableEmbeddedManager.test.ts | 43 +++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/src/__mocks__/MockRNIterableAPI.ts b/src/__mocks__/MockRNIterableAPI.ts index c7f325677..fc7ed502f 100644 --- a/src/__mocks__/MockRNIterableAPI.ts +++ b/src/__mocks__/MockRNIterableAPI.ts @@ -129,6 +129,14 @@ export class MockRNIterableAPI { static getHtmlInAppContentForMessage = jest.fn(); + static startEmbeddedSession = jest.fn(); + + static endEmbeddedSession = jest.fn(); + + static getEmbeddedPlacementIds = jest + .fn() + .mockResolvedValue([1, 2, 3] as number[]); + // set messages function is to set the messages static property // this is for testing purposes only static setMessages(messages: IterableInAppMessage[]): void { diff --git a/src/embedded/classes/IterableEmbeddedManager.test.ts b/src/embedded/classes/IterableEmbeddedManager.test.ts index 96a09ddf4..c05e38533 100644 --- a/src/embedded/classes/IterableEmbeddedManager.test.ts +++ b/src/embedded/classes/IterableEmbeddedManager.test.ts @@ -1,10 +1,18 @@ +import { MockRNIterableAPI } from '../../__mocks__/MockRNIterableAPI'; import { IterableEmbeddedManager } from './IterableEmbeddedManager'; +// Mock the RNIterableAPI module +jest.mock('../../api', () => ({ + __esModule: true, + default: MockRNIterableAPI, +})); + describe('IterableEmbeddedManager', () => { let embeddedManager: IterableEmbeddedManager; beforeEach(() => { embeddedManager = new IterableEmbeddedManager(); + jest.clearAllMocks(); }); describe('isEnabled', () => { @@ -55,5 +63,40 @@ describe('IterableEmbeddedManager', () => { expect(embeddedManager.isEnabled).toBe(false); }); }); + + describe('getPlacementIds', () => { + it('should call IterableApi.getEmbeddedPlacementIds', async () => { + // WHEN getPlacementIds is called + const result = await embeddedManager.getPlacementIds(); + + // THEN IterableApi.getEmbeddedPlacementIds is called + expect(MockRNIterableAPI.getEmbeddedPlacementIds).toHaveBeenCalledTimes( + 1 + ); + + // AND the result is returned + expect(result).toEqual([1, 2, 3]); + }); + }); + + describe('startSession', () => { + it('should call IterableApi.startEmbeddedSession', () => { + // WHEN startSession is called + embeddedManager.startSession(); + + // THEN IterableApi.startEmbeddedSession is called + expect(MockRNIterableAPI.startEmbeddedSession).toHaveBeenCalledTimes(1); + }); + }); + + describe('endSession', () => { + it('should call IterableApi.endEmbeddedSession', () => { + // WHEN endSession is called + embeddedManager.endSession(); + + // THEN IterableApi.endEmbeddedSession is called + expect(MockRNIterableAPI.endEmbeddedSession).toHaveBeenCalledTimes(1); + }); + }); }); From c942457f43cf791721214ab066d6de269c3fd4e3 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 18 Nov 2025 23:29:17 -0800 Subject: [PATCH 40/49] feat: add logging for start and pause embedded impression methods in RNIterableAPIModuleImpl --- .../com/iterable/reactnative/RNIterableAPIModuleImpl.java | 2 ++ src/embedded/classes/IterableEmbeddedManager.ts | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java index d02da8827..10fa97f03 100644 --- a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java +++ b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java @@ -659,10 +659,12 @@ public void endEmbeddedSession() { } public void startEmbeddedImpression(String messageId, int placementId) { + IterableLogger.d(TAG, "startEmbeddedImpression"); IterableApi.getInstance().getEmbeddedManager().getEmbeddedSessionManager().startImpression(messageId, placementId); } public void pauseEmbeddedImpression(String messageId) { + IterableLogger.d(TAG, "pauseEmbeddedImpression"); IterableApi.getInstance().getEmbeddedManager().getEmbeddedSessionManager().pauseImpression(messageId); } diff --git a/src/embedded/classes/IterableEmbeddedManager.ts b/src/embedded/classes/IterableEmbeddedManager.ts index 6557ae2ab..458b700ff 100644 --- a/src/embedded/classes/IterableEmbeddedManager.ts +++ b/src/embedded/classes/IterableEmbeddedManager.ts @@ -41,7 +41,7 @@ export class IterableEmbeddedManager { * Syncs embedded local cache with the server. * * When your app first launches, and each time it comes to the foreground, - * Iterable's iOS SDK automatically refresh a local, on-device cache of + * Iterable's Native SDKs automatically refresh a local, on-device cache of * embedded messages for the signed-in user. These are the messages the * signed-in user is eligible to see. * @@ -84,7 +84,7 @@ export class IterableEmbeddedManager { /** * Starts a session. * - * As session is a period of time when a user is on a screen or page that can + * A session is a period of time when a user is on a screen or page that can * display embedded messages. * * When a user comes to a screen or page in your app where embedded messages @@ -104,7 +104,7 @@ export class IterableEmbeddedManager { * * When a user leaves a screen in your app where embedded messages are * displayed, the session should be ended. This causes the SDK to send - * session and impression data back to the server. + * session an impression data back to the server. * * A session is tracked when it is ended, so you should be able to find * tracking data after this method is called. From da20dcffff58cf8891c9fd080c5c0e02f0f198e2 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 18 Nov 2025 23:31:35 -0800 Subject: [PATCH 41/49] test: add unit tests for sync and impression methods in IterableEmbeddedManager --- src/__mocks__/MockRNIterableAPI.ts | 8 ++ .../classes/IterableEmbeddedManager.test.ts | 129 ++++++++++++++++++ 2 files changed, 137 insertions(+) diff --git a/src/__mocks__/MockRNIterableAPI.ts b/src/__mocks__/MockRNIterableAPI.ts index fc7ed502f..74ee90741 100644 --- a/src/__mocks__/MockRNIterableAPI.ts +++ b/src/__mocks__/MockRNIterableAPI.ts @@ -137,6 +137,14 @@ export class MockRNIterableAPI { .fn() .mockResolvedValue([1, 2, 3] as number[]); + static syncEmbeddedMessages = jest.fn(); + + static getEmbeddedMessages = jest.fn().mockResolvedValue([]); + + static startEmbeddedImpression = jest.fn(); + + static pauseEmbeddedImpression = jest.fn(); + // set messages function is to set the messages static property // this is for testing purposes only static setMessages(messages: IterableInAppMessage[]): void { diff --git a/src/embedded/classes/IterableEmbeddedManager.test.ts b/src/embedded/classes/IterableEmbeddedManager.test.ts index c05e38533..690ddcc5d 100644 --- a/src/embedded/classes/IterableEmbeddedManager.test.ts +++ b/src/embedded/classes/IterableEmbeddedManager.test.ts @@ -64,6 +64,16 @@ describe('IterableEmbeddedManager', () => { }); }); + describe('syncMessages', () => { + it('should call IterableApi.syncEmbeddedMessages', () => { + // WHEN syncMessages is called + embeddedManager.syncMessages(); + + // THEN IterableApi.syncEmbeddedMessages is called + expect(MockRNIterableAPI.syncEmbeddedMessages).toHaveBeenCalledTimes(1); + }); + }); + describe('getPlacementIds', () => { it('should call IterableApi.getEmbeddedPlacementIds', async () => { // WHEN getPlacementIds is called @@ -79,6 +89,37 @@ describe('IterableEmbeddedManager', () => { }); }); + describe('getMessages', () => { + it('should call IterableApi.getEmbeddedMessages with placement IDs', async () => { + // GIVEN some placement IDs + const placementIds = [1, 2, 3]; + + // WHEN getMessages is called + const result = await embeddedManager.getMessages(placementIds); + + // THEN IterableApi.getEmbeddedMessages is called with the placement IDs + expect(MockRNIterableAPI.getEmbeddedMessages).toHaveBeenCalledTimes(1); + expect(MockRNIterableAPI.getEmbeddedMessages).toHaveBeenCalledWith( + placementIds + ); + + // AND the result is returned + expect(result).toEqual([]); + }); + + it('should call IterableApi.getEmbeddedMessages with null', async () => { + // WHEN getMessages is called with null + const result = await embeddedManager.getMessages(null); + + // THEN IterableApi.getEmbeddedMessages is called with null + expect(MockRNIterableAPI.getEmbeddedMessages).toHaveBeenCalledTimes(1); + expect(MockRNIterableAPI.getEmbeddedMessages).toHaveBeenCalledWith(null); + + // AND the result is returned + expect(result).toEqual([]); + }); + }); + describe('startSession', () => { it('should call IterableApi.startEmbeddedSession', () => { // WHEN startSession is called @@ -98,5 +139,93 @@ describe('IterableEmbeddedManager', () => { expect(MockRNIterableAPI.endEmbeddedSession).toHaveBeenCalledTimes(1); }); }); + + describe('startImpression', () => { + it('should call IterableApi.startEmbeddedImpression with messageId and placementId', () => { + // GIVEN a message ID and placement ID + const messageId = 'message-123'; + const placementId = 456; + + // WHEN startImpression is called + embeddedManager.startImpression(messageId, placementId); + + // THEN IterableApi.startEmbeddedImpression is called with the correct parameters + expect(MockRNIterableAPI.startEmbeddedImpression).toHaveBeenCalledTimes( + 1 + ); + expect(MockRNIterableAPI.startEmbeddedImpression).toHaveBeenCalledWith( + messageId, + placementId + ); + }); + + it('should handle multiple impression starts', () => { + // GIVEN multiple messages + const messageId1 = 'message-1'; + const placementId1 = 100; + const messageId2 = 'message-2'; + const placementId2 = 200; + + // WHEN startImpression is called multiple times + embeddedManager.startImpression(messageId1, placementId1); + embeddedManager.startImpression(messageId2, placementId2); + + // THEN IterableApi.startEmbeddedImpression is called twice + expect(MockRNIterableAPI.startEmbeddedImpression).toHaveBeenCalledTimes( + 2 + ); + expect(MockRNIterableAPI.startEmbeddedImpression).toHaveBeenNthCalledWith( + 1, + messageId1, + placementId1 + ); + expect(MockRNIterableAPI.startEmbeddedImpression).toHaveBeenNthCalledWith( + 2, + messageId2, + placementId2 + ); + }); + }); + + describe('pauseImpression', () => { + it('should call IterableApi.pauseEmbeddedImpression with messageId', () => { + // GIVEN a message ID + const messageId = 'message-123'; + + // WHEN pauseImpression is called + embeddedManager.pauseImpression(messageId); + + // THEN IterableApi.pauseEmbeddedImpression is called with the correct parameter + expect(MockRNIterableAPI.pauseEmbeddedImpression).toHaveBeenCalledTimes( + 1 + ); + expect(MockRNIterableAPI.pauseEmbeddedImpression).toHaveBeenCalledWith( + messageId + ); + }); + + it('should handle multiple impression pauses', () => { + // GIVEN multiple message IDs + const messageId1 = 'message-1'; + const messageId2 = 'message-2'; + + // WHEN pauseImpression is called multiple times + embeddedManager.pauseImpression(messageId1); + embeddedManager.pauseImpression(messageId2); + + // THEN IterableApi.pauseEmbeddedImpression is called twice + expect(MockRNIterableAPI.pauseEmbeddedImpression).toHaveBeenCalledTimes( + 2 + ); + expect(MockRNIterableAPI.pauseEmbeddedImpression).toHaveBeenNthCalledWith( + 1, + messageId1 + ); + expect(MockRNIterableAPI.pauseEmbeddedImpression).toHaveBeenNthCalledWith( + 2, + messageId2 + ); + }); + }); }); From 0984e9e9f7803b9d048ae48768edbe4f6f10cdc4 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 18 Nov 2025 23:43:36 -0800 Subject: [PATCH 42/49] test: add unit tests for syncEmbeddedMessages and getEmbeddedMessages in IterableEmbeddedManager --- src/__mocks__/MockRNIterableAPI.ts | 29 ++++++++ .../classes/IterableEmbeddedManager.test.ts | 69 +++++++++++++++++++ 2 files changed, 98 insertions(+) diff --git a/src/__mocks__/MockRNIterableAPI.ts b/src/__mocks__/MockRNIterableAPI.ts index fc7ed502f..136744ad7 100644 --- a/src/__mocks__/MockRNIterableAPI.ts +++ b/src/__mocks__/MockRNIterableAPI.ts @@ -137,6 +137,35 @@ export class MockRNIterableAPI { .fn() .mockResolvedValue([1, 2, 3] as number[]); + static syncEmbeddedMessages = jest.fn().mockResolvedValue(undefined); + + static getEmbeddedMessages = jest.fn().mockResolvedValue([ + { + metadata: { + messageId: 'msg-1', + campaignId: 123, + placementId: 1, + }, + elements: { + title: 'Test Message 1', + body: 'Test body 1', + }, + payload: { customKey: 'customValue' }, + }, + { + metadata: { + messageId: 'msg-2', + campaignId: 456, + placementId: 2, + }, + elements: { + title: 'Test Message 2', + body: 'Test body 2', + }, + payload: null, + }, + ]); + // set messages function is to set the messages static property // this is for testing purposes only static setMessages(messages: IterableInAppMessage[]): void { diff --git a/src/embedded/classes/IterableEmbeddedManager.test.ts b/src/embedded/classes/IterableEmbeddedManager.test.ts index c05e38533..a3d27804c 100644 --- a/src/embedded/classes/IterableEmbeddedManager.test.ts +++ b/src/embedded/classes/IterableEmbeddedManager.test.ts @@ -64,6 +64,19 @@ describe('IterableEmbeddedManager', () => { }); }); + describe('syncMessages', () => { + it('should call IterableApi.syncEmbeddedMessages', async () => { + // WHEN syncMessages is called + const result = await embeddedManager.syncMessages(); + + // THEN IterableApi.syncEmbeddedMessages is called + expect(MockRNIterableAPI.syncEmbeddedMessages).toHaveBeenCalledTimes(1); + + // AND the result is returned + expect(result).toBeUndefined(); + }); + }); + describe('getPlacementIds', () => { it('should call IterableApi.getEmbeddedPlacementIds', async () => { // WHEN getPlacementIds is called @@ -79,6 +92,61 @@ describe('IterableEmbeddedManager', () => { }); }); + describe('getMessages', () => { + it('should call IterableApi.getEmbeddedMessages with placement IDs', async () => { + // GIVEN placement IDs + const placementIds = [1, 2]; + + // WHEN getMessages is called + const result = await embeddedManager.getMessages(placementIds); + + // THEN IterableApi.getEmbeddedMessages is called with placement IDs + expect(MockRNIterableAPI.getEmbeddedMessages).toHaveBeenCalledTimes(1); + expect(MockRNIterableAPI.getEmbeddedMessages).toHaveBeenCalledWith( + placementIds + ); + + // AND the result contains embedded messages + expect(result).toHaveLength(2); + expect(result[0]).toEqual({ + metadata: { + messageId: 'msg-1', + campaignId: 123, + placementId: 1, + }, + elements: { + title: 'Test Message 1', + body: 'Test body 1', + }, + payload: { customKey: 'customValue' }, + }); + expect(result[1]).toEqual({ + metadata: { + messageId: 'msg-2', + campaignId: 456, + placementId: 2, + }, + elements: { + title: 'Test Message 2', + body: 'Test body 2', + }, + payload: null, + }); + }); + + it('should call IterableApi.getEmbeddedMessages with null placement IDs', async () => { + // WHEN getMessages is called with null + const result = await embeddedManager.getMessages(null); + + // THEN IterableApi.getEmbeddedMessages is called with null + expect(MockRNIterableAPI.getEmbeddedMessages).toHaveBeenCalledTimes(1); + expect(MockRNIterableAPI.getEmbeddedMessages).toHaveBeenCalledWith(null); + + // AND the result is returned + expect(result).toBeDefined(); + }); + }); + describe('startSession', () => { it('should call IterableApi.startEmbeddedSession', () => { // WHEN startSession is called @@ -98,5 +166,6 @@ describe('IterableEmbeddedManager', () => { expect(MockRNIterableAPI.endEmbeddedSession).toHaveBeenCalledTimes(1); }); }); + }); From 49f5e2f646ccba78b0db75f086dbca272c532d92 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 18 Nov 2025 23:51:05 -0800 Subject: [PATCH 43/49] docs: add internal usage note to setEnabled method in IterableEmbeddedManager --- src/embedded/classes/IterableEmbeddedManager.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/embedded/classes/IterableEmbeddedManager.ts b/src/embedded/classes/IterableEmbeddedManager.ts index c6064fb62..7f9a08c3f 100644 --- a/src/embedded/classes/IterableEmbeddedManager.ts +++ b/src/embedded/classes/IterableEmbeddedManager.ts @@ -31,6 +31,7 @@ export class IterableEmbeddedManager { /** * Sets whether the embedded manager is enabled. * + * @internal This method is for internal SDK use only and should not be called by SDK consumers. * @param enabled - Whether the embedded manager is enabled. */ setEnabled(enabled: boolean) { From 718a158c1b0895f0f84f52e5bf211903da7ffe86 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 18 Nov 2025 23:51:51 -0800 Subject: [PATCH 44/49] docs: clarify internal usage note for setEnabled method in IterableEmbeddedManager --- src/embedded/classes/IterableEmbeddedManager.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/embedded/classes/IterableEmbeddedManager.ts b/src/embedded/classes/IterableEmbeddedManager.ts index 7f9a08c3f..03013ed40 100644 --- a/src/embedded/classes/IterableEmbeddedManager.ts +++ b/src/embedded/classes/IterableEmbeddedManager.ts @@ -31,7 +31,9 @@ export class IterableEmbeddedManager { /** * Sets whether the embedded manager is enabled. * - * @internal This method is for internal SDK use only and should not be called by SDK consumers. + * @internal This method is for internal SDK use only and should not be called + * by SDK consumers, as it is meant to be called at initialization time. + * * @param enabled - Whether the embedded manager is enabled. */ setEnabled(enabled: boolean) { From 5c2bc6d9814bec5cd76bb9270c47283a5d6a5ce3 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Wed, 19 Nov 2025 01:00:28 -0800 Subject: [PATCH 45/49] refactor: rnew IterableEmbeddedManager instantiation in Iterable class --- src/core/classes/Iterable.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/core/classes/Iterable.ts b/src/core/classes/Iterable.ts index 0fcacfc54..55896c0d2 100644 --- a/src/core/classes/Iterable.ts +++ b/src/core/classes/Iterable.ts @@ -199,9 +199,7 @@ export class Iterable { IterableLogger.setLoggingEnabled(config.logReactNativeSdkCalls ?? true); IterableLogger.setLogLevel(config.logLevel); - Iterable.embeddedManager.setEnabled( - config.enableEmbeddedMessaging ?? false - ); + Iterable.embeddedManager = new IterableEmbeddedManager(config); } this.setupEventHandlers(); From 974e9b2e65e93324e43ddb178ee1d6a76523216c Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Wed, 19 Nov 2025 01:01:20 -0800 Subject: [PATCH 46/49] fix: improve URL handling and logging in callUrlHandler; --- src/core/utils/callUrlHandler.ts | 8 ++++---- src/embedded/classes/IterableEmbeddedManager.ts | 3 ++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/core/utils/callUrlHandler.ts b/src/core/utils/callUrlHandler.ts index 90f469362..4125de5d2 100644 --- a/src/core/utils/callUrlHandler.ts +++ b/src/core/utils/callUrlHandler.ts @@ -15,17 +15,17 @@ export function callUrlHandler( url: string, context: IterableActionContext ) { - // MOB-10424: Figure out if this is purposeful - // eslint-disable-next-line eqeqeq - if (config.urlHandler?.(url, context) == false) { + if (!config.urlHandler?.(url, context)) { Linking.canOpenURL(url) .then((canOpen) => { if (canOpen) { Linking.openURL(url); + } else { + IterableLogger?.log('Url cannot be opened: ' + url); } }) .catch((reason) => { - IterableLogger?.log('could not open url: ' + reason); + IterableLogger?.log('Error opening url: ' + reason); }); } } diff --git a/src/embedded/classes/IterableEmbeddedManager.ts b/src/embedded/classes/IterableEmbeddedManager.ts index 779c29658..277fcf819 100644 --- a/src/embedded/classes/IterableEmbeddedManager.ts +++ b/src/embedded/classes/IterableEmbeddedManager.ts @@ -54,6 +54,7 @@ export class IterableEmbeddedManager { constructor(config: IterableConfig) { this._config = config; + this._isEnabled = config.enableEmbeddedMessaging ?? false; } /** @@ -218,7 +219,7 @@ export class IterableEmbeddedManager { /** * Handles a click on an embedded message. * - * This will fire the correct handers set in the config, and will track the + * This will fire the correct handlers set in the config, and will track the * click. It should be use on either a button click or a click on the message itself. * * @param message - The embedded message. From 62e248f95136dd6bf762a8e3d42485c4d7dce1b5 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Wed, 19 Nov 2025 01:32:56 -0800 Subject: [PATCH 47/49] refactor: remove embedded messaging configuration from RNIterableAPIModuleImpl --- .../com/iterable/reactnative/RNIterableAPIModuleImpl.java | 8 -------- 1 file changed, 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 1dedc7fbc..6793ddcf0 100644 --- a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java +++ b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java @@ -91,10 +91,6 @@ public void initializeWithApiKey(String apiKey, ReadableMap configReadableMap, S configBuilder.setAuthHandler(this); } - if (configReadableMap.hasKey("enableEmbeddedMessaging")) { - configBuilder.setEnableEmbeddedMessaging(configReadableMap.getBoolean("enableEmbeddedMessaging")); - } - IterableApi.initialize(reactContext, apiKey, configBuilder.build()); IterableApi.getInstance().setDeviceAttribute("reactNativeSDKVersion", version); @@ -126,10 +122,6 @@ 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 6c418c34db183170150ba2a000c94a990cf3a43f Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 23 Dec 2025 17:05:08 -0800 Subject: [PATCH 48/49] test: implement click handling and tracking for embedded messages --- src/core/utils/getActionPrefix.test.ts | 227 +++++++++++++++++++++++++ 1 file changed, 227 insertions(+) create mode 100644 src/core/utils/getActionPrefix.test.ts diff --git a/src/core/utils/getActionPrefix.test.ts b/src/core/utils/getActionPrefix.test.ts new file mode 100644 index 000000000..7099d7c0f --- /dev/null +++ b/src/core/utils/getActionPrefix.test.ts @@ -0,0 +1,227 @@ +import { getActionPrefix } from './getActionPrefix'; +import { IterableCustomActionPrefix } from '../enums/IterableCustomActionPrefix'; + +/** + * Tests for getActionPrefix utility function. + */ +describe('getActionPrefix', () => { + describe('when string starts with action:// prefix', () => { + it('should return Action prefix for exact action:// string', () => { + // GIVEN a string that is exactly the action prefix + const str = 'action://'; + + // WHEN getting the action prefix + const result = getActionPrefix(str); + + // THEN it should return the Action prefix + expect(result).toBe(IterableCustomActionPrefix.Action); + }); + + it('should return Action prefix for action:// with additional path', () => { + // GIVEN a string starting with action:// and additional path + const str = 'action://some/path'; + + // WHEN getting the action prefix + const result = getActionPrefix(str); + + // THEN it should return the Action prefix + expect(result).toBe(IterableCustomActionPrefix.Action); + }); + + it('should return Action prefix for action:// with query params', () => { + // GIVEN a string starting with action:// and query parameters + const str = 'action://deeplink?param1=value1¶m2=value2'; + + // WHEN getting the action prefix + const result = getActionPrefix(str); + + // THEN it should return the Action prefix + expect(result).toBe(IterableCustomActionPrefix.Action); + }); + }); + + describe('when string starts with itbl:// prefix', () => { + it('should return Itbl prefix for exact itbl:// string', () => { + // GIVEN a string that is exactly the deprecated itbl prefix + const str = 'itbl://'; + + // WHEN getting the action prefix + const result = getActionPrefix(str); + + // THEN it should return the Itbl prefix + expect(result).toBe(IterableCustomActionPrefix.Itbl); + }); + + it('should return Itbl prefix for itbl:// with additional path', () => { + // GIVEN a string starting with itbl:// and additional path + const str = 'itbl://some/path'; + + // WHEN getting the action prefix + const result = getActionPrefix(str); + + // THEN it should return the Itbl prefix + expect(result).toBe(IterableCustomActionPrefix.Itbl); + }); + + it('should return Itbl prefix for itbl:// with query params', () => { + // GIVEN a string starting with itbl:// and query parameters + const str = 'itbl://deeplink?param1=value1¶m2=value2'; + + // WHEN getting the action prefix + const result = getActionPrefix(str); + + // THEN it should return the Itbl prefix + expect(result).toBe(IterableCustomActionPrefix.Itbl); + }); + }); + + describe('when string does not have a recognized prefix', () => { + it('should return null for regular URL', () => { + // GIVEN a regular https URL + const str = 'https://example.com'; + + // WHEN getting the action prefix + const result = getActionPrefix(str); + + // THEN it should return null + expect(result).toBeNull(); + }); + + it('should return null for http URL', () => { + // GIVEN a regular http URL + const str = 'http://example.com'; + + // WHEN getting the action prefix + const result = getActionPrefix(str); + + // THEN it should return null + expect(result).toBeNull(); + }); + + it('should return null for custom scheme URL', () => { + // GIVEN a custom scheme URL that is not action:// or itbl:// + const str = 'myapp://deeplink'; + + // WHEN getting the action prefix + const result = getActionPrefix(str); + + // THEN it should return null + expect(result).toBeNull(); + }); + + it('should return null for plain text', () => { + // GIVEN a plain text string + const str = 'just some text'; + + // WHEN getting the action prefix + const result = getActionPrefix(str); + + // THEN it should return null + expect(result).toBeNull(); + }); + + it('should return null for empty string', () => { + // GIVEN an empty string + const str = ''; + + // WHEN getting the action prefix + const result = getActionPrefix(str); + + // THEN it should return null + expect(result).toBeNull(); + }); + }); + + describe('when string is null or undefined', () => { + it('should return null for undefined string', () => { + // GIVEN an undefined string + const str = undefined; + + // WHEN getting the action prefix + const result = getActionPrefix(str); + + // THEN it should return null + expect(result).toBeNull(); + }); + + it('should return null for null string', () => { + // GIVEN a null string + const str = null; + + // WHEN getting the action prefix + const result = getActionPrefix(str); + + // THEN it should return null + expect(result).toBeNull(); + }); + }); + + describe('edge cases and case sensitivity', () => { + it('should be case sensitive and not match ACTION://', () => { + // GIVEN a string with uppercase ACTION:// + const str = 'ACTION://deeplink'; + + // WHEN getting the action prefix + const result = getActionPrefix(str); + + // THEN it should return null (case sensitive) + expect(result).toBeNull(); + }); + + it('should be case sensitive and not match ITBL://', () => { + // GIVEN a string with uppercase ITBL:// + const str = 'ITBL://deeplink'; + + // WHEN getting the action prefix + const result = getActionPrefix(str); + + // THEN it should return null (case sensitive) + expect(result).toBeNull(); + }); + + it('should not match action:// in the middle of string', () => { + // GIVEN a string with action:// not at the start + const str = 'prefix action://deeplink'; + + // WHEN getting the action prefix + const result = getActionPrefix(str); + + // THEN it should return null + expect(result).toBeNull(); + }); + + it('should not match itbl:// in the middle of string', () => { + // GIVEN a string with itbl:// not at the start + const str = 'prefix itbl://deeplink'; + + // WHEN getting the action prefix + const result = getActionPrefix(str); + + // THEN it should return null + expect(result).toBeNull(); + }); + + it('should not match partial prefix action:', () => { + // GIVEN a string with incomplete action prefix + const str = 'action:deeplink'; + + // WHEN getting the action prefix + const result = getActionPrefix(str); + + // THEN it should return null + expect(result).toBeNull(); + }); + + it('should not match partial prefix itbl:', () => { + // GIVEN a string with incomplete itbl prefix + const str = 'itbl:deeplink'; + + // WHEN getting the action prefix + const result = getActionPrefix(str); + + // THEN it should return null + expect(result).toBeNull(); + }); + }); +}); + From ecb220412ecb925db162c1424b1507e54f8d114f Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 23 Dec 2025 17:09:39 -0800 Subject: [PATCH 49/49] test: add trackEmbeddedClick method and enhance click handling for embedded messages --- src/__mocks__/MockRNIterableAPI.ts | 2 + .../classes/IterableEmbeddedManager.test.ts | 420 +++++++++++++++++- 2 files changed, 421 insertions(+), 1 deletion(-) diff --git a/src/__mocks__/MockRNIterableAPI.ts b/src/__mocks__/MockRNIterableAPI.ts index ce954bbb5..b79eebce9 100644 --- a/src/__mocks__/MockRNIterableAPI.ts +++ b/src/__mocks__/MockRNIterableAPI.ts @@ -182,6 +182,8 @@ export class MockRNIterableAPI { static pauseEmbeddedImpression = jest.fn(); + static trackEmbeddedClick = jest.fn(); + // set messages function is to set the messages static property // this is for testing purposes only static setMessages(messages: IterableInAppMessage[]): void { diff --git a/src/embedded/classes/IterableEmbeddedManager.test.ts b/src/embedded/classes/IterableEmbeddedManager.test.ts index cd7fa244b..a00a2e448 100644 --- a/src/embedded/classes/IterableEmbeddedManager.test.ts +++ b/src/embedded/classes/IterableEmbeddedManager.test.ts @@ -1,5 +1,8 @@ import { MockRNIterableAPI } from '../../__mocks__/MockRNIterableAPI'; +import { IterableAction } from '../../core/classes/IterableAction'; import { IterableConfig } from '../../core/classes/IterableConfig'; +import { IterableLogger } from '../../core/classes/IterableLogger'; +import type { IterableEmbeddedMessage } from '../types/IterableEmbeddedMessage'; import { IterableEmbeddedManager } from './IterableEmbeddedManager'; // Mock the RNIterableAPI module @@ -8,11 +11,39 @@ jest.mock('../../api', () => ({ default: MockRNIterableAPI, })); +// Mock the callUrlHandler utility +jest.mock('../../core/utils/callUrlHandler', () => ({ + callUrlHandler: jest.fn(), +})); + +// Mock the IterableLogger +jest.mock('../../core/classes/IterableLogger', () => ({ + IterableLogger: { + log: jest.fn(), + }, +})); + describe('IterableEmbeddedManager', () => { let embeddedManager: IterableEmbeddedManager; + let config: IterableConfig; + + // Mock embedded message for testing + const mockEmbeddedMessage: IterableEmbeddedMessage = { + metadata: { + messageId: 'test-message-id', + campaignId: 12345, + placementId: 1, + }, + elements: { + title: 'Test Message', + body: 'Test body', + }, + payload: { customKey: 'customValue' }, + }; beforeEach(() => { - embeddedManager = new IterableEmbeddedManager(new IterableConfig()); + config = new IterableConfig(); + embeddedManager = new IterableEmbeddedManager(config); jest.clearAllMocks(); }); @@ -255,5 +286,392 @@ describe('IterableEmbeddedManager', () => { ); }); }); + + describe('trackClick', () => { + it('should call IterableApi.trackEmbeddedClick with message, buttonId and clickedUrl', () => { + // GIVEN a message, button ID and clicked URL + const buttonId = 'button-1'; + const clickedUrl = 'https://example.com'; + + // WHEN trackClick is called + embeddedManager.trackClick(mockEmbeddedMessage, buttonId, clickedUrl); + + // THEN IterableApi.trackEmbeddedClick is called with the correct parameters + expect(MockRNIterableAPI.trackEmbeddedClick).toHaveBeenCalledTimes(1); + expect(MockRNIterableAPI.trackEmbeddedClick).toHaveBeenCalledWith( + mockEmbeddedMessage, + buttonId, + clickedUrl + ); + }); + + it('should handle null buttonId', () => { + // GIVEN a message with null buttonId + const buttonId = null; + const clickedUrl = 'https://example.com'; + + // WHEN trackClick is called + embeddedManager.trackClick(mockEmbeddedMessage, buttonId, clickedUrl); + + // THEN IterableApi.trackEmbeddedClick is called with null buttonId + expect(MockRNIterableAPI.trackEmbeddedClick).toHaveBeenCalledTimes(1); + expect(MockRNIterableAPI.trackEmbeddedClick).toHaveBeenCalledWith( + mockEmbeddedMessage, + null, + clickedUrl + ); + }); + + it('should handle null clickedUrl', () => { + // GIVEN a message with null clickedUrl + const buttonId = 'button-1'; + const clickedUrl = null; + + // WHEN trackClick is called + embeddedManager.trackClick(mockEmbeddedMessage, buttonId, clickedUrl); + + // THEN IterableApi.trackEmbeddedClick is called with null clickedUrl + expect(MockRNIterableAPI.trackEmbeddedClick).toHaveBeenCalledTimes(1); + expect(MockRNIterableAPI.trackEmbeddedClick).toHaveBeenCalledWith( + mockEmbeddedMessage, + buttonId, + null + ); + }); + + it('should handle multiple trackClick calls', () => { + // GIVEN multiple click events + const buttonId1 = 'button-1'; + const clickedUrl1 = 'https://example.com/1'; + const buttonId2 = 'button-2'; + const clickedUrl2 = 'https://example.com/2'; + + // WHEN trackClick is called multiple times + embeddedManager.trackClick(mockEmbeddedMessage, buttonId1, clickedUrl1); + embeddedManager.trackClick(mockEmbeddedMessage, buttonId2, clickedUrl2); + + // THEN IterableApi.trackEmbeddedClick is called twice + expect(MockRNIterableAPI.trackEmbeddedClick).toHaveBeenCalledTimes(2); + expect(MockRNIterableAPI.trackEmbeddedClick).toHaveBeenNthCalledWith( + 1, + mockEmbeddedMessage, + buttonId1, + clickedUrl1 + ); + expect(MockRNIterableAPI.trackEmbeddedClick).toHaveBeenNthCalledWith( + 2, + mockEmbeddedMessage, + buttonId2, + clickedUrl2 + ); + }); + }); + + describe('handleClick', () => { + // Import the mocked callUrlHandler + const { callUrlHandler } = require('../../core/utils/callUrlHandler'); + + beforeEach(() => { + // Add trackEmbeddedClick mock if not already present + MockRNIterableAPI.trackEmbeddedClick = jest.fn(); + }); + + it('should return early and log when no clickedUrl is provided', () => { + // GIVEN no action is provided + const buttonId = 'button-1'; + + // WHEN handleClick is called without an action + embeddedManager.handleClick(mockEmbeddedMessage, buttonId, null); + + // THEN it should log the error + expect(IterableLogger.log).toHaveBeenCalledWith( + 'Iterable.embeddedManager.handleClick:', + 'A url or action is required to handle an embedded click', + undefined + ); + + // AND trackClick should not be called + expect(MockRNIterableAPI.trackEmbeddedClick).not.toHaveBeenCalled(); + }); + + it('should return early and log when action has empty data and empty type', () => { + // GIVEN an action with empty data and type + const buttonId = 'button-1'; + const action = new IterableAction('', '', ''); + + // WHEN handleClick is called + embeddedManager.handleClick(mockEmbeddedMessage, buttonId, action); + + // THEN it should log the error (with empty string since that's what we get from action) + expect(IterableLogger.log).toHaveBeenCalledWith( + 'Iterable.embeddedManager.handleClick:', + 'A url or action is required to handle an embedded click', + '' + ); + + // AND trackClick should not be called + expect(MockRNIterableAPI.trackEmbeddedClick).not.toHaveBeenCalled(); + }); + + it('should handle action:// prefix and call customActionHandler', () => { + // GIVEN an action with action:// prefix and a custom action handler + const buttonId = 'button-1'; + const action = new IterableAction('', 'action://myAction', ''); + const customActionHandler = jest.fn(); + config.customActionHandler = customActionHandler; + + // WHEN handleClick is called + embeddedManager.handleClick(mockEmbeddedMessage, buttonId, action); + + // THEN it should log the click + expect(IterableLogger.log).toHaveBeenCalledWith( + 'Iterable.embeddedManager.handleClick', + mockEmbeddedMessage, + buttonId, + 'action://myAction' + ); + + // AND trackClick should be called + expect(MockRNIterableAPI.trackEmbeddedClick).toHaveBeenCalledWith( + mockEmbeddedMessage, + buttonId, + 'action://myAction' + ); + + // AND customActionHandler should be called with the correct action + expect(customActionHandler).toHaveBeenCalledTimes(1); + const calledAction = customActionHandler.mock.calls[0][0]; + const calledContext = customActionHandler.mock.calls[0][1]; + expect(calledAction.type).toBe('myAction'); + expect(calledContext.source).toBe(3); // IterableActionSource.embedded + }); + + it('should handle itbl:// prefix and call customActionHandler', () => { + // GIVEN an action with itbl:// prefix and a custom action handler + const buttonId = 'button-1'; + const action = new IterableAction('', 'itbl://legacyAction', ''); + const customActionHandler = jest.fn(); + config.customActionHandler = customActionHandler; + + // WHEN handleClick is called + embeddedManager.handleClick(mockEmbeddedMessage, buttonId, action); + + // THEN it should log the click + expect(IterableLogger.log).toHaveBeenCalledWith( + 'Iterable.embeddedManager.handleClick', + mockEmbeddedMessage, + buttonId, + 'itbl://legacyAction' + ); + + // AND trackClick should be called + expect(MockRNIterableAPI.trackEmbeddedClick).toHaveBeenCalledWith( + mockEmbeddedMessage, + buttonId, + 'itbl://legacyAction' + ); + + // AND customActionHandler should be called + expect(customActionHandler).toHaveBeenCalledTimes(1); + const calledAction = customActionHandler.mock.calls[0][0]; + expect(calledAction.type).toBe('legacyAction'); + }); + + it('should not call customActionHandler if action prefix exists but handler is not configured', () => { + // GIVEN an action with action:// prefix but no custom action handler + const buttonId = 'button-1'; + const action = new IterableAction('', 'action://myAction', ''); + + // WHEN handleClick is called + embeddedManager.handleClick(mockEmbeddedMessage, buttonId, action); + + // THEN trackClick should be called + expect(MockRNIterableAPI.trackEmbeddedClick).toHaveBeenCalledWith( + mockEmbeddedMessage, + buttonId, + 'action://myAction' + ); + + // AND customActionHandler should not error (it's undefined) + // Just verify trackClick was called + expect(MockRNIterableAPI.trackEmbeddedClick).toHaveBeenCalledTimes(1); + }); + + it('should handle regular URL and call urlHandler', () => { + // GIVEN a regular URL action and a URL handler + const buttonId = 'button-1'; + const action = new IterableAction('', 'https://example.com', ''); + const urlHandler = jest.fn().mockReturnValue(true); + config.urlHandler = urlHandler; + + // WHEN handleClick is called + embeddedManager.handleClick(mockEmbeddedMessage, buttonId, action); + + // THEN it should log the click + expect(IterableLogger.log).toHaveBeenCalledWith( + 'Iterable.embeddedManager.handleClick', + mockEmbeddedMessage, + buttonId, + 'https://example.com' + ); + + // AND trackClick should be called + expect(MockRNIterableAPI.trackEmbeddedClick).toHaveBeenCalledWith( + mockEmbeddedMessage, + buttonId, + 'https://example.com' + ); + + // AND callUrlHandler should be called + expect(callUrlHandler).toHaveBeenCalledTimes(1); + expect(callUrlHandler).toHaveBeenCalledWith( + config, + 'https://example.com', + expect.objectContaining({ + action: expect.objectContaining({ + type: 'openUrl', + data: 'https://example.com', + }), + source: 3, // IterableActionSource.embedded + }) + ); + }); + + it('should handle regular URL without urlHandler configured', () => { + // GIVEN a regular URL action without a URL handler + const buttonId = 'button-1'; + const action = new IterableAction('', 'https://example.com', ''); + + // WHEN handleClick is called + embeddedManager.handleClick(mockEmbeddedMessage, buttonId, action); + + // THEN trackClick should be called + expect(MockRNIterableAPI.trackEmbeddedClick).toHaveBeenCalledWith( + mockEmbeddedMessage, + buttonId, + 'https://example.com' + ); + + // AND callUrlHandler should be called + expect(callUrlHandler).toHaveBeenCalledTimes(1); + }); + + it('should prefer action.data over action.type when data is available', () => { + // GIVEN an action with both data and type + const buttonId = 'button-1'; + const action = new IterableAction('someType', 'https://example.com', ''); + + // WHEN handleClick is called + embeddedManager.handleClick(mockEmbeddedMessage, buttonId, action); + + // THEN it should use data as clickedUrl + expect(IterableLogger.log).toHaveBeenCalledWith( + 'Iterable.embeddedManager.handleClick', + mockEmbeddedMessage, + buttonId, + 'https://example.com' + ); + + // AND trackClick should be called with the data + expect(MockRNIterableAPI.trackEmbeddedClick).toHaveBeenCalledWith( + mockEmbeddedMessage, + buttonId, + 'https://example.com' + ); + }); + + it('should use action.type when data is empty', () => { + // GIVEN an action with empty data but valid type + const buttonId = 'button-1'; + const action = new IterableAction('https://example.com', '', ''); + + // WHEN handleClick is called + embeddedManager.handleClick(mockEmbeddedMessage, buttonId, action); + + // THEN it should use type as clickedUrl + expect(IterableLogger.log).toHaveBeenCalledWith( + 'Iterable.embeddedManager.handleClick', + mockEmbeddedMessage, + buttonId, + 'https://example.com' + ); + + // AND trackClick should be called with the type + expect(MockRNIterableAPI.trackEmbeddedClick).toHaveBeenCalledWith( + mockEmbeddedMessage, + buttonId, + 'https://example.com' + ); + }); + + it('should handle null buttonId', () => { + // GIVEN an action with null buttonId + const buttonId = null; + const action = new IterableAction('', 'https://example.com', ''); + + // WHEN handleClick is called + embeddedManager.handleClick(mockEmbeddedMessage, buttonId, action); + + // THEN trackClick should be called with null buttonId + expect(MockRNIterableAPI.trackEmbeddedClick).toHaveBeenCalledWith( + mockEmbeddedMessage, + null, + 'https://example.com' + ); + }); + + it('should handle action with undefined action parameter', () => { + // GIVEN no action parameter + const buttonId = 'button-1'; + + // WHEN handleClick is called with undefined action + embeddedManager.handleClick(mockEmbeddedMessage, buttonId, undefined); + + // THEN it should log the error and not track + expect(IterableLogger.log).toHaveBeenCalledWith( + 'Iterable.embeddedManager.handleClick:', + 'A url or action is required to handle an embedded click', + undefined + ); + expect(MockRNIterableAPI.trackEmbeddedClick).not.toHaveBeenCalled(); + }); + }); + + describe('constructor', () => { + it('should initialize with embedded messaging enabled when config flag is true', () => { + // GIVEN a config with embedded messaging enabled + const configWithEnabled = new IterableConfig(); + configWithEnabled.enableEmbeddedMessaging = true; + + // WHEN creating a new embedded manager + const manager = new IterableEmbeddedManager(configWithEnabled); + + // THEN isEnabled should be true + expect(manager.isEnabled).toBe(true); + }); + + it('should initialize with embedded messaging disabled when config flag is false', () => { + // GIVEN a config with embedded messaging disabled + const configWithDisabled = new IterableConfig(); + configWithDisabled.enableEmbeddedMessaging = false; + + // WHEN creating a new embedded manager + const manager = new IterableEmbeddedManager(configWithDisabled); + + // THEN isEnabled should be false + expect(manager.isEnabled).toBe(false); + }); + + it('should initialize with embedded messaging disabled when config flag is undefined', () => { + // GIVEN a config without the flag set + const configWithUndefined = new IterableConfig(); + + // WHEN creating a new embedded manager + const manager = new IterableEmbeddedManager(configWithUndefined); + + // THEN isEnabled should be false (default) + expect(manager.isEnabled).toBe(false); + }); + }); });