Skip to content

Commit 78a650f

Browse files
add handleJavascriptMessage in AEPMessaging (#519)
* Messaging - handle javascript message * add js event listener and ios * typecaste Message object * address PR comments * refactor log statements * address PR comments * revert env file id changes * clear js handler when onDismiss is called
1 parent 30ffaaf commit 78a650f

File tree

11 files changed

+245
-18
lines changed

11 files changed

+245
-18
lines changed

apps/AEPSampleAppNewArchEnabled/app/MessagingView.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,12 @@ const refreshInAppMessages = () => {
4444
const setMessagingDelegate = () => {
4545
Messaging.setMessagingDelegate({
4646
onDismiss: msg => console.log('dismissed!', msg),
47-
onShow: msg => console.log('show', msg),
47+
onShow: msg => {
48+
console.log('show', msg);
49+
msg.handleJavascriptMessage('myInappCallback', (content: string) => {
50+
console.log('Received webview content in onShow:', content);
51+
});
52+
},
4853
shouldShowMessage: () => true,
4954
shouldSaveMessage: () => true,
5055
urlLoaded: (url, message) => console.log(url, message),
@@ -53,10 +58,10 @@ const setMessagingDelegate = () => {
5358
};
5459
const getPropositionsForSurfaces = async () => {
5560
const messages = await Messaging.getPropositionsForSurfaces(SURFACES);
56-
console.log(JSON.stringify(messages));
61+
console.log('getPropositionsForSurfaces', JSON.stringify(messages));
5762
};
5863
const trackAction = async () => {
59-
MobileCore.trackAction('tuesday', {full: true});
64+
MobileCore.trackAction('iamjs', {full: true});
6065
};
6166

6267
const updatePropositionsForSurfaces = async () => {

packages/messaging/README.md

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,20 @@ var message: Message;
387387
message.clear();
388388
```
389389
390+
### handleJavascriptMessage
391+
392+
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.
393+
394+
**Syntax**
395+
396+
```typescript
397+
handleJavascriptMessage(handlerName: string, handler: (content: string) => void);
398+
```
399+
400+
**Example**
401+
402+
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.
403+
390404
## Programmatically control the display of in-app messages
391405
392406
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);
509523
510524
511525
## Tutorials
512-
[Content Cards](./tutorials/ContentCards.md)
513-
526+
[Native handling of Javascript Events](./tutorials/In-App%20Messaging.md)

packages/messaging/__tests__/MessagingTests.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,17 @@ describe('Messaging', () => {
8787
expect(spy).toHaveBeenCalledWith(id);
8888
});
8989

90+
it('handleJavascriptMessage is called', async () => {
91+
const spy = jest.spyOn(NativeModules.AEPMessaging, 'handleJavascriptMessage');
92+
let id = 'id';
93+
let autoTrack = true;
94+
let message = new Message({id, autoTrack});
95+
let handlerName = 'handlerName';
96+
let handler = jest.fn();
97+
await message.handleJavascriptMessage(handlerName, handler);
98+
expect(spy).toHaveBeenCalledWith(id, handlerName);
99+
});
100+
90101
it('should call updatePropositionsForSurfaces', async () => {
91102
const spy = jest.spyOn(NativeModules.AEPMessaging, 'updatePropositionsForSurfaces');
92103
await Messaging.updatePropositionsForSurfaces([
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
Copyright 2025 Adobe. All rights reserved.
3+
This file is licensed to you under the Apache License, Version 2.0 (the
4+
"License"); you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
7+
or agreed to in writing, software distributed under the License is
8+
distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS OF
9+
ANY KIND, either express or implied. See the License for the specific
10+
language governing permissions and limitations under the License.
11+
*/
12+
package com.adobe.marketing.mobile.reactnative.messaging;
13+
14+
class RCTAEPMessagingConstants {
15+
static final String MESSAGE_ID_KEY = "messageId";
16+
static final String HANDLER_NAME_KEY = "handlerName";
17+
static final String CONTENT_KEY = "content";
18+
static final String ON_JAVASCRIPT_MESSAGE_EVENT = "onJavascriptMessage";
19+
}

packages/messaging/android/src/main/java/com/adobe/marketing/mobile/reactnative/messaging/RCTAEPMessagingModule.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import com.adobe.marketing.mobile.messaging.Surface;
3232
import com.adobe.marketing.mobile.services.ServiceProvider;
3333
import com.adobe.marketing.mobile.services.ui.InAppMessage;
34+
import com.adobe.marketing.mobile.services.ui.message.InAppMessageEventHandler;
3435
import com.adobe.marketing.mobile.services.ui.Presentable;
3536
import com.adobe.marketing.mobile.services.ui.PresentationDelegate;
3637
import com.facebook.react.bridge.Arguments;
@@ -99,6 +100,7 @@ private String extractActivityId(Proposition proposition) {
99100
private boolean shouldShowMessage = false;
100101
private CountDownLatch latch = new CountDownLatch(1);
101102
private Message latestMessage = null;
103+
private final Map<String, Presentable<?>> presentableCache = new HashMap<>();
102104

103105
public RCTAEPMessagingModule(ReactApplicationContext reactContext) {
104106
super(reactContext);
@@ -235,11 +237,33 @@ public void track(final String messageId, final String interaction,
235237
}
236238
}
237239

240+
@ReactMethod
241+
public void handleJavascriptMessage(final String messageId, final String handlerName) {
242+
Presentable<?> presentable = presentableCache.get(messageId);
243+
if (presentable == null || !(presentable.getPresentation() instanceof InAppMessage)) {
244+
Log.w(TAG, "handleJavascriptMessage: No presentable found for messageId: " + messageId);
245+
return;
246+
}
247+
248+
Presentable<InAppMessage> inAppMessagePresentable = (Presentable<InAppMessage>) presentable;
249+
InAppMessageEventHandler eventHandler = inAppMessagePresentable.getPresentation().getEventHandler();
250+
251+
eventHandler.handleJavascriptMessage(handlerName, content -> {
252+
Map<String, String> params = new HashMap<>();
253+
params.put(RCTAEPMessagingConstants.MESSAGE_ID_KEY, messageId);
254+
params.put(RCTAEPMessagingConstants.HANDLER_NAME_KEY, handlerName);
255+
params.put(RCTAEPMessagingConstants.CONTENT_KEY, content);
256+
emitEvent(RCTAEPMessagingConstants.ON_JAVASCRIPT_MESSAGE_EVENT, params);
257+
});
258+
}
259+
238260
// Messaging Delegate functions
239261
@Override
240262
public void onShow(final Presentable<?> presentable) {
241263
if (!(presentable.getPresentation() instanceof InAppMessage)) return;
242264
Message message = MessagingUtils.getMessageForPresentable((Presentable<InAppMessage>) presentable);
265+
presentableCache.put(message.getId(), presentable);
266+
243267
if (message != null) {
244268
Map<String, String> data =
245269
convertMessageToMap(message);
@@ -251,6 +275,8 @@ public void onShow(final Presentable<?> presentable) {
251275
public void onDismiss(final Presentable<?> presentable) {
252276
if (!(presentable.getPresentation() instanceof InAppMessage)) return;
253277
Message message = MessagingUtils.getMessageForPresentable((Presentable<InAppMessage>) presentable);
278+
presentableCache.remove(message.getId());
279+
254280
if (message != null) {
255281
Map<String, String> data =
256282
convertMessageToMap(message);

packages/messaging/ios/src/RCTAEPMessaging.mm

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ @interface RCT_EXTERN_MODULE (RCTAEPMessaging, RCTEventEmitter)
5555
: (NSDictionary *)propositionMap contentCardMap
5656
: (NSDictionary *)contentCardMap);
5757

58+
59+
RCT_EXTERN_METHOD(handleJavascriptMessage
60+
: (NSString *)messageId handlerName
61+
: (NSString *)handlerName)
62+
5863
RCT_EXTERN_METHOD(trackPropositionItem
5964
: (NSString *)uuid interaction
6065
: (NSString * _Nullable)interaction eventType

packages/messaging/ios/src/RCTAEPMessaging.swift

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import WebKit
2121
@objc(RCTAEPMessaging)
2222
public class RCTAEPMessaging: RCTEventEmitter, MessagingDelegate {
2323
private var messageCache = [String: Message]()
24+
private var jsHandlerMessageCache = [String: Message]()
2425
private var latestMessage: Message? = nil
2526
private let semaphore = DispatchSemaphore(value: 0)
2627
private var shouldSaveMessage = false
@@ -263,6 +264,28 @@ public class RCTAEPMessaging: RCTEventEmitter, MessagingDelegate {
263264
}
264265
}
265266

267+
@objc
268+
func handleJavascriptMessage(
269+
_ messageId: String,
270+
handlerName: String
271+
) {
272+
guard let message = jsHandlerMessageCache[messageId] else {
273+
print("[RCTAEPMessaging] handleJavascriptMessage: No message found in cache for messageId: \(messageId)")
274+
return
275+
}
276+
277+
message.handleJavascriptMessage(handlerName) { [weak self] content in
278+
self?.emitNativeEvent(
279+
name: Constants.ON_JAVASCRIPT_MESSAGE_EVENT,
280+
body: [
281+
Constants.MESSAGE_ID_KEY: messageId,
282+
Constants.HANDLER_NAME_KEY: handlerName,
283+
Constants.CONTENT_KEY: content ?? ""
284+
]
285+
)
286+
}
287+
}
288+
266289
/// MARK: - Unified PropositionItem Tracking Methods
267290

268291
/**
@@ -329,6 +352,7 @@ public class RCTAEPMessaging: RCTEventEmitter, MessagingDelegate {
329352
if let fullscreenMessage = message as? FullscreenMessage,
330353
let parentMessage = fullscreenMessage.parent
331354
{
355+
jsHandlerMessageCache.removeValue(forKey: parentMessage.id)
332356
emitNativeEvent(
333357
name: Constants.ON_DISMISS_EVENT,
334358
body: RCTAEPMessagingDataBridge.transformToMessage(
@@ -342,6 +366,7 @@ public class RCTAEPMessaging: RCTEventEmitter, MessagingDelegate {
342366
if let fullscreenMessage = message as? FullscreenMessage,
343367
let message = fullscreenMessage.parent
344368
{
369+
jsHandlerMessageCache[message.id] = message
345370
emitNativeEvent(
346371
name: Constants.ON_SHOW_EVENT,
347372
body: RCTAEPMessagingDataBridge.transformToMessage(message: message)

packages/messaging/ios/src/RCTAEPMessagingConstants.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@ class Constants {
1616
static let ON_SHOW_EVENT = "onShow"
1717
static let SHOULD_SHOW_MESSAGE_EVENT = "shouldShowMessage"
1818
static let URL_LOADED_EVENT = "urlLoaded"
19+
static let ON_JAVASCRIPT_MESSAGE_EVENT = "onJavascriptMessage"
1920
static let SUPPORTED_EVENTS = [
20-
ON_DISMISS_EVENT, ON_SHOW_EVENT, SHOULD_SHOW_MESSAGE_EVENT, URL_LOADED_EVENT,
21+
ON_DISMISS_EVENT, ON_SHOW_EVENT, SHOULD_SHOW_MESSAGE_EVENT, URL_LOADED_EVENT, ON_JAVASCRIPT_MESSAGE_EVENT
2122
]
23+
static let MESSAGE_ID_KEY = "messageId"
24+
static let HANDLER_NAME_KEY = "handlerName"
25+
static let CONTENT_KEY = "content"
2226
}

packages/messaging/src/Messaging.ts

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -129,31 +129,33 @@ class Messaging {
129129

130130
const eventEmitter = new NativeEventEmitter(RCTAEPMessaging);
131131

132-
eventEmitter.addListener('onShow', (message) =>
133-
messagingDelegate?.onShow?.(message)
132+
eventEmitter.addListener('onShow', (message: Message) =>
133+
messagingDelegate?.onShow?.(new Message(message))
134134
);
135135

136-
eventEmitter.addListener('onDismiss', (message) => {
137-
messagingDelegate?.onDismiss?.(message);
136+
eventEmitter.addListener('onDismiss', (message: Message) => {
137+
message._clearJavascriptMessageHandlers();
138+
messagingDelegate?.onDismiss?.(new Message(message));
138139
});
139140

140-
eventEmitter.addListener('shouldShowMessage', (message) => {
141+
eventEmitter.addListener('shouldShowMessage', (message: Message) => {
142+
const messageInstance = new Message(message);
141143
const shouldShowMessage =
142-
messagingDelegate?.shouldShowMessage?.(message) ?? true;
144+
messagingDelegate?.shouldShowMessage?.(messageInstance) ?? true;
143145
const shouldSaveMessage =
144-
messagingDelegate?.shouldSaveMessage?.(message) ?? false;
146+
messagingDelegate?.shouldSaveMessage?.(messageInstance) ?? false;
145147
RCTAEPMessaging.setMessageSettings(shouldShowMessage, shouldSaveMessage);
146148
});
147149

148150
if (Platform.OS === 'ios') {
149-
eventEmitter.addListener('urlLoaded', (event) =>
150-
messagingDelegate?.urlLoaded?.(event.url, event.message)
151+
eventEmitter.addListener('urlLoaded', (event: {url: string, message: Message}) =>
152+
messagingDelegate?.urlLoaded?.(event.url, new Message(event.message))
151153
);
152154
}
153155

154156
if (Platform.OS === 'android') {
155-
eventEmitter.addListener('onContentLoaded', (event) =>
156-
messagingDelegate?.onContentLoaded?.(event.message)
157+
eventEmitter.addListener('onContentLoaded', (event: {message: Message}) =>
158+
messagingDelegate?.onContentLoaded?.(new Message(event.message))
157159
);
158160
}
159161

packages/messaging/src/models/Message.ts

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,23 @@ OF ANY KIND, either express or implied. See the License for the specific languag
1010
governing permissions and limitations under the License.
1111
*/
1212

13-
import { NativeModules } from 'react-native';
13+
import { NativeEventEmitter, NativeModules } from 'react-native';
14+
1415
const RCTAEPMessaging = NativeModules.AEPMessaging;
1516

17+
// Registery to store inAppMessage callbacks for each message in Message.handleJavascriptMessage
18+
// Record - {messageId : {handlerName : callback}}
19+
const jsMessageHandlers: Record<string, Record<string, (content: string) => void>> = {};
20+
const handleJSMessageEventEmitter = new NativeEventEmitter(RCTAEPMessaging);
21+
22+
// invokes the callback registered in Message.handleJavascriptMessage with the content received from the inAppMessage webview
23+
handleJSMessageEventEmitter.addListener('onJavascriptMessage', (event) => {
24+
const {messageId, handlerName, content} = event;
25+
if (jsMessageHandlers[messageId] && jsMessageHandlers[messageId][handlerName]) {
26+
jsMessageHandlers[messageId][handlerName](content);
27+
}
28+
});
29+
1630
class Message {
1731
id: string;
1832
autoTrack: boolean;
@@ -67,6 +81,42 @@ class Message {
6781
clear() {
6882
RCTAEPMessaging.clear(this.id);
6983
}
84+
85+
/**
86+
* Adds a handler for named JavaScript messages sent from the message's WebView.
87+
* The parameter passed to handler will contain the body of the message passed from the WebView's JavaScript.
88+
* @param {string} handlerName: The name of the message that should be handled by the handler
89+
* @param {function} handler: The method or closure to be called with the body of the message created in the Message's JavaScript
90+
*/
91+
handleJavascriptMessage(handlerName: string, handler: (content: string) => void) {
92+
// Validate parameters
93+
if (!handlerName) {
94+
console.warn('[AEP Messaging] handleJavascriptMessage: handlerName is required');
95+
return;
96+
}
97+
98+
if (typeof handler !== 'function') {
99+
console.warn('[AEP Messaging] handleJavascriptMessage: handler must be a function');
100+
return;
101+
}
102+
103+
// cache the callback
104+
if (!jsMessageHandlers[this.id]) {
105+
jsMessageHandlers[this.id] = {};
106+
}
107+
jsMessageHandlers[this.id][handlerName] = handler;
108+
RCTAEPMessaging.handleJavascriptMessage(this.id, handlerName);
109+
}
110+
111+
/**
112+
* @internal - For internal use only.
113+
* Clears all the javascript message handlers for the message.
114+
* This function must be called if the callbacks registered in handleJavascriptMessage are no longer needed.
115+
* Failure to call this function may lead to memory leaks.
116+
*/
117+
_clearJavascriptMessageHandlers() {
118+
delete jsMessageHandlers[this.id];
119+
}
70120
}
71121

72122
export default Message;

0 commit comments

Comments
 (0)