diff --git a/apps/AEPSampleAppNewArchEnabled/app/MessagingView.tsx b/apps/AEPSampleAppNewArchEnabled/app/MessagingView.tsx index 270d5adb..c513e79e 100644 --- a/apps/AEPSampleAppNewArchEnabled/app/MessagingView.tsx +++ b/apps/AEPSampleAppNewArchEnabled/app/MessagingView.tsx @@ -33,7 +33,16 @@ const refreshInAppMessages = () => { const setMessagingDelegate = () => { Messaging.setMessagingDelegate({ onDismiss: msg => console.log('dismissed!', msg), - onShow: msg => console.log('show', msg), + onShow: msg => { + console.log('show', msg); + Messaging.handleJavascriptMessage( + msg.id, + 'myInappCallback', + (content) => { + console.log('Received webview content:', content); + } + ); + }, shouldShowMessage: () => true, shouldSaveMessage: () => true, urlLoaded: (url, message) => console.log(url, message), diff --git a/packages/messaging/README.md b/packages/messaging/README.md index 1c05aa3d..ee4ac358 100644 --- a/packages/messaging/README.md +++ b/packages/messaging/README.md @@ -156,6 +156,20 @@ const messagingDelegate = { }; ``` +### handleJavascriptMessage + +Registers a javascript interface for the provided handler name to the WebView associated with the InAppMessage presentation to handle Javascript messages. When the registered handlers are executed via the HTML the result will be passed back to the associated callback. + +**Syntax** + +```javascript +handleJavascriptMessage(messageId: string, handlerName: string, callback: (content: string) => void) +``` + +**Example** + +It can be used for the native handling of JavaScript events. Please refer to the [tutorial](./tutorials/In-App%20Messaging.md#native-handling-of-javascript-events) for more information. + ### updatePropositionsForSurfaces Dispatches an event to fetch propositions for the provided surfaces from remote. @@ -459,4 +473,4 @@ Messaging.trackContentCardInteraction(proposition, contentCard); ## Tutorials [Content Cards](./tutorials/ContentCards.md) - +[In App Messaging](./tutorials/In-App%20Messaging.md) diff --git a/packages/messaging/__tests__/MessagingTests.ts b/packages/messaging/__tests__/MessagingTests.ts index f0ed77cd..28ba3913 100644 --- a/packages/messaging/__tests__/MessagingTests.ts +++ b/packages/messaging/__tests__/MessagingTests.ts @@ -126,4 +126,15 @@ describe('Messaging', () => { await Messaging.trackContentCardInteraction(mockProposition, mockContentCard); expect(spy).toHaveBeenCalledWith(mockProposition, mockContentCard); }); + + it('should call handleJavascriptMessage', () => { + const spy = jest.spyOn(NativeModules.AEPMessaging, 'handleJavascriptMessage'); + const messageId = 'test-message-id'; + const handlerName = 'myInappCallback'; + const callback = jest.fn(); + + Messaging.handleJavascriptMessage(messageId, handlerName, callback); + + expect(spy).toHaveBeenCalledWith(messageId, handlerName); + }); }); diff --git a/packages/messaging/android/src/main/java/com/adobe/marketing/mobile/reactnative/messaging/RCTAEPMessagingConstants.java b/packages/messaging/android/src/main/java/com/adobe/marketing/mobile/reactnative/messaging/RCTAEPMessagingConstants.java new file mode 100644 index 00000000..445c19c0 --- /dev/null +++ b/packages/messaging/android/src/main/java/com/adobe/marketing/mobile/reactnative/messaging/RCTAEPMessagingConstants.java @@ -0,0 +1,19 @@ +/* + Copyright 2025 Adobe. All rights reserved. + This file is licensed to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law + or agreed to in writing, software distributed under the License is + distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS OF + ANY KIND, either express or implied. See the License for the specific + language governing permissions and limitations under the License. + */ +package com.adobe.marketing.mobile.reactnative.messaging; + +class RCTAEPMessagingConstants { + static final String MESSAGE_ID_KEY = "messageId"; + static final String HANDLER_NAME_KEY = "handlerName"; + static final String CONTENT_KEY = "content"; + static final String ON_JAVASCRIPT_MESSAGE_EVENT = "onJavascriptMessage"; + } diff --git a/packages/messaging/android/src/main/java/com/adobe/marketing/mobile/reactnative/messaging/RCTAEPMessagingModule.java b/packages/messaging/android/src/main/java/com/adobe/marketing/mobile/reactnative/messaging/RCTAEPMessagingModule.java index 2d52139a..dad52af2 100644 --- a/packages/messaging/android/src/main/java/com/adobe/marketing/mobile/reactnative/messaging/RCTAEPMessagingModule.java +++ b/packages/messaging/android/src/main/java/com/adobe/marketing/mobile/reactnative/messaging/RCTAEPMessagingModule.java @@ -31,6 +31,7 @@ import com.adobe.marketing.mobile.messaging.Surface; import com.adobe.marketing.mobile.services.ServiceProvider; import com.adobe.marketing.mobile.services.ui.InAppMessage; +import com.adobe.marketing.mobile.services.ui.message.InAppMessageEventHandler; import com.adobe.marketing.mobile.services.ui.Presentable; import com.adobe.marketing.mobile.services.ui.PresentationDelegate; import com.facebook.react.bridge.Arguments; @@ -57,6 +58,7 @@ public final class RCTAEPMessagingModule private boolean shouldShowMessage = false; private CountDownLatch latch = new CountDownLatch(1); private Message latestMessage = null; + private final Map> presentableCache = new HashMap<>(); public RCTAEPMessagingModule(ReactApplicationContext reactContext) { super(reactContext); @@ -175,11 +177,29 @@ public void track(final String messageId, final String interaction, } } + @ReactMethod + public void handleJavascriptMessage(final String messageId, final String handlerName) { + Presentable presentable = presentableCache.get(messageId); + if (presentable == null || !(presentable.getPresentation() instanceof InAppMessage)) return; + + Presentable inAppMessagePresentable = (Presentable) presentable; + InAppMessageEventHandler eventHandler = inAppMessagePresentable.getPresentation().getEventHandler(); + + eventHandler.handleJavascriptMessage(handlerName, content -> { + Map params = new HashMap<>(); + params.put(RCTAEPMessagingConstants.MESSAGE_ID_KEY, messageId); + params.put(RCTAEPMessagingConstants.HANDLER_NAME_KEY, handlerName); + params.put(RCTAEPMessagingConstants.CONTENT_KEY, content); + emitEvent(RCTAEPMessagingConstants.ON_JAVASCRIPT_MESSAGE_EVENT, params); + }); + } + // Messaging Delegate functions @Override public void onShow(final Presentable presentable) { if (!(presentable.getPresentation() instanceof InAppMessage)) return; Message message = MessagingUtils.getMessageForPresentable((Presentable) presentable); + presentableCache.put(message.getId(), presentable); if (message != null) { Map data = convertMessageToMap(message); @@ -191,6 +211,7 @@ public void onShow(final Presentable presentable) { public void onDismiss(final Presentable presentable) { if (!(presentable.getPresentation() instanceof InAppMessage)) return; Message message = MessagingUtils.getMessageForPresentable((Presentable) presentable); + presentableCache.remove(message.getId()); if (message != null) { Map data = convertMessageToMap(message); diff --git a/packages/messaging/ios/src/RCTAEPMessaging.mm b/packages/messaging/ios/src/RCTAEPMessaging.mm index 6a227ed7..9c643eb2 100644 --- a/packages/messaging/ios/src/RCTAEPMessaging.mm +++ b/packages/messaging/ios/src/RCTAEPMessaging.mm @@ -55,4 +55,8 @@ @interface RCT_EXTERN_MODULE (RCTAEPMessaging, RCTEventEmitter) : (NSDictionary *)propositionMap contentCardMap : (NSDictionary *)contentCardMap); +RCT_EXTERN_METHOD(handleJavascriptMessage + : (NSString *)messageId handlerName + : (NSString *)handlerName) + @end diff --git a/packages/messaging/ios/src/RCTAEPMessaging.swift b/packages/messaging/ios/src/RCTAEPMessaging.swift index 86cf4cef..22596aa2 100644 --- a/packages/messaging/ios/src/RCTAEPMessaging.swift +++ b/packages/messaging/ios/src/RCTAEPMessaging.swift @@ -21,6 +21,7 @@ import WebKit @objc(RCTAEPMessaging) public class RCTAEPMessaging: RCTEventEmitter, MessagingDelegate { private var messageCache = [String: Message]() + private var jsHandlerMessageCache = [String: Message]() private var latestMessage: Message? = nil private let semaphore = DispatchSemaphore(value: 0) private var shouldSaveMessage = false @@ -249,11 +250,35 @@ public class RCTAEPMessaging: RCTEventEmitter, MessagingDelegate { } } + @objc + func handleJavascriptMessage( + _ messageId: String, + handlerName: String + ) { + guard let message = jsHandlerMessageCache[messageId] else { + print("[RCTAEPMessaging] handleJavascriptMessage: No message found in cache for messageId: \(messageId)") + return + } + + message.handleJavascriptMessage(handlerName) { [weak self] content in + self?.emitNativeEvent( + name: Constants.ON_JAVASCRIPT_MESSAGE_EVENT, + body: [ + Constants.MESSAGE_ID_KEY: messageId, + Constants.HANDLER_NAME_KEY: handlerName, + Constants.CONTENT_KEY: content ?? "" + ] + ) + } + } + // Messaging Delegate Methods public func onDismiss(message: Showable) { if let fullscreenMessage = message as? FullscreenMessage, let parentMessage = fullscreenMessage.parent { + jsHandlerMessageCache.removeValue(forKey: parentMessage.id) + emitNativeEvent( name: Constants.ON_DISMISS_EVENT, body: RCTAEPMessagingDataBridge.transformToMessage( @@ -267,6 +292,8 @@ public class RCTAEPMessaging: RCTEventEmitter, MessagingDelegate { if let fullscreenMessage = message as? FullscreenMessage, let message = fullscreenMessage.parent { + jsHandlerMessageCache[message.id] = message + emitNativeEvent( name: Constants.ON_SHOW_EVENT, body: RCTAEPMessagingDataBridge.transformToMessage(message: message) diff --git a/packages/messaging/ios/src/RCTAEPMessagingConstants.swift b/packages/messaging/ios/src/RCTAEPMessagingConstants.swift index 9b662d43..64f26aa0 100644 --- a/packages/messaging/ios/src/RCTAEPMessagingConstants.swift +++ b/packages/messaging/ios/src/RCTAEPMessagingConstants.swift @@ -16,7 +16,11 @@ class Constants { static let ON_SHOW_EVENT = "onShow" static let SHOULD_SHOW_MESSAGE_EVENT = "shouldShowMessage" static let URL_LOADED_EVENT = "urlLoaded" + static let ON_JAVASCRIPT_MESSAGE_EVENT = "onJavascriptMessage" static let SUPPORTED_EVENTS = [ - ON_DISMISS_EVENT, ON_SHOW_EVENT, SHOULD_SHOW_MESSAGE_EVENT, URL_LOADED_EVENT, + ON_DISMISS_EVENT, ON_SHOW_EVENT, SHOULD_SHOW_MESSAGE_EVENT, URL_LOADED_EVENT, ON_JAVASCRIPT_MESSAGE_EVENT ] + static let MESSAGE_ID_KEY = "messageId" + static let HANDLER_NAME_KEY = "handlerName" + static let CONTENT_KEY = "content" } diff --git a/packages/messaging/src/Messaging.ts b/packages/messaging/src/Messaging.ts index 25cecdbb..ab99d8aa 100644 --- a/packages/messaging/src/Messaging.ts +++ b/packages/messaging/src/Messaging.ts @@ -37,6 +37,7 @@ export interface NativeMessagingModule { updatePropositionsForSurfaces: (surfaces: string[]) => void; trackContentCardDisplay: (proposition: MessagingProposition, contentCard: ContentCard) => void; trackContentCardInteraction: (proposition: MessagingProposition, contentCard: ContentCard) => void; + handleJavascriptMessage: (messageId: string, handlerName: string) => void; } const RCTAEPMessaging: NativeModule & NativeMessagingModule = @@ -45,6 +46,18 @@ const RCTAEPMessaging: NativeModule & NativeMessagingModule = declare var messagingDelegate: MessagingDelegate; var messagingDelegate: MessagingDelegate; +// Registery to store callbacks for each message in handleJavascriptMessage +// Record - {messageId : {handlerName : callback}} +const jsMessageHandlers: Record void>> = {}; +const handleJSMessageEventEmitter = new NativeEventEmitter(RCTAEPMessaging); + +handleJSMessageEventEmitter.addListener('onJavascriptMessage', (event) => { + const {messageId, handlerName, content} = event; + if (jsMessageHandlers[messageId] && jsMessageHandlers[messageId][handlerName]) { + jsMessageHandlers[messageId][handlerName](content); + } +}); + class Messaging { /** * Returns the version of the AEPMessaging extension @@ -170,6 +183,24 @@ class Messaging { static updatePropositionsForSurfaces(surfaces: string[]) { RCTAEPMessaging.updatePropositionsForSurfaces(surfaces); } + + /** + * Registers a javascript interface for the provided handler name + * to the WebView associated with the InAppMessage presentation + * to handle Javascript messages. + * When the registered handlers are executed via the HTML + * the result will be passed back to the associated callback. + * @param messageId The id of the message to handle + * @param handlerName The name of the handler to handle + * @param callback The callback to handle the message + */ + static handleJavascriptMessage(messageId: string, handlerName: string, callback: (content: string) => void) { + if (!jsMessageHandlers[messageId]) { + jsMessageHandlers[messageId] = {}; + } + jsMessageHandlers[messageId][handlerName] = callback; + RCTAEPMessaging.handleJavascriptMessage(messageId, handlerName); + } } export default Messaging; diff --git a/packages/messaging/tutorials/In-App Messaging.md b/packages/messaging/tutorials/In-App Messaging.md new file mode 100644 index 00000000..4be499ad --- /dev/null +++ b/packages/messaging/tutorials/In-App Messaging.md @@ -0,0 +1,68 @@ +# Native handling of JavaScript events + +You can handle events from in-app message interactions natively within your application by completing the following steps: +- [Implement and assign a `Messaging Delegate`](#implement-and-assign-a-messaging-delegate) +- [Register a JavaScript handler for your In-App Message](#register-a-javascript-handler-for-your-in-app-message) +- [Post the JavaScript message from your In-App Message](#post-the-javascript-message-from-your-in-app-message) + +## Implement and assign a `Messaging Delegate` + +To register a JavaScript event handler with a Message object, you will first need to implement and set a MessagingDelegate. +Please read the [documentation](../README.md/#programmatically-control-the-display-of-in-app-messages) for more detailed instructions on implementing and using a MessagingDelegate. + +## Register a JavaScript handler for your In-App Message + +In the `onShow` function of `MessagingDelegate`, call `handleJavascriptMessage(messageId: string, handlerName: string, callback: (content: string) => void)` to register your handler. + +The name of the message you intend to pass from the JavaScript side should be specified in the first parameter. + +### Example + +```typescript +Messaging.setMessagingDelegate({ + onShow: msg => { + console.log('show', msg); + Messaging.handleJavascriptMessage( + msg.id, + 'myInappCallback', + (content) => { + console.log('Received webview content:', content); + } + ); + } + }); +``` + +## Post the JavaScript message from your In-App Message + +Now that the in-app message has been displayed, the final step is to post the JavaScript message. + +Continuing from the previous example, the developer is going to post the `myInappCallback` message from their HTML, which will in turn call the handler previously configured: + +```html + + + + + + + + +``` + +Note: (The above HTML is not representative of a valid in-app message, and is intended only to demonstrate how to post the JavaScript message). + +When the user clicks the button inside of this in-app message, the handler configured in the previous step will be called. The handler will send an Experience Event tracking the interaction, and print the following message to the console: + +```bash +JavaScript body passed to react native callback: callbacks are cool! +``` diff --git a/tests/jest/setup.ts b/tests/jest/setup.ts index 1cb76c82..a21f9adb 100644 --- a/tests/jest/setup.ts +++ b/tests/jest/setup.ts @@ -149,16 +149,14 @@ jest.doMock('react-native', () => { show: jest.fn(), dismiss: jest.fn(), track: jest.fn(), - handleJavascriptMessage: jest.fn( - () => new Promise((resolve) => resolve(new Object())) - ), clear: jest.fn(), updatePropositionsForSurfaces: jest.fn(), getPropositionsForSurfaces: jest.fn( () => new Promise((resolve) => resolve([])) ), trackContentCardDisplay: jest.fn(), - trackContentCardInteraction: jest.fn() + trackContentCardInteraction: jest.fn(), + handleJavascriptMessage: jest.fn(), }, AEPOptimize: { extensionVersion: jest.fn(