Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -771,6 +771,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
}
Expand Down
16 changes: 16 additions & 0 deletions android/src/main/java/com/iterable/reactnative/Serialization.java
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,22 @@ static JSONArray serializeEmbeddedMessages(List<IterableEmbeddedMessage> 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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

annotation like @nullable here will future proof for kotlin conversion if it ever happens and also for test frameworks

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);
Expand Down
5 changes: 5 additions & 0 deletions android/src/newarch/java/com/RNIterableAPIModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
5 changes: 5 additions & 0 deletions android/src/oldarch/java/com/RNIterableAPIModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
46 changes: 44 additions & 2 deletions example/src/components/Embedded/Embedded.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
import { SafeAreaView } from 'react-native-safe-area-context';
Expand Down Expand Up @@ -69,6 +70,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 (
<SafeAreaView style={styles.container}>
<Text style={styles.text}>EMBEDDED</Text>
Expand Down Expand Up @@ -105,7 +118,9 @@ export const Embedded = () => {
{embeddedMessages.map((message) => (
<View key={message.metadata.messageId}>
<View style={styles.embeddedTitleContainer}>
<Text style={styles.embeddedTitle}>Embedded message | </Text>
<Text style={styles.embeddedTitle}>Embedded message</Text>
</View>
<View style={styles.embeddedTitleContainer}>
<TouchableOpacity
onPress={() => startEmbeddedImpression(message)}
>
Expand All @@ -117,15 +132,42 @@ export const Embedded = () => {
>
<Text style={styles.link}>Pause impression</Text>
</TouchableOpacity>
<Text style={styles.embeddedTitle}> | </Text>
<TouchableOpacity
onPress={() =>
handleClick(message, null, message.elements?.defaultAction)
}
>
<Text style={styles.link}>Handle click</Text>
</TouchableOpacity>
</View>

<Text>metadata.messageId: {message.metadata.messageId}</Text>
<Text>metadata.placementId: {message.metadata.placementId}</Text>
<Text>elements.title: {message.elements?.title}</Text>
<Text>elements.body: {message.elements?.body}</Text>
<Text>
elements.defaultAction.data:{' '}
{message.elements?.defaultAction?.data}
</Text>
<Text>
elements.defaultAction.type:{' '}
{message.elements?.defaultAction?.type}
</Text>
{(message.elements?.buttons ?? []).map((button, buttonIndex) => (
<View key={`${button.id}-${buttonIndex}`}>
<Text>Button {buttonIndex + 1}</Text>
<View style={styles.embeddedTitleContainer}>
<Text>Button {buttonIndex + 1}</Text>
<Text style={styles.embeddedTitle}> | </Text>
<TouchableOpacity
onPress={() =>
handleClick(message, button.id, button.action)
}
>
<Text style={styles.link}>Handle click</Text>
</TouchableOpacity>
</View>

<Text>button.id: {button.id}</Text>
<Text>button.title: {button.title}</Text>
<Text>button.action?.data: {button.action?.data}</Text>
Expand Down
2 changes: 2 additions & 0 deletions src/__mocks__/MockRNIterableAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
5 changes: 5 additions & 0 deletions src/api/NativeRNIterableAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,11 @@ export interface Spec extends TurboModule {
getEmbeddedMessages(
placementIds: number[] | null
): Promise<EmbeddedMessage[]>;
trackEmbeddedClick(
message: EmbeddedMessage,
buttonId: string | null,
clickedUrl: string | null
): void;
Comment on lines +158 to +162
Copy link

Copilot AI Nov 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Android native implementation adds the trackEmbeddedClick method, but there's no corresponding iOS implementation visible in this PR. This will cause the feature to not work on iOS devices. Ensure that the iOS native module also implements this method, or document that this feature is Android-only if that's intentional.

Copilot uses AI. Check for mistakes.

// Wake app -- android only
wakeApp(): void;
Expand Down
38 changes: 12 additions & 26 deletions src/core/classes/Iterable.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';
Expand All @@ -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();

/**
* Checks if the response is an IterableAuthResponse
*/
Expand Down Expand Up @@ -63,7 +66,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.
Expand Down Expand Up @@ -115,8 +118,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.
Expand Down Expand Up @@ -195,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();
Expand Down Expand Up @@ -983,10 +985,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);
}
});
}
Expand Down Expand Up @@ -1081,22 +1083,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);
});
}
}
}

/**
Expand Down
12 changes: 12 additions & 0 deletions src/core/classes/IterableApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ---- //

// ====================================================== //
Expand Down
2 changes: 2 additions & 0 deletions src/core/enums/IterableActionSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
9 changes: 9 additions & 0 deletions src/core/enums/IterableCustomActionPrefix.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* Enum representing the prefix of build-in custom action URL.
Copy link

Copilot AI Nov 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in documentation: "build-in" should be "built-in".

Suggested change
* Enum representing the prefix of build-in custom action URL.
* Enum representing the prefix of built-in custom action URL.

Copilot uses AI. Check for mistakes.
*/
export enum IterableCustomActionPrefix {
/** Current action prefix */
Action = 'action://',
/** Deprecated action prefix */
Itbl = 'itbl://',
}
1 change: 1 addition & 0 deletions src/core/enums/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export * from './IterableEventName';
export * from './IterableLogLevel';
export * from './IterablePushPlatform';
export * from './IterableRetryBackoff';
export * from './IterableCustomActionPrefix';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure about the custom prefix idea. Can DM you regarding this for clarification

31 changes: 31 additions & 0 deletions src/core/utils/callUrlHandler.ts
Original file line number Diff line number Diff line change
@@ -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
) {
if (!config.urlHandler?.(url, context)) {
Linking.canOpenURL(url)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be tested thoroughly to see its behavior.

.then((canOpen) => {
if (canOpen) {
Linking.openURL(url);
} else {
IterableLogger?.log('Url cannot be opened: ' + url);
}
})
.catch((reason) => {
IterableLogger?.log('Error opening url: ' + reason);
});
}
}
Loading