diff --git a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java index 9a0f84f73..f67a64ed4 100644 --- a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java +++ b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java @@ -726,6 +726,16 @@ public void getEmbeddedMessages(@Nullable ReadableArray placementIds, Promise pr } } + public void trackEmbeddedClick(ReadableMap messageMap, String buttonId, String clickedUrl) { + IterableLogger.d(TAG, "trackEmbeddedClick: buttonId: " + buttonId + " clickedUrl: " + clickedUrl); + IterableEmbeddedMessage message = Serialization.embeddedMessageFromReadableMap(messageMap); + if (message != null) { + IterableApi.getInstance().trackEmbeddedClick(message, buttonId, clickedUrl); + } else { + IterableLogger.e(TAG, "Failed to convert message map to IterableEmbeddedMessage"); + } + } + // --------------------------------------------------------------------------------------- // endregion } diff --git a/android/src/main/java/com/iterable/reactnative/Serialization.java b/android/src/main/java/com/iterable/reactnative/Serialization.java index 2f0c4e1ca..97aa52f49 100644 --- a/android/src/main/java/com/iterable/reactnative/Serialization.java +++ b/android/src/main/java/com/iterable/reactnative/Serialization.java @@ -149,6 +149,22 @@ static JSONArray serializeEmbeddedMessages(List embedde return embeddedMessagesJson; } + /** + * Converts a ReadableMap to an IterableEmbeddedMessage. + * + * This is needed as in new arch you can only pass in basic types, which + * then need to be converted in the native layer. + */ + static IterableEmbeddedMessage embeddedMessageFromReadableMap(ReadableMap messageMap) { + try { + JSONObject messageJson = convertMapToJson(messageMap); + return IterableEmbeddedMessage.Companion.fromJSONObject(messageJson); + } catch (JSONException e) { + IterableLogger.e(TAG, "Failed to convert ReadableMap to IterableEmbeddedMessage: " + e.getLocalizedMessage()); + return null; + } + } + static IterableConfig.Builder getConfigFromReadableMap(ReadableMap iterableContextMap) { try { JSONObject iterableContextJSON = convertMapToJson(iterableContextMap); diff --git a/android/src/newarch/java/com/RNIterableAPIModule.java b/android/src/newarch/java/com/RNIterableAPIModule.java index 457788ba8..056a5649d 100644 --- a/android/src/newarch/java/com/RNIterableAPIModule.java +++ b/android/src/newarch/java/com/RNIterableAPIModule.java @@ -259,6 +259,11 @@ public void getEmbeddedMessages(@Nullable ReadableArray placementIds, Promise pr moduleImpl.getEmbeddedMessages(placementIds, promise); } + @Override + public void trackEmbeddedClick(ReadableMap message, String buttonId, String clickedUrl) { + moduleImpl.trackEmbeddedClick(message, buttonId, clickedUrl); + } + public void sendEvent(@NonNull String eventName, @Nullable Object eventData) { moduleImpl.sendEvent(eventName, eventData); } diff --git a/android/src/oldarch/java/com/RNIterableAPIModule.java b/android/src/oldarch/java/com/RNIterableAPIModule.java index fb6c76d76..f387ed681 100644 --- a/android/src/oldarch/java/com/RNIterableAPIModule.java +++ b/android/src/oldarch/java/com/RNIterableAPIModule.java @@ -263,6 +263,11 @@ public void getEmbeddedMessages(@Nullable ReadableArray placementIds, Promise pr moduleImpl.getEmbeddedMessages(placementIds, promise); } + @ReactMethod + public void trackEmbeddedClick(ReadableMap message, String buttonId, String clickedUrl) { + moduleImpl.trackEmbeddedClick(message, buttonId, clickedUrl); + } + public void sendEvent(@NonNull String eventName, @Nullable Object eventData) { moduleImpl.sendEvent(eventName, eventData); } diff --git a/example/src/components/Embedded/Embedded.tsx b/example/src/components/Embedded/Embedded.tsx index 97addf49c..68b748048 100644 --- a/example/src/components/Embedded/Embedded.tsx +++ b/example/src/components/Embedded/Embedded.tsx @@ -2,6 +2,7 @@ import { ScrollView, Text, TouchableOpacity, View } from 'react-native'; import { useCallback, useState } from 'react'; import { Iterable, + type IterableAction, type IterableEmbeddedMessage, } from '@iterable/react-native-sdk'; @@ -68,6 +69,18 @@ export const Embedded = () => { [] ); + const handleClick = useCallback( + ( + message: IterableEmbeddedMessage, + buttonId: string | null, + action?: IterableAction | null + ) => { + console.log(`handleClick:`, message); + Iterable.embeddedManager.handleClick(message, buttonId, action); + }, + [] + ); + return ( EMBEDDED @@ -104,7 +117,9 @@ export const Embedded = () => { {embeddedMessages.map((message) => ( - Embedded message | + Embedded message + + startEmbeddedImpression(message)} > @@ -116,15 +131,42 @@ export const Embedded = () => { > Pause impression + | + + handleClick(message, null, message.elements?.defaultAction) + } + > + Handle click + metadata.messageId: {message.metadata.messageId} metadata.placementId: {message.metadata.placementId} elements.title: {message.elements?.title} elements.body: {message.elements?.body} + + elements.defaultAction.data:{' '} + {message.elements?.defaultAction?.data} + + + elements.defaultAction.type:{' '} + {message.elements?.defaultAction?.type} + {(message.elements?.buttons ?? []).map((button, buttonIndex) => ( - Button {buttonIndex + 1} + + Button {buttonIndex + 1} + | + + handleClick(message, button.id, button.action) + } + > + Handle click + + + button.id: {button.id} button.title: {button.title} button.action?.data: {button.action?.data} diff --git a/src/api/NativeRNIterableAPI.ts b/src/api/NativeRNIterableAPI.ts index 6e03cb2b7..ed1590528 100644 --- a/src/api/NativeRNIterableAPI.ts +++ b/src/api/NativeRNIterableAPI.ts @@ -155,6 +155,11 @@ export interface Spec extends TurboModule { getEmbeddedMessages( placementIds: number[] | null ): Promise; + trackEmbeddedClick( + message: EmbeddedMessage, + buttonId: string | null, + clickedUrl: string | null + ): void; // Wake app -- android only wakeApp(): void; diff --git a/src/core/classes/Iterable.ts b/src/core/classes/Iterable.ts index ebb928cd6..1f4535d22 100644 --- a/src/core/classes/Iterable.ts +++ b/src/core/classes/Iterable.ts @@ -1,8 +1,9 @@ -import { Linking, NativeEventEmitter, Platform } from 'react-native'; +import { NativeEventEmitter, Platform } from 'react-native'; import { buildInfo } from '../../itblBuildInfo'; import { RNIterableAPI } from '../../api'; +import { IterableEmbeddedManager } from '../../embedded/classes/IterableEmbeddedManager'; import { IterableInAppManager } from '../../inApp/classes/IterableInAppManager'; import { IterableInAppMessage } from '../../inApp/classes/IterableInAppMessage'; import { IterableInAppCloseSource } from '../../inApp/enums/IterableInAppCloseSource'; @@ -11,6 +12,7 @@ import { IterableInAppLocation } from '../../inApp/enums/IterableInAppLocation'; import { IterableAuthResponseResult } from '../enums/IterableAuthResponseResult'; import { IterableEventName } from '../enums/IterableEventName'; import type { IterableAuthFailure } from '../types/IterableAuthFailure'; +import { callUrlHandler } from '../utils/callUrlHandler'; import { IterableAction } from './IterableAction'; import { IterableActionContext } from './IterableActionContext'; import { IterableApi } from './IterableApi'; @@ -20,10 +22,11 @@ import { IterableAuthResponse } from './IterableAuthResponse'; import type { IterableCommerceItem } from './IterableCommerceItem'; import { IterableConfig } from './IterableConfig'; import { IterableLogger } from './IterableLogger'; -import { IterableEmbeddedManager } from '../../embedded/classes/IterableEmbeddedManager'; const RNEventEmitter = new NativeEventEmitter(RNIterableAPI); +const defaultConfig = new IterableConfig(); + /* eslint-disable tsdoc/syntax */ /** * The main class for the Iterable React Native SDK. @@ -46,7 +49,7 @@ export class Iterable { /** * Current configuration of the Iterable SDK */ - static savedConfig: IterableConfig = new IterableConfig(); + static savedConfig: IterableConfig = defaultConfig; /** * In-app message manager for the current user. @@ -98,8 +101,9 @@ export class Iterable { * }); * ``` */ - static embeddedManager: IterableEmbeddedManager = - new IterableEmbeddedManager(); + static embeddedManager: IterableEmbeddedManager = new IterableEmbeddedManager( + defaultConfig + ); /** * Initializes the Iterable React Native SDK in your app's Javascript or Typescript code. @@ -177,6 +181,8 @@ export class Iterable { IterableLogger.setLoggingEnabled(config.logReactNativeSdkCalls ?? true); IterableLogger.setLogLevel(config.logLevel); + + Iterable.embeddedManager = new IterableEmbeddedManager(config); } this.setupEventHandlers(); @@ -933,10 +939,10 @@ export class Iterable { if (Platform.OS === 'android') { //Give enough time for Activity to wake up. setTimeout(() => { - callUrlHandler(url, context); + callUrlHandler(Iterable.savedConfig, url, context); }, 1000); } else { - callUrlHandler(url, context); + callUrlHandler(Iterable.savedConfig, url, context); } }); } @@ -1031,22 +1037,6 @@ export class Iterable { } ); } - - function callUrlHandler(url: string, context: IterableActionContext) { - // MOB-10424: Figure out if this is purposeful - // eslint-disable-next-line eqeqeq - if (Iterable.savedConfig.urlHandler?.(url, context) == false) { - Linking.canOpenURL(url) - .then((canOpen) => { - if (canOpen) { - Linking.openURL(url); - } - }) - .catch((reason) => { - IterableLogger?.log('could not open url: ' + reason); - }); - } - } } /** diff --git a/src/core/classes/IterableApi.ts b/src/core/classes/IterableApi.ts index bbe687605..09d0dc44f 100644 --- a/src/core/classes/IterableApi.ts +++ b/src/core/classes/IterableApi.ts @@ -571,6 +571,18 @@ export class IterableApi { return RNIterableAPI.getEmbeddedMessages(placementIds); } + /** + * Track an embedded click. + */ + static trackEmbeddedClick( + message: IterableEmbeddedMessage, + buttonId: string | null, + clickedUrl: string | null + ) { + IterableLogger.log('trackEmbeddedClick: ', message, buttonId, clickedUrl); + return RNIterableAPI.trackEmbeddedClick(message, buttonId, clickedUrl); + } + // ---- End EMBEDDED ---- // // ====================================================== // diff --git a/src/core/enums/IterableActionSource.ts b/src/core/enums/IterableActionSource.ts index 3692e6361..437bb9808 100644 --- a/src/core/enums/IterableActionSource.ts +++ b/src/core/enums/IterableActionSource.ts @@ -8,4 +8,6 @@ export enum IterableActionSource { appLink = 1, /** The action source was an in-app message */ inApp = 2, + /** The action source was an embedded message */ + embedded = 3, } diff --git a/src/core/enums/IterableCustomActionPrefix.ts b/src/core/enums/IterableCustomActionPrefix.ts new file mode 100644 index 000000000..c8135a009 --- /dev/null +++ b/src/core/enums/IterableCustomActionPrefix.ts @@ -0,0 +1,9 @@ +/** + * Enum representing the prefix of build-in custom action URL. + */ +export enum IterableCustomActionPrefix { + /** Current action prefix */ + Action = 'action://', + /** Deprecated action prefix */ + Itbl = 'itbl://', +} diff --git a/src/core/enums/index.ts b/src/core/enums/index.ts index 52f4eb20d..21f06dee7 100644 --- a/src/core/enums/index.ts +++ b/src/core/enums/index.ts @@ -6,3 +6,4 @@ export * from './IterableEventName'; export * from './IterableLogLevel'; export * from './IterablePushPlatform'; export * from './IterableRetryBackoff'; +export * from './IterableCustomActionPrefix'; diff --git a/src/core/utils/callUrlHandler.ts b/src/core/utils/callUrlHandler.ts new file mode 100644 index 000000000..90f469362 --- /dev/null +++ b/src/core/utils/callUrlHandler.ts @@ -0,0 +1,31 @@ +import { Linking } from 'react-native'; +import type { IterableActionContext } from '../classes/IterableActionContext'; +import { IterableLogger } from '../classes/IterableLogger'; +import type { IterableConfig } from '../classes/IterableConfig'; + +/** + * Calls the URL handler and attempts to open the URL if the handler returns false. + * + * @param config - The config to use. + * @param url - The URL to call. + * @param context - The context to use. + */ +export function callUrlHandler( + config: IterableConfig, + url: string, + context: IterableActionContext +) { + // MOB-10424: Figure out if this is purposeful + // eslint-disable-next-line eqeqeq + if (config.urlHandler?.(url, context) == false) { + Linking.canOpenURL(url) + .then((canOpen) => { + if (canOpen) { + Linking.openURL(url); + } + }) + .catch((reason) => { + IterableLogger?.log('could not open url: ' + reason); + }); + } +} diff --git a/src/core/utils/getActionPrefix.ts b/src/core/utils/getActionPrefix.ts new file mode 100644 index 000000000..b4fcf9fbe --- /dev/null +++ b/src/core/utils/getActionPrefix.ts @@ -0,0 +1,20 @@ +import { IterableCustomActionPrefix } from '../enums/IterableCustomActionPrefix'; + +/** + * Gets the action prefix from a string. + * + * @param str - The string to get the action prefix from. + * @returns The action prefix. + */ +export const getActionPrefix = ( + str?: string | null +): IterableCustomActionPrefix | null => { + if (!str) return null; + if (str.startsWith(IterableCustomActionPrefix.Action)) { + return IterableCustomActionPrefix.Action; + } + if (str.startsWith(IterableCustomActionPrefix.Itbl)) { + return IterableCustomActionPrefix.Itbl; + } + return null; +}; diff --git a/src/core/utils/index.ts b/src/core/utils/index.ts new file mode 100644 index 000000000..b489ab8b4 --- /dev/null +++ b/src/core/utils/index.ts @@ -0,0 +1,2 @@ +export * from './getActionPrefix'; +export * from './callUrlHandler'; diff --git a/src/embedded/classes/IterableEmbeddedManager.ts b/src/embedded/classes/IterableEmbeddedManager.ts index 6e1a6bfbc..26720dac8 100644 --- a/src/embedded/classes/IterableEmbeddedManager.ts +++ b/src/embedded/classes/IterableEmbeddedManager.ts @@ -1,4 +1,11 @@ +import { IterableAction } from '../../core/classes/IterableAction'; +import { IterableActionContext } from '../../core/classes/IterableActionContext'; import { IterableApi } from '../../core/classes/IterableApi'; +import { IterableConfig } from '../../core/classes/IterableConfig'; +import { IterableLogger } from '../../core/classes/IterableLogger'; +import { IterableActionSource } from '../../core/enums/IterableActionSource'; +import { callUrlHandler } from '../../core/utils/callUrlHandler'; +import { getActionPrefix } from '../../core/utils/getActionPrefix'; import type { IterableEmbeddedMessage } from '../types/IterableEmbeddedMessage'; /** @@ -21,6 +28,15 @@ export class IterableEmbeddedManager { */ isEnabled = false; + /** + * The config for the Iterable SDK. + */ + config: IterableConfig = new IterableConfig(); + + constructor(config: IterableConfig) { + this.config = config; + } + /** * Syncs embedded local cache with the server. * @@ -141,4 +157,86 @@ export class IterableEmbeddedManager { pauseImpression(messageId: string) { return IterableApi.pauseEmbeddedImpression(messageId); } + + /** + * Tracks a click on an embedded message. + * + * This is called internally when `Iterable.embeddedManager.handleClick` is + * called. However, if you want to implement your own click handling, you can + * use this method to track the click you implement. + * + * @param message - The embedded message. + * @param buttonId - The button ID. + * @param clickedUrl - The clicked URL. + * + * @example + * ```typescript + * IterableEmbeddedManager.trackClick(message, buttonId, clickedUrl); + * ``` + */ + trackClick( + message: IterableEmbeddedMessage, + buttonId: string | null, + clickedUrl: string | null + ) { + return IterableApi.trackEmbeddedClick(message, buttonId, clickedUrl); + } + + /** + * Handles a click on an embedded message. + * + * This will fire the correct handers set in the config, and will track the + * click. It should be use on either a button click or a click on the message itself. + * + * @param message - The embedded message. + * @param buttonId - The button ID. + * @param clickedUrl - The clicked URL. + * + * @example + * ```typescript + * IterableEmbeddedManager.handleClick(message, buttonId, clickedUrl); + * ``` + */ + handleClick( + message: IterableEmbeddedMessage, + buttonId: string | null, + action?: IterableAction | null + ) { + const { data, type: actionType } = action ?? {}; + const clickedUrl = data && data?.length > 0 ? data : actionType; + + IterableLogger.log( + 'IterableEmbeddedManager.handleClick', + message, + buttonId, + clickedUrl + ); + + if (!clickedUrl) { + IterableLogger.log( + 'IterableEmbeddedManager.handleClick:', + 'A url or action is required to handle an embedded click', + clickedUrl + ); + return; + } + + const actionPrefix = getActionPrefix(clickedUrl); + const source = IterableActionSource.embedded; + + this.trackClick(message, buttonId, clickedUrl); + + if (actionPrefix) { + const actionName = clickedUrl?.replace(actionPrefix, ''); + const actionDetails = new IterableAction(actionName, '', ''); + const context = new IterableActionContext(actionDetails, source); + if (this.config.customActionHandler) { + this.config.customActionHandler(actionDetails, context); + } + } else { + const actionDetails = new IterableAction('openUrl', clickedUrl, ''); + const context = new IterableActionContext(actionDetails, source); + callUrlHandler(this.config, clickedUrl, context); + } + } }