Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -678,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<IterableEmbeddedMessage> 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<IterableEmbeddedMessage> messages = IterableApi.getInstance().getEmbeddedManager().getMessages(0L);
if (messages != null) {
allMessages.addAll(messages);
Comment on lines +685 to +690
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.

Using 0 as a default placement ID when none are provided is questionable and may not work as expected. The comment acknowledges this with "This might need to be adjusted based on the actual SDK behavior."

This should either:

  1. Return an empty list when no placement IDs are provided (if that's the expected behavior)
  2. Fetch all messages across all known placements by iterating through getPlacementIds()
  3. Be documented clearly if 0 is a valid special value in the Iterable Android SDK

Consider implementing option 2:

if (placementIds == null || placementIds.size() == 0) {
    List<Long> allPlacementIds = IterableApi.getInstance().getEmbeddedManager().getPlacementIds();
    if (allPlacementIds != null) {
        for (Long placementId : allPlacementIds) {
            List<IterableEmbeddedMessage> messages = IterableApi.getInstance().getEmbeddedManager().getMessages(placementId);
            if (messages != null) {
                allMessages.addAll(messages);
            }
        }
    }
}
Suggested change
// 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<IterableEmbeddedMessage> messages = IterableApi.getInstance().getEmbeddedManager().getMessages(0L);
if (messages != null) {
allMessages.addAll(messages);
// If no placement IDs provided, get messages for all known placements
List<Long> allPlacementIds = IterableApi.getInstance().getEmbeddedManager().getPlacementIds();
if (allPlacementIds != null) {
for (Long placementId : allPlacementIds) {
List<IterableEmbeddedMessage> messages = IterableApi.getInstance().getEmbeddedManager().getMessages(placementId);
if (messages != null) {
allMessages.addAll(messages);
}
}

Copilot uses AI. Check for mistakes.
}
} 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<IterableEmbeddedMessage> 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());
}
Copy link

Choose a reason for hiding this comment

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

Function with high complexity (count = 11): getEmbeddedMessages [qlty:function-complexity]

}

// ---------------------------------------------------------------------------------------
// endregion
}
Expand Down
11 changes: 11 additions & 0 deletions android/src/main/java/com/iterable/reactnative/Serialization.java
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,17 @@ static JSONArray serializeInAppMessages(List<IterableInAppMessage> inAppMessages
return inAppMessagesJson;
}

static JSONArray serializeEmbeddedMessages(List<IterableEmbeddedMessage> 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);
Expand Down
10 changes: 10 additions & 0 deletions android/src/newarch/java/com/RNIterableAPIModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,11 @@ public void pauseAuthRetries(boolean pauseRetry) {
moduleImpl.pauseAuthRetries(pauseRetry);
}

@Override
public void syncEmbeddedMessages() {
moduleImpl.syncEmbeddedMessages();
}

@Override
public void startEmbeddedSession() {
moduleImpl.startEmbeddedSession();
Expand All @@ -239,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);
}
Expand Down
10 changes: 10 additions & 0 deletions android/src/oldarch/java/com/RNIterableAPIModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,11 @@ public void pauseAuthRetries(boolean pauseRetry) {
moduleImpl.pauseAuthRetries(pauseRetry);
}

@ReactMethod
public void syncEmbeddedMessages() {
moduleImpl.syncEmbeddedMessages();
}

@ReactMethod
public void startEmbeddedSession() {
moduleImpl.startEmbeddedSession();
Expand All @@ -243,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);
}
Expand Down
97 changes: 75 additions & 22 deletions example/src/components/Embedded/Embedded.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,27 @@
import { Text, TouchableOpacity, View } from 'react-native';
import { ScrollView, 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<number[]>([]);
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;
});
}, []);

Expand All @@ -27,28 +39,69 @@ 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);
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.

[nitpick] Missing error handling for the promise chain. If any of the promises reject (e.g., network error, SDK error), the error will be unhandled and could cause issues in the example app. Consider adding a .catch() block:

const getEmbeddedMessages = useCallback(() => {
  getPlacementIds()
    .then((ids: number[]) => Iterable.embeddedManager.getMessages(ids))
    .then((messages: IterableEmbeddedMessage[]) => {
      setEmbeddedMessages(messages);
      console.log(messages);
    })
    .catch((error) => {
      console.error('Failed to get embedded messages:', error);
    });
}, [getPlacementIds]);
Suggested change
console.log(messages);
console.log(messages);
})
.catch((error) => {
console.error('Failed to get embedded messages:', error);

Copilot uses AI. Check for mistakes.
});
}, [getPlacementIds]);

return (
<View style={styles.container}>
<Text style={styles.text}>EMBEDDED</Text>
<Text style={styles.text}>
Does embedded class exist? {Iterable.embeddedManager ? 'Yes' : 'No'}
</Text>
<Text style={styles.text}>
Is embedded manager enabled?
{Iterable.embeddedManager.isEnabled ? 'Yes' : 'No'}
</Text>
<Text style={styles.text}>
Placement ids: [{placementIds.join(', ')}]
</Text>
<TouchableOpacity style={styles.button} onPress={getPlacementIds}>
<Text style={styles.buttonText}>Get placement ids</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.button} onPress={startEmbeddedSession}>
<Text style={styles.buttonText}>Start embedded session</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.button} onPress={endEmbeddedSession}>
<Text style={styles.buttonText}>End embedded session</Text>
</TouchableOpacity>
<View style={styles.utilitySection}>
<Text style={styles.text}>
Does embedded class exist? {Iterable.embeddedManager ? 'Yes' : 'No'}
</Text>
<Text style={styles.text}>
Is embedded manager enabled?
{Iterable.embeddedManager.isEnabled ? 'Yes' : 'No'}
</Text>
<Text style={styles.text}>
Placement ids: [{placementIds.join(', ')}]
</Text>
<TouchableOpacity style={styles.button} onPress={syncEmbeddedMessages}>
<Text style={styles.buttonText}>Sync embedded messages</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.button} onPress={getPlacementIds}>
<Text style={styles.buttonText}>Get placement ids</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.button} onPress={startEmbeddedSession}>
<Text style={styles.buttonText}>Start embedded session</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.button} onPress={endEmbeddedSession}>
<Text style={styles.buttonText}>End embedded session</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.button} onPress={getEmbeddedMessages}>
<Text style={styles.buttonText}>Get embedded messages</Text>
</TouchableOpacity>
</View>
<View style={styles.hr} />
<ScrollView>
<View style={styles.embeddedSection}>
{embeddedMessages.map((message) => (
<View key={message.metadata.messageId}>
<Text>Embedded message</Text>
<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>
{(message.elements?.buttons ?? []).map((button, buttonIndex) => (
<View key={`${button.id}-${buttonIndex}`}>
<Text>Button {buttonIndex + 1}</Text>
<Text>button.id: {button.id}</Text>
<Text>button.title: {button.title}</Text>
<Text>button.action?.data: {button.action?.data}</Text>
<Text>button.action?.type: {button.action?.type}</Text>
</View>
))}
<Text>payload: {JSON.stringify(message.payload)}</Text>
</View>
))}
</View>
</ScrollView>
</View>
);
};
Expand Down
31 changes: 31 additions & 0 deletions src/api/NativeRNIterableAPI.ts
Original file line number Diff line number Diff line change
@@ -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;
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.

Type mismatch: The payload property in the EmbeddedMessage interface is defined as { [key: string]: string | number | boolean | null }, but in IterableEmbeddedMessage it's defined as Record<string, unknown>.

The native interface is more restrictive (only allows primitive types and null), while the TypeScript interface is more permissive (allows any value including nested objects/arrays). This could cause type safety issues.

Consider either:

  1. Aligning the types to match (likely the native version is correct based on what the native SDKs actually return)
  2. Adding a type guard or runtime validation when converting from native to TypeScript types

Suggested fix in IterableEmbeddedMessage.ts:

payload?: Record<string, string | number | boolean | null> | null;

Copilot uses AI. Check for mistakes.
}

export interface Spec extends TurboModule {
// Initialization
initializeWithApiKey(
Expand Down Expand Up @@ -119,9 +146,13 @@ export interface Spec extends TurboModule {
pauseAuthRetries(pauseRetry: boolean): void;

// Embedded Messaging
syncEmbeddedMessages(): void;
startEmbeddedSession(): void;
endEmbeddedSession(): void;
getEmbeddedPlacementIds(): Promise<number[]>;
getEmbeddedMessages(
placementIds: number[] | null
): Promise<EmbeddedMessage[]>;

// Wake app -- android only
wakeApp(): void;
Expand Down
21 changes: 21 additions & 0 deletions src/core/classes/IterableApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -510,6 +511,14 @@ export class IterableApi {
// ======================= EMBEDDED ===================== //
// ====================================================== //

/**
* Syncs embedded local cache with the server.
*/
static syncEmbeddedMessages() {
IterableLogger.log('syncEmbeddedMessages');
return RNIterableAPI.syncEmbeddedMessages();
}

/**
* Starts an embedded session.
*/
Expand All @@ -534,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<IterableEmbeddedMessage[]> {
IterableLogger.log('getEmbeddedMessages: ', placementIds);
return RNIterableAPI.getEmbeddedMessages(placementIds);
}

// ---- End EMBEDDED ---- //

// ====================================================== //
Expand Down
36 changes: 36 additions & 0 deletions src/embedded/classes/IterableEmbeddedManager.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { IterableApi } from '../../core/classes/IterableApi';
import type { IterableEmbeddedMessage } from '../types/IterableEmbeddedMessage';

/**
* Manages embedded messages from Iterable.
Expand All @@ -20,6 +21,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
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 documentation incorrectly refers to "iOS SDK" in a comment about syncing messages. Since this PR is adding Android support, the comment should mention both iOS and Android SDKs, or be platform-agnostic.

Suggested fix:

/**
 * Syncs embedded local cache with the server.
 *
 * When your app first launches, and each time it comes to the foreground,
 * Iterable's SDK automatically refreshes 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();
 * ```
 */
Suggested change
* Iterable's iOS SDK automatically refresh a local, on-device cache of
* Iterable's SDK automatically refreshes a local, on-device cache of

Copilot uses AI. Check for mistakes.
* 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.
*
Expand All @@ -29,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.
Comment on lines +75 to +76
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 documentation for getMessages() doesn't explain the behavior when placementIds is null or an empty array. Based on the Android implementation, this is an important detail to document since the behavior is unclear (currently uses placement ID 0 as a default, which may not be correct).

Suggested documentation improvement:

/**
 * Retrieves a list of embedded messages the user is eligible to see.
 *
 * @param placementIds - The placement IDs to retrieve messages for. 
 *                       Pass null or an empty array to get messages for all placements.
 * @returns A Promise that resolves to an array of embedded messages.
 * @example
 * ```typescript
 * // Get messages for specific placements
 * const messages = await Iterable.embeddedManager.getMessages([123, 456]);
 * 
 * // Get messages for all placements
 * const allMessages = await Iterable.embeddedManager.getMessages(null);
 * ```
 */
Suggested change
* @param placementIds - The placement IDs to retrieve messages for.
* @returns A Promise that resolves to an array of embedded messages.
* @param placementIds - The placement IDs to retrieve messages for.
* Pass `null` or an empty array to get messages for all placements.
* @returns A Promise that resolves to an array of embedded messages.
* @example
* ```typescript
* // Get messages for specific placements
* const messages = await Iterable.embeddedManager.getMessages([123, 456]);
*
* // Get messages for all placements
* const allMessages = await Iterable.embeddedManager.getMessages(null);
* ```

Copilot uses AI. Check for mistakes.
*/
getMessages(
placementIds: number[] | null
): Promise<IterableEmbeddedMessage[]> {
return IterableApi.getEmbeddedMessages(placementIds);
}

/**
* Starts a session.
*
Expand Down
1 change: 1 addition & 0 deletions src/embedded/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './classes';
export * from './types';
Loading