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: 10 additions & 1 deletion apps/AEPSampleAppNewArchEnabled/app/MessagingView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
16 changes: 15 additions & 1 deletion packages/messaging/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -459,4 +473,4 @@ Messaging.trackContentCardInteraction(proposition, contentCard);

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

[In App Messaging](./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 @@ -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);
});
});
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 All @@ -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<String, Presentable<?>> presentableCache = new HashMap<>();

public RCTAEPMessagingModule(ReactApplicationContext reactContext) {
super(reactContext);
Expand Down Expand Up @@ -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<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 @@ -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<InAppMessage>) presentable);
presentableCache.remove(message.getId());
if (message != null) {
Map<String, String> data =
convertMessageToMap(message);
Expand Down
4 changes: 4 additions & 0 deletions packages/messaging/ios/src/RCTAEPMessaging.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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
27 changes: 27 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 @@ -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(
Expand All @@ -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)
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"
}
31 changes: 31 additions & 0 deletions packages/messaging/src/Messaging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand All @@ -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<string, Record<string, (content: string) => 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
Expand Down Expand Up @@ -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;
68 changes: 68 additions & 0 deletions packages/messaging/tutorials/In-App Messaging.md
Original file line number Diff line number Diff line change
@@ -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
<html>
<head>
<script type="text/javascript">
function callNative(action) {
try {
// the name of the message handler is the same name that must be registered in react native code.
// in this case the message name is "myInappCallback"
webkit.messageHandlers.myInappCallback.postMessage(action);
} catch(err) {
console.log('The native context does not exist yet'); }
}
</script>
</head>
<body>
<button onclick="callNative('callbacks are cool!')">Native callback!</button>
</body>
</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!
```
6 changes: 2 additions & 4 deletions tests/jest/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down