diff --git a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java index ece9d8737..61781ecd2 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(); @@ -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 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 4c67af4e5..ade1996f6 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(); @@ -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); } diff --git a/android/src/oldarch/java/com/RNIterableAPIModule.java b/android/src/oldarch/java/com/RNIterableAPIModule.java index 868f4051d..468c2e4e4 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(); @@ -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); } diff --git a/example/src/components/Embedded/Embedded.tsx b/example/src/components/Embedded/Embedded.tsx index efedac721..2be959133 100644 --- a/example/src/components/Embedded/Embedded.tsx +++ b/example/src/components/Embedded/Embedded.tsx @@ -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([]); + 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; }); }, []); @@ -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); + }); + }, [getPlacementIds]); + return ( EMBEDDED - - Does embedded class exist? {Iterable.embeddedManager ? 'Yes' : 'No'} - - - Is embedded manager enabled? - {Iterable.embeddedManager.isEnabled ? 'Yes' : 'No'} - - - Placement ids: [{placementIds.join(', ')}] - - - 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} + {(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)} + + ))} + + ); }; diff --git a/src/api/NativeRNIterableAPI.ts b/src/api/NativeRNIterableAPI.ts index 931be6f29..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( @@ -119,9 +146,13 @@ export interface Spec extends TurboModule { pauseAuthRetries(pauseRetry: boolean): void; // Embedded Messaging + syncEmbeddedMessages(): void; 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 84c8f3a7a..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. @@ -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. */ @@ -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 { + 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 5515cb6ab..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. @@ -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 + * 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. * @@ -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. + */ + 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';