From 617cc0d5265b773bb1ace65b39358cfc29ab035f Mon Sep 17 00:00:00 2001 From: Ishita Gambhir Date: Mon, 19 May 2025 10:08:45 +0530 Subject: [PATCH 01/11] add support for content card tracking --- .../messaging/RCTAEPMessagingModule.java | 52 ++++++++++ .../messaging/RCTAEPMessagingUtil.java | 96 +++++++++++++++++++ .../messaging/ios/src/RCTAEPMessaging.swift | 64 +++++++++++++ packages/messaging/src/Messaging.ts | 63 ++++++++++++ 4 files changed, 275 insertions(+) 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 a45a5732d..52a7fed41 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 @@ -16,6 +16,7 @@ import android.app.Activity; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.adobe.marketing.mobile.AdobeCallback; import com.adobe.marketing.mobile.AdobeCallbackWithError; @@ -27,6 +28,7 @@ import com.adobe.marketing.mobile.MobileCore; import com.adobe.marketing.mobile.messaging.MessagingUtils; import com.adobe.marketing.mobile.messaging.Proposition; +import com.adobe.marketing.mobile.messaging.PropositionItem; import com.adobe.marketing.mobile.messaging.Surface; import com.adobe.marketing.mobile.services.ServiceProvider; import com.adobe.marketing.mobile.services.ui.InAppMessage; @@ -38,12 +40,15 @@ import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.WritableMap; import com.facebook.react.modules.core.DeviceEventManagerModule; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.CountDownLatch; +import com.adobe.marketing.mobile.services.Log; +import com.adobe.marketing.mobile.util.StringUtils; public final class RCTAEPMessagingModule extends ReactContextBaseJavaModule implements PresentationDelegate { @@ -269,4 +274,51 @@ private void emitEvent(final String name, final Map data) { .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) .emit(name, eventData); } + + @ReactMethod + public void trackPropositionInteraction( + ReadableMap propositionMap, + String itemId, + int eventTypeInt, + @Nullable String interaction, + @Nullable ReadableArray tokensArray + ) { + if (propositionMap == null || StringUtils.isNullOrEmpty(itemId)) { + Log.debug(TAG, "trackPropositionInteraction", "Proposition data or item ID is null/empty. Cannot track interaction."); + return; + } + + Map propositionJavaMap = RCTAEPMessagingUtil.convertReadableMapToMap(propositionMap); + Proposition nativeProposition = Proposition.fromEventData(propositionJavaMap); + + if (nativeProposition == null) { + Log.debug(TAG, "trackPropositionInteraction", "Failed to reconstruct native Proposition from event data. Cannot track interaction for item ID: %s", itemId); + return; + } + + PropositionItem targetItem = null; + for (PropositionItem item : nativeProposition.getItems()) { + if (item.getItemId().equals(itemId)) { + targetItem = item; + break; + } + } + + if (targetItem == null) { + Log.debug(TAG, "trackPropositionInteraction", "Could not find PropositionItem with id: %s in the reconstructed proposition. Cannot track interaction.", itemId); + return; + } + + MessagingEdgeEventType eventType = RCTAEPMessagingUtil.getEventType(eventTypeInt); + if (eventType == null) { + Log.debug(TAG, "trackPropositionInteraction", "Invalid event type integer received: %d. Cannot track interaction for item ID: %s", eventTypeInt, itemId); + return; + } + + List nativeTokens = tokensArray != null ? RCTAEPMessagingUtil.convertReadableArrayToStringList(tokensArray) : null; + + // Call the native track method on the PropositionItem + targetItem.track(interaction, eventType, nativeTokens); + Log.debug(TAG, "trackPropositionInteraction", "Successfully tracked interaction for item ID: %s, EventType: %s", itemId, eventType.name()); + } } \ No newline at end of file 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 cc8645166..bb5efa8fc 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 @@ -25,6 +25,7 @@ import com.facebook.react.bridge.WritableNativeMap; import java.util.ArrayList; import java.util.Collection; + import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; @@ -216,4 +217,99 @@ static ReadableMap convertToReadableMap(Map map) { } return writableMap; } + + // Helper method to convert ReadableMap to Map + static Map convertReadableMapToMap(ReadableMap readableMap) { + if (readableMap == null) { + return Collections.emptyMap(); + } + Map map = new HashMap<>(); + com.facebook.react.bridge.ReadableMapKeySetIterator iterator = readableMap.keySetIterator(); + while (iterator.hasNextKey()) { + String key = iterator.nextKey(); + com.facebook.react.bridge.ReadableType type = readableMap.getType(key); + switch (type) { + case Null: + map.put(key, null); + break; + case Boolean: + map.put(key, readableMap.getBoolean(key)); + break; + case Number: + // Can be int or double + double numberValue = readableMap.getDouble(key); + if (numberValue == (int) numberValue) { + map.put(key, (int) numberValue); + } else { + map.put(key, numberValue); + } + break; + case String: + map.put(key, readableMap.getString(key)); + break; + case Map: + map.put(key, convertReadableMapToMap(readableMap.getMap(key))); + break; + case Array: + map.put(key, convertReadableArrayToList(readableMap.getArray(key))); + break; + } + } + return map; + } + + // Helper method to convert ReadableArray to List + static List convertReadableArrayToList(ReadableArray readableArray) { + if (readableArray == null) { + return Collections.emptyList(); + } + List list = new ArrayList<>(readableArray.size()); + for (int i = 0; i < readableArray.size(); i++) { + com.facebook.react.bridge.ReadableType type = readableArray.getType(i); + switch (type) { + case Null: + list.add(null); + break; + case Boolean: + list.add(readableArray.getBoolean(i)); + break; + case Number: + double numberValue = readableArray.getDouble(i); + if (numberValue == (int) numberValue) { + list.add((int) numberValue); + } else { + list.add(numberValue); + } + break; + case String: + list.add(readableArray.getString(i)); + break; + case Map: + list.add(convertReadableMapToMap(readableArray.getMap(i))); + break; + case Array: + list.add(convertReadableArrayToList(readableArray.getArray(i))); + break; + } + } + return list; + } + + // Helper method to convert ReadableArray to List, assuming all elements are strings + static List convertReadableArrayToStringList(ReadableArray readableArray) { + if (readableArray == null) { + return Collections.emptyList(); + } + List list = new ArrayList<>(readableArray.size()); + for (int i = 0; i < readableArray.size(); i++) { + com.facebook.react.bridge.ReadableType type = readableArray.getType(i); + if (type == com.facebook.react.bridge.ReadableType.String) { + list.add(readableArray.getString(i)); + } else { + // Handle error or skip non-string elements if necessary + // For now, skipping non-string elements + } + } + return list; + } } \ No newline at end of file diff --git a/packages/messaging/ios/src/RCTAEPMessaging.swift b/packages/messaging/ios/src/RCTAEPMessaging.swift index e301ced19..e75b42b35 100644 --- a/packages/messaging/ios/src/RCTAEPMessaging.swift +++ b/packages/messaging/ios/src/RCTAEPMessaging.swift @@ -267,4 +267,68 @@ public class RCTAEPMessaging: RCTEventEmitter, MessagingDelegate { private func emitNativeEvent(name: String, body: Any) { RCTAEPMessaging.emitter.sendEvent(withName: name, body: body) } + + @objc(trackPropositionInteraction:itemId:eventType:interaction:tokens:) + func trackPropositionInteraction(propositionData: [String: Any]?, itemId: String?, eventTypeIntFromJS: Int, interaction: String?, tokensArray: [String]?) { + guard let propositionData = propositionData, let itemId = itemId, !itemId.isEmpty else { + Log.debug(label:Self.EXTENSION_NAME, "trackPropositionInteraction - Proposition data or item ID is nil/empty. Cannot track interaction.") + return + } + + // 1. Map JS eventTypeInt to native Swift MessagingEdgeEventType + var nativeEventType: MessagingEdgeEventType? + switch eventTypeIntFromJS { + case 0: // Corresponds to JS MessagingEdgeEventType.DISMISS + nativeEventType = .dismiss + case 1: // Corresponds to JS MessagingEdgeEventType.INTERACT + nativeEventType = .interact + case 3: // Corresponds to JS MessagingEdgeEventType.DISPLAY + nativeEventType = .display + // Note: Add other cases if new tracking methods are added in JS that use other event types from MessagingEdgeEventType.ts + default: + Log.debug(label:Self.EXTENSION_NAME, "trackPropositionInteraction - Unsupported event type integer from JS: \(eventTypeIntFromJS).") + return + } + + guard let eventType = nativeEventType else { + // This case should ideally not be hit if the switch is exhaustive for supported JS values + Log.debug(label:Self.EXTENSION_NAME, "trackPropositionInteraction - Native event type could not be determined.") + return + } + + // 2. Decode Proposition + var nativeProposition: Proposition? + do { + // Convert the [String: Any] dictionary to JSON Data + let jsonData = try JSONSerialization.data(withJSONObject: propositionData, options: []) + // Decode the JSON Data into a Proposition object + nativeProposition = try JSONDecoder().decode(Proposition.self, from: jsonData) + } catch { + Log.debug(label:Self.EXTENSION_NAME, "trackPropositionInteraction - Failed to decode Proposition from event data: \(error.localizedDescription)") + return + } + + guard let unwrappedProposition = nativeProposition else { + Log.debug(label:Self.EXTENSION_NAME, "trackPropositionInteraction - Reconstructed native Proposition is nil.") + return + } + + // 3. Find Target Item + var targetItem: PropositionItem? = nil + for item in unwrappedProposition.items { // Accesses the lazy var 'items' which sets up parent proposition reference + if item.itemId == itemId { // Assuming PropositionItem has 'itemId' + targetItem = item + break + } + } + + guard let foundItem = targetItem else { + Log.debug(label:Self.EXTENSION_NAME, "trackPropositionInteraction - Could not find PropositionItem with id: \(itemId) in the reconstructed proposition.") + return + } + + // 4. Call Track + foundItem.track(interaction, withEdgeEventType: eventType, forTokens: tokensArray) + Log.debug(label:Self.EXTENSION_NAME, "trackPropositionInteraction - Successfully tracked interaction for item ID: \(itemId), EventType: \(eventType.toString())") + } } diff --git a/packages/messaging/src/Messaging.ts b/packages/messaging/src/Messaging.ts index 741f1d8f5..867c1ad18 100644 --- a/packages/messaging/src/Messaging.ts +++ b/packages/messaging/src/Messaging.ts @@ -19,6 +19,9 @@ import { import Message from './models/Message'; import { MessagingDelegate } from './models/MessagingDelegate'; import { MessagingProposition } from './models/MessagingProposition'; +import { ContentCard } from './models/ContentCard'; +import MessagingEdgeEventType from './models/MessagingEdgeEventType'; +import { MessagingPropositionItem } from './models/MessagingPropositionItem'; export interface NativeMessagingModule { extensionVersion: () => Promise; @@ -34,6 +37,13 @@ export interface NativeMessagingModule { shouldSaveMessage: boolean ) => void; updatePropositionsForSurfaces: (surfaces: string[]) => void; + trackPropositionInteraction: ( + proposition: MessagingProposition, + item: MessagingPropositionItem, + eventType: MessagingEdgeEventType, + interaction: string | null, + tokens: string[] | null + ) => void; } const RCTAEPMessaging: NativeModule & NativeMessagingModule = @@ -159,6 +169,59 @@ class Messaging { static updatePropositionsForSurfaces(surfaces: string[]) { RCTAEPMessaging.updatePropositionsForSurfaces(surfaces); } + + /** + * Tracks interaction with a Content Card proposition item for a display event. + * @param {MessagingProposition} proposition - The parent MessagingProposition containing the item. + * @param {ContentCard} item - The specific ContentCard object to track. + * @memberof Messaging + */ + static trackContentCardDisplay(proposition: MessagingProposition, item: ContentCard) { + RCTAEPMessaging.trackPropositionInteraction( + proposition, + item, + MessagingEdgeEventType.DISPLAY, + null, + null + ); + } + + /** + * Tracks interaction with a Content Card proposition item for an interaction event. + * @param {MessagingProposition} proposition - The parent MessagingProposition containing the item. + * @param {ContentCard} item - The specific ContentCard object to track. + * @param {string} interaction - A string describing the interaction (e.g., 'button tapped', 'card clicked'). Required for interact events. + * @memberof Messaging + */ + static trackContentCardInteract(proposition: MessagingProposition, item: ContentCard, interaction: string) { + if (!interaction) { + console.warn('[AEPMessaging] Interaction string is required for trackContentCardInteract.'); + return; + } + RCTAEPMessaging.trackPropositionInteraction( + proposition, + item, + MessagingEdgeEventType.INTERACT, + interaction, + null + ); + } + + /** + * Tracks interaction with a Content Card proposition item for a dismiss event. + * @param {MessagingProposition} proposition - The parent MessagingProposition containing the item. + * @param {ContentCard} item - The specific ContentCard object to track. + * @memberof Messaging + */ + static trackContentCardDismiss(proposition: MessagingProposition, item: ContentCard) { + RCTAEPMessaging.trackPropositionInteraction( + proposition, + item, + MessagingEdgeEventType.DISMISS, + null, + null + ); + } } export default Messaging; From 1ce3f9f91047de8f2a50d64eefe2465a5c6a3831 Mon Sep 17 00:00:00 2001 From: Ishita Gambhir Date: Tue, 27 May 2025 10:18:29 +0530 Subject: [PATCH 02/11] add test cases --- .../messaging/__tests__/MessagingTests.ts | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/packages/messaging/__tests__/MessagingTests.ts b/packages/messaging/__tests__/MessagingTests.ts index 143be26a6..2feb0279a 100644 --- a/packages/messaging/__tests__/MessagingTests.ts +++ b/packages/messaging/__tests__/MessagingTests.ts @@ -104,4 +104,50 @@ describe('Messaging', () => { ]); expect(spy).toHaveBeenCalledWith(['testSurface1', 'testSurface2']); }); + + it('trackContentCardDisplay is called', async () => { + const spy = jest.spyOn(NativeModules.AEPMessaging, 'trackPropositionInteraction'); + const mockProposition = { id: 'propId' } as any; + const mockContentCard = { itemId: 'itemId' } as any; + + await Messaging.trackContentCardDisplay(mockProposition, mockContentCard); + expect(spy).toHaveBeenCalledWith( + mockProposition, + mockContentCard, + MessagingEdgeEventType.DISPLAY, + null, + null + ); + }); + + it('trackContentCardInteract is called', async () => { + const spy = jest.spyOn(NativeModules.AEPMessaging, 'trackPropositionInteraction'); + const mockProposition = { id: 'propId' } as any; + const mockContentCard = { itemId: 'itemId' } as any; + const interaction = 'mockInteraction'; + + await Messaging.trackContentCardInteract(mockProposition, mockContentCard, interaction); + expect(spy).toHaveBeenCalledWith( + mockProposition, + mockContentCard, + MessagingEdgeEventType.INTERACT, + interaction, + null + ); + }); + + it('trackContentCardDismiss is called', async () => { + const spy = jest.spyOn(NativeModules.AEPMessaging, 'trackPropositionInteraction'); + const mockProposition = { id: 'propId' } as any; + const mockContentCard = { itemId: 'itemId' } as any; + + await Messaging.trackContentCardDismiss(mockProposition, mockContentCard); + expect(spy).toHaveBeenCalledWith( + mockProposition, + mockContentCard, + MessagingEdgeEventType.DISMISS, + null, + null + ); + }); }); From 49848d1b2a9cdac7b2238f93f22f08aaaea10911 Mon Sep 17 00:00:00 2001 From: Ishita Gambhir Date: Tue, 27 May 2025 12:02:14 +0530 Subject: [PATCH 03/11] UTs --- .../messaging/__tests__/MessagingTests.ts | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/packages/messaging/__tests__/MessagingTests.ts b/packages/messaging/__tests__/MessagingTests.ts index 2feb0279a..8a879872d 100644 --- a/packages/messaging/__tests__/MessagingTests.ts +++ b/packages/messaging/__tests__/MessagingTests.ts @@ -13,6 +13,37 @@ governing permissions and limitations under the License. import { NativeModules } from 'react-native'; import { Messaging, Message, MessagingEdgeEventType } from '../src'; +// Mocking the NativeModules.AEPMessaging +jest.mock('react-native', () => ({ + ...jest.requireActual('react-native'), + NativeModules: { + ...jest.requireActual('react-native').NativeModules, + AEPMessaging: { + extensionVersion: jest.fn(() => Promise.resolve('test-version')), + refreshInAppMessages: jest.fn(), + setMessagingDelegate: jest.fn(), + setAutoTrack: jest.fn(), + show: jest.fn(), + dismiss: jest.fn(), + track: jest.fn(), + clear: jest.fn(), + updatePropositionsForSurfaces: jest.fn(), + getPropositionsForSurfaces: jest.fn(() => Promise.resolve({})), + // Ensure trackPropositionInteraction is part of the mock + trackPropositionInteraction: jest.fn(), + // Add any other methods from AEPMessaging that are used in tests or the module itself + getCachedMessages: jest.fn(() => Promise.resolve([])), + getLatestMessage: jest.fn(() => Promise.resolve(null)), + setMessageSettings: jest.fn() + } + }, + NativeEventEmitter: jest.fn(() => ({ + addListener: jest.fn(), + removeListeners: jest.fn(), + removeAllListeners: jest.fn() // Added removeAllListeners based on usage in Messaging.ts + })) +})); + describe('Messaging', () => { it('extensionVersion is called', async () => { const spy = jest.spyOn(NativeModules.AEPMessaging, 'extensionVersion'); From 0b2e45b1a2dd402e5cb05defb04b4d7f58594b60 Mon Sep 17 00:00:00 2001 From: Ishita Gambhir Date: Wed, 28 May 2025 02:34:33 +0530 Subject: [PATCH 04/11] simplify logic --- .../app/MessagingView.tsx | 26 ++++ .../messaging/__tests__/MessagingTests.ts | 77 ---------- .../messaging/RCTAEPMessagingModule.java | 64 +++------ .../messaging/RCTAEPMessagingUtil.java | 135 +++++------------- .../messaging/ios/src/RCTAEPMessaging.swift | 73 ++-------- packages/messaging/src/Messaging.ts | 72 ++-------- 6 files changed, 102 insertions(+), 345 deletions(-) diff --git a/apps/AEPSampleAppNewArchEnabled/app/MessagingView.tsx b/apps/AEPSampleAppNewArchEnabled/app/MessagingView.tsx index 87c2b6d72..bf71b16d9 100644 --- a/apps/AEPSampleAppNewArchEnabled/app/MessagingView.tsx +++ b/apps/AEPSampleAppNewArchEnabled/app/MessagingView.tsx @@ -64,6 +64,30 @@ const getLatestMessage = async () => { console.log('Latest Message:', message); }; +const trackPropositionInteraction = async () => { + const messages = await Messaging.getPropositionsForSurfaces(SURFACES); + for (const surface of SURFACES) { + const propositions = messages[surface] || []; + for (const proposition of propositions) { + for (const item of proposition.items) { + Messaging.trackContentCardInteraction(proposition, item); + } + } + } +} + +const trackContentCardDisplay = async () => { + const messages = await Messaging.getPropositionsForSurfaces(SURFACES); + for (const surface of SURFACES) { + const propositions = messages[surface] || []; + for (const proposition of propositions) { + for (const item of proposition.items) { + Messaging.trackContentCardDisplay(proposition, item); + } + } + } +} + function MessagingView() { const router = useRouter(); @@ -86,6 +110,8 @@ function MessagingView() {