diff --git a/apps/AEPSampleAppNewArchEnabled/app/MessagingView.tsx b/apps/AEPSampleAppNewArchEnabled/app/MessagingView.tsx
index 270d5adbf..a53109f9b 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}`);
@@ -40,12 +51,10 @@ const setMessagingDelegate = () => {
});
console.log('messaging delegate set');
};
-
const getPropositionsForSurfaces = async () => {
const messages = await Messaging.getPropositionsForSurfaces(SURFACES);
console.log(JSON.stringify(messages));
};
-
const trackAction = async () => {
MobileCore.trackAction('tuesday', {full: true});
};
@@ -75,7 +84,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 +103,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 +112,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 +156,7 @@ function MessagingView() {
+
);
diff --git a/packages/messaging/README.md b/packages/messaging/README.md
index 1c05aa3de..0439c43af 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
@@ -440,6 +487,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 +498,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**
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/RCTAEPMessagingModule.java b/packages/messaging/android/src/main/java/com/adobe/marketing/mobile/reactnative/messaging/RCTAEPMessagingModule.java
index 2d52139a6..686a5fb3c 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,17 +14,17 @@
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;
@@ -42,14 +42,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;
@@ -100,21 +142,37 @@ public void getPropositionsForSurfaces(ReadableArray surfaces,
final Promise promise) {
String bundleId = this.reactContext.getPackageName();
Messaging.getPropositionsForSurfaces(
- RCTAEPMessagingUtil.convertSurfaces(surfaces),
- new AdobeCallbackWithError