diff --git a/apps/AEPSampleAppNewArchEnabled/app/MessagingView.tsx b/apps/AEPSampleAppNewArchEnabled/app/MessagingView.tsx index a53109f9..4d82cdc9 100644 --- a/apps/AEPSampleAppNewArchEnabled/app/MessagingView.tsx +++ b/apps/AEPSampleAppNewArchEnabled/app/MessagingView.tsx @@ -44,7 +44,12 @@ const refreshInAppMessages = () => { const setMessagingDelegate = () => { Messaging.setMessagingDelegate({ onDismiss: msg => console.log('dismissed!', msg), - onShow: msg => console.log('show', msg), + onShow: msg => { + console.log('show', msg); + msg.handleJavascriptMessage('myInappCallback', (content: string) => { + console.log('Received webview content in onShow:', content); + }); + }, shouldShowMessage: () => true, shouldSaveMessage: () => true, urlLoaded: (url, message) => console.log(url, message), @@ -53,10 +58,10 @@ const setMessagingDelegate = () => { }; const getPropositionsForSurfaces = async () => { const messages = await Messaging.getPropositionsForSurfaces(SURFACES); - console.log(JSON.stringify(messages)); + console.log('getPropositionsForSurfaces', JSON.stringify(messages)); }; const trackAction = async () => { - MobileCore.trackAction('tuesday', {full: true}); + MobileCore.trackAction('iamjs', {full: true}); }; const updatePropositionsForSurfaces = async () => { diff --git a/packages/messaging/README.md b/packages/messaging/README.md index 0439c43a..58fca58d 100644 --- a/packages/messaging/README.md +++ b/packages/messaging/README.md @@ -387,6 +387,20 @@ var message: Message; message.clear(); ``` +### 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 handler. + +**Syntax** + +```typescript +handleJavascriptMessage(handlerName: string, handler: (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. + ## Programmatically control the display of in-app messages App developers can now create a type `MessagingDelegate` in order to be alerted when specific events occur during the lifecycle of an in-app message. @@ -509,5 +523,4 @@ Messaging.trackContentCardInteraction(proposition, contentCard); ## Tutorials -[Content Cards](./tutorials/ContentCards.md) - +[Native handling of Javascript Events](./tutorials/In-App%20Messaging.md) diff --git a/packages/messaging/__tests__/MessagingTests.ts b/packages/messaging/__tests__/MessagingTests.ts index f0ed77cd..c27edfe4 100644 --- a/packages/messaging/__tests__/MessagingTests.ts +++ b/packages/messaging/__tests__/MessagingTests.ts @@ -87,6 +87,17 @@ describe('Messaging', () => { expect(spy).toHaveBeenCalledWith(id); }); + it('handleJavascriptMessage is called', async () => { + const spy = jest.spyOn(NativeModules.AEPMessaging, 'handleJavascriptMessage'); + let id = 'id'; + let autoTrack = true; + let message = new Message({id, autoTrack}); + let handlerName = 'handlerName'; + let handler = jest.fn(); + await message.handleJavascriptMessage(handlerName, handler); + expect(spy).toHaveBeenCalledWith(id, handlerName); + }); + it('should call updatePropositionsForSurfaces', async () => { const spy = jest.spyOn(NativeModules.AEPMessaging, 'updatePropositionsForSurfaces'); await Messaging.updatePropositionsForSurfaces([ 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 686a5fb3..815cdbc8 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; @@ -99,6 +100,7 @@ private String extractActivityId(Proposition proposition) { 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); @@ -235,11 +237,33 @@ 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)) { + Log.w(TAG, "handleJavascriptMessage: No presentable found for messageId: " + messageId); + 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); @@ -251,6 +275,8 @@ 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 76580fd0..8c85ca9b 100644 --- a/packages/messaging/ios/src/RCTAEPMessaging.mm +++ b/packages/messaging/ios/src/RCTAEPMessaging.mm @@ -55,6 +55,11 @@ @interface RCT_EXTERN_MODULE (RCTAEPMessaging, RCTEventEmitter) : (NSDictionary *)propositionMap contentCardMap : (NSDictionary *)contentCardMap); + +RCT_EXTERN_METHOD(handleJavascriptMessage + : (NSString *)messageId handlerName + : (NSString *)handlerName) + RCT_EXTERN_METHOD(trackPropositionItem : (NSString *)uuid interaction : (NSString * _Nullable)interaction eventType diff --git a/packages/messaging/ios/src/RCTAEPMessaging.swift b/packages/messaging/ios/src/RCTAEPMessaging.swift index 3629a1a3..ee66f538 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 @@ -263,6 +264,28 @@ 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 ?? "" + ] + ) + } + } + /// MARK: - Unified PropositionItem Tracking Methods /** @@ -329,6 +352,7 @@ public class RCTAEPMessaging: RCTEventEmitter, MessagingDelegate { if let fullscreenMessage = message as? FullscreenMessage, let parentMessage = fullscreenMessage.parent { + jsHandlerMessageCache.removeValue(forKey: parentMessage.id) emitNativeEvent( name: Constants.ON_DISMISS_EVENT, body: RCTAEPMessagingDataBridge.transformToMessage( @@ -342,6 +366,7 @@ 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 34100d75..8a171efe 100644 --- a/packages/messaging/src/Messaging.ts +++ b/packages/messaging/src/Messaging.ts @@ -129,31 +129,33 @@ class Messaging { const eventEmitter = new NativeEventEmitter(RCTAEPMessaging); - eventEmitter.addListener('onShow', (message) => - messagingDelegate?.onShow?.(message) + eventEmitter.addListener('onShow', (message: Message) => + messagingDelegate?.onShow?.(new Message(message)) ); - eventEmitter.addListener('onDismiss', (message) => { - messagingDelegate?.onDismiss?.(message); + eventEmitter.addListener('onDismiss', (message: Message) => { + message._clearJavascriptMessageHandlers(); + messagingDelegate?.onDismiss?.(new Message(message)); }); - eventEmitter.addListener('shouldShowMessage', (message) => { + eventEmitter.addListener('shouldShowMessage', (message: Message) => { + const messageInstance = new Message(message); const shouldShowMessage = - messagingDelegate?.shouldShowMessage?.(message) ?? true; + messagingDelegate?.shouldShowMessage?.(messageInstance) ?? true; const shouldSaveMessage = - messagingDelegate?.shouldSaveMessage?.(message) ?? false; + messagingDelegate?.shouldSaveMessage?.(messageInstance) ?? false; RCTAEPMessaging.setMessageSettings(shouldShowMessage, shouldSaveMessage); }); if (Platform.OS === 'ios') { - eventEmitter.addListener('urlLoaded', (event) => - messagingDelegate?.urlLoaded?.(event.url, event.message) + eventEmitter.addListener('urlLoaded', (event: {url: string, message: Message}) => + messagingDelegate?.urlLoaded?.(event.url, new Message(event.message)) ); } if (Platform.OS === 'android') { - eventEmitter.addListener('onContentLoaded', (event) => - messagingDelegate?.onContentLoaded?.(event.message) + eventEmitter.addListener('onContentLoaded', (event: {message: Message}) => + messagingDelegate?.onContentLoaded?.(new Message(event.message)) ); } diff --git a/packages/messaging/src/models/Message.ts b/packages/messaging/src/models/Message.ts index bee749d3..458c7c69 100644 --- a/packages/messaging/src/models/Message.ts +++ b/packages/messaging/src/models/Message.ts @@ -10,9 +10,23 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ -import { NativeModules } from 'react-native'; +import { NativeEventEmitter, NativeModules } from 'react-native'; + const RCTAEPMessaging = NativeModules.AEPMessaging; +// Registery to store inAppMessage callbacks for each message in Message.handleJavascriptMessage +// Record - {messageId : {handlerName : callback}} +const jsMessageHandlers: Record void>> = {}; +const handleJSMessageEventEmitter = new NativeEventEmitter(RCTAEPMessaging); + +// invokes the callback registered in Message.handleJavascriptMessage with the content received from the inAppMessage webview +handleJSMessageEventEmitter.addListener('onJavascriptMessage', (event) => { + const {messageId, handlerName, content} = event; + if (jsMessageHandlers[messageId] && jsMessageHandlers[messageId][handlerName]) { + jsMessageHandlers[messageId][handlerName](content); + } +}); + class Message { id: string; autoTrack: boolean; @@ -67,6 +81,42 @@ class Message { clear() { RCTAEPMessaging.clear(this.id); } + + /** + * Adds a handler for named JavaScript messages sent from the message's WebView. + * The parameter passed to handler will contain the body of the message passed from the WebView's JavaScript. + * @param {string} handlerName: The name of the message that should be handled by the handler + * @param {function} handler: The method or closure to be called with the body of the message created in the Message's JavaScript + */ + handleJavascriptMessage(handlerName: string, handler: (content: string) => void) { + // Validate parameters + if (!handlerName) { + console.warn('[AEP Messaging] handleJavascriptMessage: handlerName is required'); + return; + } + + if (typeof handler !== 'function') { + console.warn('[AEP Messaging] handleJavascriptMessage: handler must be a function'); + return; + } + + // cache the callback + if (!jsMessageHandlers[this.id]) { + jsMessageHandlers[this.id] = {}; + } + jsMessageHandlers[this.id][handlerName] = handler; + RCTAEPMessaging.handleJavascriptMessage(this.id, handlerName); + } + + /** + * @internal - For internal use only. + * Clears all the javascript message handlers for the message. + * This function must be called if the callbacks registered in handleJavascriptMessage are no longer needed. + * Failure to call this function may lead to memory leaks. + */ + _clearJavascriptMessageHandlers() { + delete jsMessageHandlers[this.id]; + } } export default Message; diff --git a/packages/messaging/tutorials/In-App Messaging.md b/packages/messaging/tutorials/In-App Messaging.md new file mode 100644 index 00000000..4ab8ca78 --- /dev/null +++ b/packages/messaging/tutorials/In-App Messaging.md @@ -0,0 +1,67 @@ +# 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(handlerName: string, handler: (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); + msg.handleJavascriptMessage( + '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! +```