Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions apps/AEPSampleAppNewArchEnabled/app/MessagingView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Copy link

@ishwetansh ishwetansh Aug 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are we going to use console log functions directly
or there a global log method available for logging purpose?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We dont have a logging utility available, we use the console log functions directly in the test app.

msg.handleJavascriptMessage('myInappCallback', (content: string) => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: would recommend to use constants from a file

console.log('Received webview content in onShow:', content);
});
},
shouldShowMessage: () => true,
shouldSaveMessage: () => true,
urlLoaded: (url, message) => console.log(url, message),
Expand All @@ -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 () => {
Expand Down
17 changes: 15 additions & 2 deletions packages/messaging/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -509,5 +523,4 @@ Messaging.trackContentCardInteraction(proposition, contentCard);


## Tutorials
[Content Cards](./tutorials/ContentCards.md)

[Native handling of Javascript Events](./tutorials/In-App%20Messaging.md)
11 changes: 11 additions & 0 deletions packages/messaging/__tests__/MessagingTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,17 @@ describe('Messaging', () => {
expect(spy).toHaveBeenCalledWith(id);
});

it('handleJavascriptMessage is called', async () => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The unit tests seems a little incomplete.

can we add a test to verify that when a message is not found (due to a bad handlerName or expired cache), the sdk gracefully handles the error without emitting incorrect events or crashing.

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([
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<String, Presentable<?>> presentableCache = new HashMap<>();

public RCTAEPMessagingModule(ReactApplicationContext reactContext) {
super(reactContext);
Expand Down Expand Up @@ -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<InAppMessage> inAppMessagePresentable = (Presentable<InAppMessage>) presentable;
InAppMessageEventHandler eventHandler = inAppMessagePresentable.getPresentation().getEventHandler();

eventHandler.handleJavascriptMessage(handlerName, content -> {
Map<String, String> 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<InAppMessage>) presentable);
presentableCache.put(message.getId(), presentable);

if (message != null) {
Map<String, String> data =
convertMessageToMap(message);
Expand All @@ -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<InAppMessage>) presentable);
presentableCache.remove(message.getId());

if (message != null) {
Map<String, String> data =
convertMessageToMap(message);
Expand Down
5 changes: 5 additions & 0 deletions packages/messaging/ios/src/RCTAEPMessaging.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 25 additions & 0 deletions packages/messaging/ios/src/RCTAEPMessaging.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add consistency across platform for emitting events ?
in android method name is emitEvent and ios has emitNativeEvent

https://github.com/adobe/aepsdk-react-native/pull/519/files#diff-85f91ba765b073dd0256d2724430807e4320aff1adb71dfb6d6f78cc209958f0R193

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the emitEvent and emitNativeEvent functions are existing helper methods from before. Lets keep this PR focused on handleJavascriptMessage related changes. Refactoring can be taken as a separate task if needed.

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

/**
Expand Down Expand Up @@ -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(
Expand All @@ -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)
Expand Down
6 changes: 5 additions & 1 deletion packages/messaging/ios/src/RCTAEPMessagingConstants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
24 changes: 13 additions & 11 deletions packages/messaging/src/Messaging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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))
);
}

Expand Down
52 changes: 51 additions & 1 deletion packages/messaging/src/models/Message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, Record<string, (content: string) => void>> = {};
Copy link

@ishwetansh ishwetansh Aug 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every time we show a new in-app message, hanndleJavascriptMessage() adds another entry to the jsMessageHandlers object. But when the message is dismissed, the screen is unmounted, or the message is never shown again, those handlers stay in memory instead of being removed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added clearJavascriptMessageHandlers method for clearing the memory.
@namArora3112 please have another look

const handleJSMessageEventEmitter = new NativeEventEmitter(RCTAEPMessaging);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we create a type for this callback as this is being used in multiple places
type JsMessageHandler = (content: string) => void;

// 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;
Expand Down Expand Up @@ -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
Copy link

@ishwetansh ishwetansh Aug 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we add checks for the handler
for example something like this
if (!handlerName) throw new Error("handlerName required"); if (typeof handler !== "function") throw new Error("handler must be a function");

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;
Loading