Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ build/
## User settings
xcuserdata/
packages/.DS_Store
packages/*/.DS_Store
packages/*/*/.DS_Store

dist/
test/.DS_Store
Expand Down
43 changes: 41 additions & 2 deletions apps/AEPSampleAppNewArchEnabled/app/MessagingView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +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} from '@adobe/react-native-aepmessaging'
import {Messaging, PersonalizationSchema} from '@adobe/react-native-aepmessaging'
import styles from '../styles/styles';
import { useRouter } from 'expo-router';

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

const messagingExtensionVersion = async () => {
const version = await Messaging.extensionVersion();
Expand Down Expand Up @@ -64,6 +65,42 @@ const getLatestMessage = async () => {
console.log('Latest Message:', message);
};

// this method can be used to track click interactions with content cards
const trackContentCardInteraction = async () => {
const messages = await Messaging.getPropositionsForSurfaces(SURFACES_WITH_CONTENT_CARDS);

for (const surface of SURFACES_WITH_CONTENT_CARDS) {
const propositions = messages[surface] || [];

for (const proposition of propositions) {
for (const propositionItem of proposition.items) {
if (propositionItem.schema === PersonalizationSchema.CONTENT_CARD) {
Messaging.trackContentCardInteraction(proposition, propositionItem);
console.log('trackContentCardInteraction', proposition, propositionItem);
}
}
}
}
}

// this method can be used to track display interactions with content cards
const trackContentCardDisplay = async () => {
const messages = await Messaging.getPropositionsForSurfaces(SURFACES_WITH_CONTENT_CARDS);

for (const surface of SURFACES_WITH_CONTENT_CARDS) {
const propositions = messages[surface] || [];

for (const proposition of propositions) {
for (const propositionItem of proposition.items) {
if (propositionItem.schema === PersonalizationSchema.CONTENT_CARD) {
Messaging.trackContentCardDisplay(proposition, propositionItem);
console.log('trackContentCardDisplay', proposition, propositionItem);
}
}
}
}
}

function MessagingView() {
const router = useRouter();

Expand All @@ -86,6 +123,8 @@ function MessagingView() {
<Button title="getCachedMessages()" onPress={getCachedMessages} />
<Button title="getLatestMessage()" onPress={getLatestMessage} />
<Button title="trackAction()" onPress={trackAction} />
<Button title="trackPropositionInteraction()" onPress={trackContentCardInteraction} />
<Button title="trackContentCardDisplay()" onPress={trackContentCardDisplay} />
</ScrollView>
</View>
);
Expand Down
20 changes: 20 additions & 0 deletions packages/messaging/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -434,3 +434,23 @@ function otherWorkflowFinished() {
currentMessage.clearMessage();
}
```

## Tracking interactions with content cards

### trackContentCardDisplay

Tracks a Display interaction with the given ContentCard

**Syntax**
```javascript
Messaging.trackContentCardDisplay(proposition, contentCard);
```

### trackContentCardInteraction

Tracks a Click interaction with the given ContentCard

**Syntax**
```javascript
Messaging.trackContentCardInteraction(proposition, contentCard);
```
22 changes: 22 additions & 0 deletions packages/messaging/__tests__/MessagingTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,26 @@ describe('Messaging', () => {
]);
expect(spy).toHaveBeenCalledWith(['testSurface1', 'testSurface2']);
});

it('should call trackContentCardDisplay', async () => {
const spy = jest.spyOn(
NativeModules.AEPMessaging,
'trackContentCardDisplay'
);
const mockProposition = { propositionId: 'mockPropositionId' } as any;
const mockContentCard = { contentCardId: 'mockContentCardId' } as any;
await Messaging.trackContentCardDisplay(mockProposition, mockContentCard);
expect(spy).toHaveBeenCalledWith(mockProposition, mockContentCard);
});

it('should call trackContentCardInteraction', async () => {
const spy = jest.spyOn(
NativeModules.AEPMessaging,
'trackContentCardInteraction'
);
const mockProposition = { propositionId: 'mockPropositionId' } as any;
const mockContentCard = { contentCardId: 'mockContentCardId' } as any;
await Messaging.trackContentCardInteraction(mockProposition, mockContentCard);
expect(spy).toHaveBeenCalledWith(mockProposition, mockContentCard);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,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;
Expand All @@ -38,6 +39,7 @@
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;
Expand Down Expand Up @@ -269,4 +271,28 @@ private void emitEvent(final String name, final Map<String, String> data) {
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit(name, eventData);
}
}

@ReactMethod
public void trackContentCardDisplay(ReadableMap propositionMap, ReadableMap contentCardMap) {
final Map<String, Object> eventData = RCTAEPMessagingUtil.convertReadableMapToMap(propositionMap);
final Proposition proposition = Proposition.fromEventData(eventData);
for (PropositionItem item : proposition.getItems()) {
if (item.getItemId().equals(contentCardMap.getString("id"))) {
item.track(MessagingEdgeEventType.DISPLAY);
break;
}
}
}

@ReactMethod
public void trackContentCardInteraction(ReadableMap propositionMap, ReadableMap contentCardMap) {
final Map<String, Object> eventData = RCTAEPMessagingUtil.convertReadableMapToMap(propositionMap);
final Proposition proposition = Proposition.fromEventData(eventData);
for (PropositionItem item : proposition.getItems()) {
if (item.getItemId().equals(contentCardMap.getString("id"))) {
item.track("click", MessagingEdgeEventType.INTERACT, null);
break;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableMapKeySetIterator;
import com.facebook.react.bridge.ReadableType;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.WritableNativeArray;
Expand Down Expand Up @@ -216,4 +218,66 @@ static ReadableMap convertToReadableMap(Map<String, String> map) {
}
return writableMap;
}
}

/**
* Converts {@link ReadableMap} Map to {@link Map}
*
* @param readableMap instance of {@code ReadableMap}
* @return instance of {@code Map}
*/
static Map<String, Object> convertReadableMapToMap(final ReadableMap readableMap) {
ReadableMapKeySetIterator iterator = readableMap.keySetIterator();
Map<String, Object> map = new HashMap<>();
while (iterator.hasNextKey()) {
String key = iterator.nextKey();
ReadableType type = readableMap.getType(key);
switch (type) {
case Boolean:
map.put(key, readableMap.getBoolean(key));
break;
case Number:
map.put(key, readableMap.getDouble(key));
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;
default:
break;
}
}
return map;
}

static List<Object> convertReadableArrayToList(final ReadableArray readableArray) {
final List<Object> list = new ArrayList<>(readableArray.size());
for (int i = 0; i < readableArray.size(); i++) {
ReadableType indexType = readableArray.getType(i);
switch(indexType) {
case Boolean:
list.add(i, readableArray.getBoolean(i));
break;
case Number:
list.add(i, readableArray.getDouble(i));
break;
case String:
list.add(i, readableArray.getString(i));
break;
case Map:
list.add(i, convertReadableMapToMap(readableArray.getMap(i)));
break;
case Array:
list.add(i, convertReadableArrayToList(readableArray.getArray(i)));
break;
default:
break;
}
}
return list;
}
}
8 changes: 8 additions & 0 deletions packages/messaging/ios/src/RCTAEPMessaging.mm
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,12 @@ @interface RCT_EXTERN_MODULE (RCTAEPMessaging, RCTEventEmitter)
: (RCTPromiseResolveBlock)resolve withRejecter
: (RCTPromiseRejectBlock)reject);

RCT_EXTERN_METHOD(trackContentCardDisplay
: (NSDictionary *)propositionMap contentCardMap
: (NSDictionary *)contentCardMap);

RCT_EXTERN_METHOD(trackContentCardInteraction
: (NSDictionary *)propositionMap contentCardMap
: (NSDictionary *)contentCardMap);

@end
48 changes: 48 additions & 0 deletions packages/messaging/ios/src/RCTAEPMessaging.swift
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,54 @@ public class RCTAEPMessaging: RCTEventEmitter, MessagingDelegate {
reject(Constants.CACHE_MISS, nil, nil)
}

@objc
func trackContentCardDisplay(
_ propositionMap: [String: Any],
contentCardMap: [String: Any]
) {
guard let contentCardId = contentCardMap["id"] as? String else {
print("Error: Content card ID is missing or invalid")
return
}

do {
let jsonData = try JSONSerialization.data(withJSONObject: propositionMap)
let proposition = try JSONDecoder().decode(Proposition.self, from: jsonData)

if let matchingItem = proposition.items.first(where: { $0.itemId == contentCardId }) {
matchingItem.track(withEdgeEventType: MessagingEdgeEventType.display)
} else {
print("Error: No matching proposition item found for content card ID: \(contentCardId)")
}
} catch {
print("Error decoding proposition: \(error.localizedDescription)")
}
}

@objc
func trackContentCardInteraction(
_ propositionMap: [String: Any],
contentCardMap: [String: Any]
) {
guard let contentCardId = contentCardMap["id"] as? String else {
print("Error: Content card ID is missing or invalid")
return
}

do {
let jsonData = try JSONSerialization.data(withJSONObject: propositionMap)
let proposition = try JSONDecoder().decode(Proposition.self, from: jsonData)

if let matchingItem = proposition.items.first(where: { $0.itemId == contentCardId }) {
matchingItem.track("click", withEdgeEventType: MessagingEdgeEventType.interact)
} else {
print("Error: No matching proposition item found for content card ID: \(contentCardId)")
}
} catch {
print("Error decoding proposition: \(error.localizedDescription)")
}
}

// Messaging Delegate Methods
public func onDismiss(message: Showable) {
if let fullscreenMessage = message as? FullscreenMessage,
Expand Down
11 changes: 11 additions & 0 deletions packages/messaging/src/Messaging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
import Message from './models/Message';
import { MessagingDelegate } from './models/MessagingDelegate';
import { MessagingProposition } from './models/MessagingProposition';
import { ContentCard } from './models/ContentCard';

export interface NativeMessagingModule {
extensionVersion: () => Promise<string>;
Expand All @@ -34,6 +35,8 @@ export interface NativeMessagingModule {
shouldSaveMessage: boolean
) => void;
updatePropositionsForSurfaces: (surfaces: string[]) => void;
trackContentCardDisplay: (proposition: MessagingProposition, contentCard: ContentCard) => void;
trackContentCardInteraction: (proposition: MessagingProposition, contentCard: ContentCard) => void;
}

const RCTAEPMessaging: NativeModule & NativeMessagingModule =
Expand Down Expand Up @@ -90,6 +93,14 @@ class Messaging {
return await RCTAEPMessaging.getPropositionsForSurfaces(surfaces);
}

static trackContentCardDisplay(proposition: MessagingProposition, contentCard: ContentCard): void {
RCTAEPMessaging.trackContentCardDisplay(proposition, contentCard);
}

static trackContentCardInteraction(proposition: MessagingProposition, contentCard: ContentCard): void {
RCTAEPMessaging.trackContentCardInteraction(proposition, contentCard);
}

/**
* Function to set the UI Message delegate to listen the Message lifecycle events.
* @returns A function to unsubscribe from all event listeners
Expand Down
4 changes: 3 additions & 1 deletion tests/jest/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,9 @@ jest.doMock('react-native', () => {
updatePropositionsForSurfaces: jest.fn(),
getPropositionsForSurfaces: jest.fn(
() => new Promise((resolve) => resolve([]))
)
),
trackContentCardDisplay: jest.fn(),
trackContentCardInteraction: jest.fn()
},
AEPOptimize: {
extensionVersion: jest.fn(
Expand Down