Skip to content

Commit 136edc4

Browse files
add support for content card tracking (#477)
* add support for content card tracking * add test cases * UTs * simplify logic * add UTs for simplified methods * swift impl and fix UTs * update documentation * update documentation * update sample app * update swift impl * expose ios methods
1 parent a37c0bd commit 136edc4

File tree

10 files changed

+247
-5
lines changed

10 files changed

+247
-5
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ build/
109109
## User settings
110110
xcuserdata/
111111
packages/.DS_Store
112+
packages/*/.DS_Store
113+
packages/*/*/.DS_Store
112114

113115
dist/
114116
test/.DS_Store

apps/AEPSampleAppNewArchEnabled/app/MessagingView.tsx

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,12 @@ governing permissions and limitations under the License.
1313
import React from 'react';
1414
import {Button, Text, View, ScrollView} from 'react-native';
1515
import {MobileCore} from '@adobe/react-native-aepcore';
16-
import {Messaging} from '@adobe/react-native-aepmessaging'
16+
import {Messaging, PersonalizationSchema} from '@adobe/react-native-aepmessaging'
1717
import styles from '../styles/styles';
1818
import { useRouter } from 'expo-router';
1919

20-
const SURFACES = ['android-cb-preview'];
20+
const SURFACES = ['android-cbe-preview', 'cbe/json', 'android-cc'];
21+
const SURFACES_WITH_CONTENT_CARDS = ['android-cc'];
2122

2223
const messagingExtensionVersion = async () => {
2324
const version = await Messaging.extensionVersion();
@@ -64,6 +65,42 @@ const getLatestMessage = async () => {
6465
console.log('Latest Message:', message);
6566
};
6667

68+
// this method can be used to track click interactions with content cards
69+
const trackContentCardInteraction = async () => {
70+
const messages = await Messaging.getPropositionsForSurfaces(SURFACES_WITH_CONTENT_CARDS);
71+
72+
for (const surface of SURFACES_WITH_CONTENT_CARDS) {
73+
const propositions = messages[surface] || [];
74+
75+
for (const proposition of propositions) {
76+
for (const propositionItem of proposition.items) {
77+
if (propositionItem.schema === PersonalizationSchema.CONTENT_CARD) {
78+
Messaging.trackContentCardInteraction(proposition, propositionItem);
79+
console.log('trackContentCardInteraction', proposition, propositionItem);
80+
}
81+
}
82+
}
83+
}
84+
}
85+
86+
// this method can be used to track display interactions with content cards
87+
const trackContentCardDisplay = async () => {
88+
const messages = await Messaging.getPropositionsForSurfaces(SURFACES_WITH_CONTENT_CARDS);
89+
90+
for (const surface of SURFACES_WITH_CONTENT_CARDS) {
91+
const propositions = messages[surface] || [];
92+
93+
for (const proposition of propositions) {
94+
for (const propositionItem of proposition.items) {
95+
if (propositionItem.schema === PersonalizationSchema.CONTENT_CARD) {
96+
Messaging.trackContentCardDisplay(proposition, propositionItem);
97+
console.log('trackContentCardDisplay', proposition, propositionItem);
98+
}
99+
}
100+
}
101+
}
102+
}
103+
67104
function MessagingView() {
68105
const router = useRouter();
69106

@@ -86,6 +123,8 @@ function MessagingView() {
86123
<Button title="getCachedMessages()" onPress={getCachedMessages} />
87124
<Button title="getLatestMessage()" onPress={getLatestMessage} />
88125
<Button title="trackAction()" onPress={trackAction} />
126+
<Button title="trackPropositionInteraction()" onPress={trackContentCardInteraction} />
127+
<Button title="trackContentCardDisplay()" onPress={trackContentCardDisplay} />
89128
</ScrollView>
90129
</View>
91130
);

packages/messaging/README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,3 +434,23 @@ function otherWorkflowFinished() {
434434
currentMessage.clearMessage();
435435
}
436436
```
437+
438+
## Tracking interactions with content cards
439+
440+
### trackContentCardDisplay
441+
442+
Tracks a Display interaction with the given ContentCard
443+
444+
**Syntax**
445+
```javascript
446+
Messaging.trackContentCardDisplay(proposition, contentCard);
447+
```
448+
449+
### trackContentCardInteraction
450+
451+
Tracks a Click interaction with the given ContentCard
452+
453+
**Syntax**
454+
```javascript
455+
Messaging.trackContentCardInteraction(proposition, contentCard);
456+
```

packages/messaging/__tests__/MessagingTests.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,4 +104,26 @@ describe('Messaging', () => {
104104
]);
105105
expect(spy).toHaveBeenCalledWith(['testSurface1', 'testSurface2']);
106106
});
107+
108+
it('should call trackContentCardDisplay', async () => {
109+
const spy = jest.spyOn(
110+
NativeModules.AEPMessaging,
111+
'trackContentCardDisplay'
112+
);
113+
const mockProposition = { propositionId: 'mockPropositionId' } as any;
114+
const mockContentCard = { contentCardId: 'mockContentCardId' } as any;
115+
await Messaging.trackContentCardDisplay(mockProposition, mockContentCard);
116+
expect(spy).toHaveBeenCalledWith(mockProposition, mockContentCard);
117+
});
118+
119+
it('should call trackContentCardInteraction', async () => {
120+
const spy = jest.spyOn(
121+
NativeModules.AEPMessaging,
122+
'trackContentCardInteraction'
123+
);
124+
const mockProposition = { propositionId: 'mockPropositionId' } as any;
125+
const mockContentCard = { contentCardId: 'mockContentCardId' } as any;
126+
await Messaging.trackContentCardInteraction(mockProposition, mockContentCard);
127+
expect(spy).toHaveBeenCalledWith(mockProposition, mockContentCard);
128+
});
107129
});

packages/messaging/android/src/main/java/com/adobe/marketing/mobile/reactnative/messaging/RCTAEPMessagingModule.java

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import com.adobe.marketing.mobile.MobileCore;
2828
import com.adobe.marketing.mobile.messaging.MessagingUtils;
2929
import com.adobe.marketing.mobile.messaging.Proposition;
30+
import com.adobe.marketing.mobile.messaging.PropositionItem;
3031
import com.adobe.marketing.mobile.messaging.Surface;
3132
import com.adobe.marketing.mobile.services.ServiceProvider;
3233
import com.adobe.marketing.mobile.services.ui.InAppMessage;
@@ -38,6 +39,7 @@
3839
import com.facebook.react.bridge.ReactContextBaseJavaModule;
3940
import com.facebook.react.bridge.ReactMethod;
4041
import com.facebook.react.bridge.ReadableArray;
42+
import com.facebook.react.bridge.ReadableMap;
4143
import com.facebook.react.bridge.WritableMap;
4244
import com.facebook.react.modules.core.DeviceEventManagerModule;
4345
import java.util.HashMap;
@@ -269,4 +271,28 @@ private void emitEvent(final String name, final Map<String, String> data) {
269271
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
270272
.emit(name, eventData);
271273
}
272-
}
274+
275+
@ReactMethod
276+
public void trackContentCardDisplay(ReadableMap propositionMap, ReadableMap contentCardMap) {
277+
final Map<String, Object> eventData = RCTAEPMessagingUtil.convertReadableMapToMap(propositionMap);
278+
final Proposition proposition = Proposition.fromEventData(eventData);
279+
for (PropositionItem item : proposition.getItems()) {
280+
if (item.getItemId().equals(contentCardMap.getString("id"))) {
281+
item.track(MessagingEdgeEventType.DISPLAY);
282+
break;
283+
}
284+
}
285+
}
286+
287+
@ReactMethod
288+
public void trackContentCardInteraction(ReadableMap propositionMap, ReadableMap contentCardMap) {
289+
final Map<String, Object> eventData = RCTAEPMessagingUtil.convertReadableMapToMap(propositionMap);
290+
final Proposition proposition = Proposition.fromEventData(eventData);
291+
for (PropositionItem item : proposition.getItems()) {
292+
if (item.getItemId().equals(contentCardMap.getString("id"))) {
293+
item.track("click", MessagingEdgeEventType.INTERACT, null);
294+
break;
295+
}
296+
}
297+
}
298+
}

packages/messaging/android/src/main/java/com/adobe/marketing/mobile/reactnative/messaging/RCTAEPMessagingUtil.java

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
import com.facebook.react.bridge.Arguments;
2020
import com.facebook.react.bridge.ReadableArray;
2121
import com.facebook.react.bridge.ReadableMap;
22+
import com.facebook.react.bridge.ReadableMapKeySetIterator;
23+
import com.facebook.react.bridge.ReadableType;
2224
import com.facebook.react.bridge.WritableArray;
2325
import com.facebook.react.bridge.WritableMap;
2426
import com.facebook.react.bridge.WritableNativeArray;
@@ -216,4 +218,66 @@ static ReadableMap convertToReadableMap(Map<String, String> map) {
216218
}
217219
return writableMap;
218220
}
219-
}
221+
222+
/**
223+
* Converts {@link ReadableMap} Map to {@link Map}
224+
*
225+
* @param readableMap instance of {@code ReadableMap}
226+
* @return instance of {@code Map}
227+
*/
228+
static Map<String, Object> convertReadableMapToMap(final ReadableMap readableMap) {
229+
ReadableMapKeySetIterator iterator = readableMap.keySetIterator();
230+
Map<String, Object> map = new HashMap<>();
231+
while (iterator.hasNextKey()) {
232+
String key = iterator.nextKey();
233+
ReadableType type = readableMap.getType(key);
234+
switch (type) {
235+
case Boolean:
236+
map.put(key, readableMap.getBoolean(key));
237+
break;
238+
case Number:
239+
map.put(key, readableMap.getDouble(key));
240+
break;
241+
case String:
242+
map.put(key, readableMap.getString(key));
243+
break;
244+
case Map:
245+
map.put(key, convertReadableMapToMap(readableMap.getMap(key)));
246+
break;
247+
case Array:
248+
map.put(key, convertReadableArrayToList(readableMap.getArray(key)));
249+
break;
250+
default:
251+
break;
252+
}
253+
}
254+
return map;
255+
}
256+
257+
static List<Object> convertReadableArrayToList(final ReadableArray readableArray) {
258+
final List<Object> list = new ArrayList<>(readableArray.size());
259+
for (int i = 0; i < readableArray.size(); i++) {
260+
ReadableType indexType = readableArray.getType(i);
261+
switch(indexType) {
262+
case Boolean:
263+
list.add(i, readableArray.getBoolean(i));
264+
break;
265+
case Number:
266+
list.add(i, readableArray.getDouble(i));
267+
break;
268+
case String:
269+
list.add(i, readableArray.getString(i));
270+
break;
271+
case Map:
272+
list.add(i, convertReadableMapToMap(readableArray.getMap(i)));
273+
break;
274+
case Array:
275+
list.add(i, convertReadableArrayToList(readableArray.getArray(i)));
276+
break;
277+
default:
278+
break;
279+
}
280+
}
281+
return list;
282+
}
283+
}

packages/messaging/ios/src/RCTAEPMessaging.mm

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,12 @@ @interface RCT_EXTERN_MODULE (RCTAEPMessaging, RCTEventEmitter)
4747
: (RCTPromiseResolveBlock)resolve withRejecter
4848
: (RCTPromiseRejectBlock)reject);
4949

50+
RCT_EXTERN_METHOD(trackContentCardDisplay
51+
: (NSDictionary *)propositionMap contentCardMap
52+
: (NSDictionary *)contentCardMap);
53+
54+
RCT_EXTERN_METHOD(trackContentCardInteraction
55+
: (NSDictionary *)propositionMap contentCardMap
56+
: (NSDictionary *)contentCardMap);
57+
5058
@end

packages/messaging/ios/src/RCTAEPMessaging.swift

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,54 @@ public class RCTAEPMessaging: RCTEventEmitter, MessagingDelegate {
201201
reject(Constants.CACHE_MISS, nil, nil)
202202
}
203203

204+
@objc
205+
func trackContentCardDisplay(
206+
_ propositionMap: [String: Any],
207+
contentCardMap: [String: Any]
208+
) {
209+
guard let contentCardId = contentCardMap["id"] as? String else {
210+
print("Error: Content card ID is missing or invalid")
211+
return
212+
}
213+
214+
do {
215+
let jsonData = try JSONSerialization.data(withJSONObject: propositionMap)
216+
let proposition = try JSONDecoder().decode(Proposition.self, from: jsonData)
217+
218+
if let matchingItem = proposition.items.first(where: { $0.itemId == contentCardId }) {
219+
matchingItem.track(withEdgeEventType: MessagingEdgeEventType.display)
220+
} else {
221+
print("Error: No matching proposition item found for content card ID: \(contentCardId)")
222+
}
223+
} catch {
224+
print("Error decoding proposition: \(error.localizedDescription)")
225+
}
226+
}
227+
228+
@objc
229+
func trackContentCardInteraction(
230+
_ propositionMap: [String: Any],
231+
contentCardMap: [String: Any]
232+
) {
233+
guard let contentCardId = contentCardMap["id"] as? String else {
234+
print("Error: Content card ID is missing or invalid")
235+
return
236+
}
237+
238+
do {
239+
let jsonData = try JSONSerialization.data(withJSONObject: propositionMap)
240+
let proposition = try JSONDecoder().decode(Proposition.self, from: jsonData)
241+
242+
if let matchingItem = proposition.items.first(where: { $0.itemId == contentCardId }) {
243+
matchingItem.track("click", withEdgeEventType: MessagingEdgeEventType.interact)
244+
} else {
245+
print("Error: No matching proposition item found for content card ID: \(contentCardId)")
246+
}
247+
} catch {
248+
print("Error decoding proposition: \(error.localizedDescription)")
249+
}
250+
}
251+
204252
// Messaging Delegate Methods
205253
public func onDismiss(message: Showable) {
206254
if let fullscreenMessage = message as? FullscreenMessage,

packages/messaging/src/Messaging.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
import Message from './models/Message';
2020
import { MessagingDelegate } from './models/MessagingDelegate';
2121
import { MessagingProposition } from './models/MessagingProposition';
22+
import { ContentCard } from './models/ContentCard';
2223

2324
export interface NativeMessagingModule {
2425
extensionVersion: () => Promise<string>;
@@ -34,6 +35,8 @@ export interface NativeMessagingModule {
3435
shouldSaveMessage: boolean
3536
) => void;
3637
updatePropositionsForSurfaces: (surfaces: string[]) => void;
38+
trackContentCardDisplay: (proposition: MessagingProposition, contentCard: ContentCard) => void;
39+
trackContentCardInteraction: (proposition: MessagingProposition, contentCard: ContentCard) => void;
3740
}
3841

3942
const RCTAEPMessaging: NativeModule & NativeMessagingModule =
@@ -90,6 +93,14 @@ class Messaging {
9093
return await RCTAEPMessaging.getPropositionsForSurfaces(surfaces);
9194
}
9295

96+
static trackContentCardDisplay(proposition: MessagingProposition, contentCard: ContentCard): void {
97+
RCTAEPMessaging.trackContentCardDisplay(proposition, contentCard);
98+
}
99+
100+
static trackContentCardInteraction(proposition: MessagingProposition, contentCard: ContentCard): void {
101+
RCTAEPMessaging.trackContentCardInteraction(proposition, contentCard);
102+
}
103+
93104
/**
94105
* Function to set the UI Message delegate to listen the Message lifecycle events.
95106
* @returns A function to unsubscribe from all event listeners

tests/jest/setup.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,9 @@ jest.doMock('react-native', () => {
156156
updatePropositionsForSurfaces: jest.fn(),
157157
getPropositionsForSurfaces: jest.fn(
158158
() => new Promise((resolve) => resolve([]))
159-
)
159+
),
160+
trackContentCardDisplay: jest.fn(),
161+
trackContentCardInteraction: jest.fn()
160162
},
161163
AEPOptimize: {
162164
extensionVersion: jest.fn(

0 commit comments

Comments
 (0)