diff --git a/.gitignore b/.gitignore
index 1cc37aefa..48e6d3b41 100644
--- a/.gitignore
+++ b/.gitignore
@@ -149,3 +149,6 @@ out/
# Ignore VSCode settings
.vscode/
.vscode/settings.json
+
+# ignore ds store
+.DS_Store
diff --git a/apps/AEPSampleAppNewArchEnabled/app/MessagingView.tsx b/apps/AEPSampleAppNewArchEnabled/app/MessagingView.tsx
index b7ff9e1e3..973fe847f 100644
--- a/apps/AEPSampleAppNewArchEnabled/app/MessagingView.tsx
+++ b/apps/AEPSampleAppNewArchEnabled/app/MessagingView.tsx
@@ -10,19 +10,27 @@ OF ANY KIND, either express or implied. See the License for the specific languag
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 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 styles from "../styles/styles";
-import { useRouter } from "expo-router";
+ Messaging,
+ PersonalizationSchema,
+ MessagingEdgeEventType,
+ PropositionItem,
+ Message,
+ ContentCard,
+ HTMLProposition,
+ JSONPropositionItem
+} from '@adobe/react-native-aepmessaging'
+import { MessagingProposition } from '@adobe/react-native-aepmessaging';
+import styles from '../styles/styles';
+import { useRouter } from 'expo-router';
const SURFACES = ["android-cbe-preview", "cbe/json", "android-cc"];
const SURFACES_WITH_CONTENT_CARDS = ["android-cc"];
+
const messagingExtensionVersion = async () => {
const version = await Messaging.extensionVersion();
console.log(`AdobeExperienceSDK: Messaging version: ${version}`);
@@ -35,16 +43,12 @@ const refreshInAppMessages = () => {
const setMessagingDelegate = () => {
Messaging.setMessagingDelegate({
- onDismiss: (msg) => console.log("dismissed!", msg),
- onShow: (msg) => {
- console.log("show", msg);
- Messaging.handleJavascriptMessage(
- msg.id,
- "myInappCallback",
- (content) => {
- console.log("Received webview content:", content);
- }
- );
+ onDismiss: msg => console.log('dismissed!', msg),
+ onShow: msg => {
+ console.log('show', msg);
+ msg.handleJavascriptMessage('myInappCallback', (content: string) => {
+ console.log('Received webview content in onShow:', content);
+ });
},
shouldShowMessage: () => true,
shouldSaveMessage: () => true,
@@ -52,14 +56,12 @@ const setMessagingDelegate = () => {
});
console.log("messaging delegate set");
};
-
const getPropositionsForSurfaces = async () => {
const messages = await Messaging.getPropositionsForSurfaces(SURFACES);
- console.log(JSON.stringify(messages));
+ console.log('getPropositionsForSurfaces', JSON.stringify(messages));
};
-
const trackAction = async () => {
- MobileCore.trackAction("tuesday", { full: true });
+ MobileCore.trackAction('iamjs', {full: true});
};
const updatePropositionsForSurfaces = async () => {
@@ -89,12 +91,9 @@ const trackContentCardInteraction = async () => {
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
- );
+ // Cast to ContentCard for the legacy tracking method
+ Messaging.trackContentCardInteraction(proposition, propositionItem as any);
+ console.log('trackContentCardInteraction', proposition, propositionItem);
}
}
}
@@ -113,14 +112,35 @@ const trackContentCardDisplay = async () => {
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);
+ // Cast to ContentCard for the legacy tracking method
+ Messaging.trackContentCardDisplay(proposition, propositionItem as any);
+ console.log('trackContentCardDisplay', proposition, propositionItem);
}
}
}
}
};
+
+// Method demonstrating unified tracking using PropositionItem methods
+const unifiedTrackingExample = async () => {
+ const messages = await Messaging.getPropositionsForSurfaces(SURFACES);
+ for (const surface of SURFACES) {
+ const propositions = messages[surface] || [];
+
+ for (const proposition of propositions) {
+ const propositionWrapper = new MessagingProposition(proposition);
+ if (propositionWrapper.items.length > 0) {
+ const propositionItem = propositionWrapper.items[0];
+ propositionItem.track(MessagingEdgeEventType.DISPLAY);
+ propositionItem.track('content_card_clicked', MessagingEdgeEventType.INTERACT, null);
+ }
+ }
+ }
+}
+
+
+
function MessagingView() {
const router = useRouter();
@@ -146,14 +166,9 @@ function MessagingView() {
-
-
+
+
+
);
diff --git a/apps/AEPSampleAppNewArchEnabled/app/OptimizeView.tsx b/apps/AEPSampleAppNewArchEnabled/app/OptimizeView.tsx
index 5411c18be..71a5d9ade 100644
--- a/apps/AEPSampleAppNewArchEnabled/app/OptimizeView.tsx
+++ b/apps/AEPSampleAppNewArchEnabled/app/OptimizeView.tsx
@@ -15,6 +15,7 @@ import {
Optimize,
DecisionScope,
Proposition,
+ Offer,
} from '@adobe/react-native-aepoptimize';
import {WebView} from 'react-native-webview';
import styles from '../styles/styles';
@@ -73,7 +74,7 @@ export default () => {
decisionScopeImage,
decisionScopeHtml,
decisionScopeJson,
- decisionScopeTargetMbox,
+ decisionScopeTargetMbox
];
const optimizeExtensionVersion = async () => {
@@ -84,19 +85,36 @@ export default () => {
const updatePropositions = () => {
Optimize.updatePropositions(decisionScopes);
- console.log('Updated Proposition for decisionScopes:', decisionScopes);
+ console.log('Updated Propositions');
+ };
+
+ const testUpdatePropositionsCallback = () => {
+ console.log('Testing updatePropositions with callback...');
+ Optimize.updatePropositions(
+ decisionScopes,
+ undefined,
+ undefined,
+ (response) => {
+ console.log('Callback received:', response);
+ },
+ (error) => {
+ console.log('Error:', error);
+ }
+ );
};
const getPropositions = async () => {
const propositions: Map =
await Optimize.getPropositions(decisionScopes);
+ console.log(propositions.size, ' propositions size');
if (propositions) {
- console.log("get propositions", JSON.stringify(Object.fromEntries(propositions), null, 2));
setTextProposition(propositions.get(decisionScopeText.getName()));
setImageProposition(propositions.get(decisionScopeImage.getName()));
setHtmlProposition(propositions.get(decisionScopeHtml.getName()));
setJsonProposition(propositions.get(decisionScopeJson.getName()));
setTargetProposition(propositions.get(decisionScopeTargetMbox.getName()));
+ const propositionObject = Object.fromEntries(propositions);
+ console.log('propositions', JSON.stringify(propositionObject, null, 2));
}
};
@@ -120,49 +138,34 @@ export default () => {
const multipleOffersDisplayed = async () => {
const propositionsMap: Map = await Optimize.getPropositions(decisionScopes);
- const offerPairs: Array<{proposition: Proposition, offerId: string}> = [];
-
+ const offers: Array = [];
propositionsMap.forEach((proposition: Proposition) => {
- if (proposition && proposition.items) {
+ if (proposition && proposition.items && proposition.items.length > 0) {
proposition.items.forEach((offer) => {
- offerPairs.push({
- proposition: proposition,
- offerId: offer.id
- });
+ offers.push(offer);
});
}
});
-
- console.log('Extracted offer pairs:', offerPairs);
-
- if (offerPairs.length > 0) {
- Optimize.displayed(offerPairs);
- console.log(`Called multipleOffersDisplayed with ${offerPairs.length} offers`);
- } else {
- console.log('No offers found to display');
- }
+ console.log('offers', offers);
+ Optimize.displayed(offers);
};
- const generateDisplayInteractionXdmForMultipleOffers = async () => {
+ const multipleOffersGenerateDisplayInteractionXdm = async () => {
const propositionsMap: Map = await Optimize.getPropositions(decisionScopes);
- const offerPairs: Array<{proposition: Proposition, offerId: string}> = [];
-
+ const offers: Array = [];
propositionsMap.forEach((proposition: Proposition) => {
- if (proposition && proposition.items) {
+ if (proposition && proposition.items && proposition.items.length > 0) {
proposition.items.forEach((offer) => {
- offerPairs.push({
- proposition: proposition,
- offerId: offer.id
- });
+ offers.push(offer);
});
}
});
-
- const xdm = await Optimize.generateDisplayInteractionXdm(offerPairs);
- if (xdm) {
- console.log('Generated Display Interaction XDM for Multiple Offers:', JSON.stringify(xdm, null, 2));
+ console.log('offers', offers);
+ const displayInteractionXdm = await Optimize.generateDisplayInteractionXdm(offers);
+ if (displayInteractionXdm) {
+ console.log('displayInteractionXdm', JSON.stringify(displayInteractionXdm, null, 2));
} else {
- console.log('Error in generating Display interaction XDM for multiple offers.');
+ console.log('displayInteractionXdm is null');
}
};
@@ -377,6 +380,9 @@ export default () => {
+
+
+
@@ -388,20 +394,20 @@ export default () => {
diff --git a/apps/AEPSampleAppNewArchEnabled/app/_layout.tsx b/apps/AEPSampleAppNewArchEnabled/app/_layout.tsx
index ae5e2fe82..f11d45266 100644
--- a/apps/AEPSampleAppNewArchEnabled/app/_layout.tsx
+++ b/apps/AEPSampleAppNewArchEnabled/app/_layout.tsx
@@ -58,6 +58,32 @@ export default function RootLayout() {
.then(() => {
console.log("update propositions after - SUCCESS");
console.log("Propositions updated successfully in layout");
+
+ // // Set up messaging delegate after SDK initialization
+ // const unsubscribe = Messaging.setMessagingDelegate({
+ // onDismiss: (message) => {
+ // console.log('Message dismissed:', message);
+ // },
+ // onShow: (message) => {
+ // console.log('Message shown:', message);
+ // },
+ // shouldShowMessage: (message) => {
+ // console.log('Should show message:', message);
+ // return true; // Always show messages in sample app
+ // },
+ // shouldSaveMessage: (message) => {
+ // console.log('Should save message:', message);
+ // return true; // Always save messages in sample app
+ // },
+ // urlLoaded: (url, message) => {
+ // console.log('URL loaded:', url, 'for message:', message);
+ // },
+ // });
+
+ // console.log("Messaging delegate set up successfully");
+
+ // Store unsubscribe function if needed for cleanup
+ // You could return it from useEffect if you need to clean up on unmount
})
.catch((error) => {
console.error("AEP SDK Initialization error:", error);
diff --git a/packages/messaging/README.md b/packages/messaging/README.md
index a229cd072..3aaa0c3ea 100644
--- a/packages/messaging/README.md
+++ b/packages/messaging/README.md
@@ -157,20 +157,6 @@ const messagingDelegate = {
};
```
-### handleJavascriptMessage
-
-Registers a javascript interface for the provided handler name to the WebView associated with the InAppMessage presentation to handle Javascript messages. When the registered handlers are executed via the HTML the result will be passed back to the associated callback.
-
-**Syntax**
-
-```javascript
-handleJavascriptMessage(messageId: string, handlerName: string, callback: (content: string) => void)
-```
-
-**Example**
-
-It can be used for the native handling of JavaScript events. Please refer to the [tutorial](./tutorials/In-App%20Messaging.md#native-handling-of-javascript-events) for more information.
-
### updatePropositionsForSurfaces
Dispatches an event to fetch propositions for the provided surfaces from remote.
@@ -216,6 +202,53 @@ for (let item in proposition.items) {
}
```
+
+### PropositionItem.track
+
+A unified tracking API is available for any proposition item (Content Cards, HTML, JSON, code-based items). You can use the same track() method regardless of content type, making your code more consistent and maintainable.
+
+#### Using PropositionItem.track (recommended)
+
+**Syntax**
+
+```javascript
+// Track display event
+propositionItem.track(MessagingEdgeEventType.DISPLAY);
+
+// Track interaction with custom data + event + optional tokens
+propositionItem.track(
+ interaction /* string | null */,
+ MessagingEdgeEventType.INTERACT /* enum value */,
+ [/* tokens */] /* string[] | null */
+);
+```
+
+When using `getPropositionsForSurfaces`, the returned objects can be wrapped with `MessagingProposition` to get typed items and convenient tracking via `PropositionItem.track(...)`.
+
+```javascript
+import { Messaging, MessagingProposition, MessagingEdgeEventType } from '@adobe/react-native-aepmessaging';
+
+const SURFACES = ['mobileapp://my-surface'];
+const messages = await Messaging.getPropositionsForSurfaces(SURFACES);
+
+for (const surface of SURFACES) {
+ const propositions = messages[surface] || [];
+
+ for (const proposition of propositions) {
+ const msgProp = new MessagingProposition(proposition);
+
+ if (msgProp.items.length > 0) {
+ const propositionItem = msgProp.items[0];
+
+ // Track interaction with custom data
+ propositionItem.track('content_card_clicked', MessagingEdgeEventType.INTERACT, null);
+ // Track with tokens for sub-item tracking
+ propositionItem.track('button_click', MessagingEdgeEventType.INTERACT, ['token-1', 'token-2']);
+ }
+ }
+}
+```
+
### getLatestMessage
Retrieves the most recently displayed message object
@@ -355,6 +388,20 @@ var message: Message;
message.clear();
```
+### handleJavascriptMessage
+
+Registers a javascript interface for the provided handler name to the WebView associated with the InAppMessage presentation to handle Javascript messages. When the registered handlers are executed via the HTML the result will be passed back to the associated handler.
+
+**Syntax**
+
+```typescript
+handleJavascriptMessage(handlerName: string, handler: (content: string) => void);
+```
+
+**Example**
+
+It can be used for the native handling of JavaScript events. Please refer to the [tutorial](./tutorials/In-App%20Messaging.md#native-handling-of-javascript-events) for more information.
+
## Programmatically control the display of in-app messages
App developers can now create a type `MessagingDelegate` in order to be alerted when specific events occur during the lifecycle of an in-app message.
@@ -454,6 +501,8 @@ function otherWorkflowFinished() {
### trackContentCardDisplay
+Deprecated in 7.2.0: Use `Proposition.track(...)` instead. This API will be removed in a future release.
+
Tracks a Display interaction with the given ContentCard
**Syntax**
@@ -464,6 +513,8 @@ Messaging.trackContentCardDisplay(proposition, contentCard);
### trackContentCardInteraction
+Deprecated in 7.2.0: Use `Proposition.track(...)` instead. This API will be removed in a future release.
+
Tracks a Click interaction with the given ContentCard
**Syntax**
@@ -473,6 +524,4 @@ Messaging.trackContentCardInteraction(proposition, contentCard);
```
## Tutorials
-
-[Content Cards](./tutorials/ContentCards.md)
-[In App Messaging](./tutorials/In-App%20Messaging.md)
+[Native handling of Javascript Events](./tutorials/In-App%20Messaging.md)
diff --git a/packages/messaging/__tests__/MessagingTests.ts b/packages/messaging/__tests__/MessagingTests.ts
index 257305418..2f11ab989 100644
--- a/packages/messaging/__tests__/MessagingTests.ts
+++ b/packages/messaging/__tests__/MessagingTests.ts
@@ -89,6 +89,17 @@ describe('Messaging', () => {
expect(spy).toHaveBeenCalledWith(id);
});
+ it('handleJavascriptMessage is called', async () => {
+ const spy = jest.spyOn(NativeModules.AEPMessaging, 'handleJavascriptMessage');
+ let id = 'id';
+ let autoTrack = true;
+ let message = new Message({id, autoTrack});
+ let handlerName = 'handlerName';
+ let handler = jest.fn();
+ await message.handleJavascriptMessage(handlerName, handler);
+ expect(spy).toHaveBeenCalledWith(id, handlerName);
+ });
+
it('should call updatePropositionsForSurfaces', async () => {
const spy = jest.spyOn(NativeModules.AEPMessaging, 'updatePropositionsForSurfaces');
await Messaging.updatePropositionsForSurfaces([
@@ -128,15 +139,4 @@ describe('Messaging', () => {
await Messaging.trackContentCardInteraction(mockProposition, mockContentCard);
expect(spy).toHaveBeenCalledWith(mockProposition, mockContentCard);
});
-
- it('should call handleJavascriptMessage', () => {
- const spy = jest.spyOn(NativeModules.AEPMessaging, 'handleJavascriptMessage');
- const messageId = 'test-message-id';
- const handlerName = 'myInappCallback';
- const callback = jest.fn();
-
- Messaging.handleJavascriptMessage(messageId, handlerName, callback);
-
- expect(spy).toHaveBeenCalledWith(messageId, handlerName);
- });
});
diff --git a/packages/messaging/__tests__/PropositionItemTests.ts b/packages/messaging/__tests__/PropositionItemTests.ts
new file mode 100644
index 000000000..16833e705
--- /dev/null
+++ b/packages/messaging/__tests__/PropositionItemTests.ts
@@ -0,0 +1,84 @@
+/*
+Copyright 2025 Adobe. All rights reserved.
+This file is licensed to you under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License. You may obtain a copy
+of the License at http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software distributed under
+the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
+OF ANY KIND, either express or implied. See the License for the specific language
+governing permissions and limitations under the License.
+*/
+
+import { NativeModules } from 'react-native';
+import { MessagingEdgeEventType, PropositionItem, PropositionItemData, PersonalizationSchema } from '../src';
+
+describe('PropositionItem', () => {
+ const uuid = 'activity-uuid-123';
+ const id = 'item-abc';
+ const activityID = uuid; // mirrors native mapping
+
+ const baseData: PropositionItemData = {
+ id,
+ uuid,
+ activityID,
+ schema: PersonalizationSchema.CONTENT_CARD,
+ data: { foo: 'bar' },
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('track(eventType) calls native with null interaction and tokens', () => {
+ const item = new PropositionItem(baseData);
+ item.track(MessagingEdgeEventType.DISPLAY);
+
+ expect(NativeModules.AEPMessaging.trackPropositionItem).toHaveBeenCalledWith(
+ activityID,
+ null,
+ MessagingEdgeEventType.DISPLAY,
+ null
+ );
+ });
+
+ it('track(interaction, eventType, tokens=null) forwards interaction', () => {
+ const item = new PropositionItem(baseData);
+ item.track('click', MessagingEdgeEventType.INTERACT, null);
+
+ expect(NativeModules.AEPMessaging.trackPropositionItem).toHaveBeenCalledWith(
+ activityID,
+ 'click',
+ MessagingEdgeEventType.INTERACT,
+ null
+ );
+ });
+
+ it('track(interaction, eventType, tokens[]) forwards tokens array', () => {
+ const item = new PropositionItem(baseData);
+ const tokens = ['t1', 't2'];
+ item.track('click', MessagingEdgeEventType.INTERACT, tokens);
+
+ expect(NativeModules.AEPMessaging.trackPropositionItem).toHaveBeenCalledWith(
+ activityID,
+ 'click',
+ MessagingEdgeEventType.INTERACT,
+ tokens
+ );
+ });
+
+ it('track(null, eventType, tokens[]) supports null interaction with tokens', () => {
+ const item = new PropositionItem(baseData);
+ const tokens = ['a'];
+ item.track(null, MessagingEdgeEventType.DISPLAY, tokens);
+
+ expect(NativeModules.AEPMessaging.trackPropositionItem).toHaveBeenCalledWith(
+ activityID,
+ null,
+ MessagingEdgeEventType.DISPLAY,
+ tokens
+ );
+ });
+});
+
+
diff --git a/packages/messaging/android/src/main/java/com/adobe/marketing/mobile/reactnative/messaging/RCTAEPMessagingModule.java b/packages/messaging/android/src/main/java/com/adobe/marketing/mobile/reactnative/messaging/RCTAEPMessagingModule.java
index 59b961426..35757cdfc 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,15 +14,15 @@
import static com.adobe.marketing.mobile.reactnative.messaging.RCTAEPMessagingUtil.convertMessageToMap;
import android.app.Activity;
+import android.util.Log;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import com.adobe.marketing.mobile.AdobeCallback;
import com.adobe.marketing.mobile.AdobeCallbackWithError;
import com.adobe.marketing.mobile.AdobeError;
-import com.adobe.marketing.mobile.LoggingMode;
import com.adobe.marketing.mobile.Message;
import com.adobe.marketing.mobile.Messaging;
import com.adobe.marketing.mobile.MessagingEdgeEventType;
-import com.adobe.marketing.mobile.MobileCore;
import com.adobe.marketing.mobile.messaging.MessagingUtils;
import com.adobe.marketing.mobile.messaging.Proposition;
import com.adobe.marketing.mobile.messaging.PropositionItem;
@@ -41,14 +41,56 @@
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.modules.core.DeviceEventManagerModule;
+import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ConcurrentHashMap;
+
+
public final class RCTAEPMessagingModule
extends ReactContextBaseJavaModule implements PresentationDelegate {
+ // We cache uuid -> Proposition (not PropositionItem) for iOS parity: storing items on iOS lost
+ // weak parent references. Propositions currently contain a single item, so using the first item
+ // for tracking is valid.
+ private final Map propositionItemByUuid = new ConcurrentHashMap<>();
+ /**
+ * Parses the given Proposition and extracts the activity ID.
+ *
+ * Expected location: scopeDetails.activity.id in the proposition's event data.
+ * Returns null when any of the nested structures are missing or the id is not a String.
+ */
+ private String extractActivityId(Proposition proposition) {
+ try {
+ Map eventData = proposition.toEventData();
+ if (eventData == null) {
+ Log.d(TAG, "[MessagingBridge] Proposition toEventData() returned null; cannot extract activity.id");
+ return null;
+ }
+ Object scopeDetailsObj = eventData.get("scopeDetails");
+ if (!(scopeDetailsObj instanceof Map)) return null;
+ Map scopeDetails = (Map) scopeDetailsObj;
+ Object activityObj = scopeDetails.get("activity");
+ if (!(activityObj instanceof Map)) {
+ Log.d(TAG, "[MessagingBridge] Missing activity under scopeDetails; cannot extract activity.id");
+ return null;
+ }
+ Map activity = (Map) activityObj;
+ Object id = activity.get("id");
+ if (id instanceof String) {
+ return (String) id;
+ } else {
+ Log.d(TAG, "[MessagingBridge] Missing activity.id or not a String; skipping uuid cache mapping");
+ return null;
+ }
+ } catch (Exception e) {
+ Log.d(TAG, "[MessagingBridge] Exception extracting activity.id: " + e.getMessage(), e);
+ return null;
+ }
+ }
private static final String TAG = "RCTAEPMessagingModule";
private final Map messageCache = new HashMap<>();
private final ReactApplicationContext reactContext;
@@ -101,20 +143,37 @@ public void getPropositionsForSurfaces(ReadableArray surfaces,
final Promise promise) {
String bundleId = this.reactContext.getPackageName();
Messaging.getPropositionsForSurfaces(
- RCTAEPMessagingUtil.convertSurfaces(surfaces),
- new AdobeCallbackWithError
)}
{content?.body?.content && (
{content.body.content}
)}
{content?.buttons?.length &&
content?.buttons?.length > 0 &&
@@ -185,10 +129,9 @@ const SmallImageCard: React.FC = ({
key={button.id}
actionUrl={button.actionUrl}
title={button.text.content}
+ onPress={onPress}
style={styleOverrides?.button}
textStyle={[styleOverrides?.text, styleOverrides?.buttonText]}
- {...ButtonProps}
- onPress={onPress}
/>
))}
@@ -196,7 +139,6 @@ const SmallImageCard: React.FC = ({
)}
@@ -211,26 +153,27 @@ const styles = StyleSheet.create({
card: {
borderRadius: 12,
margin: 15,
- maxWidth: '100%'
+ flex: 1,
+ flexDirection: 'row',
+ gap: 8,
+ maxWidth: '100%',
+ alignItems: 'center'
},
container: {
flexDirection: 'row',
- alignItems: 'center',
- gap: 8,
- flex: 1
+ minHeight: 120
},
imageContainer: {
borderRadius: 12,
- height: '100%',
- flex: 1 / 3,
- justifyContent: 'center'
+ width: 'auto',
+ height: '100%'
},
image: {
- width: '100%',
+ height: '100%',
resizeMode: 'contain'
},
contentContainer: {
- flex: 2 / 3,
+ flex: 1,
paddingVertical: 16,
paddingHorizontal: 16,
justifyContent: 'flex-start'
diff --git a/packages/messaging/src/ui/types/Templates.ts b/packages/messaging/src/ui/types/Templates.ts
index 80ccb1f2e..f0eba2d30 100644
--- a/packages/messaging/src/ui/types/Templates.ts
+++ b/packages/messaging/src/ui/types/Templates.ts
@@ -14,6 +14,13 @@ export type BaseContentTemplate = {
readonly id: string;
};
-export type ContentTemplate = ContentCard & {
- readonly type: TemplateType;
+export type TemplateTypeLiteral = `${TemplateType}`
+
+export class ContentTemplate extends ContentCard {
+ readonly type: string;
+
+ constructor(data: ContentCard & { type: `${TemplateType}` }) {
+ super(data as any);
+ this.type = data.type;
+ }
};
diff --git a/packages/messaging/tutorials/ContentCards.md b/packages/messaging/tutorials/ContentCards.md
deleted file mode 100644
index edf50df8f..000000000
--- a/packages/messaging/tutorials/ContentCards.md
+++ /dev/null
@@ -1,747 +0,0 @@
-# Content Cards Tutorial
-
-## Overview
-
-Content Cards are a powerful feature of Adobe Journey Optimizer that allows you to deliver personalized, contextual content directly within your mobile application. Unlike push notifications or in-app messages, content cards provide a persistent, non-intrusive way to present relevant information to users when they're actively engaged with your app. Content cards are ideal for showcasing promotions, product recommendations, onboarding tips, or any contextual information that enhances the user experience without interrupting their workflow.
-
-
-## Fetching Content Cards
-
-### Prerequisites
-
-Before implementing content cards, ensure you have:
-
-1. Integrated and registered the AEPMessaging extension in your app
-2. Configured content card campaigns in Adobe Journey Optimizer
-3. Defined locations where content cards should appear in your app
-
-### Step 1: Update Propositions for Surfaces
-
-To fetch content cards for specific surfaces configured in Adobe Journey Optimizer campaigns, call the `updatePropositionsForSurfaces` API. This method retrieves the latest content cards from the server and caches them in-memory for the application's lifecycle.
-
-**Best Practice**: Batch requests for multiple surfaces in a single API call when possible to optimize performance.
-
-```typescript
-import { Messaging } from '@adobe/react-native-aepmessaging';
-
-// Define surfaces of content cards to retrieve
-const surfaces: string[] = ['homepage', 'product-detail', 'checkout'];
-
-// Fetch content cards for multiple surfaces
-const updateContentCards = async (): Promise => {
- try {
- await Messaging.updatePropositionsForSurfaces(surfaces);
- console.log('Content cards updated successfully');
- } catch (error) {
- console.error('Failed to update content cards:', error);
- }
-};
-```
-
-### Step 2: Retrieve Content Cards
-
-After updating propositions, retrieve the content cards for a specific surface using the `getPropositionsForSurfaces` API. This returns a record of surface names with their corresponding propositions that contain content cards for which the user is qualified.
-
-**Important**: Only content cards for which the user has qualified are returned. User qualification is determined by the delivery rules configured in Adobe Journey Optimizer.
-
-```typescript
-import { Messaging, MessagingProposition, ContentCard, PersonalizationSchema } from '@adobe/react-native-aepmessaging';
-
-// Retrieve propositions for specific surfaces
-const getContentCards = async (surfacePaths: string[]): Promise => {
- try {
- const propositionsMap: Record = await Messaging.getPropositionsForSurfaces(surfacePaths);
-
- const allContentCards: ContentCard[] = [];
-
- // Extract content cards from all surfaces
- Object.values(propositionsMap).forEach(propositions => {
- propositions.forEach(proposition => {
- const contentCards = proposition.items.filter(
- item => item.schema === PersonalizationSchema.CONTENT_CARD
- ) as ContentCard[];
- allContentCards.push(...contentCards);
- });
- });
-
- console.log(`Found ${allContentCards.length} content cards across all surfaces`);
- return allContentCards;
- } catch (error) {
- console.error('Error retrieving propositions:', error);
- return [];
- }
-};
-```
-
-## Rendering Content Cards
-
-Content cards can be rendered in various ways depending on your application architecture. Since React Native doesn't support the pre-built `ContentCardUI` objects, you'll need to create your own UI components based on the content card data from propositions.
-
-### React Native Implementation
-
-Here's how to implement content cards in a React Native component using propositions:
-
-```typescript
-import React, { useState, useEffect } from 'react';
-import { View, ScrollView, StyleSheet, Text, Image, TouchableOpacity } from 'react-native';
-import { Messaging, MessagingProposition, ContentCard, PersonalizationSchema } from '@adobe/react-native-aepmessaging';
-
-interface ContentCardsScreenProps {
- surfacePath?: string;
-}
-
-const ContentCardsScreen: React.FC = ({ surfacePath = 'homepage' }) => {
- const [contentCards, setContentCards] = useState([]);
- const [loading, setLoading] = useState(true);
-
- useEffect(() => {
- fetchContentCards();
- }, [surfacePath]);
-
- const fetchContentCards = async (): Promise => {
- try {
- setLoading(true);
-
- // First, update propositions for the surface
- await Messaging.updatePropositionsForSurfaces([surfacePath]);
-
- // Then retrieve the propositions and extract content cards
- const propositionsMap: Record = await Messaging.getPropositionsForSurfaces([surfacePath]);
- const propositions = propositionsMap[surfacePath] || [];
-
- const cards: ContentCard[] = propositions.flatMap(proposition =>
- proposition.items.filter(item => item.schema === PersonalizationSchema.CONTENT_CARD) as ContentCard[]
- );
-
- setContentCards(cards || []);
- } catch (error) {
- console.error('Failed to fetch content cards:', error);
- setContentCards([]);
- } finally {
- setLoading(false);
- }
- };
-
- const renderContentCard = (card: ContentCard, index: number): JSX.Element => {
- const { data } = card;
-
- return (
-
- {data.content.image?.url && (
-
- )}
-
- {data.content.title?.content && (
- {data.content.title.content}
- )}
- {data.content.body?.content && (
- {data.content.body.content}
- )}
- {data.content.actionUrl && (
-
- Learn More
-
- )}
-
-
- );
- };
-
- if (loading) {
- return (
-
- Loading content cards...
-
- );
- }
-
- return (
-
- {contentCards.map((card, index) => renderContentCard(card, index))}
-
- );
-};
-
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- padding: 16,
- },
- cardContainer: {
- backgroundColor: '#fff',
- borderRadius: 8,
- marginBottom: 16,
- shadowColor: '#000',
- shadowOffset: { width: 0, height: 2 },
- shadowOpacity: 0.1,
- shadowRadius: 4,
- elevation: 3,
- },
- cardImage: {
- width: '100%',
- height: 120,
- borderTopLeftRadius: 8,
- borderTopRightRadius: 8,
- },
- cardContent: {
- padding: 16,
- },
- cardTitle: {
- fontSize: 18,
- fontWeight: 'bold',
- marginBottom: 8,
- },
- cardBody: {
- fontSize: 14,
- color: '#666',
- marginBottom: 12,
- },
- actionButton: {
- backgroundColor: '#007AFF',
- paddingHorizontal: 16,
- paddingVertical: 8,
- borderRadius: 4,
- alignSelf: 'flex-start',
- },
- actionText: {
- color: '#fff',
- fontWeight: '600',
- },
- loading: {
- flex: 1,
- justifyContent: 'center',
- alignItems: 'center',
- },
-});
-
-export default ContentCardsScreen;
-```
-
-## Tracking Content Card Events
-
-Content cards support event tracking to measure user engagement and campaign effectiveness. The AEPMessaging extension provides methods to track two key events: display and interact. These events help you understand how users engage with your content cards and optimize your campaigns accordingly.
-
-### Event Types
-
-- **Display**: Triggered when a content card is shown to the user
-- **Interact**: Triggered when a user taps or interacts with a content card
-
-### Implementing Event Tracking
-
-Here's how to implement event tracking in your content card components:
-
-```typescript
-import React, { useState, useEffect, useRef } from 'react';
-import { View, ScrollView, StyleSheet, Text, Image, TouchableOpacity } from 'react-native';
-import { Messaging, MessagingProposition, ContentCard, PersonalizationSchema } from '@adobe/react-native-aepmessaging';
-
-interface ContentCardsWithTrackingProps {
- surfacePath?: string;
-}
-
-const ContentCardsWithTracking: React.FC = ({
- surfacePath = 'homepage'
-}) => {
- const [contentCards, setContentCards] = useState([]);
- const [propositions, setPropositions] = useState([]);
- const [loading, setLoading] = useState(true);
- const displayedCards = useRef>(new Set());
-
- useEffect(() => {
- fetchContentCards();
- }, [surfacePath]);
-
- const fetchContentCards = async (): Promise => {
- try {
- setLoading(true);
-
- await Messaging.updatePropositionsForSurfaces([surfacePath]);
- const propositionsMap: Record = await Messaging.getPropositionsForSurfaces([surfacePath]);
- const fetchedPropositions = propositionsMap[surfacePath] || [];
-
- const cards: ContentCard[] = fetchedPropositions.flatMap(proposition =>
- proposition.items.filter(item => item.schema === PersonalizationSchema.CONTENT_CARD) as ContentCard[]
- );
-
- setContentCards(cards || []);
- setPropositions(fetchedPropositions || []);
- } catch (error) {
- console.error('Failed to fetch content cards:', error);
- setContentCards([]);
- setPropositions([]);
- } finally {
- setLoading(false);
- }
- };
-
- // Track display event when card becomes visible
- const trackDisplayEvent = (proposition: MessagingProposition, contentCard: ContentCard): void => {
- const propositionId = proposition.id;
-
- // Prevent duplicate display events for the same card
- if (!displayedCards.current.has(propositionId)) {
- try {
- Messaging.trackContentCardDisplay(proposition, contentCard);
- displayedCards.current.add(propositionId);
- console.log(`Display event tracked for proposition: ${propositionId}`);
- } catch (error) {
- console.error('Failed to track display event:', error);
- }
- }
- };
-
- // Track interact event when user taps on card
- const trackInteractEvent = (proposition: MessagingProposition, contentCard: ContentCard): void => {
- try {
- Messaging.trackContentCardInteraction(proposition, contentCard);
- console.log(`Interact event tracked for proposition: ${proposition.id}`);
- } catch (error) {
- console.error('Failed to track interact event:', error);
- }
- };
-
- const handleCardPress = (card: ContentCard, proposition: MessagingProposition): void => {
- // Track interaction
- trackInteractEvent(proposition, card);
-
- // Handle card action (e.g., navigate to URL)
- if (card.data.content.actionUrl) {
- // Navigate to action URL or handle custom action
- console.log(`Navigating to: ${card.data.content.actionUrl}`);
- }
- };
-
- const handleCardDismiss = (cardIndex: number): void => {
- // Remove card from display
- setContentCards(prevCards => prevCards.filter((_, index) => index !== cardIndex));
- };
-
- const renderContentCard = (card: ContentCard, index: number): JSX.Element => {
- const { data } = card;
- const proposition = propositions.find(prop =>
- prop.items.some(item => item.id === card.id)
- );
-
- // Track display event when card is rendered
- useEffect(() => {
- if (proposition) {
- trackDisplayEvent(proposition, card);
- }
- }, [proposition]);
-
- return (
-
- handleCardDismiss(index)}
- >
- ×
-
-
- proposition && handleCardPress(card, proposition)}
- >
- {data.content.image?.url && (
-
- )}
-
- {data.content.title?.content && (
- {data.content.title.content}
- )}
- {data.content.body?.content && (
- {data.content.body.content}
- )}
- {data.content.actionUrl && (
-
- Learn More
-
- )}
-
-
-
- );
- };
-
- if (loading) {
- return (
-
- Loading content cards...
-
- );
- }
-
- return (
-
- {contentCards.map((card, index) => renderContentCard(card, index))}
-
- );
-};
-
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- padding: 16,
- },
- cardContainer: {
- backgroundColor: '#fff',
- borderRadius: 8,
- marginBottom: 16,
- shadowColor: '#000',
- shadowOffset: { width: 0, height: 2 },
- shadowOpacity: 0.1,
- shadowRadius: 4,
- elevation: 3,
- position: 'relative',
- },
- dismissButton: {
- position: 'absolute',
- top: 8,
- right: 8,
- width: 24,
- height: 24,
- borderRadius: 12,
- backgroundColor: 'rgba(0, 0, 0, 0.5)',
- justifyContent: 'center',
- alignItems: 'center',
- zIndex: 1,
- },
- dismissText: {
- color: '#fff',
- fontSize: 16,
- fontWeight: 'bold',
- },
- cardContent: {
- flex: 1,
- },
- cardImage: {
- width: '100%',
- height: 120,
- borderTopLeftRadius: 8,
- borderTopRightRadius: 8,
- },
- cardTextContent: {
- padding: 16,
- },
- cardTitle: {
- fontSize: 18,
- fontWeight: 'bold',
- marginBottom: 8,
- },
- cardBody: {
- fontSize: 14,
- color: '#666',
- marginBottom: 12,
- },
- actionButton: {
- backgroundColor: '#007AFF',
- paddingHorizontal: 16,
- paddingVertical: 8,
- borderRadius: 4,
- alignSelf: 'flex-start',
- },
- actionText: {
- color: '#fff',
- fontWeight: '600',
- },
- loading: {
- flex: 1,
- justifyContent: 'center',
- alignItems: 'center',
- },
-});
-
-export default ContentCardsWithTracking;
-```
-
-### Event Tracking Best Practices
-
-1. **Display Events**: Track display events only once per card per session to avoid duplicate analytics
-2. **Interact Events**: Track interactions immediately when they occur to ensure accurate measurement
-3. **Error Handling**: Implement proper error handling for tracking calls to prevent app crashes
-
-### Custom Event Tracking Hook
-
-For reusable event tracking logic, consider creating a custom hook:
-
-```typescript
-import { useCallback, useRef } from 'react';
-import { Messaging, MessagingProposition, ContentCard } from '@adobe/react-native-aepmessaging';
-
-interface ContentCardTracking {
- trackDisplay: (proposition: MessagingProposition, contentCard: ContentCard) => void;
- trackInteract: (proposition: MessagingProposition, contentCard: ContentCard) => void;
-}
-
-const useContentCardTracking = (): ContentCardTracking => {
- const displayedCards = useRef>(new Set());
-
- const trackDisplay = useCallback((proposition: MessagingProposition, contentCard: ContentCard): void => {
- const propositionId = proposition.id;
-
- if (!displayedCards.current.has(propositionId)) {
- try {
- Messaging.trackContentCardDisplay(proposition, contentCard);
- displayedCards.current.add(propositionId);
- console.log(`Display tracked: ${propositionId}`);
- } catch (error) {
- console.error('Display tracking failed:', error);
- }
- }
- }, []);
-
- const trackInteract = useCallback((proposition: MessagingProposition, contentCard: ContentCard): void => {
- try {
- Messaging.trackContentCardInteraction(proposition, contentCard);
- console.log(`Interact tracked: ${proposition.id}`);
- } catch (error) {
- console.error('Interact tracking failed:', error);
- }
- }, []);
-
- return {
- trackDisplay,
- trackInteract,
- };
-};
-
-export default useContentCardTracking;
-```
-
-### Advanced Usage Example
-
-Here's a more comprehensive example showing how to use the tracking methods in a production-ready implementation:
-
-```typescript
-import React, { useState, useEffect, useRef, useCallback } from 'react';
-import { View, FlatList, StyleSheet, Text, Image, TouchableOpacity, ViewToken } from 'react-native';
-import { Messaging, MessagingProposition, ContentCard, PersonalizationSchema } from '@adobe/react-native-aepmessaging';
-
-interface ContentCardWithProposition extends ContentCard {
- proposition: MessagingProposition;
- uniqueId: string;
-}
-
-interface ContentCardsFlatListProps {
- surfacePaths?: string[];
-}
-
-const ContentCardsFlatList: React.FC = ({
- surfacePaths = ['homepage']
-}) => {
- const [contentCards, setContentCards] = useState([]);
- const [loading, setLoading] = useState(true);
- const displayedCards = useRef>(new Set());
-
- useEffect(() => {
- fetchContentCards();
- }, [surfacePaths]);
-
- const fetchContentCards = async (): Promise => {
- try {
- setLoading(true);
-
- await Messaging.updatePropositionsForSurfaces(surfacePaths);
- const propositionsMap: Record = await Messaging.getPropositionsForSurfaces(surfacePaths);
-
- const cards: ContentCardWithProposition[] = [];
-
- Object.entries(propositionsMap).forEach(([surface, propositions]) => {
- propositions.forEach((proposition, propIndex) => {
- proposition.items
- .filter(item => item.schema === PersonalizationSchema.CONTENT_CARD)
- .forEach((item, itemIndex) => {
- cards.push({
- ...(item as ContentCard),
- proposition,
- uniqueId: `${surface}-${proposition.id}-${itemIndex}`,
- });
- });
- });
- });
-
- setContentCards(cards);
- } catch (error) {
- console.error('Failed to fetch content cards:', error);
- setContentCards([]);
- } finally {
- setLoading(false);
- }
- };
-
- // Track display event when card appears in viewport
- const trackDisplayEvent = useCallback((proposition: MessagingProposition, contentCard: ContentCard): void => {
- const propositionId = proposition.id;
-
- if (!displayedCards.current.has(propositionId)) {
- try {
- Messaging.trackContentCardDisplay(proposition, contentCard);
- displayedCards.current.add(propositionId);
- console.log(`Display event tracked for proposition: ${propositionId}`);
- } catch (error) {
- console.error('Failed to track display event:', error);
- }
- }
- }, []);
-
- // Track interact event
- const trackInteractEvent = useCallback((proposition: MessagingProposition, contentCard: ContentCard): void => {
- try {
- Messaging.trackContentCardInteraction(proposition, contentCard);
- console.log(`Interact event tracked for proposition: ${proposition.id}`);
- } catch (error) {
- console.error('Failed to track interact event:', error);
- }
- }, []);
-
- // Handle viewable items changed - track display events for visible cards
- const onViewableItemsChanged = useCallback(({ viewableItems }: { viewableItems: ViewToken[] }): void => {
- viewableItems.forEach(({ item }) => {
- if (item.proposition) {
- trackDisplayEvent(item.proposition, item);
- }
- });
- }, [trackDisplayEvent]);
-
- const handleCardPress = useCallback((card: ContentCardWithProposition): void => {
- trackInteractEvent(card.proposition, card);
-
- if (card.data.content.actionUrl) {
- console.log(`Navigating to: ${card.data.content.actionUrl}`);
- // Handle navigation or custom action here
- }
- }, [trackInteractEvent]);
-
- const renderContentCard = ({ item: card }: { item: ContentCardWithProposition }): JSX.Element => {
- const { data } = card;
-
- return (
- handleCardPress(card)}
- activeOpacity={0.8}
- >
- {data.content.image?.url && (
-
- )}
-
- {data.content.title?.content && (
- {data.content.title.content}
- )}
- {data.content.body?.content && (
- {data.content.body.content}
- )}
- {data.content.actionUrl && (
-
- Learn More
-
- )}
-
-
- );
- };
-
- const keyExtractor = (item: ContentCardWithProposition): string => item.uniqueId;
-
- if (loading) {
- return (
-
- Loading content cards...
-
- );
- }
-
- return (
-
- data={contentCards}
- renderItem={renderContentCard}
- keyExtractor={keyExtractor}
- onViewableItemsChanged={onViewableItemsChanged}
- viewabilityConfig={{
- itemVisiblePercentThreshold: 50,
- minimumViewTime: 500,
- }}
- contentContainerStyle={styles.flatListContainer}
- showsVerticalScrollIndicator={false}
- />
- );
-};
-
-const styles = StyleSheet.create({
- flatListContainer: {
- padding: 16,
- flexGrow: 1,
- },
- cardContainer: {
- backgroundColor: '#fff',
- borderRadius: 12,
- marginBottom: 16,
- shadowColor: '#000',
- shadowOffset: { width: 0, height: 2 },
- shadowOpacity: 0.1,
- shadowRadius: 8,
- elevation: 4,
- overflow: 'hidden',
- },
- cardImage: {
- width: '100%',
- height: 160,
- },
- cardTextContent: {
- padding: 16,
- },
- cardTitle: {
- fontSize: 18,
- fontWeight: 'bold',
- marginBottom: 8,
- color: '#333',
- },
- cardBody: {
- fontSize: 14,
- color: '#666',
- marginBottom: 16,
- lineHeight: 20,
- },
- actionButton: {
- backgroundColor: '#007AFF',
- paddingHorizontal: 20,
- paddingVertical: 10,
- borderRadius: 6,
- alignSelf: 'flex-start',
- },
- actionText: {
- color: '#fff',
- fontWeight: '600',
- fontSize: 14,
- },
- loading: {
- flex: 1,
- justifyContent: 'center',
- alignItems: 'center',
- },
-});
-
-export default ContentCardsFlatList;
-```
-
-## Performance Considerations
-
-1. **Caching**: Content cards are cached in-memory by the Messaging extension and persist through the application's lifecycle
-2. **Batching**: Always batch surface requests when fetching content cards for multiple locations
-3. **Viewport Tracking**: Use FlatList with `onViewableItemsChanged` for accurate display tracking
-4. **Error Handling**: Implement proper error handling for all tracking calls
-
-## Wrap Up
-
-This implementation provides a comprehensive foundation for fetching, rendering, and tracking content cards in your React Native application using the `trackContentCardDisplay` and `trackContentCardInteraction` methods. These methods provide specific tracking for content card interactions, allowing for better analytics and campaign optimization.
-
-The key benefits of these tracking methods:
-- **Simplified API**: Direct methods for content card tracking without needing to handle proposition-level details
-- **Better Analytics**: More specific event data for content card interactions
-- **Easier Implementation**: Cleaner code with dedicated methods for content card events
-
-Happy coding!
\ No newline at end of file
diff --git a/packages/messaging/tutorials/In-App Messaging.md b/packages/messaging/tutorials/In-App Messaging.md
index 4be499adb..4ab8ca78a 100644
--- a/packages/messaging/tutorials/In-App Messaging.md
+++ b/packages/messaging/tutorials/In-App Messaging.md
@@ -12,7 +12,7 @@ Please read the [documentation](../README.md/#programmatically-control-the-displ
## Register a JavaScript handler for your In-App Message
-In the `onShow` function of `MessagingDelegate`, call `handleJavascriptMessage(messageId: string, handlerName: string, callback: (content: string) => void)` to register your handler.
+In the `onShow` function of `MessagingDelegate`, call `handleJavascriptMessage(handlerName: string, handler: (content: string) => void)` to register your handler.
The name of the message you intend to pass from the JavaScript side should be specified in the first parameter.
@@ -22,8 +22,7 @@ The name of the message you intend to pass from the JavaScript side should be sp
Messaging.setMessagingDelegate({
onShow: msg => {
console.log('show', msg);
- Messaging.handleJavascriptMessage(
- msg.id,
+ msg.handleJavascriptMessage(
'myInappCallback',
(content) => {
console.log('Received webview content:', content);
diff --git a/packages/optimize/README.md b/packages/optimize/README.md
index 0fe8321c0..e46e811a2 100644
--- a/packages/optimize/README.md
+++ b/packages/optimize/README.md
@@ -138,20 +138,33 @@ Optimize.onPropositionUpdate({
### updating the propositions:
-This API fetches the propositions for the provided DecisionScope list.
+This API fetches the propositions for the provided DecisionScope list. The API now supports optional callback parameters for successful response and error handling.
**Syntax**
```typescript
-updatePropositions(decisionScopes: Array, xdm?: Map, data?: Map)
+updatePropositions(
+ decisionScopes: Array,
+ xdm?: Map,
+ data?: Map,
+ onSuccess?: (response: Map) => void,
+ onError?: (error: AEPOptimizeError) => void
+)
```
+**Parameters:**
+- `decisionScopes`: Array of decision scopes for which offers need to be updated
+- `xdm`: Optional XDM-formatted data to be sent in the personalization query request
+- `data`: Optional free-form data to be sent in the personalization query request
+- `onSuccess`: Optional callback function called when propositions are successfully updated
+- `onError`: Optional callback function called when an error occurs during the update
+
**Example**
```typescript
const decisionScopeText = new DecisionScope("{DecisionScope name}");
const decisionScopeImage = new DecisionScope("{DecisionScope name}");
-const decisionScopeHtml = new DecisionScope("{DecisionScope name{");
+const decisionScopeHtml = new DecisionScope("{DecisionScope name}");
const decisionScopeJson = new DecisionScope("{DecisionScope name}");
const decisionScopes = [
decisionScopeText,
@@ -160,39 +173,53 @@ const decisionScopes = [
decisionScopeJson,
];
-Optimize.updatePropositions(decisionScopes, null, null);
+// Update without callbacks
+Optimize.updatePropositions(decisionScopes);
+
+// Update with callbacks
+Optimize.updatePropositions(
+ decisionScopes,
+ undefined, // xdm parameter
+ undefined, // data parameter
+ (response: Map) => {
+ console.log('✅ Propositions updated successfully:', response);
+ // Handle the updated propositions
+ },
+ (error: AEPOptimizeError) => {
+ console.error('❌ Error updating propositions:', error);
+ // Handle the error
+ }
+);
```
-### Batching multiple display propositions track events:
+### Batching display interaction events for multiple Offers:
-The Optimize SDK now provides enhanced support for batching multiple display propositions track events. The following APIs are available:
+The Optimize SDK now provides enhanced support for batching display interaction events for multiple Offers. The following APIs are available:
#### displayed
**Syntax**
```typescript
-displayed(offerPairs: Array<{ proposition: Proposition; offerId: string }>)
+displayed(offers: Array)
```
**Example**
```typescript
-const propositions: Map = await Optimize.getPropositions(decisionScopes);
-const offerPairs: Array<{ proposition: Proposition; offerId: string }> = [];
-propositions.forEach((proposition: Proposition) => {
- if (proposition && proposition.items) {
+const propositionsMap: Map = await Optimize.getPropositions(decisionScopes);
+const offers: Array = [];
+
+propositionsMap.forEach((proposition: Proposition) => {
+ if (proposition && proposition.items && proposition.items.length > 0) {
proposition.items.forEach((offer) => {
- offerPairs.push({
- proposition,
- offerId: offer.id,
- });
+ offers.push(offer);
});
}
});
-Optimize.displayed(offerPairs);
+Optimize.displayed(offers);
```
#### generateDisplayInteractionXdm
@@ -200,29 +227,25 @@ Optimize.displayed(offerPairs);
**Syntax**
```typescript
-generateDisplayInteractionXdm(offerPairs: Array<{ proposition: Proposition; offerId: string }>): Promise>
+generateDisplayInteractionXdm(offers: Array): Promise>;
```
**Example**
```typescript
-const propositions: Map = await Optimize.getPropositions(decisionScopes);
-const offerPairs: Array<{ proposition: Proposition; offerId: string }> = [];
-propositions.forEach((proposition: Proposition) => {
- if (proposition && proposition.items) {
+const propositionsMap: Map = await Optimize.getPropositions(decisionScopes);
+const offers: Array = [];
+
+propositionsMap.forEach((proposition: Proposition) => {
+ if (proposition && proposition.items && proposition.items.length > 0) {
proposition.items.forEach((offer) => {
- offerPairs.push({
- proposition,
- offerId: offer.id,
- });
+ offers.push(offer);
});
}
});
-const xdmData = await Optimize.generateDisplayInteractionXdm(offerPairs);
-// use xdmData as needed
-
+const displayInteractionXdm = await Optimize.generateDisplayInteractionXdm(offers);
```
---
diff --git a/packages/optimize/__tests__/OptimizeTests.ts b/packages/optimize/__tests__/OptimizeTests.ts
index 0bde3d86e..d144770af 100644
--- a/packages/optimize/__tests__/OptimizeTests.ts
+++ b/packages/optimize/__tests__/OptimizeTests.ts
@@ -51,18 +51,122 @@ describe('Optimize', () => {
});
it('AEPOptimize updateProposition is called', async () => {
- const spy = jest.spyOn(NativeModules.AEPOptimize, 'updatePropositions');
- let decisionScopes = [new DecisionScope('abcdef')];
+ let spy = jest.spyOn(NativeModules.AEPOptimize, "updatePropositions");
+ let decisionScopes = [new DecisionScope("abcdef")];
let xdm = new Map();
let data = new Map();
await Optimize.updatePropositions(decisionScopes, xdm, data);
expect(spy).toHaveBeenCalledWith(
decisionScopes.map((decisionScope) => decisionScope.getName()),
xdm,
- data
+ data,
+ expect.any(Function),
+ expect.any(Function)
+ );
+ });
+
+ it('AEPOptimize updateProposition is called with callback', async () => {
+ let spy = jest.spyOn(NativeModules.AEPOptimize, "updatePropositions");
+ let decisionScopes = [new DecisionScope("abcdef")];
+ let xdm = new Map();
+ let data = new Map();
+ const callback = (_propositions: Map) => {};
+ await Optimize.updatePropositions(decisionScopes, xdm, data, callback as any, undefined);
+ expect(spy).toHaveBeenCalledWith(
+ decisionScopes.map((decisionScope) => decisionScope.getName()),
+ xdm,
+ data,
+ expect.any(Function),
+ expect.any(Function)
);
});
+ it('AEPOptimize updateProposition callback handles successful response', async () => {
+ const mockResponse = new Map();
+ mockResponse.set('scope1', new Proposition(propositionJson as any));
+ // Mock the native method to call the callback with mock data
+ const mockMethod = jest.fn().mockImplementation((...args: any[]) => {
+ const callback = args[3];
+ if (typeof callback === 'function') {
+ callback(mockResponse);
+ }
+ });
+ NativeModules.AEPOptimize.updatePropositions = mockMethod;
+ let decisionScopes = [new DecisionScope("abcdef")];
+ let callbackResponse: Map | null = null;
+ const callback = (propositions: Map) => {
+ callbackResponse = propositions;
+ };
+ await Optimize.updatePropositions(decisionScopes, undefined, undefined, callback as any, undefined);
+ expect(callbackResponse).not.toBeNull();
+ expect(callbackResponse!.get('scope1')).toBeInstanceOf(Proposition);
+ });
+
+ it('AEPOptimize updateProposition callback handles error response', async () => {
+ // For error, the callback may not be called, or may be called with an empty map or undefined. We'll simulate an empty map.
+ const mockErrorResponse = new Error('Test error');
+ // Mock the native method to call the callback with error data
+ const mockMethod = jest.fn().mockImplementation((...args: any[]) => {
+ const onError = args[4];
+ if (typeof onError === 'function') {
+ onError(mockErrorResponse);
+ }
+ });
+ NativeModules.AEPOptimize.updatePropositions = mockMethod;
+ let decisionScopes = [new DecisionScope("abcdef")];
+ let callbackResponse: any = null;
+ const onError = (error: any) => {
+ callbackResponse = error;
+ };
+ await Optimize.updatePropositions(decisionScopes, undefined, undefined, undefined, onError as any);
+ expect(callbackResponse).not.toBeNull();
+ expect(callbackResponse!.message).toBe('Test error');
+ });
+
+ it('AEPOptimize updateProposition calls both success and error callbacks', async () => {
+ const mockSuccessResponse = new Map();
+ mockSuccessResponse.set('scope1', new Proposition(propositionJson as any));
+ const mockError = { message: 'Test error', code: 500 };
+
+ // Mock the native method to call both callbacks
+ const mockMethod = jest.fn().mockImplementation((...args: any[]) => {
+ const onSuccess = args[3];
+ const onError = args[4];
+ if (typeof onSuccess === 'function') {
+ onSuccess(mockSuccessResponse);
+ }
+ if (typeof onError === 'function') {
+ onError(mockError);
+ }
+ });
+ NativeModules.AEPOptimize.updatePropositions = mockMethod;
+
+ let successCalled = false;
+ let errorCalled = false;
+ let successResponse: Map | null = null;
+ let errorResponse: any = null;
+
+ const onSuccess = (propositions: Map) => {
+ successCalled = true;
+ successResponse = propositions;
+ };
+ const onError = (error: any) => {
+ errorCalled = true;
+ errorResponse = error;
+ };
+
+ let decisionScopes = [new DecisionScope("abcdef")];
+ await Optimize.updatePropositions(decisionScopes, undefined, undefined, onSuccess as any, onError as any);
+
+ expect(successCalled).toBe(true);
+ expect(errorCalled).toBe(true);
+ expect(successResponse).not.toBeNull();
+ expect(successResponse!.get('scope1')).toBeInstanceOf(Proposition);
+ expect(errorResponse).toBeDefined();
+ expect(errorResponse.message).toBe('Test error');
+ expect(errorResponse.code).toBe(500);
+ });
+
it('Test Offer object state', async () => {
const offer = new Offer(offerJson);
//Asserts
@@ -177,47 +281,16 @@ describe('Optimize', () => {
it('Test Optimize.displayed', async () => {
const spy = jest.spyOn(NativeModules.AEPOptimize, 'multipleOffersDisplayed');
- const proposition = new Proposition(propositionJson as any);
- const offer = proposition.items[0];
- const offerPairs = [{
- proposition,
- offerId: offer.id
- }];
-
- // Clean the proposition as your implementation does
- const entries = Object.entries(proposition).filter(
- ([_, value]) => typeof value !== 'function'
- );
- const cleanedProposition = Object.fromEntries(entries);
- const cleanedPairs = [{
- proposition: cleanedProposition,
- offerId: offer.id
- }];
-
- await Optimize.displayed(offerPairs);
- expect(spy).toHaveBeenCalledWith(cleanedPairs);
+ const offers = [new Offer(offerJson)];
+ await Optimize.displayed(offers);
+ expect(spy).toHaveBeenCalledWith(offers);
});
it('Test Optimize.generateDisplayInteractionXdm', async () => {
- const spy = jest.spyOn(NativeModules.AEPOptimize, 'generateDisplayInteractionXdmForMultipleOffers');
- const proposition = new Proposition(propositionJson as any);
- const offer = proposition.items[0];
- const offerPairs = [{
- proposition,
- offerId: offer.id
- }];
-
- // Clean the proposition as your implementation does
- const entries = Object.entries(proposition).filter(
- ([_, value]) => typeof value !== 'function'
- );
- const cleanedProposition = Object.fromEntries(entries);
- const cleanedPairs = [{
- proposition: cleanedProposition,
- offerId: offer.id
- }];
-
- await Optimize.generateDisplayInteractionXdm(offerPairs);
- expect(spy).toHaveBeenCalledWith(cleanedPairs);
+ const spy = jest.spyOn(NativeModules.AEPOptimize, 'multipleOffersGenerateDisplayInteractionXdm');
+ const offers = [new Offer(offerJson)];
+ await Optimize.generateDisplayInteractionXdm(offers);
+ expect(spy).toHaveBeenCalledWith(offers);
});
+
});
diff --git a/packages/optimize/android/src/main/java/com/adobe/marketing/mobile/reactnative/optimize/RCTAEPOptimizeConstants.java b/packages/optimize/android/src/main/java/com/adobe/marketing/mobile/reactnative/optimize/RCTAEPOptimizeConstants.java
index 3a38d6803..000dac1e1 100644
--- a/packages/optimize/android/src/main/java/com/adobe/marketing/mobile/reactnative/optimize/RCTAEPOptimizeConstants.java
+++ b/packages/optimize/android/src/main/java/com/adobe/marketing/mobile/reactnative/optimize/RCTAEPOptimizeConstants.java
@@ -9,9 +9,8 @@
governing permissions and limitations under the License.
*/
- package com.adobe.marketing.mobile.reactnative.optimize;
+package com.adobe.marketing.mobile.reactnative.optimize;
- class RCTAEPOptimizeConstants {
- static final String PROPOSITION_KEY = "proposition";
- static final String OFFER_ID_KEY = "offerId";
- }
\ No newline at end of file
+class RCTAEPOptimizeConstants {
+ static final String UNIQUE_PROPOSITION_ID_KEY = "uniquePropositionId";
+ }
diff --git a/packages/optimize/android/src/main/java/com/adobe/marketing/mobile/reactnative/optimize/RCTAEPOptimizeModule.java b/packages/optimize/android/src/main/java/com/adobe/marketing/mobile/reactnative/optimize/RCTAEPOptimizeModule.java
index 656b21a10..d69b1a231 100644
--- a/packages/optimize/android/src/main/java/com/adobe/marketing/mobile/reactnative/optimize/RCTAEPOptimizeModule.java
+++ b/packages/optimize/android/src/main/java/com/adobe/marketing/mobile/reactnative/optimize/RCTAEPOptimizeModule.java
@@ -10,8 +10,11 @@
*/
package com.adobe.marketing.mobile.reactnative.optimize;
+import android.util.Log;
import com.adobe.marketing.mobile.AdobeCallback;
import com.adobe.marketing.mobile.AdobeCallbackWithError;
+import com.adobe.marketing.mobile.optimize.AdobeCallbackWithOptimizeError;
+import com.adobe.marketing.mobile.optimize.AEPOptimizeError;
import com.adobe.marketing.mobile.AdobeError;
import com.adobe.marketing.mobile.LoggingMode;
import com.adobe.marketing.mobile.MobileCore;
@@ -21,6 +24,8 @@
import com.adobe.marketing.mobile.optimize.OfferUtils;
import com.adobe.marketing.mobile.optimize.Optimize;
import com.adobe.marketing.mobile.optimize.OptimizeProposition;
+import com.adobe.marketing.mobile.util.DataReader;
+import com.adobe.marketing.mobile.util.DataReaderException;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
@@ -29,17 +34,23 @@
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.WritableNativeMap;
+import com.facebook.react.bridge.Callback;
import com.facebook.react.modules.core.DeviceEventManagerModule;
+import android.util.Log;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import androidx.annotation.Nullable;
public class RCTAEPOptimizeModule extends ReactContextBaseJavaModule {
private static final String TAG = "RCTAEPOptimizeModule";
private final ReactApplicationContext reactContext;
+ // Cache of
+ private final Map propositionCache = new ConcurrentHashMap<>();
public RCTAEPOptimizeModule(ReactApplicationContext reactContext) {
super(reactContext);
@@ -63,43 +74,6 @@ public void offerDisplayed(final String offerId, final ReadableMap propositionMa
}
}
- @ReactMethod
- public void multipleOffersDisplayed(final ReadableArray propositionOfferPairs) {
- if (propositionOfferPairs == null || propositionOfferPairs.size() == 0) {
- return;
- }
-
- List nativeOffers = new ArrayList<>();
-
- for (int i = 0; i < propositionOfferPairs.size(); i++) {
- ReadableMap propositionOfferPairMap = propositionOfferPairs.getMap(i);
- if (propositionOfferPairMap == null) {
- continue;
- }
-
- ReadableMap propositionMap = propositionOfferPairMap.getMap(RCTAEPOptimizeConstants.PROPOSITION_KEY);
- String offerId = propositionOfferPairMap.getString(RCTAEPOptimizeConstants.OFFER_ID_KEY);
- if (propositionMap == null || offerId == null) {
- continue;
- }
-
- Map propositionEventData = RCTAEPOptimizeUtil.convertReadableMapToMap(propositionMap);
- OptimizeProposition proposition = OptimizeProposition.fromEventData(propositionEventData);
- if (proposition != null) {
- for (Offer offer : proposition.getOffers()) {
- if (offer.getId().equalsIgnoreCase(offerId)) {
- nativeOffers.add(offer);
- break;
- }
- }
- }
- }
-
- if (!nativeOffers.isEmpty()) {
- OfferUtils.displayed(nativeOffers);
- }
- }
-
@ReactMethod
public void extensionVersion(final Promise promise) {
promise.resolve(Optimize.extensionVersion());
@@ -107,16 +81,43 @@ public void extensionVersion(final Promise promise) {
@ReactMethod
public void clearCachedPropositions() {
+ // clear the react native cache
+ clearPropositionsCache();
+ // clear the native cache
Optimize.clearCachedPropositions();
}
@ReactMethod
- public void updatePropositions(final ReadableArray decisionScopesArray, ReadableMap xdm, ReadableMap data) {
+ public void updatePropositions(final ReadableArray decisionScopesArray, ReadableMap xdm, ReadableMap data, @Nullable final Callback successCallback, @Nullable final Callback errorCallback) {
+ Log.d(TAG, "updatePropositions called");
final List decisionScopeList = RCTAEPOptimizeUtil.createDecisionScopes(decisionScopesArray);
Map mapXdm = xdm != null ? RCTAEPOptimizeUtil.convertReadableMapToMap(xdm) : Collections.emptyMap();
Map mapData = data != null ? RCTAEPOptimizeUtil.convertReadableMapToMap(data) : Collections.emptyMap();
- Optimize.updatePropositions(decisionScopeList, mapXdm, mapData);
+
+ Optimize.updatePropositions(decisionScopeList, mapXdm, mapData, new AdobeCallbackWithOptimizeError>() {
+ @Override
+ public void fail(final AEPOptimizeError adobeError) {
+ Log.e(TAG, "updatePropositions callback failed: " );
+ if (errorCallback != null) {
+ final WritableMap response = RCTAEPOptimizeUtil.convertAEPOptimizeErrorToWritableMap(adobeError);
+ Log.d(TAG, "Invoking JS errorCallback with error: ");
+ errorCallback.invoke(response);
+ }
+ }
+
+ @Override
+ public void call(final Map decisionScopePropositionMap) {
+
+ cachePropositionOffers(decisionScopePropositionMap);
+ Log.d(TAG, "updatePropositions callback success.");
+ if (successCallback != null) {
+ final WritableMap response = RCTAEPOptimizeUtil.createCallbackResponse(decisionScopePropositionMap);
+ Log.d(TAG, "Invoking JS successCallback with success: " + response.toString());
+ successCallback.invoke(response);
+ }
+ }
+ });
}
@ReactMethod
@@ -131,6 +132,7 @@ public void fail(final AdobeError adobeError) {
@Override
public void call(final Map decisionScopePropositionMap) {
+ cachePropositionOffers(decisionScopePropositionMap);
final WritableMap writableMap = new WritableNativeMap();
for (final Map.Entry entry : decisionScopePropositionMap.entrySet()) {
writableMap.putMap(entry.getKey().getName(), RCTAEPOptimizeUtil.convertPropositionToWritableMap(entry.getValue()));
@@ -140,11 +142,72 @@ public void call(final Map decisionScopeProp
});
}
+ private void cachePropositionOffers(final Map decisionScopePropositionMap) {
+ for (final Map.Entry entry : decisionScopePropositionMap.entrySet()) {
+ OptimizeProposition proposition = entry.getValue();
+ if (proposition == null) {
+ continue;
+ }
+
+ String activityId = null;
+ try {
+ Map activity = proposition.getActivity();
+ if (activity != null && activity.containsKey("id")) {
+ activityId = DataReader.getString(activity, "id");
+ } else {
+ Map scopeDetails = proposition.getScopeDetails();
+ if (scopeDetails != null && scopeDetails.containsKey("activity")) {
+ Map scopeDetailsActivity = DataReader.getTypedMap(Object.class, scopeDetails, "activity");
+ if (scopeDetailsActivity != null && scopeDetailsActivity.containsKey("id")) {
+ activityId = DataReader.getString(scopeDetailsActivity, "id");
+ }
+ }
+ }
+ } catch (DataReaderException e) {
+ Log.w(TAG, "Failed to extract activity ID from proposition: " + e.getMessage());
+ continue;
+ }
+
+ if (activityId != null) {
+ propositionCache.put(activityId, proposition);
+ }
+ }
+ }
+
+ private void clearPropositionsCache() {
+ propositionCache.clear();
+ }
+
+ @ReactMethod
+ public void multipleOffersDisplayed(final ReadableArray offersArray) {
+ List nativeOffers = RCTAEPOptimizeUtil.getNativeOffers(offersArray, propositionCache);
+
+ if (!nativeOffers.isEmpty()) {
+ Log.d(TAG, "multipleOffersDisplayed: calling display for: " + nativeOffers.size() + " offers: " + nativeOffers.toString());
+ OfferUtils.displayed(nativeOffers);
+ }
+ }
+
+ @ReactMethod
+ public void multipleOffersGenerateDisplayInteractionXdm(final ReadableArray offersArray, final Promise promise) {
+ List nativeOffers = RCTAEPOptimizeUtil.getNativeOffers(offersArray, propositionCache);
+
+ if (!nativeOffers.isEmpty()) {
+ Log.d(TAG, "multipleOffersGenerateDisplayInteractionXdm: calling generateDisplayInteractionXdm for: " + nativeOffers.size() + " offers: " + nativeOffers.toString());
+ final Map interactionXdm = OfferUtils.generateDisplayInteractionXdm(nativeOffers);
+ final WritableMap writableMap = RCTAEPOptimizeUtil.convertMapToWritableMap(interactionXdm);
+ promise.resolve(writableMap);
+ } else {
+ promise.reject("multipleOffersGenerateDisplayInteractionXdm", "Error in generating Display interaction XDM for multiple offers: " + offersArray.toString());
+ }
+ }
+
@ReactMethod
public void onPropositionsUpdate() {
Optimize.onPropositionsUpdate(new AdobeCallback>() {
@Override
public void call(final Map decisionScopePropositionMap) {
+ cachePropositionOffers(decisionScopePropositionMap);
sendUpdatedPropositionsEvent(decisionScopePropositionMap);
}
});
@@ -183,42 +246,6 @@ public void generateDisplayInteractionXdm(final String offerId, final ReadableMa
}
}
- @ReactMethod
- public void generateDisplayInteractionXdmForMultipleOffers(final ReadableArray propositionOfferPairs, final Promise promise) {
- final List nativeOffers = new ArrayList<>();
-
- for (int i = 0; i < propositionOfferPairs.size(); i++) {
- ReadableMap propositionOfferPairMap = propositionOfferPairs.getMap(i);
- if (propositionOfferPairMap == null) {
- continue;
- }
-
- ReadableMap propositionMap = propositionOfferPairMap.getMap(RCTAEPOptimizeConstants.PROPOSITION_KEY);
- String offerId = propositionOfferPairMap.getString(RCTAEPOptimizeConstants.OFFER_ID_KEY);
- if (propositionMap == null || offerId == null) {
- continue;
- }
-
- Map propositionEventData = RCTAEPOptimizeUtil.convertReadableMapToMap(propositionMap);
- OptimizeProposition proposition = OptimizeProposition.fromEventData(propositionEventData);
-
- for (Offer offer : proposition.getOffers()) {
- if (offer.getId().equalsIgnoreCase(offerId)) {
- nativeOffers.add(offer);
- break;
- }
- }
- }
-
- if (nativeOffers.size() > 0) {
- final Map interactionXdm = OfferUtils.generateDisplayInteractionXdm(nativeOffers);
- final WritableMap writableMap = RCTAEPOptimizeUtil.convertMapToWritableMap(interactionXdm);
- promise.resolve(writableMap);
- } else {
- promise.reject("generateDisplayInteractionXdmForMultipleOffers", "Error in generating Display interaction XDM for multiple offers.");
- }
- }
-
@ReactMethod
public void generateTapInteractionXdm(final String offerId, final ReadableMap propositionMap, final Promise promise) {
final Map eventData = RCTAEPOptimizeUtil.convertReadableMapToMap(propositionMap);
diff --git a/packages/optimize/android/src/main/java/com/adobe/marketing/mobile/reactnative/optimize/RCTAEPOptimizeUtil.java b/packages/optimize/android/src/main/java/com/adobe/marketing/mobile/reactnative/optimize/RCTAEPOptimizeUtil.java
index b3779101d..2ba1b3896 100644
--- a/packages/optimize/android/src/main/java/com/adobe/marketing/mobile/reactnative/optimize/RCTAEPOptimizeUtil.java
+++ b/packages/optimize/android/src/main/java/com/adobe/marketing/mobile/reactnative/optimize/RCTAEPOptimizeUtil.java
@@ -16,6 +16,7 @@
import com.adobe.marketing.mobile.optimize.DecisionScope;
import com.adobe.marketing.mobile.optimize.Offer;
import com.adobe.marketing.mobile.optimize.OptimizeProposition;
+import com.adobe.marketing.mobile.optimize.AEPOptimizeError;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableMapKeySetIterator;
@@ -47,6 +48,12 @@ static WritableMap convertPropositionToWritableMap(final OptimizeProposition pro
offersWritableArray.pushMap(convertOfferToWritableMap(offer));
}
propositionWritableMap.putArray("items", offersWritableArray);
+ if (proposition.getActivity() != null) {
+ propositionWritableMap.putMap("activity", convertMapToWritableMap(proposition.getActivity()));
+ }
+ if (proposition.getPlacement() != null) {
+ propositionWritableMap.putMap("placement", convertMapToWritableMap(proposition.getPlacement()));
+ }
return propositionWritableMap;
}
static WritableMap convertOfferToWritableMap(final Offer offer) {
@@ -185,4 +192,84 @@ private static List