From 02a791ac38572577e80f706e973af5881ac54de2 Mon Sep 17 00:00:00 2001 From: Naman Arora Date: Thu, 7 Aug 2025 14:37:43 +0530 Subject: [PATCH 01/28] proposition item classes, track funcitonality etc --- ITEMID_FLOW_EXAMPLE.md | 231 ++++++++++ NATIVE_IMPLEMENTATION_GUIDE.md | 346 ++++++++++++++ .../app/MessagingView.tsx | 134 +++++- .../app/_layout.tsx | 2 +- .../CONTENT_CARD_TRACKING_EXAMPLE.md | 259 +++++++++++ .../PROPOSITION_ITEM_TRACKING_EXAMPLE.md | 427 ++++++++++++++++++ .../messaging/RCTAEPMessagingModule.java | 247 ++++++++++ .../messaging/ios/src/RCTAEPMessaging.swift | 193 ++++++++ packages/messaging/src/Messaging.ts | 29 ++ packages/messaging/src/index.ts | 12 +- packages/messaging/src/models/ContentCard.ts | 162 ++++++- .../messaging/src/models/HTMLProposition.ts | 135 +++++- .../src/models/JSONPropositionItem.ts | 34 +- .../src/models/MessagingPropositionItem.ts | 6 +- .../messaging/src/models/PropositionItem.ts | 245 ++++++++++ 15 files changed, 2453 insertions(+), 9 deletions(-) create mode 100644 ITEMID_FLOW_EXAMPLE.md create mode 100644 NATIVE_IMPLEMENTATION_GUIDE.md create mode 100644 packages/messaging/CONTENT_CARD_TRACKING_EXAMPLE.md create mode 100644 packages/messaging/PROPOSITION_ITEM_TRACKING_EXAMPLE.md create mode 100644 packages/messaging/src/models/PropositionItem.ts diff --git a/ITEMID_FLOW_EXAMPLE.md b/ITEMID_FLOW_EXAMPLE.md new file mode 100644 index 000000000..9cc3628c6 --- /dev/null +++ b/ITEMID_FLOW_EXAMPLE.md @@ -0,0 +1,231 @@ +# ItemId Flow Example: User Code → Native Implementation + +This example demonstrates how the `itemId` flows from user React Native code down to the native `trackPropositionItem` method. + +## Step-by-Step Flow + +### **Step 1: User Retrieves Propositions** + +```typescript +import { Messaging } from '@adobe/react-native-aepmessaging'; + +// User calls this to get propositions +const propositions = await Messaging.getPropositionsForSurfaces(['homepage', 'product-page']); +``` + +### **Step 2: Native Response (What Gets Returned)** + +The native code returns proposition data like this: + +```typescript +// Example response structure +{ + "homepage": [ + { + "uniqueId": "AT:eyJhY3Rpdml0eUlkIjoiMTM3NTg5IiwiZXhwZXJpZW5jZUlkIjoiMCJ9", + "items": [ + { + "id": "xcore:personalized-offer:124e36a756c8", // <- This is the itemId + "schema": "https://ns.adobe.com/personalization/content-card", + "data": { + "content": { + "title": "Summer Sale!", + "body": "Get 50% off on summer collection", + "imageUrl": "https://example.com/summer.jpg" + } + } + } + ], + "scope": { + "surface": "homepage" + } + } + ], + "product-page": [ + { + "uniqueId": "AT:eyJhY3Rpdml0eUlkIjoiMTM3NTkwIiwiZXhwZXJpZW5jZUlkIjoiMCJ9", + "items": [ + { + "id": "xcore:personalized-offer:789xyz456def", // <- Another itemId + "schema": "https://ns.adobe.com/personalization/html-content-item", + "data": { + "content": "
Special offer for this product!
", + "format": "text/html" + } + } + ], + "scope": { + "surface": "product-page" + } + } + ] +} +``` + +### **Step 3: Native Caching (Happens Automatically)** + +When the propositions are retrieved, the native code automatically caches the PropositionItems: + +**Android Cache State After Step 2:** +```java +// propositionItemCache contents: +{ + "xcore:personalized-offer:124e36a756c8" -> PropositionItem(id="xcore:personalized-offer:124e36a756c8", schema=CONTENT_CARD, ...), + "xcore:personalized-offer:789xyz456def" -> PropositionItem(id="xcore:personalized-offer:789xyz456def", schema=HTML_CONTENT, ...) +} + +// propositionCache contents: +{ + "xcore:personalized-offer:124e36a756c8" -> Proposition(uniqueId="AT:eyJhY3Rpdml0eUlkIjoiMTM3NTg5IiwiZXhwZXJpZW5jZUlkIjoiMCJ9", ...), + "xcore:personalized-offer:789xyz456def" -> Proposition(uniqueId="AT:eyJhY3Rpdml0eUlkIjoiMTM3NTkwIiwiZXhwZXJpZW5jZUlkIjoiMCJ9", ...) +} +``` + +### **Step 4: User Creates Typed Objects** + +```typescript +import { ContentCard, HTMLProposition } from '@adobe/react-native-aepmessaging'; + +// User creates ContentCard from homepage proposition +const homepageItems = propositions['homepage'][0].items; +const contentCard = new ContentCard({ + id: homepageItems[0].id, // "xcore:personalized-offer:124e36a756c8" + schema: homepageItems[0].schema, + data: homepageItems[0].data, + // ... other fields +}); + +// User creates HTMLProposition from product-page proposition +const productPageItems = propositions['product-page'][0].items; +const htmlProposition = new HTMLProposition({ + id: productPageItems[0].id, // "xcore:personalized-offer:789xyz456def" + schema: productPageItems[0].schema, + data: productPageItems[0].data, + // ... other fields +}); +``` + +### **Step 5: User Calls Tracking Methods** + +```typescript +// User tracks content card display +contentCard.trackDisplay(); + +// User tracks HTML proposition interaction +htmlProposition.trackInteraction("view-details"); +``` + +### **Step 6: React Native → Native Method Calls** + +When `contentCard.trackDisplay()` is called: + +```typescript +// Inside ContentCard class (extends PropositionItem) +trackDisplay(): void { + this.track(MessagingEdgeEventType.DISPLAY); +} + +// Inside PropositionItem base class +track(eventType: MessagingEdgeEventType): void { + this.trackWithDetails(null, eventType, null); +} + +private trackWithDetails(interaction: string | null, eventType: MessagingEdgeEventType, tokens: string[] | null): void { + // this.id = "xcore:personalized-offer:124e36a756c8" + RCTAEPMessaging.trackPropositionItem(this.id, interaction, eventType, tokens); + // ^^^^^^^ + // itemId passed to native! +} +``` + +### **Step 7: Native Method Execution** + +**Android:** +```java +@ReactMethod +public void trackPropositionItem(String itemId, @Nullable String interaction, int eventType, @Nullable ReadableArray tokens) { + // itemId = "xcore:personalized-offer:124e36a756c8" + // interaction = null + // eventType = 4 (MessagingEdgeEventType.DISPLAY) + // tokens = null + + try { + MessagingEdgeEventType edgeEventType = RCTAEPMessagingUtil.getEventType(eventType); + // edgeEventType = MessagingEdgeEventType.DISPLAY + + PropositionItem propositionItem = findPropositionItemById(itemId); + // Looks up "xcore:personalized-offer:124e36a756c8" in propositionItemCache + + if (propositionItem != null) { + // Found the cached ContentCard PropositionItem! + propositionItem.track(edgeEventType); + // Calls the native Android SDK PropositionItem.track(MessagingEdgeEventType.DISPLAY) + + Log.debug(TAG, "Successfully tracked PropositionItem: " + itemId + " with eventType: " + edgeEventType); + } else { + Log.warning(TAG, "PropositionItem not found for ID: " + itemId); + } + } catch (Exception e) { + Log.error(TAG, "Error tracking PropositionItem: " + itemId, e); + } +} + +private PropositionItem findPropositionItemById(String itemId) { + return propositionItemCache.get(itemId); + // Returns the cached PropositionItem for "xcore:personalized-offer:124e36a756c8" +} +``` + +## Key Points + +### **1. ItemId Origin** +The `itemId` comes from the Adobe Experience Edge response and is included in the proposition data structure. + +### **2. No Manual ID Management** +Users don't need to manually manage or remember IDs - they're automatically included in the proposition data. + +### **3. Automatic Caching** +The native code automatically caches PropositionItems by their ID when propositions are first retrieved. + +### **4. Transparent to User** +The user just calls `contentCard.track()` - the ID handling is transparent. + +### **5. Type Safety** +The TypeScript classes ensure the correct `itemId` is always passed to the native methods. + +## Error Scenarios + +### **What if itemId is not found in cache?** + +```java +// Android +if (propositionItem == null) { + Log.warning(TAG, "PropositionItem not found for ID: " + itemId); + return; // Gracefully handle - no crash +} +``` + +```swift +// iOS +guard let propositionItem = findPropositionItemById(itemId) else { + print("Warning: PropositionItem not found for ID: \(itemId)") + resolve(nil) // Gracefully handle - no crash + return +} +``` + +### **Common causes:** +1. Propositions were never retrieved via `getPropositionsForSurfaces()` +2. Cache was cleared +3. PropositionItem was created with incorrect/invalid data + +## Summary + +The `itemId` flows naturally through the system: +1. **Adobe Edge** → generates unique IDs for each PropositionItem +2. **Native SDK** → receives propositions with IDs, caches by ID +3. **React Native** → receives proposition data including IDs +4. **User Code** → creates objects using the IDs from proposition data +5. **Tracking** → uses the ID to look up cached native objects + +The system is designed so users never need to manually handle or remember IDs - they're embedded in the objects and flow transparently through the tracking system. \ No newline at end of file diff --git a/NATIVE_IMPLEMENTATION_GUIDE.md b/NATIVE_IMPLEMENTATION_GUIDE.md new file mode 100644 index 000000000..2ce506b3e --- /dev/null +++ b/NATIVE_IMPLEMENTATION_GUIDE.md @@ -0,0 +1,346 @@ +# Native Implementation Guide: Unified PropositionItem Tracking + +This guide explains the complete native implementation for the unified `PropositionItem` tracking system that enables `ContentCard`, `HTMLProposition`, `JSONPropositionItem`, and other proposition types to use the same tracking methods. + +## Overview + +The unified tracking system allows all proposition item types to use the same `track()` and `generateInteractionXdm()` methods by: + +1. **Caching PropositionItems**: When propositions are retrieved via `getPropositionsForSurfaces`, all `PropositionItem` objects are cached by their ID +2. **Unified Native Methods**: New native methods `trackPropositionItem` and `generatePropositionInteractionXdm` that work with cached items +3. **Automatic Cleanup**: Cache management methods for memory optimization + +## Architecture + +``` +React Native Layer: +├── PropositionItem (base class) +├── ContentCard extends PropositionItem +├── HTMLProposition extends PropositionItem +├── JSONPropositionItem extends PropositionItem +└── All call unified native methods + +Native Layer (Android/iOS): +├── PropositionItem Cache (itemId -> PropositionItem) +├── Proposition Cache (itemId -> parent Proposition) +├── trackPropositionItem() native method +├── generatePropositionInteractionXdm() native method +└── Automatic caching in getPropositionsForSurfaces() +``` + +## Android Implementation + +### Key Changes Made + +The following methods were added to `RCTAEPMessagingModule.java`: + +#### 1. Cache Properties +```java +// Cache to store PropositionItem objects by their ID for unified tracking +private final Map propositionItemCache = new ConcurrentHashMap<>(); +// Cache to store the parent Proposition for each PropositionItem +private final Map propositionCache = new ConcurrentHashMap<>(); +``` + +#### 2. Core Tracking Method +```java +@ReactMethod +public void trackPropositionItem(String itemId, @Nullable String interaction, int eventType, @Nullable ReadableArray tokens) { + try { + // Convert eventType int to MessagingEdgeEventType enum + MessagingEdgeEventType edgeEventType = RCTAEPMessagingUtil.getEventType(eventType); + + // Find the PropositionItem by ID + PropositionItem propositionItem = findPropositionItemById(itemId); + + if (propositionItem == null) { + Log.warning(TAG, "trackPropositionItem - PropositionItem not found for ID: " + itemId); + return; + } + + // Convert ReadableArray to List if provided + List tokenList = null; + if (tokens != null) { + tokenList = new ArrayList<>(); + for (int i = 0; i < tokens.size(); i++) { + tokenList.add(tokens.getString(i)); + } + } + + // Call the appropriate track method based on provided parameters + if (interaction != null && tokenList != null) { + propositionItem.track(interaction, edgeEventType, tokenList); + } else if (interaction != null) { + propositionItem.track(interaction, edgeEventType, null); + } else { + propositionItem.track(edgeEventType); + } + + Log.debug(TAG, "Successfully tracked PropositionItem: " + itemId + " with eventType: " + edgeEventType); + + } catch (Exception e) { + Log.error(TAG, "Error tracking PropositionItem: " + itemId, e); + } +} +``` + +#### 3. XDM Generation Method +```java +@ReactMethod +public void generatePropositionInteractionXdm(String itemId, @Nullable String interaction, int eventType, @Nullable ReadableArray tokens, Promise promise) { + try { + // Convert eventType int to MessagingEdgeEventType enum + MessagingEdgeEventType edgeEventType = RCTAEPMessagingUtil.getEventType(eventType); + + // Find the PropositionItem by ID + PropositionItem propositionItem = findPropositionItemById(itemId); + + if (propositionItem == null) { + promise.reject("PropositionItemNotFound", "No PropositionItem found with ID: " + itemId); + return; + } + + // Generate XDM data using the appropriate method + Map xdmData; + if (interaction != null && tokenList != null) { + xdmData = propositionItem.generateInteractionXdm(interaction, edgeEventType, tokenList); + } else if (interaction != null) { + xdmData = propositionItem.generateInteractionXdm(interaction, edgeEventType, null); + } else { + xdmData = propositionItem.generateInteractionXdm(edgeEventType); + } + + if (xdmData != null) { + WritableMap result = RCTAEPMessagingUtil.toWritableMap(xdmData); + promise.resolve(result); + } else { + promise.reject("XDMGenerationFailed", "Failed to generate XDM data for PropositionItem: " + itemId); + } + + } catch (Exception e) { + promise.reject("XDMGenerationError", "Error generating XDM data: " + e.getMessage(), e); + } +} +``` + +#### 4. Automatic Caching +```java +@ReactMethod +public void getPropositionsForSurfaces(ReadableArray surfaces, final Promise promise) { + String bundleId = this.reactContext.getPackageName(); + Messaging.getPropositionsForSurfaces( + RCTAEPMessagingUtil.convertSurfaces(surfaces), + new AdobeCallbackWithError>>() { + @Override + public void call(Map> propositionsMap) { + + // Cache PropositionItems for unified tracking when propositions are retrieved + for (Map.Entry> entry : propositionsMap.entrySet()) { + List propositions = entry.getValue(); + if (propositions != null) { + cachePropositionsItems(propositions); + } + } + + promise.resolve(RCTAEPMessagingUtil.convertSurfacePropositions(propositionsMap, bundleId)); + } + }); +} +``` + +## iOS Implementation + +### Key Changes Made + +The following methods were added to `RCTAEPMessaging.swift`: + +#### 1. Cache Properties +```swift +// Cache to store PropositionItem objects by their ID for unified tracking +private var propositionItemCache = [String: PropositionItem]() +// Cache to store the parent Proposition for each PropositionItem +private var propositionCache = [String: Proposition]() +``` + +#### 2. Core Tracking Method +```swift +@objc +func trackPropositionItem( + _ itemId: String, + interaction: String?, + eventType: Int, + tokens: [String]?, + withResolver resolve: @escaping RCTPromiseResolveBlock, + withRejecter reject: @escaping RCTPromiseRejectBlock +) { + guard let edgeEventType = MessagingEdgeEventType(rawValue: eventType) else { + reject("InvalidEventType", "Invalid eventType: \(eventType)", nil) + return + } + + guard let propositionItem = findPropositionItemById(itemId) else { + print("Warning: PropositionItem not found for ID: \(itemId)") + resolve(nil) + return + } + + // Call the appropriate track method based on provided parameters + if let interaction = interaction, let tokens = tokens { + propositionItem.track(interaction, withEdgeEventType: edgeEventType, forTokens: tokens) + } else if let interaction = interaction { + propositionItem.track(interaction, withEdgeEventType: edgeEventType) + } else { + propositionItem.track(withEdgeEventType: edgeEventType) + } + + print("Successfully tracked PropositionItem: \(itemId) with eventType: \(edgeEventType)") + resolve(nil) +} +``` + +#### 3. XDM Generation Method +```swift +@objc +func generatePropositionInteractionXdm( + _ itemId: String, + interaction: String?, + eventType: Int, + tokens: [String]?, + withResolver resolve: @escaping RCTPromiseResolveBlock, + withRejecter reject: @escaping RCTPromiseRejectBlock +) { + guard let edgeEventType = MessagingEdgeEventType(rawValue: eventType) else { + reject("InvalidEventType", "Invalid eventType: \(eventType)", nil) + return + } + + guard let propositionItem = findPropositionItemById(itemId) else { + reject("PropositionItemNotFound", "No PropositionItem found with ID: \(itemId)", nil) + return + } + + // Generate XDM data using the appropriate method + var xdmData: [String: Any]? + + if let interaction = interaction, let tokens = tokens { + xdmData = propositionItem.generateInteractionXdm(interaction, withEdgeEventType: edgeEventType, forTokens: tokens) + } else if let interaction = interaction { + xdmData = propositionItem.generateInteractionXdm(interaction, withEdgeEventType: edgeEventType) + } else { + xdmData = propositionItem.generateInteractionXdm(withEdgeEventType: edgeEventType) + } + + if let xdmData = xdmData { + resolve(xdmData) + } else { + reject("XDMGenerationFailed", "Failed to generate XDM data for PropositionItem: \(itemId)", nil) + } +} +``` + +#### 4. Automatic Caching +```swift +@objc +func getPropositionsForSurfaces( + _ surfaces: [String], + withResolver resolve: @escaping RCTPromiseResolveBlock, + withRejecter reject: @escaping RCTPromiseRejectBlock +) { + let surfacePaths = surfaces.map { $0.isEmpty ? Surface() : Surface(path: $0) } + Messaging.getPropositionsForSurfaces(surfacePaths) { propositions, error in + guard error == nil else { + reject("Unable to Retrieve Propositions", nil, nil) + return + } + + // Cache PropositionItems for unified tracking when propositions are retrieved + if let propositionsDict = propositions { + let allPropositions = Array(propositionsDict.values).flatMap { $0 } + self.cachePropositionsItems(allPropositions) + } + + resolve(RCTAEPMessagingDataBridge.transformPropositionDict(dict: propositions!)) + } +} +``` + +## Key Features + +### 1. Automatic Caching +- **When**: PropositionItems are automatically cached when `getPropositionsForSurfaces()` is called +- **What**: Both the `PropositionItem` object and its parent `Proposition` are cached +- **Why**: Enables tracking without needing to pass full proposition data each time + +### 2. Flexible Method Signatures +The tracking methods support multiple call patterns: +```javascript +// Event type only +propositionItem.track(MessagingEdgeEventType.DISPLAY); + +// With interaction +propositionItem.track("button-click", MessagingEdgeEventType.INTERACT, null); + +// With interaction and tokens +propositionItem.track("carousel-item", MessagingEdgeEventType.INTERACT, ["token1", "token2"]); +``` + +### 3. Memory Management +- Uses `ConcurrentHashMap` (Android) and thread-safe collections (iOS) +- Provides cache management methods (`clearPropositionItemCache`, `getPropositionItemCacheSize`, `hasPropositionItem`) +- Automatic cleanup when propositions are refreshed + +### 4. Error Handling +- Graceful handling of missing PropositionItems +- Detailed error messages for debugging +- Promise-based error reporting for XDM generation + +## Integration Requirements + +### React Native Interface Updates + +Update your `Messaging.ts` interface to include the new methods: + +```typescript +export interface NativeMessagingModule { + // ... existing methods ... + trackPropositionItem: (itemId: string, interaction: string | null, eventType: number, tokens: string[] | null) => void; + generatePropositionInteractionXdm: (itemId: string, interaction: string | null, eventType: number, tokens: string[] | null) => Promise; +} +``` + +### Usage Flow + +1. **App calls `getPropositionsForSurfaces()`** + - Native code retrieves propositions from Adobe Edge + - All PropositionItems are automatically cached + - React Native receives proposition data + +2. **App creates PropositionItem objects** + - `ContentCard`, `HTMLProposition`, `JSONPropositionItem` extend base `PropositionItem` + - Each contains an `id` that maps to cached native objects + +3. **App calls tracking methods** + - `track()` methods call `RCTAEPMessaging.trackPropositionItem()` + - Native code finds cached PropositionItem by ID + - Calls appropriate native tracking method + +## Benefits + +1. **Unified API**: All proposition types use the same tracking interface +2. **Performance**: Avoids passing large proposition objects on each tracking call +3. **Flexibility**: Supports all native tracking method variations +4. **Consistency**: Mirrors the native Android SDK architecture +5. **Memory Efficient**: Automatic cache management prevents memory leaks +6. **Developer Experience**: Simple, consistent API across all proposition types + +## Testing + +Test the implementation by: + +1. Retrieving propositions via `getPropositionsForSurfaces()` +2. Creating `ContentCard`, `HTMLProposition`, or `JSONPropositionItem` objects +3. Calling `track()` methods with different parameter combinations +4. Verifying tracking events are sent to Adobe Experience Edge +5. Testing cache management methods for proper memory handling + +This unified approach provides a robust, scalable foundation for proposition tracking across all content types in your React Native Adobe Experience SDK implementation. \ No newline at end of file diff --git a/apps/AEPSampleAppNewArchEnabled/app/MessagingView.tsx b/apps/AEPSampleAppNewArchEnabled/app/MessagingView.tsx index 270d5adbf..754ae6812 100644 --- a/apps/AEPSampleAppNewArchEnabled/app/MessagingView.tsx +++ b/apps/AEPSampleAppNewArchEnabled/app/MessagingView.tsx @@ -13,7 +13,12 @@ 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 // Add this import +} from '@adobe/react-native-aepmessaging' import styles from '../styles/styles'; import { useRouter } from 'expo-router'; @@ -75,7 +80,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 +99,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 +108,123 @@ const trackContentCardDisplay = async () => { } } +// New method demonstrating trackPropositionItem API +const trackPropositionItemExample = async () => { + const messages = await Messaging.getPropositionsForSurfaces(SURFACES); + + for (const surface of SURFACES) { + const propositions = messages[surface] || []; + + for (const proposition of propositions) { + for (const propositionItem of proposition.items) { + // Track proposition item interaction using the new API + Messaging.trackPropositionItem( + propositionItem.id, + 'button_clicked', + MessagingEdgeEventType.INTERACT, + null + ); + console.log('trackPropositionItem called for:', propositionItem.id); + } + } + } +} + +// New method demonstrating generatePropositionInteractionXdm API +// const generatePropositionInteractionXdmExample = async () => { +// const messages = await Messaging.getPropositionsForSurfaces(SURFACES); + +// for (const surface of SURFACES) { +// const propositions = messages[surface] || []; + +// for (const proposition of propositions) { +// for (const propositionItem of proposition.items) { +// // Generate XDM data for proposition item interaction +// try { +// const xdmData = await Messaging.generatePropositionInteractionXdm( +// propositionItem.id, +// 'link_clicked', +// MessagingEdgeEventType.INTERACT, +// ['token1', 'token2'] +// ); +// console.log('Generated XDM data:', JSON.stringify(xdmData)); +// } catch (error) { +// console.error('Error generating XDM data:', error); +// } +// } +// } +// } +// } + +// Method demonstrating unified tracking using PropositionItem methods +const unifiedTrackingExample = async () => { + const messages = await Messaging.getPropositionsForSurfaces(SURFACES); + console.log('messages here are ', messages); + + for (const surface of SURFACES) { + const propositions = messages[surface] || []; + + for (const proposition of propositions) { + for (const propositionItemData of proposition.items) { + // Create PropositionItem instance from the plain data object + const propositionItem = new PropositionItem(propositionItemData); + console.log('propositionItem here is created:', propositionItem); + + // Use the unified tracking approach via PropositionItem + if (propositionItem.schema === PersonalizationSchema.CONTENT_CARD) { + // Track display for content cards + propositionItem.track(MessagingEdgeEventType.DISPLAY); + console.log('Tracked content card display using unified API'); + + // Track interaction with custom interaction string + propositionItem.track('card_clicked', MessagingEdgeEventType.INTERACT, null); + console.log('Tracked content card interaction using unified API'); + } else if (propositionItem.schema === PersonalizationSchema.JSON_CONTENT) { + // Track display for JSON content + propositionItem.track(MessagingEdgeEventType.DISPLAY); + console.log('Tracked JSON content display using unified API'); + } + } + } + } +} + +// // Method demonstrating unified XDM generation using PropositionItem methods +// const unifiedXdmGenerationExample = async () => { +// const messages = await Messaging.getPropositionsForSurfaces(SURFACES); + +// for (const surface of SURFACES) { +// const propositions = messages[surface] || []; + +// for (const proposition of propositions) { +// for (const propositionItem of proposition.items) { +// try { +// // Generate XDM using the unified approach - check if the method exists +// if ('generateInteractionXdm' in propositionItem) { +// const xdmData = await propositionItem.generateInteractionXdm( +// 'unified_interaction', +// MessagingEdgeEventType.INTERACT, +// ['unified_token'] +// ); +// console.log('Generated XDM using unified API:', JSON.stringify(xdmData)); +// } else { +// // Fall back to the static method for items that don't have the instance method +// const xdmData = await Messaging.generatePropositionInteractionXdm( +// propositionItem.id, +// 'unified_interaction', +// MessagingEdgeEventType.INTERACT, +// ['unified_token'] +// ); +// console.log('Generated XDM using static API fallback:', JSON.stringify(xdmData)); +// } +// } catch (error) { +// console.error('Error generating XDM with unified API:', error); +// } +// } +// } +// } +// } + function MessagingView() { const router = useRouter(); @@ -125,6 +249,10 @@ function MessagingView() { + Terms & Conditions + + ` + }, + schema: PersonalizationSchema.HTML_CONTENT +}; + +const htmlProposition = new HTMLProposition(htmlData); + +// Same tracking methods available as other proposition items +htmlProposition.track(MessagingEdgeEventType.DISPLAY); +htmlProposition.track("banner_viewed", MessagingEdgeEventType.INTERACT, null); + +// HTMLProposition-specific methods +console.log(htmlProposition.hasInteractiveElements()); // true (has button and link) +console.log(htmlProposition.isFullPageExperience()); // false (just a banner) +console.log(htmlProposition.extractLinks()); // ["https://example.com/terms"] +console.log(htmlProposition.getWordCount()); // Approximately 12 words + +// Convenience tracking methods +htmlProposition.trackDisplay(); // Track when banner is shown +htmlProposition.trackInteraction("button_clicked"); // Track button clicks +htmlProposition.trackDismiss("user_closed"); // Track when user closes banner +``` + +## Advanced Tracking Features + +### XDM Data Generation + +```typescript +// Generate XDM data for analytics (mirrors native functionality) +const xdmData = await contentCard.generateInteractionXdm(MessagingEdgeEventType.DISPLAY); +console.log(xdmData); // XDM-formatted tracking data + +// With full parameters +const xdmDataWithTokens = await contentCard.generateInteractionXdm( + "button_clicked", + MessagingEdgeEventType.INTERACT, + ["token1", "token2"] +); +``` + +### Generic PropositionItem Handling + +```typescript +// Function that can handle any proposition item type +function trackPropositionDisplay(item: PropositionItem) { + if (item.isContentCard()) { + console.log("Tracking ContentCard display"); + item.track(MessagingEdgeEventType.DISPLAY); + } else if (item.isJsonContent()) { + console.log("Tracking JSON content display"); + item.track("json_content_viewed", MessagingEdgeEventType.DISPLAY, null); + } else if (item.isHtmlContent()) { + console.log("Tracking HTML content display"); + item.track(MessagingEdgeEventType.DISPLAY); + } else { + console.log("Tracking unknown proposition type display"); + item.track(MessagingEdgeEventType.DISPLAY); + } +} + +// Works with any proposition item type +trackPropositionDisplay(contentCard); +trackPropositionDisplay(jsonPropositionItem); +trackPropositionDisplay(htmlProposition); +``` + +## React Native Component Integration + +### Universal Proposition Item Component + +```typescript +import React, { useEffect } from 'react'; +import { View, Text, TouchableOpacity } from 'react-native'; +import { PropositionItem, MessagingEdgeEventType } from '@adobe/react-native-aepMessaging'; + +interface PropositionItemComponentProps { + propositionItem: PropositionItem; + onInteraction?: (interaction: string) => void; +} + +const PropositionItemComponent: React.FC = ({ + propositionItem, + onInteraction +}) => { + // Track display when component mounts + useEffect(() => { + propositionItem.track(MessagingEdgeEventType.DISPLAY); + }, [propositionItem]); + + const handlePress = () => { + const interaction = propositionItem.isContentCard() ? "card_clicked" : "content_clicked"; + propositionItem.track(interaction, MessagingEdgeEventType.INTERACT, null); + onInteraction?.(interaction); + }; + + const renderContent = () => { + if (propositionItem.isContentCard()) { + const contentCard = propositionItem as ContentCard; + return ( + + + {contentCard.getTitle()} + + {contentCard.getBody()} + + ); + } else if (propositionItem.isJsonContent()) { + const jsonItem = propositionItem as JSONPropositionItem; + const content = jsonItem.getParsedContent(); + return ( + + JSON Content: {JSON.stringify(content)} + + ); + } else if (propositionItem.isHtmlContent()) { + const htmlItem = propositionItem as HTMLProposition; + return ( + + HTML Content + {htmlItem.getContent()} + {htmlItem.hasInteractiveElements() && ( + + ⚡ Interactive content available + + )} + + ); + } + return Unknown content type; + }; + + return ( + + {renderContent()} + + ); +}; +``` + +### Proposition Management Hook + +```typescript +import { useState, useEffect } from 'react'; +import { PropositionItem, MessagingEdgeEventType } from '@adobe/react-native-aepMessaging'; + +export const usePropositionTracking = (items: PropositionItem[]) => { + const [viewedItems, setViewedItems] = useState>(new Set()); + + const trackItemDisplay = (item: PropositionItem) => { + if (!viewedItems.has(item.getItemId())) { + item.track(MessagingEdgeEventType.DISPLAY); + setViewedItems(prev => new Set(prev).add(item.getItemId())); + } + }; + + const trackItemInteraction = (item: PropositionItem, interaction: string) => { + item.track(interaction, MessagingEdgeEventType.INTERACT, null); + }; + + const trackItemDismiss = (item: PropositionItem, reason: string = "user_dismissed") => { + item.track(reason, MessagingEdgeEventType.DISMISS, null); + }; + + return { + trackItemDisplay, + trackItemInteraction, + trackItemDismiss, + viewedItems + }; +}; +``` + +## Native Implementation Requirements + +To support this unified approach, you'll need to implement these methods in your native modules: + +### Android Implementation + +```java +@ReactMethod +public void trackPropositionItem(String itemId, @Nullable String interaction, int eventType, @Nullable ReadableArray tokens) { + MessagingEdgeEventType edgeEventType = MessagingEdgeEventType.values()[eventType]; + + // Find the PropositionItem by ID (you'll need to maintain a mapping) + PropositionItem propositionItem = findPropositionItemById(itemId); + + if (propositionItem != null) { + List tokenList = tokens != null ? tokens.toArrayList().stream() + .map(Object::toString) + .collect(Collectors.toList()) : null; + + if (interaction != null && tokenList != null) { + propositionItem.track(interaction, edgeEventType, tokenList); + } else if (interaction != null) { + propositionItem.track(interaction, edgeEventType, null); + } else { + propositionItem.track(edgeEventType); + } + } +} + +@ReactMethod +public void generatePropositionInteractionXdm(String itemId, @Nullable String interaction, int eventType, @Nullable ReadableArray tokens, Promise promise) { + MessagingEdgeEventType edgeEventType = MessagingEdgeEventType.values()[eventType]; + PropositionItem propositionItem = findPropositionItemById(itemId); + + if (propositionItem != null) { + List tokenList = tokens != null ? tokens.toArrayList().stream() + .map(Object::toString) + .collect(Collectors.toList()) : null; + + Map xdmData = propositionItem.generateInteractionXdm(interaction, edgeEventType, tokenList); + promise.resolve(Arguments.makeNativeMap(xdmData)); + } else { + promise.reject("PropositionItem not found", "No PropositionItem found with ID: " + itemId); + } +} +``` + +### iOS Implementation + +```objc +RCT_EXPORT_METHOD(trackPropositionItem:(NSString *)itemId + interaction:(NSString * _Nullable)interaction + eventType:(NSInteger)eventType + tokens:(NSArray * _Nullable)tokens) { + AEPMessagingEdgeEventType edgeEventType = (AEPMessagingEdgeEventType)eventType; + AEPPropositionItem *propositionItem = [self findPropositionItemById:itemId]; + + if (propositionItem) { + if (interaction && tokens) { + [propositionItem track:interaction eventType:edgeEventType tokens:tokens]; + } else if (interaction) { + [propositionItem track:interaction eventType:edgeEventType]; + } else { + [propositionItem track:edgeEventType]; + } + } +} + +RCT_EXPORT_METHOD(generatePropositionInteractionXdm:(NSString *)itemId + interaction:(NSString * _Nullable)interaction + eventType:(NSInteger)eventType + tokens:(NSArray * _Nullable)tokens + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) { + AEPMessagingEdgeEventType edgeEventType = (AEPMessagingEdgeEventType)eventType; + AEPPropositionItem *propositionItem = [self findPropositionItemById:itemId]; + + if (propositionItem) { + NSDictionary *xdmData = [propositionItem generateInteractionXdm:interaction eventType:edgeEventType tokens:tokens]; + resolve(xdmData); + } else { + reject(@"PropositionItem not found", [NSString stringWithFormat:@"No PropositionItem found with ID: %@", itemId], nil); + } +} +``` + +## Benefits of This Approach + +1. **Unified Architecture**: Mirrors the native Android/iOS SDK structure +2. **Consistent API**: Same tracking methods across all proposition types +3. **Extensibility**: Easy to add new proposition item types +4. **Type Safety**: Strong TypeScript typing with inheritance +5. **Code Reuse**: Common tracking logic shared across all types +6. **Future-Proof**: Scalable for additional Adobe Journey Optimizer features + +## Migration from Previous Implementation + +```typescript +// Old approach (if you had any HTML-specific tracking) +// No previous HTML tracking methods existed + +// New unified approach (recommended for all proposition types) +const contentCard = new ContentCard(contentCardData); +contentCard.track(MessagingEdgeEventType.DISPLAY); +contentCard.track("user_interaction", MessagingEdgeEventType.INTERACT, null); + +// For JSON code-based experiences +const jsonItem = new JSONPropositionItem(jsonData); +jsonItem.track(MessagingEdgeEventType.DISPLAY); +jsonItem.track("offer_clicked", MessagingEdgeEventType.INTERACT, ["offer_token"]); + +// For HTML code-based experiences (NEW) +const htmlItem = new HTMLProposition(htmlData); +htmlItem.track(MessagingEdgeEventType.DISPLAY); +htmlItem.track("banner_interaction", MessagingEdgeEventType.INTERACT, null); + +// Universal approach - works with any proposition type +function trackAnyProposition(item: PropositionItem, interaction?: string) { + item.track(MessagingEdgeEventType.DISPLAY); + if (interaction) { + item.track(interaction, MessagingEdgeEventType.INTERACT, null); + } +} + +// Works seamlessly across all types +trackAnyProposition(contentCard); +trackAnyProposition(jsonItem, "json_viewed"); +trackAnyProposition(htmlItem, "html_viewed"); +``` + +This approach provides a much more scalable and maintainable solution that aligns perfectly with the native SDK architecture! \ No newline at end of file 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..d9213d221 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,8 +14,10 @@ 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; @@ -42,10 +44,12 @@ 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 { @@ -58,6 +62,11 @@ public final class RCTAEPMessagingModule private CountDownLatch latch = new CountDownLatch(1); private Message latestMessage = null; + // Cache to store PropositionItem objects by their ID for unified tracking + private final Map propositionItemCache = new ConcurrentHashMap<>(); + // Cache to store the parent Proposition for each PropositionItem + private final Map propositionCache = new ConcurrentHashMap<>(); + public RCTAEPMessagingModule(ReactApplicationContext reactContext) { super(reactContext); this.reactContext = reactContext; @@ -111,6 +120,15 @@ public void fail(final AdobeError adobeError) { @Override public void call( Map> propositionsMap) { + + // Cache PropositionItems for unified tracking when propositions are retrieved + for (Map.Entry> entry : propositionsMap.entrySet()) { + List propositions = entry.getValue(); + if (propositions != null) { + cachePropositionsItems(propositions); + } + } + promise.resolve(RCTAEPMessagingUtil.convertSurfacePropositions( propositionsMap, bundleId)); } @@ -295,4 +313,233 @@ public void trackContentCardInteraction(ReadableMap propositionMap, ReadableMap } } } + + /** + * Tracks interactions with a PropositionItem using the provided interaction and event type. + * This method is used by the React Native PropositionItem.track() method. + * + * @param itemId The unique identifier of the PropositionItem + * @param interaction A custom string value to be recorded in the interaction (nullable) + * @param eventType The MessagingEdgeEventType numeric value + * @param tokens Array containing the sub-item tokens for recording interaction (nullable) + */ + @ReactMethod + public void trackPropositionItem(String itemId, @Nullable String interaction, int eventType, @Nullable ReadableArray tokens) { + Log.d(TAG, "trackPropositionItem called with itemId: " + itemId + ", interaction: " + interaction + ", eventType: " + eventType); + + try { + // Convert eventType int to MessagingEdgeEventType enum + MessagingEdgeEventType edgeEventType = RCTAEPMessagingUtil.getEventType(eventType); + if (edgeEventType == null) { + Log.d(TAG, "Invalid eventType provided: " + eventType + " for itemId: " + itemId); + return; + } + + Log.d(TAG, "Converted eventType " + eventType + " to MessagingEdgeEventType: " + edgeEventType.name()); + + // Find the PropositionItem by ID + PropositionItem propositionItem = findPropositionItemById(itemId); + + if (propositionItem == null) { + Log.d(TAG, "PropositionItem not found in cache for itemId: " + itemId); + return; + } + + Log.d(TAG, "Found PropositionItem in cache for itemId: " + itemId); + + // Convert ReadableArray to List if provided + List tokenList = null; + if (tokens != null) { + tokenList = new ArrayList<>(); + for (int i = 0; i < tokens.size(); i++) { + tokenList.add(tokens.getString(i)); + } + Log.d(TAG, "Converted tokens array to list with " + tokenList.size() + " items for itemId: " + itemId); + } else { + Log.d(TAG, "No tokens provided for itemId: " + itemId); + } + + // Call the appropriate track method based on provided parameters + if (interaction != null && tokenList != null) { + // Track with interaction and tokens + Log.d(TAG, "Tracking PropositionItem with interaction '" + interaction + "' and " + tokenList.size() + " tokens for itemId: " + itemId); + propositionItem.track(interaction, edgeEventType, tokenList); + } else if (interaction != null) { + // Track with interaction only + Log.d(TAG, "Tracking PropositionItem with interaction '" + interaction + "' for itemId: " + itemId); + propositionItem.track(interaction, edgeEventType, null); + } else { + // Track with event type only + Log.d(TAG, "Tracking PropositionItem with eventType only for itemId: " + itemId); + propositionItem.track(edgeEventType); + } + + Log.d(TAG, "Successfully tracked PropositionItem for itemId: " + itemId); + + } catch (Exception e) { + Log.d(TAG, "Error tracking PropositionItem: " + itemId + ", error: " + e.getMessage(), e); + } + } + /** + * Generates XDM data for PropositionItem interactions. + * This method is used by the React Native PropositionItem.generateInteractionXdm() method. + * + * @param itemId The unique identifier of the PropositionItem + * @param interaction A custom string value to be recorded in the interaction (nullable) + * @param eventType The MessagingEdgeEventType numeric value + * @param tokens Array containing the sub-item tokens for recording interaction (nullable) + * @param promise Promise to resolve with XDM data for the proposition interaction + */ + @ReactMethod + public void generatePropositionInteractionXdm(String itemId, @Nullable String interaction, int eventType, @Nullable ReadableArray tokens, Promise promise) { + try { + // Convert eventType int to MessagingEdgeEventType enum + MessagingEdgeEventType edgeEventType = RCTAEPMessagingUtil.getEventType(eventType); + if (edgeEventType == null) { + promise.reject("InvalidEventType", "Invalid eventType: " + eventType); + return; + } + + // Find the PropositionItem by ID + PropositionItem propositionItem = findPropositionItemById(itemId); + + if (propositionItem == null) { + promise.reject("PropositionItemNotFound", "No PropositionItem found with ID: " + itemId); + return; + } + + // Convert ReadableArray to List if provided + List tokenList = null; + if (tokens != null) { + tokenList = new ArrayList<>(); + for (int i = 0; i < tokens.size(); i++) { + tokenList.add(tokens.getString(i)); + } + } + + // Generate XDM data using the appropriate method + Map xdmData; + if (interaction != null && tokenList != null) { + xdmData = propositionItem.generateInteractionXdm(interaction, edgeEventType, tokenList); + } else if (interaction != null) { + xdmData = propositionItem.generateInteractionXdm(interaction, edgeEventType, null); + } else { + xdmData = propositionItem.generateInteractionXdm(edgeEventType); + } + + if (xdmData != null) { + // Convert Map to WritableMap for React Native + WritableMap result = RCTAEPMessagingUtil.toWritableMap(xdmData); + promise.resolve(result); + } else { + promise.reject("XDMGenerationFailed", "Failed to generate XDM data for PropositionItem: " + itemId); + } + + } catch (Exception e) { +// Log.error(TAG, "Error generating XDM data for PropositionItem: " + itemId + ", error: " + e.getMessage()); + promise.reject("XDMGenerationError", "Error generating XDM data: " + e.getMessage(), e); + } + } + + /** + * Caches a PropositionItem and its parent Proposition for later tracking. + * This method should be called when PropositionItems are created from propositions. + * + * @param propositionItem The PropositionItem to cache + * @param parentProposition The parent Proposition containing this item + */ + public void cachePropositionItem(PropositionItem propositionItem, Proposition parentProposition) { + if (propositionItem != null && propositionItem.getItemId() != null) { + String itemId = propositionItem.getItemId(); + + // Cache the PropositionItem + propositionItemCache.put(itemId, propositionItem); + + // Cache the parent Proposition + if (parentProposition != null) { + propositionCache.put(itemId, parentProposition); + + // Set the proposition reference in the PropositionItem if possible + try { + // Use reflection to set the proposition reference + java.lang.reflect.Field propositionRefField = propositionItem.getClass().getDeclaredField("propositionReference"); + propositionRefField.setAccessible(true); + propositionRefField.set(propositionItem, new java.lang.ref.SoftReference<>(parentProposition)); + } catch (Exception e) { + + } + } + + } + } + + /** + * Caches multiple PropositionItems from a list of propositions. + * This is a convenience method for caching all items from multiple propositions. + * + * @param propositions List of propositions containing items to cache + */ + public void cachePropositionsItems(List propositions) { + if (propositions != null) { + for (Proposition proposition : propositions) { + if (proposition.getItems() != null) { + for (PropositionItem item : proposition.getItems()) { + cachePropositionItem(item, proposition); + } + } + } + } + } + + /** + * Finds a cached PropositionItem by its ID. + * + * @param itemId The ID of the PropositionItem to find + * @return The PropositionItem if found, null otherwise + */ + private PropositionItem findPropositionItemById(String itemId) { + return propositionItemCache.get(itemId); + } + + /** + * Finds a cached parent Proposition by PropositionItem ID. + * + * @param itemId The ID of the PropositionItem whose parent to find + * @return The parent Proposition if found, null otherwise + */ + private Proposition findPropositionByItemId(String itemId) { + return propositionCache.get(itemId); + } + + /** + * Clears the PropositionItem cache. + * This should be called when propositions are refreshed or when memory cleanup is needed. + */ + @ReactMethod + public void clearPropositionItemCache() { + propositionItemCache.clear(); + propositionCache.clear(); + } + + /** + * Gets the current size of the PropositionItem cache. + * Useful for debugging and monitoring. + * + * @param promise Promise that resolves with the cache size + */ + @ReactMethod + public void getPropositionItemCacheSize(Promise promise) { + promise.resolve(propositionItemCache.size()); + } + + /** + * Checks if a PropositionItem exists in the cache. + * + * @param itemId The ID of the PropositionItem to check + * @param promise Promise that resolves with boolean indicating if item exists + */ + @ReactMethod + public void hasPropositionItem(String itemId, Promise promise) { + promise.resolve(propositionItemCache.containsKey(itemId)); + } } diff --git a/packages/messaging/ios/src/RCTAEPMessaging.swift b/packages/messaging/ios/src/RCTAEPMessaging.swift index 86cf4cef5..111955099 100644 --- a/packages/messaging/ios/src/RCTAEPMessaging.swift +++ b/packages/messaging/ios/src/RCTAEPMessaging.swift @@ -27,6 +27,11 @@ public class RCTAEPMessaging: RCTEventEmitter, MessagingDelegate { private var shouldShowMessage = true public static var emitter: RCTEventEmitter! + // Cache to store PropositionItem objects by their ID for unified tracking + private var propositionItemCache = [String: PropositionItem]() + // Cache to store the parent Proposition for each PropositionItem + private var propositionCache = [String: Proposition]() + override init() { super.init() RCTAEPMessaging.emitter = self @@ -81,6 +86,13 @@ public class RCTAEPMessaging: RCTEventEmitter, MessagingDelegate { resolve([String: Any]()); return; } + + // Cache PropositionItems for unified tracking when propositions are retrieved + if let propositionsDict = propositions { + let allPropositions = Array(propositionsDict.values).flatMap { $0 } + self.cachePropositionsItems(allPropositions) + } + resolve(RCTAEPMessagingDataBridge.transformPropositionDict(dict: propositions!)) } } @@ -249,6 +261,187 @@ public class RCTAEPMessaging: RCTEventEmitter, MessagingDelegate { } } + /// MARK: - Unified PropositionItem Tracking Methods + + /** + * Tracks interactions with a PropositionItem using the provided interaction and event type. + * This method is used by the React Native PropositionItem.track() method. + * + * - Parameters: + * - itemId: The unique identifier of the PropositionItem + * - interaction: A custom string value to be recorded in the interaction (optional) + * - eventType: The MessagingEdgeEventType numeric value + * - tokens: Array containing the sub-item tokens for recording interaction (optional) + */ + @objc + func trackPropositionItem( + _ itemId: String, + interaction: String?, + eventType: Int, + tokens: [String]?, + withResolver resolve: @escaping RCTPromiseResolveBlock, + withRejecter reject: @escaping RCTPromiseRejectBlock + ) { + guard let edgeEventType = MessagingEdgeEventType(rawValue: eventType) else { + reject("InvalidEventType", "Invalid eventType: \(eventType)", nil) + return + } + + guard let propositionItem = findPropositionItemById(itemId) else { + print("Warning: PropositionItem not found for ID: \(itemId)") + resolve(nil) + return + } + + // Call the appropriate track method based on provided parameters + if let interaction = interaction, let tokens = tokens { + // Track with interaction and tokens + propositionItem.track(interaction, withEdgeEventType: edgeEventType, forTokens: tokens) + } else if let interaction = interaction { + // Track with interaction only + propositionItem.track(interaction, withEdgeEventType: edgeEventType) + } else { + // Track with event type only + propositionItem.track(withEdgeEventType: edgeEventType) + } + + print("Successfully tracked PropositionItem: \(itemId) with eventType: \(edgeEventType)") + resolve(nil) + } + + /** + * Generates XDM data for PropositionItem interactions. + * This method is used by the React Native PropositionItem.generateInteractionXdm() method. + * + * - Parameters: + * - itemId: The unique identifier of the PropositionItem + * - interaction: A custom string value to be recorded in the interaction (optional) + * - eventType: The MessagingEdgeEventType numeric value + * - tokens: Array containing the sub-item tokens for recording interaction (optional) + * - resolve: Promise resolver with XDM data for the proposition interaction + * - reject: Promise rejecter for errors + */ + @objc + func generatePropositionInteractionXdm( + _ itemId: String, + interaction: String?, + eventType: Int, + tokens: [String]?, + withResolver resolve: @escaping RCTPromiseResolveBlock, + withRejecter reject: @escaping RCTPromiseRejectBlock + ) { + guard let edgeEventType = MessagingEdgeEventType(rawValue: eventType) else { + reject("InvalidEventType", "Invalid eventType: \(eventType)", nil) + return + } + + guard let propositionItem = findPropositionItemById(itemId) else { + reject("PropositionItemNotFound", "No PropositionItem found with ID: \(itemId)", nil) + return + } + + // Generate XDM data using the appropriate method + var xdmData: [String: Any]? + + if let interaction = interaction, let tokens = tokens { + xdmData = propositionItem.generateInteractionXdm(interaction, withEdgeEventType: edgeEventType, forTokens: tokens) + } else if let interaction = interaction { + xdmData = propositionItem.generateInteractionXdm(interaction, withEdgeEventType: edgeEventType) + } else { + xdmData = propositionItem.generateInteractionXdm(withEdgeEventType: edgeEventType) + } + + if let xdmData = xdmData { + resolve(xdmData) + print("Successfully generated XDM data for PropositionItem: \(itemId)") + } else { + reject("XDMGenerationFailed", "Failed to generate XDM data for PropositionItem: \(itemId)", nil) + } + } + + /// MARK: - PropositionItem Cache Management + + /** + * Caches a PropositionItem and its parent Proposition for later tracking. + * This method should be called when PropositionItems are created from propositions. + */ + private func cachePropositionItem(_ propositionItem: PropositionItem, parentProposition: Proposition) { + let itemId = propositionItem.itemId + + // Cache the PropositionItem + propositionItemCache[itemId] = propositionItem + + // Cache the parent Proposition + propositionCache[itemId] = parentProposition + + print("Cached PropositionItem with ID: \(itemId)") + } + + /** + * Caches multiple PropositionItems from a list of propositions. + * This is a convenience method for caching all items from multiple propositions. + */ + private func cachePropositionsItems(_ propositions: [Proposition]) { + for proposition in propositions { + for item in proposition.items { + cachePropositionItem(item, parentProposition: proposition) + } + } + } + + /** + * Finds a cached PropositionItem by its ID. + */ + private func findPropositionItemById(_ itemId: String) -> PropositionItem? { + return propositionItemCache[itemId] + } + + /** + * Finds a cached parent Proposition by PropositionItem ID. + */ + private func findPropositionByItemId(_ itemId: String) -> Proposition? { + return propositionCache[itemId] + } + + /** + * Clears the PropositionItem cache. + * This should be called when propositions are refreshed or when memory cleanup is needed. + */ + @objc + func clearPropositionItemCache( + withResolver resolve: @escaping RCTPromiseResolveBlock, + withRejecter reject: @escaping RCTPromiseRejectBlock + ) { + propositionItemCache.removeAll() + propositionCache.removeAll() + print("PropositionItem cache cleared") + resolve(nil) + } + + /** + * Gets the current size of the PropositionItem cache. + * Useful for debugging and monitoring. + */ + @objc + func getPropositionItemCacheSize( + withResolver resolve: @escaping RCTPromiseResolveBlock, + withRejecter reject: @escaping RCTPromiseRejectBlock + ) { + resolve(propositionItemCache.count) + } + + /** + * Checks if a PropositionItem exists in the cache. + */ + @objc + func hasPropositionItem( + _ itemId: String, + withResolver resolve: @escaping RCTPromiseResolveBlock, + withRejecter reject: @escaping RCTPromiseRejectBlock + ) { + resolve(propositionItemCache.keys.contains(itemId)) + } + // Messaging Delegate Methods public func onDismiss(message: Showable) { if let fullscreenMessage = message as? FullscreenMessage, diff --git a/packages/messaging/src/Messaging.ts b/packages/messaging/src/Messaging.ts index 25cecdbb3..bea49fa9d 100644 --- a/packages/messaging/src/Messaging.ts +++ b/packages/messaging/src/Messaging.ts @@ -37,6 +37,8 @@ export interface NativeMessagingModule { updatePropositionsForSurfaces: (surfaces: string[]) => void; trackContentCardDisplay: (proposition: MessagingProposition, contentCard: ContentCard) => void; trackContentCardInteraction: (proposition: MessagingProposition, contentCard: ContentCard) => void; + trackPropositionItem: (itemId: string, interaction: string | null, eventType: number, tokens: string[] | null) => void; + generatePropositionInteractionXdm: (itemId: string, interaction: string | null, eventType: number, tokens: string[] | null) => Promise; } const RCTAEPMessaging: NativeModule & NativeMessagingModule = @@ -101,6 +103,33 @@ class Messaging { RCTAEPMessaging.trackContentCardInteraction(proposition, contentCard); } + /** + * Tracks interactions with a PropositionItem using the provided interaction and event type. + * This method is used internally by the PropositionItem.track() method. + * + * @param {string} itemId - The unique identifier of the PropositionItem + * @param {string | null} interaction - A custom string value to be recorded in the interaction + * @param {number} eventType - The MessagingEdgeEventType numeric value + * @param {string[] | null} tokens - Array containing the sub-item tokens for recording interaction + */ + static trackPropositionItem(itemId: string, interaction: string | null, eventType: number, tokens: string[] | null): void { + RCTAEPMessaging.trackPropositionItem(itemId, interaction, eventType, tokens); + } + + /** + * Generates XDM data for PropositionItem interactions. + * This method is used internally by the PropositionItem.generateInteractionXdm() method. + * + * @param {string} itemId - The unique identifier of the PropositionItem + * @param {string | null} interaction - A custom string value to be recorded in the interaction + * @param {number} eventType - The MessagingEdgeEventType numeric value + * @param {string[] | null} tokens - Array containing the sub-item tokens for recording interaction + * @returns {Promise} Promise containing XDM data for the proposition interaction + */ + static async generatePropositionInteractionXdm(itemId: string, interaction: string | null, eventType: number, tokens: string[] | null): Promise { + return await RCTAEPMessaging.generatePropositionInteractionXdm(itemId, interaction, eventType, tokens); + } + /** * Function to set the UI Message delegate to listen the Message lifecycle events. * @returns A function to unsubscribe from all event listeners diff --git a/packages/messaging/src/index.ts b/packages/messaging/src/index.ts index ba54a59d1..6c95f4459 100644 --- a/packages/messaging/src/index.ts +++ b/packages/messaging/src/index.ts @@ -11,25 +11,33 @@ governing permissions and limitations under the License. */ import Messaging from './Messaging'; -import { ContentCard } from './models/ContentCard'; +import { ContentCard, ContentCardData } from './models/ContentCard'; +// import { HTMLProposition, HTMLPropositionData } from './models/HTMLProposition'; import { HTMLProposition } from './models/HTMLProposition'; + import { InAppMessage } from './models/InAppMessage'; import { JSONPropositionItem } from './models/JSONPropositionItem'; +// import { JSONPropositionItem, JSONPropositionItemData } from './models/JSONPropositionItem'; + import Message from './models/Message'; import { MessagingDelegate } from './models/MessagingDelegate'; import MessagingEdgeEventType from './models/MessagingEdgeEventType'; import { MessagingProposition } from './models/MessagingProposition'; import { MessagingPropositionItem } from './models/MessagingPropositionItem'; import { PersonalizationSchema } from './models/PersonalizationSchema'; +import { PropositionItem, PropositionItemData } from './models/PropositionItem'; import { Activity, Characteristics } from './models/ScopeDetails'; export { Activity, Characteristics, ContentCard, + ContentCardData, HTMLProposition, + //HTMLPropositionData, InAppMessage, JSONPropositionItem, + // JSONPropositionItemData, Messaging, Message, MessagingDelegate, @@ -37,4 +45,6 @@ export { MessagingProposition, MessagingPropositionItem, PersonalizationSchema, + PropositionItem, + PropositionItemData, }; diff --git a/packages/messaging/src/models/ContentCard.ts b/packages/messaging/src/models/ContentCard.ts index f732d8f80..bed48fea9 100644 --- a/packages/messaging/src/models/ContentCard.ts +++ b/packages/messaging/src/models/ContentCard.ts @@ -11,11 +11,13 @@ */ import { PersonalizationSchema } from './PersonalizationSchema'; +import MessagingEdgeEventType from './MessagingEdgeEventType'; +import { PropositionItem, PropositionItemData } from './PropositionItem'; type ContentCardTemplate = 'SmallImage'; type DismissButtonStyle = 'circle' | 'none' | 'simple'; -export interface ContentCard { +export interface ContentCardData extends PropositionItemData { id: string; data: { contentType: 'application/json'; @@ -45,3 +47,161 @@ export interface ContentCard { }; schema: PersonalizationSchema.CONTENT_CARD; } + +export class ContentCard extends PropositionItem { + declare data: ContentCardData['data']; // Override data type for better typing + + constructor(contentCardData: ContentCardData) { + super(contentCardData); + this.data = contentCardData.data; + } + + /** + * Convenience method to track when this ContentCard is displayed. + * Equivalent to calling track(MessagingEdgeEventType.DISPLAY). + */ + trackDisplay(): void { + this.track(MessagingEdgeEventType.DISPLAY); + } + + /** + * Convenience method to track when this ContentCard is dismissed. + * + * @param {string | null} interaction - Optional interaction identifier (e.g., "user_dismissed", "auto_dismissed") + */ + trackDismiss(interaction: string | null = null): void { + this.track(interaction, MessagingEdgeEventType.DISMISS, null); + } + + /** + * Convenience method to track user interactions with this ContentCard. + * + * @param {string} interaction - The interaction identifier (e.g., "clicked", "button_pressed", "action_taken") + */ + trackInteraction(interaction: string): void { + this.track(interaction, MessagingEdgeEventType.INTERACT, null); + } + + /** + * Gets the title content of this ContentCard. + * + * @returns {string} The title content + */ + getTitle(): string { + return this.data.content.title.content; + } + + /** + * Gets the body content of this ContentCard. + * + * @returns {string} The body content + */ + getBody(): string { + return this.data.content.body.content; + } + + /** + * Gets the action URL of this ContentCard. + * + * @returns {string} The action URL + */ + getActionUrl(): string { + return this.data.content.actionUrl; + } + + /** + * Gets the image URL of this ContentCard. + * + * @returns {string} The image URL + */ + getImageUrl(): string { + return this.data.content.image.url; + } + + /** + * Gets the image alt text of this ContentCard. + * + * @returns {string} The image alt text + */ + getImageAlt(): string { + return this.data.content.image.alt; + } + + /** + * Gets the buttons array of this ContentCard. + * + * @returns {Array} The buttons array + */ + getButtons(): Array<{ + actionUrl: string; + id: string; + text: { content: string }; + interactId: string; + }> { + return this.data.content.buttons; + } + + /** + * Checks if this ContentCard has been dismissed. + * + * @returns {boolean} True if dismissed, false otherwise + */ + isDismissed(): boolean { + return this.data.meta.dismissState; + } + + /** + * Checks if this ContentCard has been read. + * + * @returns {boolean} True if read, false otherwise + */ + isRead(): boolean { + return this.data.meta.readState; + } + + /** + * Gets the surface name for this ContentCard. + * + * @returns {string} The surface name + */ + getSurface(): string { + return this.data.meta.surface; + } + + /** + * Gets the template type for this ContentCard. + * + * @returns {ContentCardTemplate} The template type + */ + getTemplate(): ContentCardTemplate { + return this.data.meta.adobe.template; + } + + /** + * Gets the expiry date of this ContentCard as epoch seconds. + * + * @returns {number} The expiry date + */ + getExpiryDate(): number { + return this.data.expiryDate; + } + + /** + * Gets the published date of this ContentCard as epoch seconds. + * + * @returns {number} The published date + */ + getPublishedDate(): number { + return this.data.publishedDate; + } + + /** + * Checks if this ContentCard has expired. + * + * @returns {boolean} True if expired, false otherwise + */ + isExpired(): boolean { + const now = Math.floor(Date.now() / 1000); + return this.data.expiryDate > 0 && now > this.data.expiryDate; + } +} diff --git a/packages/messaging/src/models/HTMLProposition.ts b/packages/messaging/src/models/HTMLProposition.ts index c966ed4c4..f84ff25f3 100644 --- a/packages/messaging/src/models/HTMLProposition.ts +++ b/packages/messaging/src/models/HTMLProposition.ts @@ -11,11 +11,144 @@ */ import { PersonalizationSchema } from './PersonalizationSchema'; +import MessagingEdgeEventType from './MessagingEdgeEventType'; +import { PropositionItem, PropositionItemData } from './PropositionItem'; -export interface HTMLProposition { +export interface HTMLPropositionData extends PropositionItemData { id: string; data: { content: string; }; schema: PersonalizationSchema.HTML_CONTENT; } + +export class HTMLProposition extends PropositionItem { + declare data: HTMLPropositionData['data']; // Override data type for better typing + + constructor(htmlPropositionData: HTMLPropositionData) { + super(htmlPropositionData); + this.data = htmlPropositionData.data; + } + + /** + * Convenience method to track when this HTMLProposition is displayed. + * Equivalent to calling track(MessagingEdgeEventType.DISPLAY). + */ + trackDisplay(): void { + this.track(MessagingEdgeEventType.DISPLAY); + } + + /** + * Convenience method to track when this HTMLProposition is dismissed. + * + * @param {string | null} interaction - Optional interaction identifier (e.g., "user_dismissed", "auto_dismissed") + */ + trackDismiss(interaction: string | null = null): void { + this.track(interaction, MessagingEdgeEventType.DISMISS, null); + } + + /** + * Convenience method to track user interactions with this HTMLProposition. + * + * @param {string} interaction - The interaction identifier (e.g., "clicked", "link_pressed", "action_taken") + */ + trackInteraction(interaction: string): void { + this.track(interaction, MessagingEdgeEventType.INTERACT, null); + } + + /** + * Gets the HTML content string of this proposition. + * + * @returns {string} The HTML content + */ + getContent(): string { + return this.data.content; + } + + /** + * Gets the HTML content as a formatted string for display purposes. + * This is an alias for getContent() for consistency with other proposition types. + * + * @returns {string} The HTML content + */ + getHtmlContent(): string { + return this.data.content; + } + + /** + * Checks if the HTML content contains specific elements or patterns. + * + * @param {string} pattern - The pattern to search for (can be a tag, class, id, etc.) + * @returns {boolean} True if the pattern is found in the HTML content + */ + containsPattern(pattern: string): boolean { + return this.data.content.includes(pattern); + } + + /** + * Checks if the HTML content contains interactive elements (buttons, links, forms). + * + * @returns {boolean} True if interactive elements are found + */ + hasInteractiveElements(): boolean { + const interactivePatterns = [' word.length > 0); + return words.length; + } + + /** + * Checks if the HTML content is likely to be a full-page experience. + * This is determined by the presence of certain HTML structure elements. + * + * @returns {boolean} True if this appears to be a full-page HTML experience + */ + isFullPageExperience(): boolean { + const fullPageIndicators = [' this.data.content.includes(indicator)); + } + + /** + * Extracts all link URLs from the HTML content. + * + * @returns {string[]} Array of URLs found in href attributes + */ + extractLinks(): string[] { + const linkRegex = /href\s*=\s*["']([^"']+)["']/gi; + const links: string[] = []; + let match; + + while ((match = linkRegex.exec(this.data.content)) !== null) { + links.push(match[1]); + } + + return links; + } + + /** + * Extracts all image URLs from the HTML content. + * + * @returns {string[]} Array of image URLs found in src attributes + */ + extractImages(): string[] { + const imageRegex = /src\s*=\s*["']([^"']+)["']/gi; + const images: string[] = []; + let match; + + while ((match = imageRegex.exec(this.data.content)) !== null) { + images.push(match[1]); + } + + return images; + } +} diff --git a/packages/messaging/src/models/JSONPropositionItem.ts b/packages/messaging/src/models/JSONPropositionItem.ts index 8ae5dfa28..3d17573e6 100644 --- a/packages/messaging/src/models/JSONPropositionItem.ts +++ b/packages/messaging/src/models/JSONPropositionItem.ts @@ -11,11 +11,43 @@ */ import { PersonalizationSchema } from './PersonalizationSchema'; +import { PropositionItem, PropositionItemData } from './PropositionItem'; -export interface JSONPropositionItem { +export interface JSONPropositionItemData extends PropositionItemData { id: string; data: { content: string; }; schema: PersonalizationSchema.JSON_CONTENT; } + +export class JSONPropositionItem extends PropositionItem { + declare data: JSONPropositionItemData['data']; // Override data type for better typing + + constructor(jsonPropositionItemData: JSONPropositionItemData) { + super(jsonPropositionItemData); + this.data = jsonPropositionItemData.data; + } + + /** + * Gets the JSON content string of this proposition item. + * + * @returns {string} The JSON content + */ + getContent(): string { + return this.data.content; + } + + /** + * Attempts to parse the content as JSON. + * + * @returns {object | null} Parsed JSON object or null if parsing fails + */ + getParsedContent(): object | null { + try { + return JSON.parse(this.data.content); + } catch (error) { + return null; + } + } +} diff --git a/packages/messaging/src/models/MessagingPropositionItem.ts b/packages/messaging/src/models/MessagingPropositionItem.ts index 65d418660..06b385cfa 100644 --- a/packages/messaging/src/models/MessagingPropositionItem.ts +++ b/packages/messaging/src/models/MessagingPropositionItem.ts @@ -14,9 +14,13 @@ import { ContentCard } from './ContentCard'; import { HTMLProposition } from './HTMLProposition'; import { JSONPropositionItem } from './JSONPropositionItem'; import { InAppMessage } from './InAppMessage'; +import { PropositionItem } from './PropositionItem'; +// Union type for all possible proposition item types +// All items now extend PropositionItem and have unified tracking capabilities export type MessagingPropositionItem = | ContentCard | HTMLProposition | InAppMessage - | JSONPropositionItem; + | JSONPropositionItem + | PropositionItem; // Base PropositionItem for any other schema types diff --git a/packages/messaging/src/models/PropositionItem.ts b/packages/messaging/src/models/PropositionItem.ts new file mode 100644 index 000000000..c1c55bdf4 --- /dev/null +++ b/packages/messaging/src/models/PropositionItem.ts @@ -0,0 +1,245 @@ +/* + Copyright 2024 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 { PersonalizationSchema } from './PersonalizationSchema'; +import MessagingEdgeEventType from './MessagingEdgeEventType'; + +const RCTAEPMessaging = NativeModules.AEPMessaging; + +/** + * Base PropositionItem interface that all proposition items implement + */ +export interface PropositionItemData { + id: string; + schema: PersonalizationSchema; + data: { + [key: string]: any; + }; +} + +/** + * A PropositionItem represents a personalization JSON object returned by Konductor. + * This is the base class that provides tracking functionality for all proposition items + * including ContentCards, InApp messages, and code-based experiences. + * + * This mirrors the native Android PropositionItem class functionality. + */ +export class PropositionItem { + id: string; + schema: PersonalizationSchema; + data: { [key: string]: any }; + + constructor(propositionItemData: PropositionItemData) { + this.id = propositionItemData.id; + this.schema = propositionItemData.schema; + this.data = propositionItemData.data; + } + + /** + * Gets the PropositionItem identifier. + * + * @returns {string} The PropositionItem identifier + */ + getItemId(): string { + return this.id; + } + + /** + * Gets the PropositionItem content schema. + * + * @returns {PersonalizationSchema} The PropositionItem content schema + */ + getSchema(): PersonalizationSchema { + return this.schema; + } + + /** + * Gets the PropositionItem data. + * + * @returns {object} The PropositionItem data + */ + getItemData(): { [key: string]: any } { + return this.data; + } + + /** + * Tracks interaction with this proposition item. + * This is the core tracking method that all proposition items use. + * + * @param {MessagingEdgeEventType} eventType - The MessagingEdgeEventType specifying event type for the interaction + * + * @example + * propositionItem.track(MessagingEdgeEventType.DISPLAY); + */ + track(eventType: MessagingEdgeEventType): void; + + /** + * Tracks interaction with this proposition item. + * + * @param {string | null} interaction - String describing the interaction + * @param {MessagingEdgeEventType} eventType - The MessagingEdgeEventType specifying event type for the interaction + * @param {string[] | null} tokens - Array containing the sub-item tokens for recording interaction + * + * @example + * // Track display + * propositionItem.track(null, MessagingEdgeEventType.DISPLAY, null); + * + * // Track interaction + * propositionItem.track("button_clicked", MessagingEdgeEventType.INTERACT, null); + * + * // Track with tokens + * propositionItem.track("click", MessagingEdgeEventType.INTERACT, ["token1", "token2"]); + */ + track(interaction: string | null, eventType: MessagingEdgeEventType, tokens: string[] | null): void; + + // Implementation + track( + interactionOrEventType: string | null | MessagingEdgeEventType, + eventType?: MessagingEdgeEventType, + tokens?: string[] | null + ): void { + console.log('i am in track method '); + // Handle overloaded method signatures + if (typeof interactionOrEventType === 'number' && eventType === undefined) { + // First overload: track(eventType) + console.log('track(eventType) here ia m doing awesome '); + this.trackWithDetails(null, interactionOrEventType, null); + } else if (typeof interactionOrEventType === 'string' || interactionOrEventType === null) { + // Second overload: track(interaction, eventType, tokens) + console.log("i am in the second overload"); + this.trackWithDetails(interactionOrEventType, eventType!, tokens || null); + } + } + + /** + * Internal method that performs the actual tracking + */ + private trackWithDetails(interaction: string | null, eventType: MessagingEdgeEventType, tokens: string[] | null): void { + RCTAEPMessaging.trackPropositionItem(this.id, interaction, eventType, tokens); + } + + /** + * Creates a Map containing XDM data for interaction with this proposition item. + * + * @param {MessagingEdgeEventType} eventType - The MessagingEdgeEventType specifying event type for the interaction + * @returns {Promise} Promise containing XDM data for the proposition interaction + */ + async generateInteractionXdm(eventType: MessagingEdgeEventType): Promise; + + /** + * Creates a Map containing XDM data for interaction with this proposition item. + * + * @param {string | null} interaction - Custom string describing the interaction + * @param {MessagingEdgeEventType} eventType - The MessagingEdgeEventType specifying event type for the interaction + * @param {string[] | null} tokens - Array containing the sub-item tokens for recording interaction + * @returns {Promise} Promise containing XDM data for the proposition interaction + */ + async generateInteractionXdm( + interaction: string | null, + eventType: MessagingEdgeEventType, + tokens: string[] | null + ): Promise; + + // Implementation + async generateInteractionXdm( + interactionOrEventType: string | null | MessagingEdgeEventType, + eventType?: MessagingEdgeEventType, + tokens?: string[] | null + ): Promise { + // Handle overloaded method signatures + if (typeof interactionOrEventType === 'number' && eventType === undefined) { + // First overload: generateInteractionXdm(eventType) + return await RCTAEPMessaging.generatePropositionInteractionXdm(this.id, null, interactionOrEventType, null); + } else if (typeof interactionOrEventType === 'string' || interactionOrEventType === null) { + // Second overload: generateInteractionXdm(interaction, eventType, tokens) + return await RCTAEPMessaging.generatePropositionInteractionXdm(this.id, interactionOrEventType, eventType!, tokens || null); + } + throw new Error('Invalid arguments for generateInteractionXdm'); + } + + /** + * Returns this PropositionItem's content as a JSON content Map. + * Only works if the schema is JSON_CONTENT. + * + * @returns {object | null} Object containing the PropositionItem's content, or null if not JSON content + */ + getJsonContentMap(): object | null { + if (this.schema !== PersonalizationSchema.JSON_CONTENT) { + return null; + } + return this.data.content || null; + } + + /** + * Returns this PropositionItem's content as a JSON content array. + * Only works if the schema is JSON_CONTENT. + * + * @returns {any[] | null} Array containing the PropositionItem's content, or null if not JSON content array + */ + getJsonContentArrayList(): any[] | null { + if (this.schema !== PersonalizationSchema.JSON_CONTENT) { + return null; + } + const content = this.data.content; + return Array.isArray(content) ? content : null; + } + + /** + * Returns this PropositionItem's content as HTML content string. + * Only works if the schema is HTML_CONTENT. + * + * @returns {string | null} String containing the PropositionItem's content, or null if not HTML content + */ + getHtmlContent(): string | null { + if (this.schema !== PersonalizationSchema.HTML_CONTENT) { + return null; + } + return this.data.content || null; + } + + /** + * Checks if this PropositionItem is of ContentCard schema type. + * + * @returns {boolean} True if this is a content card proposition item + */ + isContentCard(): boolean { + return this.schema === PersonalizationSchema.CONTENT_CARD; + } + + /** + * Checks if this PropositionItem is of InApp schema type. + * + * @returns {boolean} True if this is an in-app message proposition item + */ + isInAppMessage(): boolean { + return this.schema === PersonalizationSchema.IN_APP; + } + + /** + * Checks if this PropositionItem is of JSON content schema type. + * + * @returns {boolean} True if this is a JSON content proposition item + */ + isJsonContent(): boolean { + return this.schema === PersonalizationSchema.JSON_CONTENT; + } + + /** + * Checks if this PropositionItem is of HTML content schema type. + * + * @returns {boolean} True if this is an HTML content proposition item + */ + isHtmlContent(): boolean { + return this.schema === PersonalizationSchema.HTML_CONTENT; + } +} \ No newline at end of file From af236621e514d850182e899f1deaae147797ba2b Mon Sep 17 00:00:00 2001 From: Naman Arora Date: Mon, 18 Aug 2025 20:36:59 +0530 Subject: [PATCH 02/28] proposition item uuid added --- ITEMID_FLOW_EXAMPLE.md | 231 ---------- .../app/MessagingView.tsx | 108 ++++- .../PROPOSITION_ITEM_TRACKING_EXAMPLE.md | 427 ------------------ .../messaging/RCTAEPMessagingModule.java | 235 ++++++++-- .../messaging/RCTAEPMessagingUtil.java | 128 +++++- .../messaging/src/models/HTMLProposition.ts | 154 ------- .../messaging/src/models/PropositionItem.ts | 5 +- 7 files changed, 418 insertions(+), 870 deletions(-) delete mode 100644 ITEMID_FLOW_EXAMPLE.md delete mode 100644 packages/messaging/PROPOSITION_ITEM_TRACKING_EXAMPLE.md delete mode 100644 packages/messaging/src/models/HTMLProposition.ts diff --git a/ITEMID_FLOW_EXAMPLE.md b/ITEMID_FLOW_EXAMPLE.md deleted file mode 100644 index 9cc3628c6..000000000 --- a/ITEMID_FLOW_EXAMPLE.md +++ /dev/null @@ -1,231 +0,0 @@ -# ItemId Flow Example: User Code → Native Implementation - -This example demonstrates how the `itemId` flows from user React Native code down to the native `trackPropositionItem` method. - -## Step-by-Step Flow - -### **Step 1: User Retrieves Propositions** - -```typescript -import { Messaging } from '@adobe/react-native-aepmessaging'; - -// User calls this to get propositions -const propositions = await Messaging.getPropositionsForSurfaces(['homepage', 'product-page']); -``` - -### **Step 2: Native Response (What Gets Returned)** - -The native code returns proposition data like this: - -```typescript -// Example response structure -{ - "homepage": [ - { - "uniqueId": "AT:eyJhY3Rpdml0eUlkIjoiMTM3NTg5IiwiZXhwZXJpZW5jZUlkIjoiMCJ9", - "items": [ - { - "id": "xcore:personalized-offer:124e36a756c8", // <- This is the itemId - "schema": "https://ns.adobe.com/personalization/content-card", - "data": { - "content": { - "title": "Summer Sale!", - "body": "Get 50% off on summer collection", - "imageUrl": "https://example.com/summer.jpg" - } - } - } - ], - "scope": { - "surface": "homepage" - } - } - ], - "product-page": [ - { - "uniqueId": "AT:eyJhY3Rpdml0eUlkIjoiMTM3NTkwIiwiZXhwZXJpZW5jZUlkIjoiMCJ9", - "items": [ - { - "id": "xcore:personalized-offer:789xyz456def", // <- Another itemId - "schema": "https://ns.adobe.com/personalization/html-content-item", - "data": { - "content": "
Special offer for this product!
", - "format": "text/html" - } - } - ], - "scope": { - "surface": "product-page" - } - } - ] -} -``` - -### **Step 3: Native Caching (Happens Automatically)** - -When the propositions are retrieved, the native code automatically caches the PropositionItems: - -**Android Cache State After Step 2:** -```java -// propositionItemCache contents: -{ - "xcore:personalized-offer:124e36a756c8" -> PropositionItem(id="xcore:personalized-offer:124e36a756c8", schema=CONTENT_CARD, ...), - "xcore:personalized-offer:789xyz456def" -> PropositionItem(id="xcore:personalized-offer:789xyz456def", schema=HTML_CONTENT, ...) -} - -// propositionCache contents: -{ - "xcore:personalized-offer:124e36a756c8" -> Proposition(uniqueId="AT:eyJhY3Rpdml0eUlkIjoiMTM3NTg5IiwiZXhwZXJpZW5jZUlkIjoiMCJ9", ...), - "xcore:personalized-offer:789xyz456def" -> Proposition(uniqueId="AT:eyJhY3Rpdml0eUlkIjoiMTM3NTkwIiwiZXhwZXJpZW5jZUlkIjoiMCJ9", ...) -} -``` - -### **Step 4: User Creates Typed Objects** - -```typescript -import { ContentCard, HTMLProposition } from '@adobe/react-native-aepmessaging'; - -// User creates ContentCard from homepage proposition -const homepageItems = propositions['homepage'][0].items; -const contentCard = new ContentCard({ - id: homepageItems[0].id, // "xcore:personalized-offer:124e36a756c8" - schema: homepageItems[0].schema, - data: homepageItems[0].data, - // ... other fields -}); - -// User creates HTMLProposition from product-page proposition -const productPageItems = propositions['product-page'][0].items; -const htmlProposition = new HTMLProposition({ - id: productPageItems[0].id, // "xcore:personalized-offer:789xyz456def" - schema: productPageItems[0].schema, - data: productPageItems[0].data, - // ... other fields -}); -``` - -### **Step 5: User Calls Tracking Methods** - -```typescript -// User tracks content card display -contentCard.trackDisplay(); - -// User tracks HTML proposition interaction -htmlProposition.trackInteraction("view-details"); -``` - -### **Step 6: React Native → Native Method Calls** - -When `contentCard.trackDisplay()` is called: - -```typescript -// Inside ContentCard class (extends PropositionItem) -trackDisplay(): void { - this.track(MessagingEdgeEventType.DISPLAY); -} - -// Inside PropositionItem base class -track(eventType: MessagingEdgeEventType): void { - this.trackWithDetails(null, eventType, null); -} - -private trackWithDetails(interaction: string | null, eventType: MessagingEdgeEventType, tokens: string[] | null): void { - // this.id = "xcore:personalized-offer:124e36a756c8" - RCTAEPMessaging.trackPropositionItem(this.id, interaction, eventType, tokens); - // ^^^^^^^ - // itemId passed to native! -} -``` - -### **Step 7: Native Method Execution** - -**Android:** -```java -@ReactMethod -public void trackPropositionItem(String itemId, @Nullable String interaction, int eventType, @Nullable ReadableArray tokens) { - // itemId = "xcore:personalized-offer:124e36a756c8" - // interaction = null - // eventType = 4 (MessagingEdgeEventType.DISPLAY) - // tokens = null - - try { - MessagingEdgeEventType edgeEventType = RCTAEPMessagingUtil.getEventType(eventType); - // edgeEventType = MessagingEdgeEventType.DISPLAY - - PropositionItem propositionItem = findPropositionItemById(itemId); - // Looks up "xcore:personalized-offer:124e36a756c8" in propositionItemCache - - if (propositionItem != null) { - // Found the cached ContentCard PropositionItem! - propositionItem.track(edgeEventType); - // Calls the native Android SDK PropositionItem.track(MessagingEdgeEventType.DISPLAY) - - Log.debug(TAG, "Successfully tracked PropositionItem: " + itemId + " with eventType: " + edgeEventType); - } else { - Log.warning(TAG, "PropositionItem not found for ID: " + itemId); - } - } catch (Exception e) { - Log.error(TAG, "Error tracking PropositionItem: " + itemId, e); - } -} - -private PropositionItem findPropositionItemById(String itemId) { - return propositionItemCache.get(itemId); - // Returns the cached PropositionItem for "xcore:personalized-offer:124e36a756c8" -} -``` - -## Key Points - -### **1. ItemId Origin** -The `itemId` comes from the Adobe Experience Edge response and is included in the proposition data structure. - -### **2. No Manual ID Management** -Users don't need to manually manage or remember IDs - they're automatically included in the proposition data. - -### **3. Automatic Caching** -The native code automatically caches PropositionItems by their ID when propositions are first retrieved. - -### **4. Transparent to User** -The user just calls `contentCard.track()` - the ID handling is transparent. - -### **5. Type Safety** -The TypeScript classes ensure the correct `itemId` is always passed to the native methods. - -## Error Scenarios - -### **What if itemId is not found in cache?** - -```java -// Android -if (propositionItem == null) { - Log.warning(TAG, "PropositionItem not found for ID: " + itemId); - return; // Gracefully handle - no crash -} -``` - -```swift -// iOS -guard let propositionItem = findPropositionItemById(itemId) else { - print("Warning: PropositionItem not found for ID: \(itemId)") - resolve(nil) // Gracefully handle - no crash - return -} -``` - -### **Common causes:** -1. Propositions were never retrieved via `getPropositionsForSurfaces()` -2. Cache was cleared -3. PropositionItem was created with incorrect/invalid data - -## Summary - -The `itemId` flows naturally through the system: -1. **Adobe Edge** → generates unique IDs for each PropositionItem -2. **Native SDK** → receives propositions with IDs, caches by ID -3. **React Native** → receives proposition data including IDs -4. **User Code** → creates objects using the IDs from proposition data -5. **Tracking** → uses the ID to look up cached native objects - -The system is designed so users never need to manually handle or remember IDs - they're embedded in the objects and flow transparently through the tracking system. \ No newline at end of file diff --git a/apps/AEPSampleAppNewArchEnabled/app/MessagingView.tsx b/apps/AEPSampleAppNewArchEnabled/app/MessagingView.tsx index 754ae6812..86ea557e6 100644 --- a/apps/AEPSampleAppNewArchEnabled/app/MessagingView.tsx +++ b/apps/AEPSampleAppNewArchEnabled/app/MessagingView.tsx @@ -17,12 +17,13 @@ import { Messaging, PersonalizationSchema, MessagingEdgeEventType, - PropositionItem // Add this import + PropositionItem, // Add this import + Message } 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 = ['android-cbe-preview', 'android-cc', 'android-cc-naman']; const SURFACES_WITH_CONTENT_CARDS = ['android-cc']; const messagingExtensionVersion = async () => { @@ -62,6 +63,8 @@ const updatePropositionsForSurfaces = async () => { const getCachedMessages = async () => { const messages = await Messaging.getCachedMessages(); + const newMessage = new Message(messages[0]); + newMessage.track("button_clicked", MessagingEdgeEventType.INTERACT); console.log('Cached messages:', messages); }; @@ -167,8 +170,8 @@ const unifiedTrackingExample = async () => { for (const proposition of propositions) { for (const propositionItemData of proposition.items) { // Create PropositionItem instance from the plain data object - const propositionItem = new PropositionItem(propositionItemData); - console.log('propositionItem here is created:', propositionItem); + const propositionItem = new PropositionItem(propositionItemData); + console.log('propositionItem here is created:', propositionItem, propositionItem.uuid); // Use the unified tracking approach via PropositionItem if (propositionItem.schema === PersonalizationSchema.CONTENT_CARD) { @@ -225,6 +228,91 @@ const unifiedTrackingExample = async () => { // } // } +// Function to track in-app message interactions +const trackInAppMessage = async () => { + try { + // Get cached messages first + const messages = await Messaging.getCachedMessages(); + console.log('Cached messages:', messages); + + if (messages && messages.length > 0) { + const message = messages[0]; // Get first message + + // Call track on the message instance + // This calls the Java track(messageId, interaction, eventType) method + message.track("button_clicked", MessagingEdgeEventType.INTERACT); + + console.log(`Tracked interaction on message: ${message.id}`); + } else { + console.log('No cached messages available to track'); + } + } catch (error) { + console.error('Error tracking in-app message:', error); + } +}; + +// Function to track proposition items using cached approach +const trackPropositionItems = async () => { + try { + // Get propositions - this automatically caches them + const propositions = await Messaging.getPropositionsForSurfaces(['android-cc', 'homepage', 'android-cbe-preview']); + console.log('Retrieved propositions:', propositions); + + // Iterate through surfaces + for (const [surface, propositionList] of Object.entries(propositions)) { + console.log(`Processing surface: ${surface}`); + + // Iterate through propositions for this surface + for (const proposition of propositionList) { + console.log(`Processing proposition: ${proposition.id}`); + + // Iterate through items in the proposition + for (const itemData of proposition.items) { + // Create PropositionItem instance (this gets cached automatically) + const propositionItem = new PropositionItem(itemData); + + // Track display event + propositionItem.track(MessagingEdgeEventType.DISPLAY); + console.log(`Tracked display for item: ${propositionItem.id}`); + + // Track interaction event + propositionItem.track("user_clicked", MessagingEdgeEventType.INTERACT, null); + console.log(`Tracked interaction for item: ${propositionItem.id}`); + + // Track with tokens (for embedded decisions) + if (itemData.data?.tokens) { + const tokens = itemData.data.tokens; // Extract from your data + propositionItem.track("token_interaction", MessagingEdgeEventType.INTERACT, tokens); + console.log(`Tracked with tokens for item: ${propositionItem.id}`); + } + } + } + } + } catch (error) { + console.error('Error tracking proposition items:', error); + } +}; + +// Direct static method call (bypasses caching) +const trackDirectly = () => { + // This calls trackPropositionItem directly without creating instances + Messaging.trackPropositionItem( + "item_123", // itemId + "direct_call", // interaction + MessagingEdgeEventType.INTERACT, // eventType + ["token1", "token2"] // tokens + ); + + console.log('Tracked directly via static method'); +}; + +const trackEdgeCaseMessageWithPropositionItem = async () => { + const messages = await Messaging.getCachedMessages(); + const newMessage = new PropositionItem({id: "12", autoTrack: true}); + newMessage.track("button_clicked", MessagingEdgeEventType.INTERACT); + console.log('Cached messages:', messages); +} + function MessagingView() { const router = useRouter(); @@ -252,6 +340,18 @@ function MessagingView() { - Terms & Conditions - - ` - }, - schema: PersonalizationSchema.HTML_CONTENT -}; - -const htmlProposition = new HTMLProposition(htmlData); - -// Same tracking methods available as other proposition items -htmlProposition.track(MessagingEdgeEventType.DISPLAY); -htmlProposition.track("banner_viewed", MessagingEdgeEventType.INTERACT, null); - -// HTMLProposition-specific methods -console.log(htmlProposition.hasInteractiveElements()); // true (has button and link) -console.log(htmlProposition.isFullPageExperience()); // false (just a banner) -console.log(htmlProposition.extractLinks()); // ["https://example.com/terms"] -console.log(htmlProposition.getWordCount()); // Approximately 12 words - -// Convenience tracking methods -htmlProposition.trackDisplay(); // Track when banner is shown -htmlProposition.trackInteraction("button_clicked"); // Track button clicks -htmlProposition.trackDismiss("user_closed"); // Track when user closes banner -``` - -## Advanced Tracking Features - -### XDM Data Generation - -```typescript -// Generate XDM data for analytics (mirrors native functionality) -const xdmData = await contentCard.generateInteractionXdm(MessagingEdgeEventType.DISPLAY); -console.log(xdmData); // XDM-formatted tracking data - -// With full parameters -const xdmDataWithTokens = await contentCard.generateInteractionXdm( - "button_clicked", - MessagingEdgeEventType.INTERACT, - ["token1", "token2"] -); -``` - -### Generic PropositionItem Handling - -```typescript -// Function that can handle any proposition item type -function trackPropositionDisplay(item: PropositionItem) { - if (item.isContentCard()) { - console.log("Tracking ContentCard display"); - item.track(MessagingEdgeEventType.DISPLAY); - } else if (item.isJsonContent()) { - console.log("Tracking JSON content display"); - item.track("json_content_viewed", MessagingEdgeEventType.DISPLAY, null); - } else if (item.isHtmlContent()) { - console.log("Tracking HTML content display"); - item.track(MessagingEdgeEventType.DISPLAY); - } else { - console.log("Tracking unknown proposition type display"); - item.track(MessagingEdgeEventType.DISPLAY); - } -} - -// Works with any proposition item type -trackPropositionDisplay(contentCard); -trackPropositionDisplay(jsonPropositionItem); -trackPropositionDisplay(htmlProposition); -``` - -## React Native Component Integration - -### Universal Proposition Item Component - -```typescript -import React, { useEffect } from 'react'; -import { View, Text, TouchableOpacity } from 'react-native'; -import { PropositionItem, MessagingEdgeEventType } from '@adobe/react-native-aepMessaging'; - -interface PropositionItemComponentProps { - propositionItem: PropositionItem; - onInteraction?: (interaction: string) => void; -} - -const PropositionItemComponent: React.FC = ({ - propositionItem, - onInteraction -}) => { - // Track display when component mounts - useEffect(() => { - propositionItem.track(MessagingEdgeEventType.DISPLAY); - }, [propositionItem]); - - const handlePress = () => { - const interaction = propositionItem.isContentCard() ? "card_clicked" : "content_clicked"; - propositionItem.track(interaction, MessagingEdgeEventType.INTERACT, null); - onInteraction?.(interaction); - }; - - const renderContent = () => { - if (propositionItem.isContentCard()) { - const contentCard = propositionItem as ContentCard; - return ( - - - {contentCard.getTitle()} - - {contentCard.getBody()} - - ); - } else if (propositionItem.isJsonContent()) { - const jsonItem = propositionItem as JSONPropositionItem; - const content = jsonItem.getParsedContent(); - return ( - - JSON Content: {JSON.stringify(content)} - - ); - } else if (propositionItem.isHtmlContent()) { - const htmlItem = propositionItem as HTMLProposition; - return ( - - HTML Content - {htmlItem.getContent()} - {htmlItem.hasInteractiveElements() && ( - - ⚡ Interactive content available - - )} - - ); - } - return Unknown content type; - }; - - return ( - - {renderContent()} - - ); -}; -``` - -### Proposition Management Hook - -```typescript -import { useState, useEffect } from 'react'; -import { PropositionItem, MessagingEdgeEventType } from '@adobe/react-native-aepMessaging'; - -export const usePropositionTracking = (items: PropositionItem[]) => { - const [viewedItems, setViewedItems] = useState>(new Set()); - - const trackItemDisplay = (item: PropositionItem) => { - if (!viewedItems.has(item.getItemId())) { - item.track(MessagingEdgeEventType.DISPLAY); - setViewedItems(prev => new Set(prev).add(item.getItemId())); - } - }; - - const trackItemInteraction = (item: PropositionItem, interaction: string) => { - item.track(interaction, MessagingEdgeEventType.INTERACT, null); - }; - - const trackItemDismiss = (item: PropositionItem, reason: string = "user_dismissed") => { - item.track(reason, MessagingEdgeEventType.DISMISS, null); - }; - - return { - trackItemDisplay, - trackItemInteraction, - trackItemDismiss, - viewedItems - }; -}; -``` - -## Native Implementation Requirements - -To support this unified approach, you'll need to implement these methods in your native modules: - -### Android Implementation - -```java -@ReactMethod -public void trackPropositionItem(String itemId, @Nullable String interaction, int eventType, @Nullable ReadableArray tokens) { - MessagingEdgeEventType edgeEventType = MessagingEdgeEventType.values()[eventType]; - - // Find the PropositionItem by ID (you'll need to maintain a mapping) - PropositionItem propositionItem = findPropositionItemById(itemId); - - if (propositionItem != null) { - List tokenList = tokens != null ? tokens.toArrayList().stream() - .map(Object::toString) - .collect(Collectors.toList()) : null; - - if (interaction != null && tokenList != null) { - propositionItem.track(interaction, edgeEventType, tokenList); - } else if (interaction != null) { - propositionItem.track(interaction, edgeEventType, null); - } else { - propositionItem.track(edgeEventType); - } - } -} - -@ReactMethod -public void generatePropositionInteractionXdm(String itemId, @Nullable String interaction, int eventType, @Nullable ReadableArray tokens, Promise promise) { - MessagingEdgeEventType edgeEventType = MessagingEdgeEventType.values()[eventType]; - PropositionItem propositionItem = findPropositionItemById(itemId); - - if (propositionItem != null) { - List tokenList = tokens != null ? tokens.toArrayList().stream() - .map(Object::toString) - .collect(Collectors.toList()) : null; - - Map xdmData = propositionItem.generateInteractionXdm(interaction, edgeEventType, tokenList); - promise.resolve(Arguments.makeNativeMap(xdmData)); - } else { - promise.reject("PropositionItem not found", "No PropositionItem found with ID: " + itemId); - } -} -``` - -### iOS Implementation - -```objc -RCT_EXPORT_METHOD(trackPropositionItem:(NSString *)itemId - interaction:(NSString * _Nullable)interaction - eventType:(NSInteger)eventType - tokens:(NSArray * _Nullable)tokens) { - AEPMessagingEdgeEventType edgeEventType = (AEPMessagingEdgeEventType)eventType; - AEPPropositionItem *propositionItem = [self findPropositionItemById:itemId]; - - if (propositionItem) { - if (interaction && tokens) { - [propositionItem track:interaction eventType:edgeEventType tokens:tokens]; - } else if (interaction) { - [propositionItem track:interaction eventType:edgeEventType]; - } else { - [propositionItem track:edgeEventType]; - } - } -} - -RCT_EXPORT_METHOD(generatePropositionInteractionXdm:(NSString *)itemId - interaction:(NSString * _Nullable)interaction - eventType:(NSInteger)eventType - tokens:(NSArray * _Nullable)tokens - resolve:(RCTPromiseResolveBlock)resolve - reject:(RCTPromiseRejectBlock)reject) { - AEPMessagingEdgeEventType edgeEventType = (AEPMessagingEdgeEventType)eventType; - AEPPropositionItem *propositionItem = [self findPropositionItemById:itemId]; - - if (propositionItem) { - NSDictionary *xdmData = [propositionItem generateInteractionXdm:interaction eventType:edgeEventType tokens:tokens]; - resolve(xdmData); - } else { - reject(@"PropositionItem not found", [NSString stringWithFormat:@"No PropositionItem found with ID: %@", itemId], nil); - } -} -``` - -## Benefits of This Approach - -1. **Unified Architecture**: Mirrors the native Android/iOS SDK structure -2. **Consistent API**: Same tracking methods across all proposition types -3. **Extensibility**: Easy to add new proposition item types -4. **Type Safety**: Strong TypeScript typing with inheritance -5. **Code Reuse**: Common tracking logic shared across all types -6. **Future-Proof**: Scalable for additional Adobe Journey Optimizer features - -## Migration from Previous Implementation - -```typescript -// Old approach (if you had any HTML-specific tracking) -// No previous HTML tracking methods existed - -// New unified approach (recommended for all proposition types) -const contentCard = new ContentCard(contentCardData); -contentCard.track(MessagingEdgeEventType.DISPLAY); -contentCard.track("user_interaction", MessagingEdgeEventType.INTERACT, null); - -// For JSON code-based experiences -const jsonItem = new JSONPropositionItem(jsonData); -jsonItem.track(MessagingEdgeEventType.DISPLAY); -jsonItem.track("offer_clicked", MessagingEdgeEventType.INTERACT, ["offer_token"]); - -// For HTML code-based experiences (NEW) -const htmlItem = new HTMLProposition(htmlData); -htmlItem.track(MessagingEdgeEventType.DISPLAY); -htmlItem.track("banner_interaction", MessagingEdgeEventType.INTERACT, null); - -// Universal approach - works with any proposition type -function trackAnyProposition(item: PropositionItem, interaction?: string) { - item.track(MessagingEdgeEventType.DISPLAY); - if (interaction) { - item.track(interaction, MessagingEdgeEventType.INTERACT, null); - } -} - -// Works seamlessly across all types -trackAnyProposition(contentCard); -trackAnyProposition(jsonItem, "json_viewed"); -trackAnyProposition(htmlItem, "html_viewed"); -``` - -This approach provides a much more scalable and maintainable solution that aligns perfectly with the native SDK architecture! \ No newline at end of file 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 d9213d221..a45d07daf 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 @@ -42,6 +42,8 @@ import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.ReadableType; +import com.facebook.react.bridge.WritableArray; import com.facebook.react.bridge.WritableMap; import com.facebook.react.modules.core.DeviceEventManagerModule; import java.util.ArrayList; @@ -50,10 +52,44 @@ import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ConcurrentHashMap; +import org.json.JSONObject; + + +import java.nio.charset.StandardCharsets; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicLong; + + public final class RCTAEPMessagingModule extends ReactContextBaseJavaModule implements PresentationDelegate { + private final AtomicLong globalUuidCounter = new AtomicLong(0L); + private final Map propositionItemByUuid = new ConcurrentHashMap<>(); + public void registerPropositionItemUuid(@NonNull final String uuid, @NonNull final PropositionItem item) { + if (uuid != null && item != null) { + propositionItemByUuid.put(uuid, item); + } + } + private String generateItemUuid(String activityId, long counter) { + String key = (activityId != null ? activityId : "") + "#" + counter; + return UUID.nameUUIDFromBytes(key.getBytes(StandardCharsets.UTF_8)).toString(); + } + @SuppressWarnings("unchecked") + private String extractActivityId(Proposition proposition) { + try { + Map eventData = proposition.toEventData(); + if (eventData == null) return null; + Object sd = eventData.get("scopeDetails"); + if (!(sd instanceof Map)) return null; + Object act = ((Map) sd).get("activity"); + if (!(act instanceof Map)) return null; + Object id = ((Map) act).get("id"); + return (id instanceof String) ? (String) id : null; + } catch (Exception e) { + return null; + } + } private static final String TAG = "RCTAEPMessagingModule"; private final Map messageCache = new HashMap<>(); private final ReactApplicationContext reactContext; @@ -105,36 +141,133 @@ public void getLatestMessage(final Promise promise) { } @ReactMethod - public void getPropositionsForSurfaces(ReadableArray surfaces, - final Promise promise) { - String bundleId = this.reactContext.getPackageName(); + public void getPropositionsForSurfaces(ReadableArray surfaces, final Promise promise) { + final String bundleId = this.reactContext.getPackageName(); + Messaging.getPropositionsForSurfaces( - RCTAEPMessagingUtil.convertSurfaces(surfaces), - new AdobeCallbackWithError>>() { - @Override - public void fail(final AdobeError adobeError) { - promise.reject(adobeError.getErrorName(), - "Unable to get Propositions"); - } + RCTAEPMessagingUtil.convertSurfaces(surfaces), + new AdobeCallbackWithError>>() { + @Override + public void fail(final AdobeError adobeError) { + Log.d(TAG, "getPropositionsForSurfaces: fail: " + adobeError.getErrorName()); + promise.reject(adobeError.getErrorName(), "Unable to get Propositions"); + } - @Override - public void call( - Map> propositionsMap) { - - // Cache PropositionItems for unified tracking when propositions are retrieved - for (Map.Entry> entry : propositionsMap.entrySet()) { - List propositions = entry.getValue(); - if (propositions != null) { - cachePropositionsItems(propositions); + @Override + public void call(Map> propositionsMap) { + try { + // Optional: clear UUID cache per fetch to avoid stale entries + propositionItemByUuid.clear(); + + WritableMap out = Arguments.createMap(); + + for (Map.Entry> entry : propositionsMap.entrySet()) { + final String surfaceKey = (entry.getKey() != null) + ? entry.getKey().getUri().replace("mobileapp://" + bundleId + "/", "") + : "unknown"; + final List propositions = entry.getValue(); + final WritableArray jsProps = Arguments.createArray(); + + Log.d(TAG, "Surface [" + surfaceKey + "] propCount=" + (propositions != null ? propositions.size() : 0)); + + if (propositions != null) { + for (Proposition p : propositions) { + try { + // Start from SDK map for proposition to keep full parity + WritableMap pMap = RCTAEPMessagingUtil.toWritableMap(p.toEventData()); + + // Derive activityId for UUID generation + final String activityId = extractActivityId(p); + Log.d(TAG, "activityId=" + activityId); + + // If SDK already included items, we'll replace them with UUID-injected ones + final WritableArray newItems = Arguments.createArray(); + + final List items = p.getItems(); + if (items != null && !items.isEmpty()) { + Log.d(TAG, "items.size=" + items.size()); + + // If you want to "overlay" UUIDs on top of SDK's item maps, fetch them: + ReadableArray sdkItems = null; + if (pMap.hasKey("items") && pMap.getType("items") == ReadableType.Array) { + sdkItems = pMap.getArray("items"); + } + + for (int i = 0; i < items.size(); i++) { + final PropositionItem item = items.get(i); + + // Base item map: prefer SDK's item map at same index; fallback to manual conversion + WritableMap itemMap = Arguments.createMap(); + try { + if (sdkItems != null && i < sdkItems.size() && sdkItems.getType(i) == ReadableType.Map) { + itemMap.merge(sdkItems.getMap(i)); // like JS spread + } else { + itemMap = RCTAEPMessagingUtil.convertPropositionItem(item); + } + } catch (Throwable t) { + Log.d(TAG, "Fallback to manual item conversion (index " + i + "): " + t.getMessage()); + itemMap = RCTAEPMessagingUtil.convertPropositionItem(item); + } + + // Inject UUID and cache native item for future tracking + final String uuid = generateItemUuid(activityId, globalUuidCounter.incrementAndGet()); + itemMap.putString("uuid", uuid); + propositionItemByUuid.put(uuid, item); + + // Helpful log + try { Log.d(TAG, "itemId=" + item.getItemId() + " uuid=" + uuid); } catch (Throwable ignore) {} + + newItems.pushMap(itemMap); + } + } else { + Log.d(TAG, "Proposition has no items."); + } + + // Overwrite items with UUID-injected array + pMap.putArray("items", newItems); + + jsProps.pushMap(pMap); + } catch (Throwable propEx) { + Log.d(TAG, "Error building proposition payload, falling back to SDK map: " + propEx.getMessage()); + // Fallback: push SDK's raw proposition if something went wrong + jsProps.pushMap(RCTAEPMessagingUtil.toWritableMap(p.toEventData())); + } + } + } + + // (Optional) per-surface payload log (size only to keep logs light) + Log.d(TAG, "Surface [" + surfaceKey + "] payload size=" + jsProps.size()); + + out.putArray(surfaceKey, jsProps); + } + + // Log the full 'out' map lightly (size of top-level keys) + try { + Map outSnapshot = RCTAEPMessagingUtil.convertReadableMapToMap(out); + Log.d(TAG, "OUT keys=" + outSnapshot.keySet()); + } catch (Throwable e) { + Log.d(TAG, "OUT log failed: " + e.getMessage()); + } + + promise.resolve(out); + } catch (Throwable topEx) { + Log.d(TAG, "Top-level error building propositions: " + topEx.getMessage()); + // As a last resort, mirror the previous behavior: + try { + String pkg = bundleId; + WritableMap fallback = RCTAEPMessagingUtil.convertSurfacePropositions(propositionsMap, pkg); + promise.resolve(fallback); + } catch (Throwable finalEx) { + promise.reject("BuildError", "Failed to build propositions", finalEx); + } + } } - } - - promise.resolve(RCTAEPMessagingUtil.convertSurfacePropositions( - propositionsMap, bundleId)); - } - }); + }); } + + + @ReactMethod public void refreshInAppMessages() { Messaging.refreshInAppMessages(); @@ -249,9 +382,11 @@ public boolean canShow(final Presentable presentable) { } if (shouldSaveMessage) { + Log.d("MessageCache", "Saving message with ID: " + message.getId() + + ", Content: " + message); + messageCache.put(message.getId(), message); } - return shouldShowMessage; } @@ -324,62 +459,64 @@ public void trackContentCardInteraction(ReadableMap propositionMap, ReadableMap * @param tokens Array containing the sub-item tokens for recording interaction (nullable) */ @ReactMethod - public void trackPropositionItem(String itemId, @Nullable String interaction, int eventType, @Nullable ReadableArray tokens) { - Log.d(TAG, "trackPropositionItem called with itemId: " + itemId + ", interaction: " + interaction + ", eventType: " + eventType); + public void trackPropositionItem(String uuid, @Nullable String interaction, int eventType, @Nullable ReadableArray tokens) { + Log.d(TAG, "trackPropositionItem called with uuid: " + uuid + ", interaction: " + interaction + ", eventType: " + eventType); try { // Convert eventType int to MessagingEdgeEventType enum - MessagingEdgeEventType edgeEventType = RCTAEPMessagingUtil.getEventType(eventType); + final MessagingEdgeEventType edgeEventType = RCTAEPMessagingUtil.getEventType(eventType); if (edgeEventType == null) { - Log.d(TAG, "Invalid eventType provided: " + eventType + " for itemId: " + itemId); + Log.d(TAG, "Invalid eventType provided: " + eventType + " for uuid: " + uuid); return; } + Log.d(TAG, "Converted eventType " + eventType + " to " + edgeEventType.name()); - Log.d(TAG, "Converted eventType " + eventType + " to MessagingEdgeEventType: " + edgeEventType.name()); - - // Find the PropositionItem by ID - PropositionItem propositionItem = findPropositionItemById(itemId); + // Resolve PropositionItem strictly by UUID + if (uuid == null) { + Log.d(TAG, "UUID is null; cannot track."); + return; + } + final PropositionItem propositionItem = propositionItemByUuid.get(uuid); if (propositionItem == null) { - Log.d(TAG, "PropositionItem not found in cache for itemId: " + itemId); + Log.d(TAG, "PropositionItem not found in uuid cache for uuid: " + uuid); return; } + Log.d(TAG, "Found PropositionItem in uuid cache for uuid: " + uuid); - Log.d(TAG, "Found PropositionItem in cache for itemId: " + itemId); - - // Convert ReadableArray to List if provided + // Convert ReadableArray tokens -> List List tokenList = null; if (tokens != null) { tokenList = new ArrayList<>(); for (int i = 0; i < tokens.size(); i++) { tokenList.add(tokens.getString(i)); } - Log.d(TAG, "Converted tokens array to list with " + tokenList.size() + " items for itemId: " + itemId); + Log.d(TAG, "Converted tokens array to list with " + tokenList.size() + " items for uuid: " + uuid); } else { - Log.d(TAG, "No tokens provided for itemId: " + itemId); + Log.d(TAG, "No tokens provided for uuid: " + uuid); } - // Call the appropriate track method based on provided parameters + // Track if (interaction != null && tokenList != null) { - // Track with interaction and tokens - Log.d(TAG, "Tracking PropositionItem with interaction '" + interaction + "' and " + tokenList.size() + " tokens for itemId: " + itemId); + Log.d(TAG, "Tracking PropositionItem with interaction '" + interaction + "' and " + tokenList.size() + " tokens for uuid: " + uuid); propositionItem.track(interaction, edgeEventType, tokenList); } else if (interaction != null) { - // Track with interaction only - Log.d(TAG, "Tracking PropositionItem with interaction '" + interaction + "' for itemId: " + itemId); + Log.d(TAG, "Tracking PropositionItem with interaction '" + interaction + "' for uuid: " + uuid); propositionItem.track(interaction, edgeEventType, null); } else { - // Track with event type only - Log.d(TAG, "Tracking PropositionItem with eventType only for itemId: " + itemId); + Log.d(TAG, "Tracking PropositionItem with eventType only for uuid: " + uuid); propositionItem.track(edgeEventType); } - Log.d(TAG, "Successfully tracked PropositionItem for itemId: " + itemId); + Log.d(TAG, "Successfully tracked PropositionItem for uuid: " + uuid); } catch (Exception e) { - Log.d(TAG, "Error tracking PropositionItem: " + itemId + ", error: " + e.getMessage(), e); + Log.d(TAG, "Error tracking PropositionItem for uuid: " + uuid + ", error: " + e.getMessage(), e); } } + + + /** * Generates XDM data for PropositionItem interactions. * This method is used by the React Native PropositionItem.generateInteractionXdm() method. diff --git a/packages/messaging/android/src/main/java/com/adobe/marketing/mobile/reactnative/messaging/RCTAEPMessagingUtil.java b/packages/messaging/android/src/main/java/com/adobe/marketing/mobile/reactnative/messaging/RCTAEPMessagingUtil.java index ce6199ca5..adf1c3ed7 100644 --- a/packages/messaging/android/src/main/java/com/adobe/marketing/mobile/reactnative/messaging/RCTAEPMessagingUtil.java +++ b/packages/messaging/android/src/main/java/com/adobe/marketing/mobile/reactnative/messaging/RCTAEPMessagingUtil.java @@ -12,9 +12,12 @@ package com.adobe.marketing.mobile.reactnative.messaging; + import android.util.Log; + import com.adobe.marketing.mobile.Message; import com.adobe.marketing.mobile.MessagingEdgeEventType; import com.adobe.marketing.mobile.messaging.Proposition; + import com.adobe.marketing.mobile.messaging.PropositionItem; import com.adobe.marketing.mobile.messaging.Surface; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.ReadableArray; @@ -31,6 +34,9 @@ import java.util.Iterator; import java.util.List; import java.util.Map; + import java.util.concurrent.atomic.AtomicLong; + import java.util.UUID; + import java.nio.charset.StandardCharsets; /** * Utility class for converting data models to {@link @@ -40,8 +46,28 @@ class RCTAEPMessagingUtil { private static final String TAG = "RCTAEPMessaging"; - - private RCTAEPMessagingUtil() {} + private static final AtomicLong GLOBAL_UUID_COUNTER = new AtomicLong(0L); + @SuppressWarnings("unchecked") + private static String extractActivityIdFromPropositionEventData(final Proposition p) { + try { + final Map ed = p.toEventData(); + if (ed == null) return null; + final Object sd = ed.get("scopeDetails"); + if (!(sd instanceof Map)) return null; + final Object act = ((Map) sd).get("activity"); + if (!(act instanceof Map)) return null; + final Object id = ((Map) act).get("id"); + return (id instanceof String) ? (String) id : null; + } catch (Exception ignored) { + return null; + } + } + + private static String generateItemUuid(final String activityId, final long counter) { + final String key = (activityId != null ? activityId : "") + "#" + counter; + return UUID.nameUUIDFromBytes(key.getBytes(StandardCharsets.UTF_8)).toString(); + } + private RCTAEPMessagingUtil() {} // From React Native static MessagingEdgeEventType getEventType(final int eventType) { @@ -187,7 +213,99 @@ static ReadableArray convertMessagesToJS(final Collection messages) { return result; } - + + /** Ensures a nested map exists at the given key; creates an empty one if missing. */ + private static void ensurePath(final WritableMap parent, final String key) { + if (parent == null) return; + if (!parent.hasKey(key) || parent.getType(key) != ReadableType.Map) { + parent.putMap(key, Arguments.createMap()); + } + } + + public static WritableMap convertPropositionItem(final PropositionItem item) { + WritableMap map = Arguments.createMap(); + try { + // Core fields (public getters only) + map.putString("id", item.getItemId()); + if (item.getSchema() != null) { + map.putString("schema", item.getSchema().toString()); + } + + // Data payload + if (item.getItemData() != null && !item.getItemData().isEmpty()) { + map.putMap("data", toWritableMap(item.getItemData())); + } + + // Optional convenience fields (schema-derived) + Map jsonMap = item.getJsonContentMap(); + if (jsonMap != null) { + map.putMap("jsonContentMap", toWritableMap(jsonMap)); + } + + List> jsonArr = item.getJsonContentArrayList(); + if (jsonArr != null) { + map.putArray("jsonContentArray", toWritableArray(jsonArr)); + } + + String html = item.getHtmlContent(); + if (html != null) { + map.putString("htmlContent", html); + } + } catch (Exception e) { + Log.w("RCTAEPMessagingUtil", "Error converting PropositionItem: " + e.getMessage()); + } + return map; + } + + public static WritableMap convertSingleProposition(final Proposition p, final String bundleId) { + WritableMap map = Arguments.createMap(); + + // 1) Start from Proposition's own event data (Proposition.toEventData is public) + try { + Map eventData = p.toEventData(); + if (eventData != null) { + map = toWritableMap(eventData); + } + } catch (Exception ignored) {} + + // 2) Ensure scopeDetails.activity is present & writable + WritableMap scopeDetails = Arguments.createMap(); + if (map.hasKey("scopeDetails") && map.getType("scopeDetails") == ReadableType.Map) { + scopeDetails.merge(map.getMap("scopeDetails")); + } + WritableMap activity = Arguments.createMap(); + if (scopeDetails.hasKey("activity") && scopeDetails.getType("activity") == ReadableType.Map) { + activity.merge(scopeDetails.getMap("activity")); + } + scopeDetails.putMap("activity", activity); + map.putMap("scopeDetails", scopeDetails); + + // 3) Extract activityId for UUID generation (safe only if present) + String activityId = null; + if (activity.hasKey("id") && activity.getType("id") == ReadableType.String) { + activityId = activity.getString("id"); + } + + // 4) Build items array from actual PropositionItems and inject uuid + WritableArray itemsArr = Arguments.createArray(); + try { + List items = p.getItems(); + if (items != null) { + for (final PropositionItem item : items) { + WritableMap itemMap = convertPropositionItem(item); + String uuid = generateItemUuid(activityId, GLOBAL_UUID_COUNTER.incrementAndGet()); + itemMap.putString("uuid", uuid); // <-- inject UUID here + itemsArr.pushMap(itemMap); + } + } + } catch (Exception e) { + Log.w("RCTAEPMessagingUtil", "Error building items with UUID: " + e.getMessage()); + } + map.putArray("items", itemsArr); + + return map; + } + static WritableMap convertSurfacePropositions( final Map> propositionMap, String packageName) { @@ -201,7 +319,9 @@ static WritableMap convertSurfacePropositions( for (Iterator iterator = entry.getValue().iterator(); iterator.hasNext();) { - propositions.pushMap(toWritableMap(iterator.next().toEventData())); + //propositions.pushMap(toWritableMap(iterator.next().toEventData())); + Proposition nextProp = iterator.next(); + propositions.pushMap(convertSingleProposition(nextProp, packageName)); } data.putArray(key, propositions); diff --git a/packages/messaging/src/models/HTMLProposition.ts b/packages/messaging/src/models/HTMLProposition.ts deleted file mode 100644 index f84ff25f3..000000000 --- a/packages/messaging/src/models/HTMLProposition.ts +++ /dev/null @@ -1,154 +0,0 @@ -/* - Copyright 2024 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 { PersonalizationSchema } from './PersonalizationSchema'; -import MessagingEdgeEventType from './MessagingEdgeEventType'; -import { PropositionItem, PropositionItemData } from './PropositionItem'; - -export interface HTMLPropositionData extends PropositionItemData { - id: string; - data: { - content: string; - }; - schema: PersonalizationSchema.HTML_CONTENT; -} - -export class HTMLProposition extends PropositionItem { - declare data: HTMLPropositionData['data']; // Override data type for better typing - - constructor(htmlPropositionData: HTMLPropositionData) { - super(htmlPropositionData); - this.data = htmlPropositionData.data; - } - - /** - * Convenience method to track when this HTMLProposition is displayed. - * Equivalent to calling track(MessagingEdgeEventType.DISPLAY). - */ - trackDisplay(): void { - this.track(MessagingEdgeEventType.DISPLAY); - } - - /** - * Convenience method to track when this HTMLProposition is dismissed. - * - * @param {string | null} interaction - Optional interaction identifier (e.g., "user_dismissed", "auto_dismissed") - */ - trackDismiss(interaction: string | null = null): void { - this.track(interaction, MessagingEdgeEventType.DISMISS, null); - } - - /** - * Convenience method to track user interactions with this HTMLProposition. - * - * @param {string} interaction - The interaction identifier (e.g., "clicked", "link_pressed", "action_taken") - */ - trackInteraction(interaction: string): void { - this.track(interaction, MessagingEdgeEventType.INTERACT, null); - } - - /** - * Gets the HTML content string of this proposition. - * - * @returns {string} The HTML content - */ - getContent(): string { - return this.data.content; - } - - /** - * Gets the HTML content as a formatted string for display purposes. - * This is an alias for getContent() for consistency with other proposition types. - * - * @returns {string} The HTML content - */ - getHtmlContent(): string { - return this.data.content; - } - - /** - * Checks if the HTML content contains specific elements or patterns. - * - * @param {string} pattern - The pattern to search for (can be a tag, class, id, etc.) - * @returns {boolean} True if the pattern is found in the HTML content - */ - containsPattern(pattern: string): boolean { - return this.data.content.includes(pattern); - } - - /** - * Checks if the HTML content contains interactive elements (buttons, links, forms). - * - * @returns {boolean} True if interactive elements are found - */ - hasInteractiveElements(): boolean { - const interactivePatterns = [' word.length > 0); - return words.length; - } - - /** - * Checks if the HTML content is likely to be a full-page experience. - * This is determined by the presence of certain HTML structure elements. - * - * @returns {boolean} True if this appears to be a full-page HTML experience - */ - isFullPageExperience(): boolean { - const fullPageIndicators = [' this.data.content.includes(indicator)); - } - - /** - * Extracts all link URLs from the HTML content. - * - * @returns {string[]} Array of URLs found in href attributes - */ - extractLinks(): string[] { - const linkRegex = /href\s*=\s*["']([^"']+)["']/gi; - const links: string[] = []; - let match; - - while ((match = linkRegex.exec(this.data.content)) !== null) { - links.push(match[1]); - } - - return links; - } - - /** - * Extracts all image URLs from the HTML content. - * - * @returns {string[]} Array of image URLs found in src attributes - */ - extractImages(): string[] { - const imageRegex = /src\s*=\s*["']([^"']+)["']/gi; - const images: string[] = []; - let match; - - while ((match = imageRegex.exec(this.data.content)) !== null) { - images.push(match[1]); - } - - return images; - } -} diff --git a/packages/messaging/src/models/PropositionItem.ts b/packages/messaging/src/models/PropositionItem.ts index c1c55bdf4..a045c2d3b 100644 --- a/packages/messaging/src/models/PropositionItem.ts +++ b/packages/messaging/src/models/PropositionItem.ts @@ -21,6 +21,7 @@ const RCTAEPMessaging = NativeModules.AEPMessaging; */ export interface PropositionItemData { id: string; + uuid: string; schema: PersonalizationSchema; data: { [key: string]: any; @@ -36,6 +37,7 @@ export interface PropositionItemData { */ export class PropositionItem { id: string; + uuid: string; schema: PersonalizationSchema; data: { [key: string]: any }; @@ -43,6 +45,7 @@ export class PropositionItem { this.id = propositionItemData.id; this.schema = propositionItemData.schema; this.data = propositionItemData.data; + this.uuid = propositionItemData.uuid; } /** @@ -125,7 +128,7 @@ export class PropositionItem { * Internal method that performs the actual tracking */ private trackWithDetails(interaction: string | null, eventType: MessagingEdgeEventType, tokens: string[] | null): void { - RCTAEPMessaging.trackPropositionItem(this.id, interaction, eventType, tokens); + RCTAEPMessaging.trackPropositionItem(this.uuid, interaction, eventType, tokens); } /** From c8ccc751fc99af7fd574364581bd0a40a4d0c9f7 Mon Sep 17 00:00:00 2001 From: Naman Arora Date: Mon, 18 Aug 2025 20:40:16 +0530 Subject: [PATCH 03/28] json proposition item removed --- packages/messaging/src/index.ts | 5 +- .../src/models/JSONPropositionItem.ts | 53 ------------------- 2 files changed, 1 insertion(+), 57 deletions(-) delete mode 100644 packages/messaging/src/models/JSONPropositionItem.ts diff --git a/packages/messaging/src/index.ts b/packages/messaging/src/index.ts index 6c95f4459..4951f65b9 100644 --- a/packages/messaging/src/index.ts +++ b/packages/messaging/src/index.ts @@ -13,10 +13,9 @@ governing permissions and limitations under the License. import Messaging from './Messaging'; import { ContentCard, ContentCardData } from './models/ContentCard'; // import { HTMLProposition, HTMLPropositionData } from './models/HTMLProposition'; -import { HTMLProposition } from './models/HTMLProposition'; +// import { HTMLProposition } from './models/HTMLProposition'; import { InAppMessage } from './models/InAppMessage'; -import { JSONPropositionItem } from './models/JSONPropositionItem'; // import { JSONPropositionItem, JSONPropositionItemData } from './models/JSONPropositionItem'; import Message from './models/Message'; @@ -33,10 +32,8 @@ export { Characteristics, ContentCard, ContentCardData, - HTMLProposition, //HTMLPropositionData, InAppMessage, - JSONPropositionItem, // JSONPropositionItemData, Messaging, Message, diff --git a/packages/messaging/src/models/JSONPropositionItem.ts b/packages/messaging/src/models/JSONPropositionItem.ts deleted file mode 100644 index 3d17573e6..000000000 --- a/packages/messaging/src/models/JSONPropositionItem.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - Copyright 2024 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 { PersonalizationSchema } from './PersonalizationSchema'; -import { PropositionItem, PropositionItemData } from './PropositionItem'; - -export interface JSONPropositionItemData extends PropositionItemData { - id: string; - data: { - content: string; - }; - schema: PersonalizationSchema.JSON_CONTENT; -} - -export class JSONPropositionItem extends PropositionItem { - declare data: JSONPropositionItemData['data']; // Override data type for better typing - - constructor(jsonPropositionItemData: JSONPropositionItemData) { - super(jsonPropositionItemData); - this.data = jsonPropositionItemData.data; - } - - /** - * Gets the JSON content string of this proposition item. - * - * @returns {string} The JSON content - */ - getContent(): string { - return this.data.content; - } - - /** - * Attempts to parse the content as JSON. - * - * @returns {object | null} Parsed JSON object or null if parsing fails - */ - getParsedContent(): object | null { - try { - return JSON.parse(this.data.content); - } catch (error) { - return null; - } - } -} From a17849a465ddd9da802bd4f05213d8418f5a47c8 Mon Sep 17 00:00:00 2001 From: Naman Arora Date: Tue, 19 Aug 2025 14:17:01 +0530 Subject: [PATCH 04/28] removed unused guide --- NATIVE_IMPLEMENTATION_GUIDE.md | 346 ------------------ .../CONTENT_CARD_TRACKING_EXAMPLE.md | 259 ------------- .../messaging/src/models/HTMLProposition.ts | 21 ++ .../messaging/src/models/JSONProposition.ts | 21 ++ 4 files changed, 42 insertions(+), 605 deletions(-) delete mode 100644 NATIVE_IMPLEMENTATION_GUIDE.md delete mode 100644 packages/messaging/CONTENT_CARD_TRACKING_EXAMPLE.md create mode 100644 packages/messaging/src/models/HTMLProposition.ts create mode 100644 packages/messaging/src/models/JSONProposition.ts diff --git a/NATIVE_IMPLEMENTATION_GUIDE.md b/NATIVE_IMPLEMENTATION_GUIDE.md deleted file mode 100644 index 2ce506b3e..000000000 --- a/NATIVE_IMPLEMENTATION_GUIDE.md +++ /dev/null @@ -1,346 +0,0 @@ -# Native Implementation Guide: Unified PropositionItem Tracking - -This guide explains the complete native implementation for the unified `PropositionItem` tracking system that enables `ContentCard`, `HTMLProposition`, `JSONPropositionItem`, and other proposition types to use the same tracking methods. - -## Overview - -The unified tracking system allows all proposition item types to use the same `track()` and `generateInteractionXdm()` methods by: - -1. **Caching PropositionItems**: When propositions are retrieved via `getPropositionsForSurfaces`, all `PropositionItem` objects are cached by their ID -2. **Unified Native Methods**: New native methods `trackPropositionItem` and `generatePropositionInteractionXdm` that work with cached items -3. **Automatic Cleanup**: Cache management methods for memory optimization - -## Architecture - -``` -React Native Layer: -├── PropositionItem (base class) -├── ContentCard extends PropositionItem -├── HTMLProposition extends PropositionItem -├── JSONPropositionItem extends PropositionItem -└── All call unified native methods - -Native Layer (Android/iOS): -├── PropositionItem Cache (itemId -> PropositionItem) -├── Proposition Cache (itemId -> parent Proposition) -├── trackPropositionItem() native method -├── generatePropositionInteractionXdm() native method -└── Automatic caching in getPropositionsForSurfaces() -``` - -## Android Implementation - -### Key Changes Made - -The following methods were added to `RCTAEPMessagingModule.java`: - -#### 1. Cache Properties -```java -// Cache to store PropositionItem objects by their ID for unified tracking -private final Map propositionItemCache = new ConcurrentHashMap<>(); -// Cache to store the parent Proposition for each PropositionItem -private final Map propositionCache = new ConcurrentHashMap<>(); -``` - -#### 2. Core Tracking Method -```java -@ReactMethod -public void trackPropositionItem(String itemId, @Nullable String interaction, int eventType, @Nullable ReadableArray tokens) { - try { - // Convert eventType int to MessagingEdgeEventType enum - MessagingEdgeEventType edgeEventType = RCTAEPMessagingUtil.getEventType(eventType); - - // Find the PropositionItem by ID - PropositionItem propositionItem = findPropositionItemById(itemId); - - if (propositionItem == null) { - Log.warning(TAG, "trackPropositionItem - PropositionItem not found for ID: " + itemId); - return; - } - - // Convert ReadableArray to List if provided - List tokenList = null; - if (tokens != null) { - tokenList = new ArrayList<>(); - for (int i = 0; i < tokens.size(); i++) { - tokenList.add(tokens.getString(i)); - } - } - - // Call the appropriate track method based on provided parameters - if (interaction != null && tokenList != null) { - propositionItem.track(interaction, edgeEventType, tokenList); - } else if (interaction != null) { - propositionItem.track(interaction, edgeEventType, null); - } else { - propositionItem.track(edgeEventType); - } - - Log.debug(TAG, "Successfully tracked PropositionItem: " + itemId + " with eventType: " + edgeEventType); - - } catch (Exception e) { - Log.error(TAG, "Error tracking PropositionItem: " + itemId, e); - } -} -``` - -#### 3. XDM Generation Method -```java -@ReactMethod -public void generatePropositionInteractionXdm(String itemId, @Nullable String interaction, int eventType, @Nullable ReadableArray tokens, Promise promise) { - try { - // Convert eventType int to MessagingEdgeEventType enum - MessagingEdgeEventType edgeEventType = RCTAEPMessagingUtil.getEventType(eventType); - - // Find the PropositionItem by ID - PropositionItem propositionItem = findPropositionItemById(itemId); - - if (propositionItem == null) { - promise.reject("PropositionItemNotFound", "No PropositionItem found with ID: " + itemId); - return; - } - - // Generate XDM data using the appropriate method - Map xdmData; - if (interaction != null && tokenList != null) { - xdmData = propositionItem.generateInteractionXdm(interaction, edgeEventType, tokenList); - } else if (interaction != null) { - xdmData = propositionItem.generateInteractionXdm(interaction, edgeEventType, null); - } else { - xdmData = propositionItem.generateInteractionXdm(edgeEventType); - } - - if (xdmData != null) { - WritableMap result = RCTAEPMessagingUtil.toWritableMap(xdmData); - promise.resolve(result); - } else { - promise.reject("XDMGenerationFailed", "Failed to generate XDM data for PropositionItem: " + itemId); - } - - } catch (Exception e) { - promise.reject("XDMGenerationError", "Error generating XDM data: " + e.getMessage(), e); - } -} -``` - -#### 4. Automatic Caching -```java -@ReactMethod -public void getPropositionsForSurfaces(ReadableArray surfaces, final Promise promise) { - String bundleId = this.reactContext.getPackageName(); - Messaging.getPropositionsForSurfaces( - RCTAEPMessagingUtil.convertSurfaces(surfaces), - new AdobeCallbackWithError>>() { - @Override - public void call(Map> propositionsMap) { - - // Cache PropositionItems for unified tracking when propositions are retrieved - for (Map.Entry> entry : propositionsMap.entrySet()) { - List propositions = entry.getValue(); - if (propositions != null) { - cachePropositionsItems(propositions); - } - } - - promise.resolve(RCTAEPMessagingUtil.convertSurfacePropositions(propositionsMap, bundleId)); - } - }); -} -``` - -## iOS Implementation - -### Key Changes Made - -The following methods were added to `RCTAEPMessaging.swift`: - -#### 1. Cache Properties -```swift -// Cache to store PropositionItem objects by their ID for unified tracking -private var propositionItemCache = [String: PropositionItem]() -// Cache to store the parent Proposition for each PropositionItem -private var propositionCache = [String: Proposition]() -``` - -#### 2. Core Tracking Method -```swift -@objc -func trackPropositionItem( - _ itemId: String, - interaction: String?, - eventType: Int, - tokens: [String]?, - withResolver resolve: @escaping RCTPromiseResolveBlock, - withRejecter reject: @escaping RCTPromiseRejectBlock -) { - guard let edgeEventType = MessagingEdgeEventType(rawValue: eventType) else { - reject("InvalidEventType", "Invalid eventType: \(eventType)", nil) - return - } - - guard let propositionItem = findPropositionItemById(itemId) else { - print("Warning: PropositionItem not found for ID: \(itemId)") - resolve(nil) - return - } - - // Call the appropriate track method based on provided parameters - if let interaction = interaction, let tokens = tokens { - propositionItem.track(interaction, withEdgeEventType: edgeEventType, forTokens: tokens) - } else if let interaction = interaction { - propositionItem.track(interaction, withEdgeEventType: edgeEventType) - } else { - propositionItem.track(withEdgeEventType: edgeEventType) - } - - print("Successfully tracked PropositionItem: \(itemId) with eventType: \(edgeEventType)") - resolve(nil) -} -``` - -#### 3. XDM Generation Method -```swift -@objc -func generatePropositionInteractionXdm( - _ itemId: String, - interaction: String?, - eventType: Int, - tokens: [String]?, - withResolver resolve: @escaping RCTPromiseResolveBlock, - withRejecter reject: @escaping RCTPromiseRejectBlock -) { - guard let edgeEventType = MessagingEdgeEventType(rawValue: eventType) else { - reject("InvalidEventType", "Invalid eventType: \(eventType)", nil) - return - } - - guard let propositionItem = findPropositionItemById(itemId) else { - reject("PropositionItemNotFound", "No PropositionItem found with ID: \(itemId)", nil) - return - } - - // Generate XDM data using the appropriate method - var xdmData: [String: Any]? - - if let interaction = interaction, let tokens = tokens { - xdmData = propositionItem.generateInteractionXdm(interaction, withEdgeEventType: edgeEventType, forTokens: tokens) - } else if let interaction = interaction { - xdmData = propositionItem.generateInteractionXdm(interaction, withEdgeEventType: edgeEventType) - } else { - xdmData = propositionItem.generateInteractionXdm(withEdgeEventType: edgeEventType) - } - - if let xdmData = xdmData { - resolve(xdmData) - } else { - reject("XDMGenerationFailed", "Failed to generate XDM data for PropositionItem: \(itemId)", nil) - } -} -``` - -#### 4. Automatic Caching -```swift -@objc -func getPropositionsForSurfaces( - _ surfaces: [String], - withResolver resolve: @escaping RCTPromiseResolveBlock, - withRejecter reject: @escaping RCTPromiseRejectBlock -) { - let surfacePaths = surfaces.map { $0.isEmpty ? Surface() : Surface(path: $0) } - Messaging.getPropositionsForSurfaces(surfacePaths) { propositions, error in - guard error == nil else { - reject("Unable to Retrieve Propositions", nil, nil) - return - } - - // Cache PropositionItems for unified tracking when propositions are retrieved - if let propositionsDict = propositions { - let allPropositions = Array(propositionsDict.values).flatMap { $0 } - self.cachePropositionsItems(allPropositions) - } - - resolve(RCTAEPMessagingDataBridge.transformPropositionDict(dict: propositions!)) - } -} -``` - -## Key Features - -### 1. Automatic Caching -- **When**: PropositionItems are automatically cached when `getPropositionsForSurfaces()` is called -- **What**: Both the `PropositionItem` object and its parent `Proposition` are cached -- **Why**: Enables tracking without needing to pass full proposition data each time - -### 2. Flexible Method Signatures -The tracking methods support multiple call patterns: -```javascript -// Event type only -propositionItem.track(MessagingEdgeEventType.DISPLAY); - -// With interaction -propositionItem.track("button-click", MessagingEdgeEventType.INTERACT, null); - -// With interaction and tokens -propositionItem.track("carousel-item", MessagingEdgeEventType.INTERACT, ["token1", "token2"]); -``` - -### 3. Memory Management -- Uses `ConcurrentHashMap` (Android) and thread-safe collections (iOS) -- Provides cache management methods (`clearPropositionItemCache`, `getPropositionItemCacheSize`, `hasPropositionItem`) -- Automatic cleanup when propositions are refreshed - -### 4. Error Handling -- Graceful handling of missing PropositionItems -- Detailed error messages for debugging -- Promise-based error reporting for XDM generation - -## Integration Requirements - -### React Native Interface Updates - -Update your `Messaging.ts` interface to include the new methods: - -```typescript -export interface NativeMessagingModule { - // ... existing methods ... - trackPropositionItem: (itemId: string, interaction: string | null, eventType: number, tokens: string[] | null) => void; - generatePropositionInteractionXdm: (itemId: string, interaction: string | null, eventType: number, tokens: string[] | null) => Promise; -} -``` - -### Usage Flow - -1. **App calls `getPropositionsForSurfaces()`** - - Native code retrieves propositions from Adobe Edge - - All PropositionItems are automatically cached - - React Native receives proposition data - -2. **App creates PropositionItem objects** - - `ContentCard`, `HTMLProposition`, `JSONPropositionItem` extend base `PropositionItem` - - Each contains an `id` that maps to cached native objects - -3. **App calls tracking methods** - - `track()` methods call `RCTAEPMessaging.trackPropositionItem()` - - Native code finds cached PropositionItem by ID - - Calls appropriate native tracking method - -## Benefits - -1. **Unified API**: All proposition types use the same tracking interface -2. **Performance**: Avoids passing large proposition objects on each tracking call -3. **Flexibility**: Supports all native tracking method variations -4. **Consistency**: Mirrors the native Android SDK architecture -5. **Memory Efficient**: Automatic cache management prevents memory leaks -6. **Developer Experience**: Simple, consistent API across all proposition types - -## Testing - -Test the implementation by: - -1. Retrieving propositions via `getPropositionsForSurfaces()` -2. Creating `ContentCard`, `HTMLProposition`, or `JSONPropositionItem` objects -3. Calling `track()` methods with different parameter combinations -4. Verifying tracking events are sent to Adobe Experience Edge -5. Testing cache management methods for proper memory handling - -This unified approach provides a robust, scalable foundation for proposition tracking across all content types in your React Native Adobe Experience SDK implementation. \ No newline at end of file diff --git a/packages/messaging/CONTENT_CARD_TRACKING_EXAMPLE.md b/packages/messaging/CONTENT_CARD_TRACKING_EXAMPLE.md deleted file mode 100644 index d8042c1fb..000000000 --- a/packages/messaging/CONTENT_CARD_TRACKING_EXAMPLE.md +++ /dev/null @@ -1,259 +0,0 @@ -# ContentCard Tracking Example - -This example demonstrates how to use the new ContentCard tracking functionality in the React Native Messaging wrapper. - -## Basic Usage - -```typescript -import { ContentCard, ContentCardData, MessagingEdgeEventType } from '@adobe/react-native-aepMessaging'; - -// Create a ContentCard instance from your data -const contentCardData: ContentCardData = { - id: "card-123", - data: { - contentType: 'application/json', - expiryDate: 1735689600, // Some future timestamp - publishedDate: 1735603200, // Some past timestamp - content: { - actionUrl: "https://example.com", - body: { content: "This is the card body" }, - title: { content: "Card Title" }, - buttons: [ - { - actionUrl: "https://example.com/action", - id: "btn-1", - text: { content: "Click Me" }, - interactId: "button-interact-1" - } - ], - image: { alt: "Card Image", url: "https://example.com/image.jpg" }, - dismissBtn: { style: "circle" } - }, - meta: { - adobe: { template: "SmallImage" }, - dismissState: false, - readState: false, - surface: "mobileapp://com.example.app/main" - } - }, - schema: PersonalizationSchema.CONTENT_CARD -}; - -const contentCard = new ContentCard(contentCardData); -``` - -## Tracking Examples - -### 1. Track Display (when card is shown to user) -```typescript -// Method 1: Using convenience method -contentCard.trackDisplay(); - -// Method 2: Using general track method -contentCard.track(null, MessagingEdgeEventType.DISPLAY); -``` - -### 2. Track User Interactions -```typescript -// Track general card click -contentCard.trackInteraction("card_clicked"); - -// Track specific button clicks -contentCard.track("button_1_clicked", MessagingEdgeEventType.INTERACT); - -// Track action URL clicks -contentCard.track("action_url_clicked", MessagingEdgeEventType.INTERACT); -``` - -### 3. Track Dismissal -```typescript -// Method 1: Using convenience method (no specific interaction) -contentCard.trackDismiss(); - -// Method 2: With specific interaction identifier -contentCard.trackDismiss("user_dismissed"); - -// Method 3: Using general track method -contentCard.track("auto_dismissed", MessagingEdgeEventType.DISMISS); -``` - -### 4. Track Custom Events -```typescript -// Track when user reads the content -contentCard.track("content_read", MessagingEdgeEventType.INTERACT); - -// Track when card expires -if (contentCard.isExpired()) { - contentCard.track("card_expired", MessagingEdgeEventType.DISPLAY); -} - -// Track based on card state -if (!contentCard.isRead()) { - contentCard.track("card_viewed_first_time", MessagingEdgeEventType.DISPLAY); -} -``` - -## Integration with React Native Components - -### Basic ContentCard Component -```typescript -import React, { useEffect } from 'react'; -import { View, Text, Image, TouchableOpacity } from 'react-native'; -import { ContentCard, MessagingEdgeEventType } from '@adobe/react-native-aepMessaging'; - -interface ContentCardComponentProps { - contentCard: ContentCard; -} - -const ContentCardComponent: React.FC = ({ contentCard }) => { - // Track display when component mounts - useEffect(() => { - contentCard.trackDisplay(); - }, [contentCard]); - - const handleCardPress = () => { - contentCard.trackInteraction("card_body_clicked"); - // Handle navigation or action - }; - - const handleButtonPress = (buttonId: string) => { - contentCard.track(`button_${buttonId}_clicked`, MessagingEdgeEventType.INTERACT); - // Handle button action - }; - - const handleDismiss = () => { - contentCard.trackDismiss("user_dismissed"); - // Handle card dismissal - }; - - return ( - - - {contentCard.getImageAlt()} - - {contentCard.getTitle()} - - - {contentCard.getBody()} - - - {/* Render buttons */} - {contentCard.getButtons().map((button) => ( - handleButtonPress(button.id)} - style={{ backgroundColor: 'blue', padding: 8, marginVertical: 4 }} - > - {button.text.content} - - ))} - - {/* Dismiss button */} - - - - - - ); -}; -``` - -### Advanced Usage with State Management -```typescript -import React, { useState, useEffect } from 'react'; -import { ContentCard } from '@adobe/react-native-aepMessaging'; - -const ContentCardList: React.FC = () => { - const [contentCards, setContentCards] = useState([]); - const [viewedCards, setViewedCards] = useState>(new Set()); - - const handleCardViewed = (card: ContentCard) => { - if (!viewedCards.has(card.id)) { - card.trackDisplay(); - setViewedCards(prev => new Set(prev).add(card.id)); - } - }; - - const handleCardInteraction = (card: ContentCard, interaction: string) => { - card.trackInteraction(interaction); - }; - - const handleCardDismiss = (card: ContentCard) => { - card.trackDismiss("user_dismissed"); - setContentCards(prev => prev.filter(c => c.id !== card.id)); - }; - - return ( - - {contentCards.map(card => ( - handleCardViewed(card)} - onInteraction={(interaction) => handleCardInteraction(card, interaction)} - onDismiss={() => handleCardDismiss(card)} - /> - ))} - - ); -}; -``` - -## Helper Utilities - -### ContentCard Utility Functions -```typescript -// Utility to check if tracking should occur based on card state -export const shouldTrackCard = (card: ContentCard): boolean => { - return !card.isExpired() && !card.isDismissed(); -}; - -// Utility to get appropriate interaction based on card template -export const getTemplateSpecificInteraction = (card: ContentCard, action: string): string => { - const template = card.getTemplate(); - return `${template.toLowerCase()}_${action}`; -}; - -// Utility to track card lifecycle events -export const trackCardLifecycle = (card: ContentCard, event: 'shown' | 'hidden' | 'expired') => { - if (!shouldTrackCard(card)) return; - - switch (event) { - case 'shown': - card.trackDisplay(); - break; - case 'hidden': - card.trackDismiss('auto_hidden'); - break; - case 'expired': - card.track('card_expired', MessagingEdgeEventType.DISPLAY); - break; - } -}; -``` - -## Migration from Legacy trackContentCardDisplay/trackContentCardInteraction - -If you were previously using the static methods from the Messaging class: - -```typescript -// Old way (still supported) -Messaging.trackContentCardDisplay(proposition, contentCard); -Messaging.trackContentCardInteraction(proposition, contentCard); - -// New way (recommended) -const card = new ContentCard(contentCard); -card.trackDisplay(); -card.trackInteraction("user_clicked"); -``` - -The new approach provides: -- Better encapsulation and object-oriented design -- More granular tracking options -- Convenience methods for common operations -- Better TypeScript support and intellisense -- Consistent API with the Message class \ No newline at end of file diff --git a/packages/messaging/src/models/HTMLProposition.ts b/packages/messaging/src/models/HTMLProposition.ts new file mode 100644 index 000000000..e6aeb0c51 --- /dev/null +++ b/packages/messaging/src/models/HTMLProposition.ts @@ -0,0 +1,21 @@ +/* + Copyright 2024 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 { PersonalizationSchema } from './PersonalizationSchema'; + +export interface HTMLProposition { + id: string; + data: { + content: string; + }; + schema: PersonalizationSchema.HTML_CONTENT; +} \ No newline at end of file diff --git a/packages/messaging/src/models/JSONProposition.ts b/packages/messaging/src/models/JSONProposition.ts new file mode 100644 index 000000000..b92b1b9eb --- /dev/null +++ b/packages/messaging/src/models/JSONProposition.ts @@ -0,0 +1,21 @@ +/* + Copyright 2024 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 { PersonalizationSchema } from './PersonalizationSchema'; + +export interface JSONPropositionItem { + id: string; + data: { + content: string; + }; + schema: PersonalizationSchema.JSON_CONTENT; +} \ No newline at end of file From 3e36a2929581bdb61aa2c3703ab4e808de54ac5d Mon Sep 17 00:00:00 2001 From: Naman Arora Date: Tue, 19 Aug 2025 15:07:05 +0530 Subject: [PATCH 05/28] added htmlProposition , jsonPropositon item class --- .../app/MessagingView.tsx | 71 +++++++++++-------- packages/messaging/src/index.ts | 6 ++ packages/messaging/src/models/ContentCard.ts | 27 ------- .../messaging/src/models/HTMLProposition.ts | 21 ++++-- .../messaging/src/models/JSONProposition.ts | 21 ++++-- .../src/models/MessagingPropositionItem.ts | 4 +- .../messaging/src/models/PropositionItem.ts | 3 +- 7 files changed, 80 insertions(+), 73 deletions(-) diff --git a/apps/AEPSampleAppNewArchEnabled/app/MessagingView.tsx b/apps/AEPSampleAppNewArchEnabled/app/MessagingView.tsx index 86ea557e6..254f4a3d5 100644 --- a/apps/AEPSampleAppNewArchEnabled/app/MessagingView.tsx +++ b/apps/AEPSampleAppNewArchEnabled/app/MessagingView.tsx @@ -17,8 +17,11 @@ import { Messaging, PersonalizationSchema, MessagingEdgeEventType, - PropositionItem, // Add this import - Message + PropositionItem, + Message, + ContentCard, + HTMLProposition, + JSONPropositionItem, } from '@adobe/react-native-aepmessaging' import styles from '../styles/styles'; import { useRouter } from 'expo-router'; @@ -26,6 +29,20 @@ import { useRouter } from 'expo-router'; const SURFACES = ['android-cbe-preview', 'android-cc', 'android-cc-naman']; const SURFACES_WITH_CONTENT_CARDS = ['android-cc']; +// Helper: instantiate the appropriate class based on schema +const toItemInstance = (itemData: any) => { + switch (itemData?.schema) { + case PersonalizationSchema.CONTENT_CARD: + return new ContentCard(itemData as any); + case PersonalizationSchema.HTML_CONTENT: + return new HTMLProposition(itemData as any); + case PersonalizationSchema.JSON_CONTENT: + return new JSONPropositionItem(itemData as any); + default: + return new PropositionItem(itemData as any); + } +}; + const messagingExtensionVersion = async () => { const version = await Messaging.extensionVersion(); console.log(`AdobeExperienceSDK: Messaging version: ${version}`); @@ -169,22 +186,22 @@ const unifiedTrackingExample = async () => { for (const proposition of propositions) { for (const propositionItemData of proposition.items) { - // Create PropositionItem instance from the plain data object - const propositionItem = new PropositionItem(propositionItemData); - console.log('propositionItem here is created:', propositionItem, propositionItem.uuid); + // Create strongly-typed instance based on schema + const item = toItemInstance(propositionItemData); + console.log('propositionItem here is created:', item, (item as any).uuid); - // Use the unified tracking approach via PropositionItem - if (propositionItem.schema === PersonalizationSchema.CONTENT_CARD) { - // Track display for content cards - propositionItem.track(MessagingEdgeEventType.DISPLAY); + // Use the unified tracking approach via base class + if (item.schema === PersonalizationSchema.CONTENT_CARD) { + item.track(MessagingEdgeEventType.DISPLAY); console.log('Tracked content card display using unified API'); - - // Track interaction with custom interaction string - propositionItem.track('card_clicked', MessagingEdgeEventType.INTERACT, null); + item.track('content_card_clicked', MessagingEdgeEventType.INTERACT, null); + // item.track('content_card_clicked', MessagingEdgeEventType.INTERACT, null); + item.track(MessagingEdgeEventType.DISPLAY); + console.log('Tracked content card interaction using unified API'); - } else if (propositionItem.schema === PersonalizationSchema.JSON_CONTENT) { - // Track display for JSON content - propositionItem.track(MessagingEdgeEventType.DISPLAY); + } else if (item.schema === PersonalizationSchema.JSON_CONTENT) { + // item.track(MessagingEdgeEventType.DISPLAY); + item.track('token clicked', MessagingEdgeEventType.INTERACT, null); console.log('Tracked JSON content display using unified API'); } } @@ -263,28 +280,21 @@ const trackPropositionItems = async () => { console.log(`Processing surface: ${surface}`); // Iterate through propositions for this surface - for (const proposition of propositionList) { + for (const proposition of propositionList as any[]) { console.log(`Processing proposition: ${proposition.id}`); // Iterate through items in the proposition for (const itemData of proposition.items) { - // Create PropositionItem instance (this gets cached automatically) - const propositionItem = new PropositionItem(itemData); + // Create instance based on schema (this gets cached automatically) + const item = toItemInstance(itemData); // Track display event - propositionItem.track(MessagingEdgeEventType.DISPLAY); - console.log(`Tracked display for item: ${propositionItem.id}`); + item.track(MessagingEdgeEventType.DISPLAY); + console.log(`Tracked display for item: ${item.id}`); // Track interaction event - propositionItem.track("user_clicked", MessagingEdgeEventType.INTERACT, null); - console.log(`Tracked interaction for item: ${propositionItem.id}`); - - // Track with tokens (for embedded decisions) - if (itemData.data?.tokens) { - const tokens = itemData.data.tokens; // Extract from your data - propositionItem.track("token_interaction", MessagingEdgeEventType.INTERACT, tokens); - console.log(`Tracked with tokens for item: ${propositionItem.id}`); - } + item.track('user_clicked', MessagingEdgeEventType.INTERACT, null); + console.log(`Tracked interaction for item: ${item.id}`); } } } @@ -308,8 +318,7 @@ const trackDirectly = () => { const trackEdgeCaseMessageWithPropositionItem = async () => { const messages = await Messaging.getCachedMessages(); - const newMessage = new PropositionItem({id: "12", autoTrack: true}); - newMessage.track("button_clicked", MessagingEdgeEventType.INTERACT); + // Removed invalid manual PropositionItem construction console.log('Cached messages:', messages); } diff --git a/packages/messaging/src/index.ts b/packages/messaging/src/index.ts index 4951f65b9..ff4674aff 100644 --- a/packages/messaging/src/index.ts +++ b/packages/messaging/src/index.ts @@ -17,6 +17,8 @@ import { ContentCard, ContentCardData } from './models/ContentCard'; import { InAppMessage } from './models/InAppMessage'; // import { JSONPropositionItem, JSONPropositionItemData } from './models/JSONPropositionItem'; +import { HTMLProposition, HTMLPropositionData } from './models/HTMLProposition'; +import { JSONPropositionItem, JSONPropositionData } from './models/JSONProposition'; import Message from './models/Message'; import { MessagingDelegate } from './models/MessagingDelegate'; @@ -44,4 +46,8 @@ export { PersonalizationSchema, PropositionItem, PropositionItemData, + HTMLProposition, + HTMLPropositionData, + JSONPropositionItem, + JSONPropositionData, }; diff --git a/packages/messaging/src/models/ContentCard.ts b/packages/messaging/src/models/ContentCard.ts index bed48fea9..e4faaa998 100644 --- a/packages/messaging/src/models/ContentCard.ts +++ b/packages/messaging/src/models/ContentCard.ts @@ -11,7 +11,6 @@ */ import { PersonalizationSchema } from './PersonalizationSchema'; -import MessagingEdgeEventType from './MessagingEdgeEventType'; import { PropositionItem, PropositionItemData } from './PropositionItem'; type ContentCardTemplate = 'SmallImage'; @@ -56,32 +55,6 @@ export class ContentCard extends PropositionItem { this.data = contentCardData.data; } - /** - * Convenience method to track when this ContentCard is displayed. - * Equivalent to calling track(MessagingEdgeEventType.DISPLAY). - */ - trackDisplay(): void { - this.track(MessagingEdgeEventType.DISPLAY); - } - - /** - * Convenience method to track when this ContentCard is dismissed. - * - * @param {string | null} interaction - Optional interaction identifier (e.g., "user_dismissed", "auto_dismissed") - */ - trackDismiss(interaction: string | null = null): void { - this.track(interaction, MessagingEdgeEventType.DISMISS, null); - } - - /** - * Convenience method to track user interactions with this ContentCard. - * - * @param {string} interaction - The interaction identifier (e.g., "clicked", "button_pressed", "action_taken") - */ - trackInteraction(interaction: string): void { - this.track(interaction, MessagingEdgeEventType.INTERACT, null); - } - /** * Gets the title content of this ContentCard. * diff --git a/packages/messaging/src/models/HTMLProposition.ts b/packages/messaging/src/models/HTMLProposition.ts index e6aeb0c51..6714b3d89 100644 --- a/packages/messaging/src/models/HTMLProposition.ts +++ b/packages/messaging/src/models/HTMLProposition.ts @@ -11,11 +11,20 @@ */ import { PersonalizationSchema } from './PersonalizationSchema'; +import { PropositionItem, PropositionItemData } from './PropositionItem'; -export interface HTMLProposition { - id: string; - data: { - content: string; - }; - schema: PersonalizationSchema.HTML_CONTENT; +export interface HTMLPropositionData extends PropositionItemData { + data: { + content: string; + }; + schema: PersonalizationSchema.HTML_CONTENT; +} + +export class HTMLProposition extends PropositionItem { + declare data: HTMLPropositionData['data']; + + constructor(htmlData: HTMLPropositionData) { + super(htmlData); + this.data = htmlData.data; + } } \ No newline at end of file diff --git a/packages/messaging/src/models/JSONProposition.ts b/packages/messaging/src/models/JSONProposition.ts index b92b1b9eb..6fdd55f0c 100644 --- a/packages/messaging/src/models/JSONProposition.ts +++ b/packages/messaging/src/models/JSONProposition.ts @@ -11,11 +11,20 @@ */ import { PersonalizationSchema } from './PersonalizationSchema'; +import { PropositionItem, PropositionItemData } from './PropositionItem'; -export interface JSONPropositionItem { - id: string; - data: { - content: string; - }; - schema: PersonalizationSchema.JSON_CONTENT; +export interface JSONPropositionData extends PropositionItemData { + data: { + content: any; + }; + schema: PersonalizationSchema.JSON_CONTENT; +} + +export class JSONPropositionItem extends PropositionItem { + declare data: JSONPropositionData['data']; + + constructor(jsonData: JSONPropositionData) { + super(jsonData); + this.data = jsonData.data; + } } \ No newline at end of file diff --git a/packages/messaging/src/models/MessagingPropositionItem.ts b/packages/messaging/src/models/MessagingPropositionItem.ts index 06b385cfa..25c752cf8 100644 --- a/packages/messaging/src/models/MessagingPropositionItem.ts +++ b/packages/messaging/src/models/MessagingPropositionItem.ts @@ -12,7 +12,7 @@ import { ContentCard } from './ContentCard'; import { HTMLProposition } from './HTMLProposition'; -import { JSONPropositionItem } from './JSONPropositionItem'; +import { JSONPropositionItem } from './JSONProposition'; import { InAppMessage } from './InAppMessage'; import { PropositionItem } from './PropositionItem'; @@ -23,4 +23,4 @@ export type MessagingPropositionItem = | HTMLProposition | InAppMessage | JSONPropositionItem - | PropositionItem; // Base PropositionItem for any other schema types + | PropositionItem; diff --git a/packages/messaging/src/models/PropositionItem.ts b/packages/messaging/src/models/PropositionItem.ts index a045c2d3b..6010debd1 100644 --- a/packages/messaging/src/models/PropositionItem.ts +++ b/packages/messaging/src/models/PropositionItem.ts @@ -128,7 +128,8 @@ export class PropositionItem { * Internal method that performs the actual tracking */ private trackWithDetails(interaction: string | null, eventType: MessagingEdgeEventType, tokens: string[] | null): void { - RCTAEPMessaging.trackPropositionItem(this.uuid, interaction, eventType, tokens); + const nativeIdentifier = this.uuid ?? this.id; + RCTAEPMessaging.trackPropositionItem(nativeIdentifier, interaction, eventType, tokens); } /** From 8e5f825c65985d992d63532acec773272082b69e Mon Sep 17 00:00:00 2001 From: Naman Arora Date: Tue, 19 Aug 2025 15:15:38 +0530 Subject: [PATCH 06/28] removed unused apis of content card --- packages/messaging/src/Messaging.ts | 15 --- packages/messaging/src/models/ContentCard.ts | 122 ------------------ .../messaging/src/models/PropositionItem.ts | 117 ----------------- 3 files changed, 254 deletions(-) diff --git a/packages/messaging/src/Messaging.ts b/packages/messaging/src/Messaging.ts index bea49fa9d..bc28068e5 100644 --- a/packages/messaging/src/Messaging.ts +++ b/packages/messaging/src/Messaging.ts @@ -38,7 +38,6 @@ export interface NativeMessagingModule { trackContentCardDisplay: (proposition: MessagingProposition, contentCard: ContentCard) => void; trackContentCardInteraction: (proposition: MessagingProposition, contentCard: ContentCard) => void; trackPropositionItem: (itemId: string, interaction: string | null, eventType: number, tokens: string[] | null) => void; - generatePropositionInteractionXdm: (itemId: string, interaction: string | null, eventType: number, tokens: string[] | null) => Promise; } const RCTAEPMessaging: NativeModule & NativeMessagingModule = @@ -116,20 +115,6 @@ class Messaging { RCTAEPMessaging.trackPropositionItem(itemId, interaction, eventType, tokens); } - /** - * Generates XDM data for PropositionItem interactions. - * This method is used internally by the PropositionItem.generateInteractionXdm() method. - * - * @param {string} itemId - The unique identifier of the PropositionItem - * @param {string | null} interaction - A custom string value to be recorded in the interaction - * @param {number} eventType - The MessagingEdgeEventType numeric value - * @param {string[] | null} tokens - Array containing the sub-item tokens for recording interaction - * @returns {Promise} Promise containing XDM data for the proposition interaction - */ - static async generatePropositionInteractionXdm(itemId: string, interaction: string | null, eventType: number, tokens: string[] | null): Promise { - return await RCTAEPMessaging.generatePropositionInteractionXdm(itemId, interaction, eventType, tokens); - } - /** * Function to set the UI Message delegate to listen the Message lifecycle events. * @returns A function to unsubscribe from all event listeners diff --git a/packages/messaging/src/models/ContentCard.ts b/packages/messaging/src/models/ContentCard.ts index e4faaa998..d6d299612 100644 --- a/packages/messaging/src/models/ContentCard.ts +++ b/packages/messaging/src/models/ContentCard.ts @@ -55,126 +55,4 @@ export class ContentCard extends PropositionItem { this.data = contentCardData.data; } - /** - * Gets the title content of this ContentCard. - * - * @returns {string} The title content - */ - getTitle(): string { - return this.data.content.title.content; - } - - /** - * Gets the body content of this ContentCard. - * - * @returns {string} The body content - */ - getBody(): string { - return this.data.content.body.content; - } - - /** - * Gets the action URL of this ContentCard. - * - * @returns {string} The action URL - */ - getActionUrl(): string { - return this.data.content.actionUrl; - } - - /** - * Gets the image URL of this ContentCard. - * - * @returns {string} The image URL - */ - getImageUrl(): string { - return this.data.content.image.url; - } - - /** - * Gets the image alt text of this ContentCard. - * - * @returns {string} The image alt text - */ - getImageAlt(): string { - return this.data.content.image.alt; - } - - /** - * Gets the buttons array of this ContentCard. - * - * @returns {Array} The buttons array - */ - getButtons(): Array<{ - actionUrl: string; - id: string; - text: { content: string }; - interactId: string; - }> { - return this.data.content.buttons; - } - - /** - * Checks if this ContentCard has been dismissed. - * - * @returns {boolean} True if dismissed, false otherwise - */ - isDismissed(): boolean { - return this.data.meta.dismissState; - } - - /** - * Checks if this ContentCard has been read. - * - * @returns {boolean} True if read, false otherwise - */ - isRead(): boolean { - return this.data.meta.readState; - } - - /** - * Gets the surface name for this ContentCard. - * - * @returns {string} The surface name - */ - getSurface(): string { - return this.data.meta.surface; - } - - /** - * Gets the template type for this ContentCard. - * - * @returns {ContentCardTemplate} The template type - */ - getTemplate(): ContentCardTemplate { - return this.data.meta.adobe.template; - } - - /** - * Gets the expiry date of this ContentCard as epoch seconds. - * - * @returns {number} The expiry date - */ - getExpiryDate(): number { - return this.data.expiryDate; - } - - /** - * Gets the published date of this ContentCard as epoch seconds. - * - * @returns {number} The published date - */ - getPublishedDate(): number { - return this.data.publishedDate; - } - - /** - * Checks if this ContentCard has expired. - * - * @returns {boolean} True if expired, false otherwise - */ - isExpired(): boolean { - const now = Math.floor(Date.now() / 1000); - return this.data.expiryDate > 0 && now > this.data.expiryDate; - } } diff --git a/packages/messaging/src/models/PropositionItem.ts b/packages/messaging/src/models/PropositionItem.ts index 6010debd1..64b7d5f98 100644 --- a/packages/messaging/src/models/PropositionItem.ts +++ b/packages/messaging/src/models/PropositionItem.ts @@ -111,15 +111,12 @@ export class PropositionItem { eventType?: MessagingEdgeEventType, tokens?: string[] | null ): void { - console.log('i am in track method '); // Handle overloaded method signatures if (typeof interactionOrEventType === 'number' && eventType === undefined) { // First overload: track(eventType) - console.log('track(eventType) here ia m doing awesome '); this.trackWithDetails(null, interactionOrEventType, null); } else if (typeof interactionOrEventType === 'string' || interactionOrEventType === null) { // Second overload: track(interaction, eventType, tokens) - console.log("i am in the second overload"); this.trackWithDetails(interactionOrEventType, eventType!, tokens || null); } } @@ -132,118 +129,4 @@ export class PropositionItem { RCTAEPMessaging.trackPropositionItem(nativeIdentifier, interaction, eventType, tokens); } - /** - * Creates a Map containing XDM data for interaction with this proposition item. - * - * @param {MessagingEdgeEventType} eventType - The MessagingEdgeEventType specifying event type for the interaction - * @returns {Promise} Promise containing XDM data for the proposition interaction - */ - async generateInteractionXdm(eventType: MessagingEdgeEventType): Promise; - - /** - * Creates a Map containing XDM data for interaction with this proposition item. - * - * @param {string | null} interaction - Custom string describing the interaction - * @param {MessagingEdgeEventType} eventType - The MessagingEdgeEventType specifying event type for the interaction - * @param {string[] | null} tokens - Array containing the sub-item tokens for recording interaction - * @returns {Promise} Promise containing XDM data for the proposition interaction - */ - async generateInteractionXdm( - interaction: string | null, - eventType: MessagingEdgeEventType, - tokens: string[] | null - ): Promise; - - // Implementation - async generateInteractionXdm( - interactionOrEventType: string | null | MessagingEdgeEventType, - eventType?: MessagingEdgeEventType, - tokens?: string[] | null - ): Promise { - // Handle overloaded method signatures - if (typeof interactionOrEventType === 'number' && eventType === undefined) { - // First overload: generateInteractionXdm(eventType) - return await RCTAEPMessaging.generatePropositionInteractionXdm(this.id, null, interactionOrEventType, null); - } else if (typeof interactionOrEventType === 'string' || interactionOrEventType === null) { - // Second overload: generateInteractionXdm(interaction, eventType, tokens) - return await RCTAEPMessaging.generatePropositionInteractionXdm(this.id, interactionOrEventType, eventType!, tokens || null); - } - throw new Error('Invalid arguments for generateInteractionXdm'); - } - - /** - * Returns this PropositionItem's content as a JSON content Map. - * Only works if the schema is JSON_CONTENT. - * - * @returns {object | null} Object containing the PropositionItem's content, or null if not JSON content - */ - getJsonContentMap(): object | null { - if (this.schema !== PersonalizationSchema.JSON_CONTENT) { - return null; - } - return this.data.content || null; - } - - /** - * Returns this PropositionItem's content as a JSON content array. - * Only works if the schema is JSON_CONTENT. - * - * @returns {any[] | null} Array containing the PropositionItem's content, or null if not JSON content array - */ - getJsonContentArrayList(): any[] | null { - if (this.schema !== PersonalizationSchema.JSON_CONTENT) { - return null; - } - const content = this.data.content; - return Array.isArray(content) ? content : null; - } - - /** - * Returns this PropositionItem's content as HTML content string. - * Only works if the schema is HTML_CONTENT. - * - * @returns {string | null} String containing the PropositionItem's content, or null if not HTML content - */ - getHtmlContent(): string | null { - if (this.schema !== PersonalizationSchema.HTML_CONTENT) { - return null; - } - return this.data.content || null; - } - - /** - * Checks if this PropositionItem is of ContentCard schema type. - * - * @returns {boolean} True if this is a content card proposition item - */ - isContentCard(): boolean { - return this.schema === PersonalizationSchema.CONTENT_CARD; - } - - /** - * Checks if this PropositionItem is of InApp schema type. - * - * @returns {boolean} True if this is an in-app message proposition item - */ - isInAppMessage(): boolean { - return this.schema === PersonalizationSchema.IN_APP; - } - - /** - * Checks if this PropositionItem is of JSON content schema type. - * - * @returns {boolean} True if this is a JSON content proposition item - */ - isJsonContent(): boolean { - return this.schema === PersonalizationSchema.JSON_CONTENT; - } - - /** - * Checks if this PropositionItem is of HTML content schema type. - * - * @returns {boolean} True if this is an HTML content proposition item - */ - isHtmlContent(): boolean { - return this.schema === PersonalizationSchema.HTML_CONTENT; - } } \ No newline at end of file From 1ef7a89d87ac28a250584ad3594d7e53b538fed0 Mon Sep 17 00:00:00 2001 From: Naman Arora Date: Wed, 20 Aug 2025 09:44:02 +0530 Subject: [PATCH 07/28] generatePropositionInteractionXdm removed --- .../messaging/RCTAEPMessagingModule.java | 190 +----------------- .../messaging/RCTAEPMessagingUtil.java | 7 - 2 files changed, 1 insertion(+), 196 deletions(-) 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 a45d07daf..fc9556aeb 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 @@ -22,11 +22,9 @@ 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; @@ -52,29 +50,13 @@ import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ConcurrentHashMap; -import org.json.JSONObject; - - -import java.nio.charset.StandardCharsets; -import java.util.UUID; -import java.util.concurrent.atomic.AtomicLong; public final class RCTAEPMessagingModule extends ReactContextBaseJavaModule implements PresentationDelegate { - private final AtomicLong globalUuidCounter = new AtomicLong(0L); private final Map propositionItemByUuid = new ConcurrentHashMap<>(); - public void registerPropositionItemUuid(@NonNull final String uuid, @NonNull final PropositionItem item) { - if (uuid != null && item != null) { - propositionItemByUuid.put(uuid, item); - } - } - private String generateItemUuid(String activityId, long counter) { - String key = (activityId != null ? activityId : "") + "#" + counter; - return UUID.nameUUIDFromBytes(key.getBytes(StandardCharsets.UTF_8)).toString(); - } @SuppressWarnings("unchecked") private String extractActivityId(Proposition proposition) { try { @@ -98,11 +80,6 @@ private String extractActivityId(Proposition proposition) { private CountDownLatch latch = new CountDownLatch(1); private Message latestMessage = null; - // Cache to store PropositionItem objects by their ID for unified tracking - private final Map propositionItemCache = new ConcurrentHashMap<>(); - // Cache to store the parent Proposition for each PropositionItem - private final Map propositionCache = new ConcurrentHashMap<>(); - public RCTAEPMessagingModule(ReactApplicationContext reactContext) { super(reactContext); this.reactContext = reactContext; @@ -210,7 +187,7 @@ public void call(Map> propositionsMap) { } // Inject UUID and cache native item for future tracking - final String uuid = generateItemUuid(activityId, globalUuidCounter.incrementAndGet()); + final String uuid = activityId; itemMap.putString("uuid", uuid); propositionItemByUuid.put(uuid, item); @@ -453,7 +430,6 @@ public void trackContentCardInteraction(ReadableMap propositionMap, ReadableMap * Tracks interactions with a PropositionItem using the provided interaction and event type. * This method is used by the React Native PropositionItem.track() method. * - * @param itemId The unique identifier of the PropositionItem * @param interaction A custom string value to be recorded in the interaction (nullable) * @param eventType The MessagingEdgeEventType numeric value * @param tokens Array containing the sub-item tokens for recording interaction (nullable) @@ -515,168 +491,4 @@ public void trackPropositionItem(String uuid, @Nullable String interaction, int } } - - - /** - * Generates XDM data for PropositionItem interactions. - * This method is used by the React Native PropositionItem.generateInteractionXdm() method. - * - * @param itemId The unique identifier of the PropositionItem - * @param interaction A custom string value to be recorded in the interaction (nullable) - * @param eventType The MessagingEdgeEventType numeric value - * @param tokens Array containing the sub-item tokens for recording interaction (nullable) - * @param promise Promise to resolve with XDM data for the proposition interaction - */ - @ReactMethod - public void generatePropositionInteractionXdm(String itemId, @Nullable String interaction, int eventType, @Nullable ReadableArray tokens, Promise promise) { - try { - // Convert eventType int to MessagingEdgeEventType enum - MessagingEdgeEventType edgeEventType = RCTAEPMessagingUtil.getEventType(eventType); - if (edgeEventType == null) { - promise.reject("InvalidEventType", "Invalid eventType: " + eventType); - return; - } - - // Find the PropositionItem by ID - PropositionItem propositionItem = findPropositionItemById(itemId); - - if (propositionItem == null) { - promise.reject("PropositionItemNotFound", "No PropositionItem found with ID: " + itemId); - return; - } - - // Convert ReadableArray to List if provided - List tokenList = null; - if (tokens != null) { - tokenList = new ArrayList<>(); - for (int i = 0; i < tokens.size(); i++) { - tokenList.add(tokens.getString(i)); - } - } - - // Generate XDM data using the appropriate method - Map xdmData; - if (interaction != null && tokenList != null) { - xdmData = propositionItem.generateInteractionXdm(interaction, edgeEventType, tokenList); - } else if (interaction != null) { - xdmData = propositionItem.generateInteractionXdm(interaction, edgeEventType, null); - } else { - xdmData = propositionItem.generateInteractionXdm(edgeEventType); - } - - if (xdmData != null) { - // Convert Map to WritableMap for React Native - WritableMap result = RCTAEPMessagingUtil.toWritableMap(xdmData); - promise.resolve(result); - } else { - promise.reject("XDMGenerationFailed", "Failed to generate XDM data for PropositionItem: " + itemId); - } - - } catch (Exception e) { -// Log.error(TAG, "Error generating XDM data for PropositionItem: " + itemId + ", error: " + e.getMessage()); - promise.reject("XDMGenerationError", "Error generating XDM data: " + e.getMessage(), e); - } - } - - /** - * Caches a PropositionItem and its parent Proposition for later tracking. - * This method should be called when PropositionItems are created from propositions. - * - * @param propositionItem The PropositionItem to cache - * @param parentProposition The parent Proposition containing this item - */ - public void cachePropositionItem(PropositionItem propositionItem, Proposition parentProposition) { - if (propositionItem != null && propositionItem.getItemId() != null) { - String itemId = propositionItem.getItemId(); - - // Cache the PropositionItem - propositionItemCache.put(itemId, propositionItem); - - // Cache the parent Proposition - if (parentProposition != null) { - propositionCache.put(itemId, parentProposition); - - // Set the proposition reference in the PropositionItem if possible - try { - // Use reflection to set the proposition reference - java.lang.reflect.Field propositionRefField = propositionItem.getClass().getDeclaredField("propositionReference"); - propositionRefField.setAccessible(true); - propositionRefField.set(propositionItem, new java.lang.ref.SoftReference<>(parentProposition)); - } catch (Exception e) { - - } - } - - } - } - - /** - * Caches multiple PropositionItems from a list of propositions. - * This is a convenience method for caching all items from multiple propositions. - * - * @param propositions List of propositions containing items to cache - */ - public void cachePropositionsItems(List propositions) { - if (propositions != null) { - for (Proposition proposition : propositions) { - if (proposition.getItems() != null) { - for (PropositionItem item : proposition.getItems()) { - cachePropositionItem(item, proposition); - } - } - } - } - } - - /** - * Finds a cached PropositionItem by its ID. - * - * @param itemId The ID of the PropositionItem to find - * @return The PropositionItem if found, null otherwise - */ - private PropositionItem findPropositionItemById(String itemId) { - return propositionItemCache.get(itemId); - } - - /** - * Finds a cached parent Proposition by PropositionItem ID. - * - * @param itemId The ID of the PropositionItem whose parent to find - * @return The parent Proposition if found, null otherwise - */ - private Proposition findPropositionByItemId(String itemId) { - return propositionCache.get(itemId); - } - - /** - * Clears the PropositionItem cache. - * This should be called when propositions are refreshed or when memory cleanup is needed. - */ - @ReactMethod - public void clearPropositionItemCache() { - propositionItemCache.clear(); - propositionCache.clear(); - } - - /** - * Gets the current size of the PropositionItem cache. - * Useful for debugging and monitoring. - * - * @param promise Promise that resolves with the cache size - */ - @ReactMethod - public void getPropositionItemCacheSize(Promise promise) { - promise.resolve(propositionItemCache.size()); - } - - /** - * Checks if a PropositionItem exists in the cache. - * - * @param itemId The ID of the PropositionItem to check - * @param promise Promise that resolves with boolean indicating if item exists - */ - @ReactMethod - public void hasPropositionItem(String itemId, Promise promise) { - promise.resolve(propositionItemCache.containsKey(itemId)); - } } diff --git a/packages/messaging/android/src/main/java/com/adobe/marketing/mobile/reactnative/messaging/RCTAEPMessagingUtil.java b/packages/messaging/android/src/main/java/com/adobe/marketing/mobile/reactnative/messaging/RCTAEPMessagingUtil.java index adf1c3ed7..805d0e165 100644 --- a/packages/messaging/android/src/main/java/com/adobe/marketing/mobile/reactnative/messaging/RCTAEPMessagingUtil.java +++ b/packages/messaging/android/src/main/java/com/adobe/marketing/mobile/reactnative/messaging/RCTAEPMessagingUtil.java @@ -214,13 +214,6 @@ static ReadableArray convertMessagesToJS(final Collection messages) { return result; } - /** Ensures a nested map exists at the given key; creates an empty one if missing. */ - private static void ensurePath(final WritableMap parent, final String key) { - if (parent == null) return; - if (!parent.hasKey(key) || parent.getType(key) != ReadableType.Map) { - parent.putMap(key, Arguments.createMap()); - } - } public static WritableMap convertPropositionItem(final PropositionItem item) { WritableMap map = Arguments.createMap(); From ef8977de881f47273a8bcabc24c78feb0757d4b5 Mon Sep 17 00:00:00 2001 From: Naman Arora Date: Wed, 20 Aug 2025 20:22:59 +0530 Subject: [PATCH 08/28] ios implementation of track functionality --- packages/messaging/ios/src/RCTAEPMessaging.mm | 8 + .../messaging/ios/src/RCTAEPMessaging.swift | 216 +++++++++++++++--- 2 files changed, 193 insertions(+), 31 deletions(-) diff --git a/packages/messaging/ios/src/RCTAEPMessaging.mm b/packages/messaging/ios/src/RCTAEPMessaging.mm index 6a227ed73..76580fd09 100644 --- a/packages/messaging/ios/src/RCTAEPMessaging.mm +++ b/packages/messaging/ios/src/RCTAEPMessaging.mm @@ -55,4 +55,12 @@ @interface RCT_EXTERN_MODULE (RCTAEPMessaging, RCTEventEmitter) : (NSDictionary *)propositionMap contentCardMap : (NSDictionary *)contentCardMap); +RCT_EXTERN_METHOD(trackPropositionItem + : (NSString *)uuid interaction + : (NSString * _Nullable)interaction eventType + : (NSInteger)eventType tokens + : (NSArray * _Nullable)tokens withResolver + : (RCTPromiseResolveBlock)resolve withRejecter + : (RCTPromiseRejectBlock)reject); + @end diff --git a/packages/messaging/ios/src/RCTAEPMessaging.swift b/packages/messaging/ios/src/RCTAEPMessaging.swift index 111955099..ecc5bdb3b 100644 --- a/packages/messaging/ios/src/RCTAEPMessaging.swift +++ b/packages/messaging/ios/src/RCTAEPMessaging.swift @@ -31,6 +31,36 @@ public class RCTAEPMessaging: RCTEventEmitter, MessagingDelegate { private var propositionItemCache = [String: PropositionItem]() // Cache to store the parent Proposition for each PropositionItem private var propositionCache = [String: Proposition]() + // UUID-based cache for PropositionItem to mirror Android implementation + // Weak parent map: uuid -> parent (weak) + private let parentByUuid = NSMapTable( + keyOptions: .strongMemory, + valueOptions: .weakMemory + ) + // Holds the SDK item (strong) + its parent Proposition (weak) + private final class ItemHandle { + let item: PropositionItem + weak var parent: Proposition? + init(item: PropositionItem, parent: Proposition?) { + self.item = item + self.parent = parent + } + } + + // Adjust the types to your SDK (AEP Messaging) if names vary + private struct CachedItem { + let parent: Proposition // strong + let item: PropositionItem // strong + } + + /// Build your uuid. If you already have a single unique uuid, just return it. + /// Otherwise, compose it as "propositionId#itemId" (matches your logs). + private func makeUuid(for parent: Proposition, item: PropositionItem) -> String { + // If you already have a provided uuid -> return that. + // return item.uuid + return "\(parent)" + } + // Map uuid (activityId) to parent Proposition override init() { super.init() @@ -77,23 +107,90 @@ public class RCTAEPMessaging: RCTEventEmitter, MessagingDelegate { withRejecter reject: @escaping RCTPromiseRejectBlock ) { let surfacePaths = surfaces.map { $0.isEmpty ? Surface() : Surface(path: $0) } - Messaging.getPropositionsForSurfaces(surfacePaths) { propositions, error in - guard error == nil else { + Messaging.getPropositionsForSurfaces(surfacePaths) {[weak self] propositions, error in + NSLog("[MessagingBridge] getPropositionsForSurfaces called with surfaces: \(surfacePaths)") + + guard let self, error == nil else { + NSLog("[MessagingBridge] Error retrieving propositions: \(String(describing: error))") reject("Unable to Retrieve Propositions", nil, nil) return } - if (propositions != nil && propositions!.isEmpty) { - resolve([String: Any]()); - return; + + if let p = propositions, p.isEmpty { + NSLog("[MessagingBridge] No propositions returned from SDK") + resolve([String: Any]()) + return } - - // Cache PropositionItems for unified tracking when propositions are retrieved - if let propositionsDict = propositions { - let allPropositions = Array(propositionsDict.values).flatMap { $0 } - self.cachePropositionsItems(allPropositions) + + // Clear per fetch (Android parity) + self.propositionByUuid.removeAll() + NSLog("[MessagingBridge] Cleared propositionByUuid cache") + + guard let propositionsDict = propositions else { + NSLog("[MessagingBridge] propositionsDict is nil") + resolve([String: Any]()) + return } - resolve(RCTAEPMessagingDataBridge.transformPropositionDict(dict: propositions!)) + + var out: [String: [Any]] = [:] + let bundlePrefix = "mobileapp://" + (Bundle.main.bundleIdentifier ?? "") + "/" + NSLog("[MessagingBridge] bundlePrefix: \(bundlePrefix)") + + for (surface, propList) in propositionsDict { + let surfaceKey = surface.uri.hasPrefix(bundlePrefix) + ? String(surface.uri.dropFirst(bundlePrefix.count)) + : surface.uri + NSLog("[MessagingBridge] Processing surface: \(surface.uri), mapped key: \(surfaceKey), proposition count: \(propList.count)") + + var jsProps: [Any] = [] + + for (index, proposition) in propList.enumerated() { + var pMap: [String: Any] = proposition.asDictionary() ?? [:] + NSLog("[MessagingBridge] Proposition[\(index)] raw dictionary: \(pMap)") + + // Android parity: use activityId as uuid for all items in this proposition + let activityId = self.extractActivityId(from: pMap) + NSLog("[MessagingBridge] Extracted activityId: \(String(describing: activityId))") + + var newItems: [[String: Any]] = [] + let items = proposition.items + let sdkItems = (pMap["items"] as? [[String: Any]]) + NSLog("[MessagingBridge] Proposition[\(index)] items count: \(items.count), sdkItems count: \(String(describing: sdkItems?.count))") + + if !items.isEmpty { + for (itemIndex, item) in items.enumerated() { + var itemMap: [String: Any] + if let sdkItems = sdkItems, itemIndex < sdkItems.count { + itemMap = sdkItems[itemIndex] // keep SDK’s fields + } else { + itemMap = ["id": item.itemId] // minimal fallback + } + + if let uuid = activityId { + // Inject Android-style uuid (note: same for every item under this proposition) + itemMap["uuid"] = uuid + + // Map uuid to parent proposition for simpler tracking: uuid -> proposition + self.propositionByUuid[uuid] = proposition + + NSLog("[MessagingBridge] Injected uuid=\(uuid) for item[\(itemIndex)] id=\(item.itemId)") + } + + newItems.append(itemMap) + } + } + + pMap["items"] = newItems + jsProps.append(pMap) + } + + out[surfaceKey] = jsProps + NSLog("[MessagingBridge] Finished surfaceKey=\(surfaceKey) with \(jsProps.count) propositions") + } + + NSLog("[MessagingBridge] Final output built for \(out.keys.count) surfaces") + resolve(out) } } @@ -268,46 +365,75 @@ public class RCTAEPMessaging: RCTEventEmitter, MessagingDelegate { * This method is used by the React Native PropositionItem.track() method. * * - Parameters: - * - itemId: The unique identifier of the PropositionItem + * - uuid: The UUID mapped to the PropositionItem (derived from activityId) * - interaction: A custom string value to be recorded in the interaction (optional) * - eventType: The MessagingEdgeEventType numeric value * - tokens: Array containing the sub-item tokens for recording interaction (optional) */ + @objc func trackPropositionItem( - _ itemId: String, + _ uuid: String, interaction: String?, eventType: Int, tokens: [String]?, withResolver resolve: @escaping RCTPromiseResolveBlock, withRejecter reject: @escaping RCTPromiseRejectBlock ) { - guard let edgeEventType = MessagingEdgeEventType(rawValue: eventType) else { - reject("InvalidEventType", "Invalid eventType: \(eventType)", nil) + NSLog("[MessagingBridge] trackPropositionItem called with eventType=\(eventType), uuid=\(uuid), interaction=\(String(describing: interaction)), tokens=\(String(describing: tokens))") + + guard !uuid.isEmpty else { + NSLog("[MessagingBridge] Empty uuid provided; no-op.") + resolve(nil) return } - - guard let propositionItem = findPropositionItemById(itemId) else { - print("Warning: PropositionItem not found for ID: \(itemId)") + + guard let proposition = propositionByUuid[uuid] else { + NSLog("[MessagingBridge] No cached proposition for uuid=\(uuid); no-op.") resolve(nil) return } - - // Call the appropriate track method based on provided parameters - if let interaction = interaction, let tokens = tokens { - // Track with interaction and tokens - propositionItem.track(interaction, withEdgeEventType: edgeEventType, forTokens: tokens) - } else if let interaction = interaction { - // Track with interaction only - propositionItem.track(interaction, withEdgeEventType: edgeEventType) + + NSLog("[MessagingBridge] Found proposition for uuid=\(uuid). scope=\(proposition.scope), items=\(proposition.items.count)") + + // Event type mapping (Android parity) + let edgeEventType = mapEdgeEventType(eventType) ?? .display + + // Track on the first item under this proposition + guard let item = proposition.items.first else { + NSLog("[MessagingBridge] Proposition for uuid=\(uuid) has no items; no-op.") + resolve(nil) + return + } + + // Normalize inputs + let trimmedInteraction = interaction?.trimmingCharacters(in: .whitespacesAndNewlines) + let nonEmptyTokens = tokens?.compactMap { token in + let t = token.trimmingCharacters(in: .whitespacesAndNewlines) + return t.isEmpty ? nil : t + } + + if let i = trimmedInteraction, !i.isEmpty { + if let t = nonEmptyTokens, !t.isEmpty { + NSLog("[MessagingBridge] Tracking with interaction and tokens. uuid=\(uuid), interaction=\(i), tokens=\(t), eventType=\(edgeEventType)") + item.track(i, withEdgeEventType: edgeEventType, forTokens: t) + } else { + NSLog("[MessagingBridge] Tracking with interaction only. uuid=\(uuid), interaction=\(i), eventType=\(edgeEventType.rawValue)") + item.track(i, withEdgeEventType: edgeEventType) + } + } else if let t = nonEmptyTokens, !t.isEmpty { + NSLog("[MessagingBridge] Tracking with tokens only (no interaction). uuid=\(uuid), tokens=\(t), eventType=\(edgeEventType.rawValue)") + item.track(withEdgeEventType: edgeEventType) } else { - // Track with event type only - propositionItem.track(withEdgeEventType: edgeEventType) + NSLog("[MessagingBridge] Tracking with event only. uuid=\(uuid), eventType=\(edgeEventType.rawValue)") + item.track(withEdgeEventType: edgeEventType) } - - print("Successfully tracked PropositionItem: \(itemId) with eventType: \(edgeEventType)") + + NSLog("[MessagingBridge] Tracking complete for uuid=\(uuid)") resolve(nil) } + + /** * Generates XDM data for PropositionItem interactions. @@ -402,7 +528,8 @@ public class RCTAEPMessaging: RCTEventEmitter, MessagingDelegate { private func findPropositionByItemId(_ itemId: String) -> Proposition? { return propositionCache[itemId] } - + // Map uuid (scopeDetails.activity.id) -> parent Proposition + private var propositionByUuid = [String: Proposition]() /** * Clears the PropositionItem cache. * This should be called when propositions are refreshed or when memory cleanup is needed. @@ -509,3 +636,30 @@ public class RCTAEPMessaging: RCTEventEmitter, MessagingDelegate { RCTAEPMessaging.emitter.sendEvent(withName: name, body: body) } } + +// MARK: - Private helpers +private extension RCTAEPMessaging { + /// Extracts activityId from a proposition dictionary at scopeDetails.activity.id + func extractActivityId(from propositionDict: [String: Any]) -> String? { + guard let scopeDetails = propositionDict["scopeDetails"] as? [String: Any], + let activity = scopeDetails["activity"] as? [String: Any], + let id = activity["id"] as? String else { + return nil + } + return id + } + + /// Maps JS MessagingEdgeEventType integer values to AEPMessaging.MessagingEdgeEventType cases + /// JS enum values: DISMISS=0, INTERACT=1, TRIGGER=2, DISPLAY=3, PUSH_APPLICATION_OPENED=4, PUSH_CUSTOM_ACTION=5 + func mapEdgeEventType(_ value: Int) -> MessagingEdgeEventType? { + switch value { + case 0: return .dismiss + case 1: return .interact + case 2: return .trigger + case 3: return .display + case 4: return .pushApplicationOpened + case 5: return .pushCustomAction + default: return nil + } + } +} From fc21588168f95f6ab5b6ff8cbcef1a60d177e5e9 Mon Sep 17 00:00:00 2001 From: Naman Arora Date: Wed, 20 Aug 2025 21:14:34 +0530 Subject: [PATCH 09/28] uuid vs proposition map added --- .../messaging/RCTAEPMessagingModule.java | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) 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 fc9556aeb..1cd2bb4d8 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 @@ -55,7 +55,7 @@ public final class RCTAEPMessagingModule extends ReactContextBaseJavaModule implements PresentationDelegate { - private final Map propositionItemByUuid = new ConcurrentHashMap<>(); + private final Map propositionItemByUuid = new ConcurrentHashMap<>(); @SuppressWarnings("unchecked") private String extractActivityId(Proposition proposition) { @@ -189,7 +189,7 @@ public void call(Map> propositionsMap) { // Inject UUID and cache native item for future tracking final String uuid = activityId; itemMap.putString("uuid", uuid); - propositionItemByUuid.put(uuid, item); + propositionItemByUuid.put(uuid, p); // Helpful log try { Log.d(TAG, "itemId=" + item.getItemId() + " uuid=" + uuid); } catch (Throwable ignore) {} @@ -452,12 +452,20 @@ public void trackPropositionItem(String uuid, @Nullable String interaction, int Log.d(TAG, "UUID is null; cannot track."); return; } + final Proposition proposition = propositionItemByUuid.get(uuid); - final PropositionItem propositionItem = propositionItemByUuid.get(uuid); - if (propositionItem == null) { + if (proposition == null) { Log.d(TAG, "PropositionItem not found in uuid cache for uuid: " + uuid); return; + + } + final List items = proposition.getItems(); + if (items == null || items.isEmpty()) { + Log.d(TAG, "Proposition has no items for uuid: " + uuid); + return; } + final PropositionItem propositionItem = items.get(0); + Log.d(TAG, "Found PropositionItem in uuid cache for uuid: " + uuid); // Convert ReadableArray tokens -> List From cf02267e67240fce01fe94a135e8dd87d43e332f Mon Sep 17 00:00:00 2001 From: Naman Arora Date: Mon, 25 Aug 2025 11:35:50 +0530 Subject: [PATCH 10/28] MessagingProposition class fixes --- .../app/MessagingView.tsx | 23 +++++++-- .../src/models/MessagingProposition.ts | 48 +++++++++++++++++-- .../messaging/src/models/PropositionItem.ts | 7 ++- 3 files changed, 70 insertions(+), 8 deletions(-) diff --git a/apps/AEPSampleAppNewArchEnabled/app/MessagingView.tsx b/apps/AEPSampleAppNewArchEnabled/app/MessagingView.tsx index 254f4a3d5..f611aa355 100644 --- a/apps/AEPSampleAppNewArchEnabled/app/MessagingView.tsx +++ b/apps/AEPSampleAppNewArchEnabled/app/MessagingView.tsx @@ -21,8 +21,9 @@ import { Message, ContentCard, HTMLProposition, - JSONPropositionItem, + JSONPropositionItem } from '@adobe/react-native-aepmessaging' +import { MessagingProposition } from '@adobe/react-native-aepmessaging'; import styles from '../styles/styles'; import { useRouter } from 'expo-router'; @@ -63,12 +64,26 @@ const setMessagingDelegate = () => { }); console.log('messaging delegate set'); }; - const getPropositionsForSurfaces = async () => { const messages = await Messaging.getPropositionsForSurfaces(SURFACES); - console.log(JSON.stringify(messages)); -}; + // console.log("messages", messages); + + for (const surface of SURFACES) { + const propositions = messages[surface] || []; + for (const proposition of propositions) { + const newMessage = new MessagingProposition(proposition); + console.log("newMessage here", newMessage); + newMessage.items[0].track(MessagingEdgeEventType.DISPLAY); + newMessage.items[0].track('content_card_clicked', MessagingEdgeEventType.INTERACT, null); + newMessage.items[0].track(MessagingEdgeEventType.DISPLAY); + console.log('Tracked content card display using unified API'); + newMessage.items[0].track('content_card_clicked', MessagingEdgeEventType.INTERACT, null); + console.log('Tracked content card interaction using unified API'); + } + } + //console.log(JSON.stringify(mapped)); +}; const trackAction = async () => { MobileCore.trackAction('tuesday', {full: true}); }; diff --git a/packages/messaging/src/models/MessagingProposition.ts b/packages/messaging/src/models/MessagingProposition.ts index 7fda707e4..d6c404320 100644 --- a/packages/messaging/src/models/MessagingProposition.ts +++ b/packages/messaging/src/models/MessagingProposition.ts @@ -10,12 +10,54 @@ language governing permissions and limitations under the License. */ -import { MessagingPropositionItem } from './MessagingPropositionItem'; import { ScopeDetails } from './ScopeDetails'; +import { PersonalizationSchema } from './PersonalizationSchema'; +import { ContentCard } from './ContentCard'; +import { HTMLProposition } from './HTMLProposition'; +import { JSONPropositionItem } from './JSONProposition'; +import { PropositionItem } from './PropositionItem'; -export interface MessagingProposition { +export class MessageProposition { id: string; scope: string; scopeDetails: ScopeDetails; - items: MessagingPropositionItem[]; + items: PropositionItem[]; + + constructor(raw: { id: string; scope: string; scopeDetails: ScopeDetails; items?: any[] }) { + this.id = raw?.id ?? ''; + this.scope = raw?.scope ?? ''; + this.scopeDetails = (raw?.scopeDetails as ScopeDetails) ?? ({} as ScopeDetails); + + // Mirror activity.id into activity.activityID for convenience + const activityIdFromScope = this.scopeDetails?.activity?.id ?? ''; + if (this.scopeDetails?.activity) { + (this.scopeDetails.activity as any).activityID = activityIdFromScope; + } + + const rawItems = Array.isArray(raw?.items) ? raw.items : []; + this.items = rawItems.map((itemData: any) => { + const activityId = this.scopeDetails?.activity?.id ?? ''; + let instance: any; + switch (itemData?.schema) { + case PersonalizationSchema.CONTENT_CARD: + instance = new ContentCard(itemData as any); + (instance as any).activityID = activityId; + return instance; + case PersonalizationSchema.HTML_CONTENT: + instance = new HTMLProposition(itemData as any); + (instance as any).activityID = activityId; + return instance; + case PersonalizationSchema.JSON_CONTENT: + instance = new JSONPropositionItem(itemData as any); + (instance as any).activityID = activityId; + return instance; + default: + instance = new PropositionItem(itemData as any); + (instance as any).activityID = activityId; + return instance; + } + }); + } } + +export { MessageProposition as MessagingProposition }; diff --git a/packages/messaging/src/models/PropositionItem.ts b/packages/messaging/src/models/PropositionItem.ts index 64b7d5f98..331411468 100644 --- a/packages/messaging/src/models/PropositionItem.ts +++ b/packages/messaging/src/models/PropositionItem.ts @@ -23,6 +23,7 @@ export interface PropositionItemData { id: string; uuid: string; schema: PersonalizationSchema; + activityID: string; data: { [key: string]: any; }; @@ -38,6 +39,7 @@ export interface PropositionItemData { export class PropositionItem { id: string; uuid: string; + activityID: string; schema: PersonalizationSchema; data: { [key: string]: any }; @@ -46,6 +48,7 @@ export class PropositionItem { this.schema = propositionItemData.schema; this.data = propositionItemData.data; this.uuid = propositionItemData.uuid; + this.activityID = propositionItemData.activityID; } /** @@ -125,7 +128,9 @@ export class PropositionItem { * Internal method that performs the actual tracking */ private trackWithDetails(interaction: string | null, eventType: MessagingEdgeEventType, tokens: string[] | null): void { - const nativeIdentifier = this.uuid ?? this.id; + console.log("activityID here", this.activityID); + const nativeIdentifier = this.activityID ?? null; + console.log("nativeIdentifier here", nativeIdentifier); RCTAEPMessaging.trackPropositionItem(nativeIdentifier, interaction, eventType, tokens); } From 911271cfb0f0493551b40908055ebc8cffcac81a Mon Sep 17 00:00:00 2001 From: Naman Arora Date: Mon, 25 Aug 2025 11:36:50 +0530 Subject: [PATCH 11/28] get proposition for surfaces android fixes --- .../messaging/RCTAEPMessagingModule.java | 272 +++++++++++------- .../messaging/RCTAEPMessagingUtil.java | 66 +++-- 2 files changed, 212 insertions(+), 126 deletions(-) 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 1cd2bb4d8..528881358 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 @@ -118,130 +118,194 @@ public void getLatestMessage(final Promise promise) { } @ReactMethod - public void getPropositionsForSurfaces(ReadableArray surfaces, final Promise promise) { - final String bundleId = this.reactContext.getPackageName(); - + public void getPropositionsForSurfaces(ReadableArray surfaces, + final Promise promise) { + String bundleId = this.reactContext.getPackageName(); Messaging.getPropositionsForSurfaces( RCTAEPMessagingUtil.convertSurfaces(surfaces), new AdobeCallbackWithError>>() { @Override public void fail(final AdobeError adobeError) { - Log.d(TAG, "getPropositionsForSurfaces: fail: " + adobeError.getErrorName()); - promise.reject(adobeError.getErrorName(), "Unable to get Propositions"); + promise.reject(adobeError.getErrorName(), + "Unable to get Propositions"); } @Override - public void call(Map> propositionsMap) { + public void call( + Map> propositionsMap) { + propositionItemByUuid.clear(); + // Build UUID->Proposition map keyed by scopeDetails.activity.activityID when available try { - // Optional: clear UUID cache per fetch to avoid stale entries - propositionItemByUuid.clear(); - - WritableMap out = Arguments.createMap(); - for (Map.Entry> entry : propositionsMap.entrySet()) { - final String surfaceKey = (entry.getKey() != null) - ? entry.getKey().getUri().replace("mobileapp://" + bundleId + "/", "") - : "unknown"; - final List propositions = entry.getValue(); - final WritableArray jsProps = Arguments.createArray(); - - Log.d(TAG, "Surface [" + surfaceKey + "] propCount=" + (propositions != null ? propositions.size() : 0)); - - if (propositions != null) { - for (Proposition p : propositions) { - try { - // Start from SDK map for proposition to keep full parity - WritableMap pMap = RCTAEPMessagingUtil.toWritableMap(p.toEventData()); - - // Derive activityId for UUID generation - final String activityId = extractActivityId(p); - Log.d(TAG, "activityId=" + activityId); - - // If SDK already included items, we'll replace them with UUID-injected ones - final WritableArray newItems = Arguments.createArray(); - - final List items = p.getItems(); - if (items != null && !items.isEmpty()) { - Log.d(TAG, "items.size=" + items.size()); - - // If you want to "overlay" UUIDs on top of SDK's item maps, fetch them: - ReadableArray sdkItems = null; - if (pMap.hasKey("items") && pMap.getType("items") == ReadableType.Array) { - sdkItems = pMap.getArray("items"); + List propositions = entry.getValue(); + if (propositions == null) continue; + for (Proposition p : propositions) { + try { + Map eventData = p.toEventData(); + if (eventData == null) continue; + Object sd = eventData.get("scopeDetails"); + String key = null; + if (sd instanceof Map) { + Object act = ((Map) sd).get("activity"); + if (act instanceof Map) { + Object activityID = ((Map) act).get("activityID"); + if (activityID instanceof String) { + key = (String) activityID; + } else { + Object id = ((Map) act).get("id"); + if (id instanceof String) key = (String) id; } - - for (int i = 0; i < items.size(); i++) { - final PropositionItem item = items.get(i); - - // Base item map: prefer SDK's item map at same index; fallback to manual conversion - WritableMap itemMap = Arguments.createMap(); - try { - if (sdkItems != null && i < sdkItems.size() && sdkItems.getType(i) == ReadableType.Map) { - itemMap.merge(sdkItems.getMap(i)); // like JS spread - } else { - itemMap = RCTAEPMessagingUtil.convertPropositionItem(item); - } - } catch (Throwable t) { - Log.d(TAG, "Fallback to manual item conversion (index " + i + "): " + t.getMessage()); - itemMap = RCTAEPMessagingUtil.convertPropositionItem(item); - } - - // Inject UUID and cache native item for future tracking - final String uuid = activityId; - itemMap.putString("uuid", uuid); - propositionItemByUuid.put(uuid, p); - - // Helpful log - try { Log.d(TAG, "itemId=" + item.getItemId() + " uuid=" + uuid); } catch (Throwable ignore) {} - - newItems.pushMap(itemMap); - } - } else { - Log.d(TAG, "Proposition has no items."); } - - // Overwrite items with UUID-injected array - pMap.putArray("items", newItems); - - jsProps.pushMap(pMap); - } catch (Throwable propEx) { - Log.d(TAG, "Error building proposition payload, falling back to SDK map: " + propEx.getMessage()); - // Fallback: push SDK's raw proposition if something went wrong - jsProps.pushMap(RCTAEPMessagingUtil.toWritableMap(p.toEventData())); } - } + if (key == null) key = extractActivityId(p); + if (key != null) { + propositionItemByUuid.put(key, p); + } + } catch (Throwable ignore) {} } - - // (Optional) per-surface payload log (size only to keep logs light) - Log.d(TAG, "Surface [" + surfaceKey + "] payload size=" + jsProps.size()); - - out.putArray(surfaceKey, jsProps); } + } catch (Throwable ignore) {} - // Log the full 'out' map lightly (size of top-level keys) - try { - Map outSnapshot = RCTAEPMessagingUtil.convertReadableMapToMap(out); - Log.d(TAG, "OUT keys=" + outSnapshot.keySet()); - } catch (Throwable e) { - Log.d(TAG, "OUT log failed: " + e.getMessage()); - } - - promise.resolve(out); - } catch (Throwable topEx) { - Log.d(TAG, "Top-level error building propositions: " + topEx.getMessage()); - // As a last resort, mirror the previous behavior: - try { - String pkg = bundleId; - WritableMap fallback = RCTAEPMessagingUtil.convertSurfacePropositions(propositionsMap, pkg); - promise.resolve(fallback); - } catch (Throwable finalEx) { - promise.reject("BuildError", "Failed to build propositions", finalEx); - } - } + promise.resolve(RCTAEPMessagingUtil.convertSurfacePropositions( + propositionsMap, bundleId)); } }); } +// public void getPropositionsForSurfaces(ReadableArray surfaces, final Promise promise) { +// final String bundleId = this.reactContext.getPackageName(); +// +// Messaging.getPropositionsForSurfaces( +// RCTAEPMessagingUtil.convertSurfaces(surfaces), +// new AdobeCallbackWithError>>() { +// @Override +// public void fail(final AdobeError adobeError) { +// Log.d(TAG, "getPropositionsForSurfaces: fail: " + adobeError.getErrorName()); +// promise.reject(adobeError.getErrorName(), "Unable to get Propositions"); +// } +// +// @Override +// public void call(Map> propositionsMap) { +// try { +// // Optional: clear UUID cache per fetch to avoid stale entries +// propositionItemByUuid.clear(); +// +// WritableMap out = Arguments.createMap(); +// +// for (Map.Entry> entry : propositionsMap.entrySet()) { +// final String surfaceKey = (entry.getKey() != null) +// ? entry.getKey().getUri().replace("mobileapp://" + bundleId + "/", "") +// : "unknown"; +// final List propositions = entry.getValue(); +// final WritableArray jsProps = Arguments.createArray(); +// +// Log.d(TAG, "Surface [" + surfaceKey + "] propCount=" + (propositions != null ? propositions.size() : 0)); +// +// if (propositions != null) { +// for (Proposition p : propositions) { +// try { +// // Start from SDK map for proposition to keep full parity +// WritableMap pMap = RCTAEPMessagingUtil.toWritableMap(p.toEventData()); +// +// // Derive activityId for UUID generation +// final String activityId = extractActivityId(p); +// Log.d(TAG, "activityId=" + activityId); +// +// // If SDK already included items, we'll replace them with UUID-injected ones +// final WritableArray newItems = Arguments.createArray(); +// +// final List items = p.getItems(); +// if (items != null && !items.isEmpty()) { +// Log.d(TAG, "items.size=" + items.size()); +// +// // If you want to "overlay" UUIDs on top of SDK's item maps, fetch them: +// ReadableArray sdkItems = null; +// if (pMap.hasKey("items") && pMap.getType("items") == ReadableType.Array) { +// sdkItems = pMap.getArray("items"); +// } +// +// for (int i = 0; i < items.size(); i++) { +// final PropositionItem item = items.get(i); +// +// // Base item map: prefer SDK's item map at same index; fallback to manual conversion +// WritableMap itemMap = Arguments.createMap(); +// try { +// if (sdkItems != null && i < sdkItems.size() && sdkItems.getType(i) == ReadableType.Map) { +// itemMap.merge(sdkItems.getMap(i)); // like JS spread +// } else { +// itemMap = RCTAEPMessagingUtil.convertPropositionItem(item); +// } +// } catch (Throwable t) { +// Log.d(TAG, "Fallback to manual item conversion (index " + i + "): " + t.getMessage()); +// itemMap = RCTAEPMessagingUtil.convertPropositionItem(item); +// } +// +// // Inject UUID and cache native item for future tracking +// final String uuid = activityId; +// itemMap.putString("uuid", uuid); +// // Use scopeDetails.activity.activityID as the map key when available +// try { +// Map eventData = p.toEventData(); +// Map scopeDetails = (Map) eventData.get("scopeDetails"); +// Map activity = scopeDetails != null ? (Map) scopeDetails.get("activity") : null; +// String activityID = activity != null ? (String) activity.get("activityID") : null; +// String key = activityID != null ? activityID : uuid; +// propositionItemByUuid.put(key, p); +// } catch (Throwable ignore) { +// propositionItemByUuid.put(uuid, p); +// } +// +// // Helpful log +// try { Log.d(TAG, "itemId=" + item.getItemId() + " uuid=" + uuid); } catch (Throwable ignore) {} +// +// newItems.pushMap(itemMap); +// } +// } else { +// Log.d(TAG, "Proposition has no items."); +// } +// +// // Overwrite items with UUID-injected array +// pMap.putArray("items", newItems); +// +// jsProps.pushMap(pMap); +// } catch (Throwable propEx) { +// Log.d(TAG, "Error building proposition payload, falling back to SDK map: " + propEx.getMessage()); +// // Fallback: push SDK's raw proposition if something went wrong +// jsProps.pushMap(RCTAEPMessagingUtil.toWritableMap(p.toEventData())); +// } +// } +// } +// +// // (Optional) per-surface payload log (size only to keep logs light) +// Log.d(TAG, "Surface [" + surfaceKey + "] payload size=" + jsProps.size()); +// +// out.putArray(surfaceKey, jsProps); +// } +// +// // Log the full 'out' map lightly (size of top-level keys) +// try { +// Map outSnapshot = RCTAEPMessagingUtil.convertReadableMapToMap(out); +// Log.d(TAG, "OUT keys=" + outSnapshot.keySet()); +// } catch (Throwable e) { +// Log.d(TAG, "OUT log failed: " + e.getMessage()); +// } +// +// promise.resolve(out); +// } catch (Throwable topEx) { +// Log.d(TAG, "Top-level error building propositions: " + topEx.getMessage()); +// // As a last resort, mirror the previous behavior: +// try { +// String pkg = bundleId; +// WritableMap fallback = RCTAEPMessagingUtil.convertSurfacePropositions(propositionsMap, pkg); +// promise.resolve(fallback); +// } catch (Throwable finalEx) { +// promise.reject("BuildError", "Failed to build propositions", finalEx); +// } +// } +// } +// }); +// } + diff --git a/packages/messaging/android/src/main/java/com/adobe/marketing/mobile/reactnative/messaging/RCTAEPMessagingUtil.java b/packages/messaging/android/src/main/java/com/adobe/marketing/mobile/reactnative/messaging/RCTAEPMessagingUtil.java index 805d0e165..c5da5e832 100644 --- a/packages/messaging/android/src/main/java/com/adobe/marketing/mobile/reactnative/messaging/RCTAEPMessagingUtil.java +++ b/packages/messaging/android/src/main/java/com/adobe/marketing/mobile/reactnative/messaging/RCTAEPMessagingUtil.java @@ -299,29 +299,51 @@ public static WritableMap convertSingleProposition(final Proposition p, final St return map; } - static WritableMap convertSurfacePropositions( - final Map> propositionMap, - String packageName) { - WritableMap data = new WritableNativeMap(); - - for (Map.Entry> entry : - propositionMap.entrySet()) { - String key = entry.getKey().getUri().replace( - "mobileapp://" + packageName + "/", ""); - WritableArray propositions = new WritableNativeArray(); - - for (Iterator iterator = entry.getValue().iterator(); - iterator.hasNext();) { - //propositions.pushMap(toWritableMap(iterator.next().toEventData())); - Proposition nextProp = iterator.next(); - propositions.pushMap(convertSingleProposition(nextProp, packageName)); - } - - data.putArray(key, propositions); + static WritableMap convertSurfacePropositions( + final Map> propositionMap, + String packageName) { + WritableMap data = new WritableNativeMap(); + + for (Map.Entry> entry : + propositionMap.entrySet()) { + String key = entry.getKey().getUri().replace( + "mobileapp://" + packageName + "/", ""); + WritableArray propositions = new WritableNativeArray(); + + for (Iterator iterator = entry.getValue().iterator(); + iterator.hasNext();) { + propositions.pushMap(toWritableMap(iterator.next().toEventData())); + } + + data.putArray(key, propositions); + } + + return data; } - - return data; - } + +// static WritableMap convertSurfacePropositions( +// final Map> propositionMap, +// String packageName) { +// WritableMap data = new WritableNativeMap(); +// +// for (Map.Entry> entry : +// propositionMap.entrySet()) { +// String key = entry.getKey().getUri().replace( +// "mobileapp://" + packageName + "/", ""); +// WritableArray propositions = new WritableNativeArray(); +// +// for (Iterator iterator = entry.getValue().iterator(); +// iterator.hasNext();) { +// //propositions.pushMap(toWritableMap(iterator.next().toEventData())); +// Proposition nextProp = iterator.next(); +// propositions.pushMap(convertSingleProposition(nextProp, packageName)); +// } +// +// data.putArray(key, propositions); +// } +// +// return data; +// } static ReadableMap convertToReadableMap(Map map) { WritableMap writableMap = Arguments.createMap(); From c116f69c81f33a6b220e9e758812030b48e7cb15 Mon Sep 17 00:00:00 2001 From: Naman Arora Date: Mon, 25 Aug 2025 12:27:40 +0530 Subject: [PATCH 12/28] ios get proposition for surface fixes --- .../messaging/ios/src/RCTAEPMessaging.swift | 144 +++--------------- 1 file changed, 24 insertions(+), 120 deletions(-) diff --git a/packages/messaging/ios/src/RCTAEPMessaging.swift b/packages/messaging/ios/src/RCTAEPMessaging.swift index ecc5bdb3b..c703d0a85 100644 --- a/packages/messaging/ios/src/RCTAEPMessaging.swift +++ b/packages/messaging/ios/src/RCTAEPMessaging.swift @@ -99,7 +99,7 @@ public class RCTAEPMessaging: RCTEventEmitter, MessagingDelegate { ) { resolve(self.latestMessage != nil ? RCTAEPMessagingDataBridge.transformToMessage(message: self.latestMessage!) : nil) } - + @objc func getPropositionsForSurfaces( _ surfaces: [String], @@ -107,90 +107,44 @@ public class RCTAEPMessaging: RCTEventEmitter, MessagingDelegate { withRejecter reject: @escaping RCTPromiseRejectBlock ) { let surfacePaths = surfaces.map { $0.isEmpty ? Surface() : Surface(path: $0) } - Messaging.getPropositionsForSurfaces(surfacePaths) {[weak self] propositions, error in - NSLog("[MessagingBridge] getPropositionsForSurfaces called with surfaces: \(surfacePaths)") - - guard let self, error == nil else { - NSLog("[MessagingBridge] Error retrieving propositions: \(String(describing: error))") + Messaging.getPropositionsForSurfaces(surfacePaths) { [weak self] propositions, error in + guard let self = self else { return } + guard error == nil else { reject("Unable to Retrieve Propositions", nil, nil) return } - - if let p = propositions, p.isEmpty { - NSLog("[MessagingBridge] No propositions returned from SDK") + guard let propositions = propositions, !propositions.isEmpty else { resolve([String: Any]()) return } - // Clear per fetch (Android parity) + // Clear cache per fetch (Android parity) self.propositionByUuid.removeAll() - NSLog("[MessagingBridge] Cleared propositionByUuid cache") - - guard let propositionsDict = propositions else { - NSLog("[MessagingBridge] propositionsDict is nil") - resolve([String: Any]()) - return - } - - - var out: [String: [Any]] = [:] - let bundlePrefix = "mobileapp://" + (Bundle.main.bundleIdentifier ?? "") + "/" - NSLog("[MessagingBridge] bundlePrefix: \(bundlePrefix)") - - for (surface, propList) in propositionsDict { - let surfaceKey = surface.uri.hasPrefix(bundlePrefix) - ? String(surface.uri.dropFirst(bundlePrefix.count)) - : surface.uri - NSLog("[MessagingBridge] Processing surface: \(surface.uri), mapped key: \(surfaceKey), proposition count: \(propList.count)") - - var jsProps: [Any] = [] - - for (index, proposition) in propList.enumerated() { - var pMap: [String: Any] = proposition.asDictionary() ?? [:] - NSLog("[MessagingBridge] Proposition[\(index)] raw dictionary: \(pMap)") - - // Android parity: use activityId as uuid for all items in this proposition - let activityId = self.extractActivityId(from: pMap) - NSLog("[MessagingBridge] Extracted activityId: \(String(describing: activityId))") - - var newItems: [[String: Any]] = [] - let items = proposition.items - let sdkItems = (pMap["items"] as? [[String: Any]]) - NSLog("[MessagingBridge] Proposition[\(index)] items count: \(items.count), sdkItems count: \(String(describing: sdkItems?.count))") - - if !items.isEmpty { - for (itemIndex, item) in items.enumerated() { - var itemMap: [String: Any] - if let sdkItems = sdkItems, itemIndex < sdkItems.count { - itemMap = sdkItems[itemIndex] // keep SDK’s fields - } else { - itemMap = ["id": item.itemId] // minimal fallback - } - - if let uuid = activityId { - // Inject Android-style uuid (note: same for every item under this proposition) - itemMap["uuid"] = uuid - // Map uuid to parent proposition for simpler tracking: uuid -> proposition - self.propositionByUuid[uuid] = proposition - - NSLog("[MessagingBridge] Injected uuid=\(uuid) for item[\(itemIndex)] id=\(item.itemId)") + // Populate uuid->Proposition map using scopeDetails.activity.activityID when available, else activity.id + for (_, list) in propositions { + for proposition in list { + if var pMap = proposition.asDictionary() { + var key: String? = nil + if let sd = pMap["scopeDetails"] as? [String: Any], + let act = sd["activity"] as? [String: Any] { + if let activityID = act["activityID"] as? String, !activityID.isEmpty { + key = activityID + } else if let id = act["id"] as? String, !id.isEmpty { + key = id } - - newItems.append(itemMap) + } + if key == nil { + key = self.extractActivityId(from: pMap) + } + if let key = key { + self.propositionByUuid[key] = proposition } } - - pMap["items"] = newItems - jsProps.append(pMap) } - - out[surfaceKey] = jsProps - NSLog("[MessagingBridge] Finished surfaceKey=\(surfaceKey) with \(jsProps.count) propositions") } - NSLog("[MessagingBridge] Final output built for \(out.keys.count) surfaces") - resolve(out) + resolve(RCTAEPMessagingDataBridge.transformPropositionDict(dict: propositions)) } } @@ -435,56 +389,6 @@ public class RCTAEPMessaging: RCTEventEmitter, MessagingDelegate { - /** - * Generates XDM data for PropositionItem interactions. - * This method is used by the React Native PropositionItem.generateInteractionXdm() method. - * - * - Parameters: - * - itemId: The unique identifier of the PropositionItem - * - interaction: A custom string value to be recorded in the interaction (optional) - * - eventType: The MessagingEdgeEventType numeric value - * - tokens: Array containing the sub-item tokens for recording interaction (optional) - * - resolve: Promise resolver with XDM data for the proposition interaction - * - reject: Promise rejecter for errors - */ - @objc - func generatePropositionInteractionXdm( - _ itemId: String, - interaction: String?, - eventType: Int, - tokens: [String]?, - withResolver resolve: @escaping RCTPromiseResolveBlock, - withRejecter reject: @escaping RCTPromiseRejectBlock - ) { - guard let edgeEventType = MessagingEdgeEventType(rawValue: eventType) else { - reject("InvalidEventType", "Invalid eventType: \(eventType)", nil) - return - } - - guard let propositionItem = findPropositionItemById(itemId) else { - reject("PropositionItemNotFound", "No PropositionItem found with ID: \(itemId)", nil) - return - } - - // Generate XDM data using the appropriate method - var xdmData: [String: Any]? - - if let interaction = interaction, let tokens = tokens { - xdmData = propositionItem.generateInteractionXdm(interaction, withEdgeEventType: edgeEventType, forTokens: tokens) - } else if let interaction = interaction { - xdmData = propositionItem.generateInteractionXdm(interaction, withEdgeEventType: edgeEventType) - } else { - xdmData = propositionItem.generateInteractionXdm(withEdgeEventType: edgeEventType) - } - - if let xdmData = xdmData { - resolve(xdmData) - print("Successfully generated XDM data for PropositionItem: \(itemId)") - } else { - reject("XDMGenerationFailed", "Failed to generate XDM data for PropositionItem: \(itemId)", nil) - } - } - /// MARK: - PropositionItem Cache Management /** From 0fe863351015d1e9461968809c900a12101fce8a Mon Sep 17 00:00:00 2001 From: Naman Arora Date: Mon, 25 Aug 2025 12:29:34 +0530 Subject: [PATCH 13/28] getproposition for surface updates --- .../messaging/RCTAEPMessagingModule.java | 137 ------------------ 1 file changed, 137 deletions(-) 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 528881358..795dbf61f 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 @@ -172,143 +172,6 @@ public void call( }); } -// public void getPropositionsForSurfaces(ReadableArray surfaces, final Promise promise) { -// final String bundleId = this.reactContext.getPackageName(); -// -// Messaging.getPropositionsForSurfaces( -// RCTAEPMessagingUtil.convertSurfaces(surfaces), -// new AdobeCallbackWithError>>() { -// @Override -// public void fail(final AdobeError adobeError) { -// Log.d(TAG, "getPropositionsForSurfaces: fail: " + adobeError.getErrorName()); -// promise.reject(adobeError.getErrorName(), "Unable to get Propositions"); -// } -// -// @Override -// public void call(Map> propositionsMap) { -// try { -// // Optional: clear UUID cache per fetch to avoid stale entries -// propositionItemByUuid.clear(); -// -// WritableMap out = Arguments.createMap(); -// -// for (Map.Entry> entry : propositionsMap.entrySet()) { -// final String surfaceKey = (entry.getKey() != null) -// ? entry.getKey().getUri().replace("mobileapp://" + bundleId + "/", "") -// : "unknown"; -// final List propositions = entry.getValue(); -// final WritableArray jsProps = Arguments.createArray(); -// -// Log.d(TAG, "Surface [" + surfaceKey + "] propCount=" + (propositions != null ? propositions.size() : 0)); -// -// if (propositions != null) { -// for (Proposition p : propositions) { -// try { -// // Start from SDK map for proposition to keep full parity -// WritableMap pMap = RCTAEPMessagingUtil.toWritableMap(p.toEventData()); -// -// // Derive activityId for UUID generation -// final String activityId = extractActivityId(p); -// Log.d(TAG, "activityId=" + activityId); -// -// // If SDK already included items, we'll replace them with UUID-injected ones -// final WritableArray newItems = Arguments.createArray(); -// -// final List items = p.getItems(); -// if (items != null && !items.isEmpty()) { -// Log.d(TAG, "items.size=" + items.size()); -// -// // If you want to "overlay" UUIDs on top of SDK's item maps, fetch them: -// ReadableArray sdkItems = null; -// if (pMap.hasKey("items") && pMap.getType("items") == ReadableType.Array) { -// sdkItems = pMap.getArray("items"); -// } -// -// for (int i = 0; i < items.size(); i++) { -// final PropositionItem item = items.get(i); -// -// // Base item map: prefer SDK's item map at same index; fallback to manual conversion -// WritableMap itemMap = Arguments.createMap(); -// try { -// if (sdkItems != null && i < sdkItems.size() && sdkItems.getType(i) == ReadableType.Map) { -// itemMap.merge(sdkItems.getMap(i)); // like JS spread -// } else { -// itemMap = RCTAEPMessagingUtil.convertPropositionItem(item); -// } -// } catch (Throwable t) { -// Log.d(TAG, "Fallback to manual item conversion (index " + i + "): " + t.getMessage()); -// itemMap = RCTAEPMessagingUtil.convertPropositionItem(item); -// } -// -// // Inject UUID and cache native item for future tracking -// final String uuid = activityId; -// itemMap.putString("uuid", uuid); -// // Use scopeDetails.activity.activityID as the map key when available -// try { -// Map eventData = p.toEventData(); -// Map scopeDetails = (Map) eventData.get("scopeDetails"); -// Map activity = scopeDetails != null ? (Map) scopeDetails.get("activity") : null; -// String activityID = activity != null ? (String) activity.get("activityID") : null; -// String key = activityID != null ? activityID : uuid; -// propositionItemByUuid.put(key, p); -// } catch (Throwable ignore) { -// propositionItemByUuid.put(uuid, p); -// } -// -// // Helpful log -// try { Log.d(TAG, "itemId=" + item.getItemId() + " uuid=" + uuid); } catch (Throwable ignore) {} -// -// newItems.pushMap(itemMap); -// } -// } else { -// Log.d(TAG, "Proposition has no items."); -// } -// -// // Overwrite items with UUID-injected array -// pMap.putArray("items", newItems); -// -// jsProps.pushMap(pMap); -// } catch (Throwable propEx) { -// Log.d(TAG, "Error building proposition payload, falling back to SDK map: " + propEx.getMessage()); -// // Fallback: push SDK's raw proposition if something went wrong -// jsProps.pushMap(RCTAEPMessagingUtil.toWritableMap(p.toEventData())); -// } -// } -// } -// -// // (Optional) per-surface payload log (size only to keep logs light) -// Log.d(TAG, "Surface [" + surfaceKey + "] payload size=" + jsProps.size()); -// -// out.putArray(surfaceKey, jsProps); -// } -// -// // Log the full 'out' map lightly (size of top-level keys) -// try { -// Map outSnapshot = RCTAEPMessagingUtil.convertReadableMapToMap(out); -// Log.d(TAG, "OUT keys=" + outSnapshot.keySet()); -// } catch (Throwable e) { -// Log.d(TAG, "OUT log failed: " + e.getMessage()); -// } -// -// promise.resolve(out); -// } catch (Throwable topEx) { -// Log.d(TAG, "Top-level error building propositions: " + topEx.getMessage()); -// // As a last resort, mirror the previous behavior: -// try { -// String pkg = bundleId; -// WritableMap fallback = RCTAEPMessagingUtil.convertSurfacePropositions(propositionsMap, pkg); -// promise.resolve(fallback); -// } catch (Throwable finalEx) { -// promise.reject("BuildError", "Failed to build propositions", finalEx); -// } -// } -// } -// }); -// } - - - - @ReactMethod public void refreshInAppMessages() { Messaging.refreshInAppMessages(); From 77a0b2db6ae89e999a9a1c3153845c5769beb5e2 Mon Sep 17 00:00:00 2001 From: Naman Arora Date: Mon, 25 Aug 2025 12:30:47 +0530 Subject: [PATCH 14/28] MessagingView commented code removed --- .../app/MessagingView.tsx | 82 ------------------- 1 file changed, 82 deletions(-) diff --git a/apps/AEPSampleAppNewArchEnabled/app/MessagingView.tsx b/apps/AEPSampleAppNewArchEnabled/app/MessagingView.tsx index f611aa355..85fd941f9 100644 --- a/apps/AEPSampleAppNewArchEnabled/app/MessagingView.tsx +++ b/apps/AEPSampleAppNewArchEnabled/app/MessagingView.tsx @@ -165,31 +165,6 @@ const trackPropositionItemExample = async () => { } } -// New method demonstrating generatePropositionInteractionXdm API -// const generatePropositionInteractionXdmExample = async () => { -// const messages = await Messaging.getPropositionsForSurfaces(SURFACES); - -// for (const surface of SURFACES) { -// const propositions = messages[surface] || []; - -// for (const proposition of propositions) { -// for (const propositionItem of proposition.items) { -// // Generate XDM data for proposition item interaction -// try { -// const xdmData = await Messaging.generatePropositionInteractionXdm( -// propositionItem.id, -// 'link_clicked', -// MessagingEdgeEventType.INTERACT, -// ['token1', 'token2'] -// ); -// console.log('Generated XDM data:', JSON.stringify(xdmData)); -// } catch (error) { -// console.error('Error generating XDM data:', error); -// } -// } -// } -// } -// } // Method demonstrating unified tracking using PropositionItem methods const unifiedTrackingExample = async () => { @@ -224,41 +199,6 @@ const unifiedTrackingExample = async () => { } } -// // Method demonstrating unified XDM generation using PropositionItem methods -// const unifiedXdmGenerationExample = async () => { -// const messages = await Messaging.getPropositionsForSurfaces(SURFACES); - -// for (const surface of SURFACES) { -// const propositions = messages[surface] || []; - -// for (const proposition of propositions) { -// for (const propositionItem of proposition.items) { -// try { -// // Generate XDM using the unified approach - check if the method exists -// if ('generateInteractionXdm' in propositionItem) { -// const xdmData = await propositionItem.generateInteractionXdm( -// 'unified_interaction', -// MessagingEdgeEventType.INTERACT, -// ['unified_token'] -// ); -// console.log('Generated XDM using unified API:', JSON.stringify(xdmData)); -// } else { -// // Fall back to the static method for items that don't have the instance method -// const xdmData = await Messaging.generatePropositionInteractionXdm( -// propositionItem.id, -// 'unified_interaction', -// MessagingEdgeEventType.INTERACT, -// ['unified_token'] -// ); -// console.log('Generated XDM using static API fallback:', JSON.stringify(xdmData)); -// } -// } catch (error) { -// console.error('Error generating XDM with unified API:', error); -// } -// } -// } -// } -// } // Function to track in-app message interactions const trackInAppMessage = async () => { @@ -318,24 +258,6 @@ const trackPropositionItems = async () => { } }; -// Direct static method call (bypasses caching) -const trackDirectly = () => { - // This calls trackPropositionItem directly without creating instances - Messaging.trackPropositionItem( - "item_123", // itemId - "direct_call", // interaction - MessagingEdgeEventType.INTERACT, // eventType - ["token1", "token2"] // tokens - ); - - console.log('Tracked directly via static method'); -}; - -const trackEdgeCaseMessageWithPropositionItem = async () => { - const messages = await Messaging.getCachedMessages(); - // Removed invalid manual PropositionItem construction - console.log('Cached messages:', messages); -} function MessagingView() { const router = useRouter(); @@ -372,10 +294,6 @@ function MessagingView() { title="Track Proposition Items (Cached)" onPress={trackPropositionItems} /> -