diff --git a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java index 98340a1f7..979bced70 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; @@ -23,11 +24,12 @@ 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.IterableAuthManager; import com.iterable.iterableapi.IterableConfig; import com.iterable.iterableapi.IterableCustomActionHandler; -import com.iterable.iterableapi.IterableAttributionInfo; +import com.iterable.iterableapi.IterableEmbeddedMessage; import com.iterable.iterableapi.IterableHelper; import com.iterable.iterableapi.IterableInAppCloseAction; import com.iterable.iterableapi.IterableInAppHandler; @@ -46,6 +48,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; @@ -122,6 +125,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 @@ -185,6 +189,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 @@ -683,14 +688,90 @@ public void sendEvent(@NonNull String eventName, @Nullable Object eventData) { public void onInboxUpdated() { sendEvent(EventName.receivedIterableInboxChanged.name(), null); } + // --------------------------------------------------------------------------------------- + // endregion + + // --------------------------------------------------------------------------------------- + // 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(); + } + + public void endEmbeddedSession() { + IterableLogger.d(TAG, "endEmbeddedSession"); + IterableApi.getInstance().getEmbeddedManager().getEmbeddedSessionManager().endSession(); + } + + 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); + + 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 } 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"); diff --git a/android/src/newarch/java/com/RNIterableAPIModule.java b/android/src/newarch/java/com/RNIterableAPIModule.java index f145bab10..ade1996f6 100644 --- a/android/src/newarch/java/com/RNIterableAPIModule.java +++ b/android/src/newarch/java/com/RNIterableAPIModule.java @@ -224,6 +224,31 @@ public void pauseAuthRetries(boolean pauseRetry) { moduleImpl.pauseAuthRetries(pauseRetry); } + @Override + public void syncEmbeddedMessages() { + moduleImpl.syncEmbeddedMessages(); + } + + @Override + public void startEmbeddedSession() { + moduleImpl.startEmbeddedSession(); + } + + @Override + public void endEmbeddedSession() { + moduleImpl.endEmbeddedSession(); + } + + @Override + 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 c3a72339b..468c2e4e4 100644 --- a/android/src/oldarch/java/com/RNIterableAPIModule.java +++ b/android/src/oldarch/java/com/RNIterableAPIModule.java @@ -228,6 +228,30 @@ public void pauseAuthRetries(boolean pauseRetry) { moduleImpl.pauseAuthRetries(pauseRetry); } + @ReactMethod + public void syncEmbeddedMessages() { + moduleImpl.syncEmbeddedMessages(); + } + + @ReactMethod + public void startEmbeddedSession() { + moduleImpl.startEmbeddedSession(); + } + + @ReactMethod + public void endEmbeddedSession() { + moduleImpl.endEmbeddedSession(); + } + + @ReactMethod + 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/App/App.constants.ts b/example/src/components/App/App.constants.ts index 65bf314e6..84f8cf78f 100644 --- a/example/src/components/App/App.constants.ts +++ b/example/src/components/App/App.constants.ts @@ -12,8 +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]: chatIcon, [Route.Inbox]: mailIcon, [Route.User]: personIcon, }; 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/App/Main.tsx b/example/src/components/App/Main.tsx index b944801fe..956973149 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), + })} + /> { }; 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.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..384b6675f --- /dev/null +++ b/example/src/components/Embedded/Embedded.tsx @@ -0,0 +1,110 @@ +import { ScrollView, Text, TouchableOpacity, View } from 'react-native'; +import { useCallback, useState } from 'react'; +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([]); + const [embeddedMessages, setEmbeddedMessages] = useState< + IterableEmbeddedMessage[] + >([]); + + const syncEmbeddedMessages = useCallback(() => { + Iterable.embeddedManager.syncMessages(); + }, []); + + const getPlacementIds = useCallback(() => { + return Iterable.embeddedManager.getPlacementIds().then((ids: unknown) => { + console.log(ids); + setPlacementIds(ids as number[]); + return ids; + }); + }, []); + + const startEmbeddedSession = useCallback(() => { + console.log( + 'startEmbeddedSession --> check android/ios logs to check if it worked' + ); + Iterable.embeddedManager.startSession(); + }, []); + + const endEmbeddedSession = useCallback(() => { + console.log( + 'endEmbeddedSession --> check android/ios logs to check if it worked' + ); + 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 + + + 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)} + + ))} + + + + ); +}; + +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..f608dd476 --- /dev/null +++ b/example/src/components/Embedded/index.ts @@ -0,0 +1 @@ +export * from './Embedded'; 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/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/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, diff --git a/example/src/hooks/useIterableApp.tsx b/example/src/hooks/useIterableApp.tsx index 0022fdb4c..0affc08ce 100644 --- a/example/src/hooks/useIterableApp.tsx +++ b/example/src/hooks/useIterableApp.tsx @@ -125,7 +125,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'); @@ -134,12 +134,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) => { @@ -155,6 +161,8 @@ export const IterableAppProvider: FunctionComponent< retryBackoff: IterableRetryBackoff.linear, }; + config.enableEmbeddedMessaging = true; + config.onJwtError = (authFailure) => { console.log('onJwtError', authFailure); @@ -192,6 +200,8 @@ export const IterableAppProvider: FunctionComponent< config.logLevel = IterableLogLevel.debug; + config.enableEmbeddedMessaging = true; + config.inAppHandler = () => IterableInAppShowResponse.show; if ( 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; }; diff --git a/src/__mocks__/MockRNIterableAPI.ts b/src/__mocks__/MockRNIterableAPI.ts index 4094afcb0..b78620a79 100644 --- a/src/__mocks__/MockRNIterableAPI.ts +++ b/src/__mocks__/MockRNIterableAPI.ts @@ -141,6 +141,43 @@ export class MockRNIterableAPI { static updateVisibleRows = jest.fn(); + static startEmbeddedSession = jest.fn(); + + static endEmbeddedSession = jest.fn(); + + static getEmbeddedPlacementIds = jest + .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/api/NativeRNIterableAPI.ts b/src/api/NativeRNIterableAPI.ts index 391fadbb7..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( @@ -118,6 +145,15 @@ export interface Spec extends TurboModule { passAlongAuthToken(authToken?: string | null): void; 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/Iterable.test.ts b/src/core/classes/Iterable.test.ts index bfe4c26f6..b0a789f21 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.enableEmbeddedMessaging).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.enableEmbeddedMessaging).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); }); }); @@ -1212,4 +1214,19 @@ describe('Iterable', () => { }); }); }); + + describe('embeddedManager', () => { + it('should be disabled by default', () => { + const config = new IterableConfig(); + expect(config.enableEmbeddedMessaging).toBe(false); + expect(Iterable.embeddedManager.isEnabled).toBe(false); + }); + + it('should enable embeddedManager when config is set', async () => { + const config = new IterableConfig(); + config.enableEmbeddedMessaging = true; + await Iterable.initialize('test-key', config); + expect(Iterable.embeddedManager.isEnabled).toBe(true); + }); + }); }); diff --git a/src/core/classes/Iterable.ts b/src/core/classes/Iterable.ts index 6b9431fc6..63cbe527a 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); @@ -96,6 +97,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. * @@ -172,6 +194,10 @@ export class Iterable { IterableLogger.setLoggingEnabled(config.logReactNativeSdkCalls ?? true); IterableLogger.setLogLevel(config.logLevel); + + Iterable.embeddedManager.setEnabled( + config.enableEmbeddedMessaging ?? false + ); } this.setupEventHandlers(); diff --git a/src/core/classes/IterableApi.ts b/src/core/classes/IterableApi.ts index fe2b446a3..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. @@ -507,7 +508,57 @@ export class IterableApi { // ---- End IN-APP ---- // // ====================================================== // - // ======================= MOSC ======================= // + // ======================= EMBEDDED ===================== // + // ====================================================== // + + /** + * Syncs embedded local cache with the server. + */ + static syncEmbeddedMessages() { + IterableLogger.log('syncEmbeddedMessages'); + return RNIterableAPI.syncEmbeddedMessages(); + } + + /** + * Starts an embedded session. + */ + static startEmbeddedSession() { + IterableLogger.log('startEmbeddedSession'); + return RNIterableAPI.startEmbeddedSession(); + } + + /** + * Ends an embedded session. + */ + static endEmbeddedSession() { + IterableLogger.log('endEmbeddedSession'); + return RNIterableAPI.endEmbeddedSession(); + } + + /** + * Get the embedded placement IDs. + */ + static getEmbeddedPlacementIds() { + IterableLogger.log('getEmbeddedPlacementIds'); + 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 ---- // + + // ====================================================== // + // ======================= MISCELLANEOUS ================ // // ====================================================== // /** diff --git a/src/core/classes/IterableConfig.ts b/src/core/classes/IterableConfig.ts index 664e08f1f..aeebfb91e 100644 --- a/src/core/classes/IterableConfig.ts +++ b/src/core/classes/IterableConfig.ts @@ -329,6 +329,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) + */ + enableEmbeddedMessaging = false; + /** * Converts the IterableConfig instance to a dictionary object. * @@ -378,6 +388,7 @@ export class IterableConfig { pushPlatform: this.pushPlatform, encryptionEnforced: this.encryptionEnforced, retryPolicy: this.retryPolicy, + enableEmbeddedMessaging: this.enableEmbeddedMessaging, }; } } diff --git a/src/embedded/classes/IterableEmbeddedManager.test.ts b/src/embedded/classes/IterableEmbeddedManager.test.ts new file mode 100644 index 000000000..a3d27804c --- /dev/null +++ b/src/embedded/classes/IterableEmbeddedManager.test.ts @@ -0,0 +1,171 @@ +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', () => { + 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); + }); + }); + + 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 + 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('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 + 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); + }); + }); + +}); + diff --git a/src/embedded/classes/IterableEmbeddedManager.ts b/src/embedded/classes/IterableEmbeddedManager.ts new file mode 100644 index 000000000..f2ed0ccc9 --- /dev/null +++ b/src/embedded/classes/IterableEmbeddedManager.ts @@ -0,0 +1,120 @@ +import { IterableApi } from '../../core/classes/IterableApi'; +import type { IterableEmbeddedMessage } from '../types/IterableEmbeddedMessage'; + +/** + * 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) + * - [iOS Embedded Messaging](https://support.iterable.com/hc/en-us/articles/23061840746900-Embedded-Messages-with-Iterable-s-iOS-SDK) + */ +export class IterableEmbeddedManager { + /** + * Whether the embedded manager is enabled. + * + * This is set through the `enableEmbeddedMessaging` flag in the + * `IterableConfig` class. + */ + private _isEnabled = false; + + /** + * Gets whether the embedded manager is enabled. + */ + get isEnabled(): boolean { + return this._isEnabled; + } + + /** + * Sets whether the embedded manager is enabled. + * + * @param enabled - Whether the embedded manager is enabled. + */ + setEnabled(enabled: boolean) { + this._isEnabled = enabled; + } + + /** + * 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. + * + * [Placement Documentation](https://support.iterable.com/hc/en-us/articles/23060529977364-Embedded-Messaging-Overview#placements-and-prioritization) + */ + getPlacementIds() { + 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. + * + * 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 + * Iterable.embeddedManager.startSession(); + * ``` + */ + startSession() { + return IterableApi.startEmbeddedSession(); + } + + /** + * 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. + * + * A session is tracked when it is ended, so you should be able to find + * tracking data after this method is called. + * + * @example + * ```typescript + * Iterable.embeddedManager.endSession(); + * ``` + */ + endSession() { + return IterableApi.endEmbeddedSession(); + } +} 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..15eb796c9 --- /dev/null +++ b/src/embedded/index.ts @@ -0,0 +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 240ac51f5..75c8489ec 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -59,3 +59,7 @@ export { type IterableInboxProps, type IterableInboxRowViewModel, } from './inbox'; +export { + IterableEmbeddedManager, + type IterableEmbeddedMessage, +} from './embedded';