Skip to content
Open
Show file tree
Hide file tree
Changes from all 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 @@ -643,6 +643,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 @@ -670,6 +675,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,16 +1,28 @@
import { Text, TouchableOpacity } 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 { SafeAreaView } from 'react-native-safe-area-context';

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 @@ -28,28 +40,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 (
<SafeAreaView 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>
</SafeAreaView>
);
};
Expand Down
29 changes: 29 additions & 0 deletions src/__mocks__/MockRNIterableAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
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
Loading