diff --git a/apps/AEPSampleAppNewArchEnabled/app/MessagingView.tsx b/apps/AEPSampleAppNewArchEnabled/app/MessagingView.tsx
index 270d5adbf..4d82cdc9d 100644
--- a/apps/AEPSampleAppNewArchEnabled/app/MessagingView.tsx
+++ b/apps/AEPSampleAppNewArchEnabled/app/MessagingView.tsx
@@ -13,13 +13,24 @@ governing permissions and limitations under the License.
import React from 'react';
import {Button, Text, View, ScrollView} from 'react-native';
import {MobileCore} from '@adobe/react-native-aepcore';
-import {Messaging, PersonalizationSchema} from '@adobe/react-native-aepmessaging'
+import {
+ Messaging,
+ PersonalizationSchema,
+ MessagingEdgeEventType,
+ PropositionItem,
+ Message,
+ ContentCard,
+ HTMLProposition,
+ JSONPropositionItem
+} from '@adobe/react-native-aepmessaging'
+import { MessagingProposition } from '@adobe/react-native-aepmessaging';
import styles from '../styles/styles';
import { useRouter } from 'expo-router';
const SURFACES = ['android-cbe-preview', 'cbe/json', 'android-cc'];
const SURFACES_WITH_CONTENT_CARDS = ['android-cc'];
+
const messagingExtensionVersion = async () => {
const version = await Messaging.extensionVersion();
console.log(`AdobeExperienceSDK: Messaging version: ${version}`);
@@ -33,21 +44,24 @@ 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),
});
console.log('messaging delegate set');
};
-
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 () => {
@@ -75,7 +89,8 @@ const trackContentCardInteraction = async () => {
for (const proposition of propositions) {
for (const propositionItem of proposition.items) {
if (propositionItem.schema === PersonalizationSchema.CONTENT_CARD) {
- Messaging.trackContentCardInteraction(proposition, propositionItem);
+ // Cast to ContentCard for the legacy tracking method
+ Messaging.trackContentCardInteraction(proposition, propositionItem as any);
console.log('trackContentCardInteraction', proposition, propositionItem);
}
}
@@ -93,7 +108,8 @@ const trackContentCardDisplay = async () => {
for (const proposition of propositions) {
for (const propositionItem of proposition.items) {
if (propositionItem.schema === PersonalizationSchema.CONTENT_CARD) {
- Messaging.trackContentCardDisplay(proposition, propositionItem);
+ // Cast to ContentCard for the legacy tracking method
+ Messaging.trackContentCardDisplay(proposition, propositionItem as any);
console.log('trackContentCardDisplay', proposition, propositionItem);
}
}
@@ -101,6 +117,26 @@ const trackContentCardDisplay = async () => {
}
}
+
+// Method demonstrating unified tracking using PropositionItem methods
+const unifiedTrackingExample = async () => {
+ const messages = await Messaging.getPropositionsForSurfaces(SURFACES);
+ for (const surface of SURFACES) {
+ const propositions = messages[surface] || [];
+
+ for (const proposition of propositions) {
+ const propositionWrapper = new MessagingProposition(proposition);
+ if (propositionWrapper.items.length > 0) {
+ const propositionItem = propositionWrapper.items[0];
+ propositionItem.track(MessagingEdgeEventType.DISPLAY);
+ propositionItem.track('content_card_clicked', MessagingEdgeEventType.INTERACT, null);
+ }
+ }
+ }
+}
+
+
+
function MessagingView() {
const router = useRouter();
@@ -125,6 +161,7 @@ function MessagingView() {
+
);
diff --git a/packages/messaging/README.md b/packages/messaging/README.md
index 1c05aa3de..58fca58d7 100644
--- a/packages/messaging/README.md
+++ b/packages/messaging/README.md
@@ -201,6 +201,53 @@ for (let item in proposition.items) {
}
```
+
+### PropositionItem.track
+
+A unified tracking API is available for any proposition item (Content Cards, HTML, JSON, code-based items). You can use the same track() method regardless of content type, making your code more consistent and maintainable.
+
+#### Using PropositionItem.track (recommended)
+
+**Syntax**
+
+```javascript
+// Track display event
+propositionItem.track(MessagingEdgeEventType.DISPLAY);
+
+// Track interaction with custom data + event + optional tokens
+propositionItem.track(
+ interaction /* string | null */,
+ MessagingEdgeEventType.INTERACT /* enum value */,
+ [/* tokens */] /* string[] | null */
+);
+```
+
+When using `getPropositionsForSurfaces`, the returned objects can be wrapped with `MessagingProposition` to get typed items and convenient tracking via `PropositionItem.track(...)`.
+
+```javascript
+import { Messaging, MessagingProposition, MessagingEdgeEventType } from '@adobe/react-native-aepmessaging';
+
+const SURFACES = ['mobileapp://my-surface'];
+const messages = await Messaging.getPropositionsForSurfaces(SURFACES);
+
+for (const surface of SURFACES) {
+ const propositions = messages[surface] || [];
+
+ for (const proposition of propositions) {
+ const msgProp = new MessagingProposition(proposition);
+
+ if (msgProp.items.length > 0) {
+ const propositionItem = msgProp.items[0];
+
+ // Track interaction with custom data
+ propositionItem.track('content_card_clicked', MessagingEdgeEventType.INTERACT, null);
+ // Track with tokens for sub-item tracking
+ propositionItem.track('button_click', MessagingEdgeEventType.INTERACT, ['token-1', 'token-2']);
+ }
+ }
+}
+```
+
### getLatestMessage
Retrieves the most recently displayed message object
@@ -340,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.
@@ -440,6 +501,8 @@ function otherWorkflowFinished() {
### trackContentCardDisplay
+Deprecated: Use `PropositionItem.track(...)` instead. This API will be removed in a future release.
+
Tracks a Display interaction with the given ContentCard
**Syntax**
@@ -449,6 +512,8 @@ Messaging.trackContentCardDisplay(proposition, contentCard);
### trackContentCardInteraction
+Deprecated: Use `PropositionItem.track(...)` instead. This API will be removed in a future release.
+
Tracks a Click interaction with the given ContentCard
**Syntax**
@@ -458,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 f0ed77cd8..c27edfe4a 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/__tests__/PropositionItemTests.ts b/packages/messaging/__tests__/PropositionItemTests.ts
new file mode 100644
index 000000000..16833e705
--- /dev/null
+++ b/packages/messaging/__tests__/PropositionItemTests.ts
@@ -0,0 +1,84 @@
+/*
+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.
+*/
+
+import { NativeModules } from 'react-native';
+import { MessagingEdgeEventType, PropositionItem, PropositionItemData, PersonalizationSchema } from '../src';
+
+describe('PropositionItem', () => {
+ const uuid = 'activity-uuid-123';
+ const id = 'item-abc';
+ const activityID = uuid; // mirrors native mapping
+
+ const baseData: PropositionItemData = {
+ id,
+ uuid,
+ activityID,
+ schema: PersonalizationSchema.CONTENT_CARD,
+ data: { foo: 'bar' },
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('track(eventType) calls native with null interaction and tokens', () => {
+ const item = new PropositionItem(baseData);
+ item.track(MessagingEdgeEventType.DISPLAY);
+
+ expect(NativeModules.AEPMessaging.trackPropositionItem).toHaveBeenCalledWith(
+ activityID,
+ null,
+ MessagingEdgeEventType.DISPLAY,
+ null
+ );
+ });
+
+ it('track(interaction, eventType, tokens=null) forwards interaction', () => {
+ const item = new PropositionItem(baseData);
+ item.track('click', MessagingEdgeEventType.INTERACT, null);
+
+ expect(NativeModules.AEPMessaging.trackPropositionItem).toHaveBeenCalledWith(
+ activityID,
+ 'click',
+ MessagingEdgeEventType.INTERACT,
+ null
+ );
+ });
+
+ it('track(interaction, eventType, tokens[]) forwards tokens array', () => {
+ const item = new PropositionItem(baseData);
+ const tokens = ['t1', 't2'];
+ item.track('click', MessagingEdgeEventType.INTERACT, tokens);
+
+ expect(NativeModules.AEPMessaging.trackPropositionItem).toHaveBeenCalledWith(
+ activityID,
+ 'click',
+ MessagingEdgeEventType.INTERACT,
+ tokens
+ );
+ });
+
+ it('track(null, eventType, tokens[]) supports null interaction with tokens', () => {
+ const item = new PropositionItem(baseData);
+ const tokens = ['a'];
+ item.track(null, MessagingEdgeEventType.DISPLAY, tokens);
+
+ expect(NativeModules.AEPMessaging.trackPropositionItem).toHaveBeenCalledWith(
+ activityID,
+ null,
+ MessagingEdgeEventType.DISPLAY,
+ tokens
+ );
+ });
+});
+
+
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 000000000..445c19c0f
--- /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 2d52139a6..815cdbc8a 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
@@ -14,23 +14,24 @@
import static com.adobe.marketing.mobile.reactnative.messaging.RCTAEPMessagingUtil.convertMessageToMap;
import android.app.Activity;
+import android.util.Log;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import com.adobe.marketing.mobile.AdobeCallback;
import com.adobe.marketing.mobile.AdobeCallbackWithError;
import com.adobe.marketing.mobile.AdobeError;
-import com.adobe.marketing.mobile.LoggingMode;
import com.adobe.marketing.mobile.Message;
import com.adobe.marketing.mobile.Messaging;
import com.adobe.marketing.mobile.MessagingEdgeEventType;
-import com.adobe.marketing.mobile.MobileCore;
import com.adobe.marketing.mobile.messaging.MessagingUtils;
import com.adobe.marketing.mobile.messaging.Proposition;
import com.adobe.marketing.mobile.messaging.PropositionItem;
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;
@@ -42,14 +43,56 @@
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.modules.core.DeviceEventManagerModule;
+import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ConcurrentHashMap;
+
+
public final class RCTAEPMessagingModule
extends ReactContextBaseJavaModule implements PresentationDelegate {
+ // We cache uuid -> Proposition (not PropositionItem) for iOS parity: storing items on iOS lost
+ // weak parent references. Propositions currently contain a single item, so using the first item
+ // for tracking is valid.
+ private final Map propositionItemByUuid = new ConcurrentHashMap<>();
+ /**
+ * Parses the given Proposition and extracts the activity ID.
+ *
+ * Expected location: scopeDetails.activity.id in the proposition's event data.
+ * Returns null when any of the nested structures are missing or the id is not a String.
+ */
+ private String extractActivityId(Proposition proposition) {
+ try {
+ Map eventData = proposition.toEventData();
+ if (eventData == null) {
+ Log.d(TAG, "[MessagingBridge] Proposition toEventData() returned null; cannot extract activity.id");
+ return null;
+ }
+ Object scopeDetailsObj = eventData.get("scopeDetails");
+ if (!(scopeDetailsObj instanceof Map)) return null;
+ Map scopeDetails = (Map) scopeDetailsObj;
+ Object activityObj = scopeDetails.get("activity");
+ if (!(activityObj instanceof Map)) {
+ Log.d(TAG, "[MessagingBridge] Missing activity under scopeDetails; cannot extract activity.id");
+ return null;
+ }
+ Map activity = (Map) activityObj;
+ Object id = activity.get("id");
+ if (id instanceof String) {
+ return (String) id;
+ } else {
+ Log.d(TAG, "[MessagingBridge] Missing activity.id or not a String; skipping uuid cache mapping");
+ return null;
+ }
+ } catch (Exception e) {
+ Log.d(TAG, "[MessagingBridge] Exception extracting activity.id: " + e.getMessage(), e);
+ return null;
+ }
+ }
private static final String TAG = "RCTAEPMessagingModule";
private final Map messageCache = new HashMap<>();
private final ReactApplicationContext reactContext;
@@ -57,6 +100,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);
@@ -100,21 +144,37 @@ public void getPropositionsForSurfaces(ReadableArray surfaces,
final Promise promise) {
String bundleId = this.reactContext.getPackageName();
Messaging.getPropositionsForSurfaces(
- RCTAEPMessagingUtil.convertSurfaces(surfaces),
- new AdobeCallbackWithError