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
52 changes: 50 additions & 2 deletions apps/AEPSampleAppNewArchEnabled/app/OptimizeView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -73,7 +74,7 @@ export default () => {
decisionScopeImage,
decisionScopeHtml,
decisionScopeJson,
decisionScopeTargetMbox
decisionScopeTargetMbox,
];

const optimizeExtensionVersion = async () => {
Expand Down Expand Up @@ -105,13 +106,15 @@ export default () => {
const getPropositions = async () => {
const propositions: Map<string, Proposition> =
await Optimize.getPropositions(decisionScopes);
console.log(propositions);
console.log(propositions.size, ' propositions size');
if (propositions) {
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));
}
};

Expand All @@ -133,6 +136,39 @@ export default () => {
},
});

const multipleOffersDisplayed = async () => {
const propositionsMap: Map<string, Proposition> = await Optimize.getPropositions(decisionScopes);
const offers: Array<Offer> = [];
propositionsMap.forEach((proposition: Proposition) => {
if (proposition && proposition.items && proposition.items.length > 0) {
proposition.items.forEach((offer) => {
offers.push(offer);
});
}
});
console.log('offers', offers);
Optimize.displayed(offers);
};

const multipleOffersGenerateDisplayInteractionXdm = async () => {
const propositionsMap: Map<string, Proposition> = await Optimize.getPropositions(decisionScopes);
const offers: Array<Offer> = [];
propositionsMap.forEach((proposition: Proposition) => {
if (proposition && proposition.items && proposition.items.length > 0) {
proposition.items.forEach((offer) => {
offers.push(offer);
});
}
});
console.log('offers', offers);
const displayInteractionXdm = await Optimize.generateDisplayInteractionXdm(offers);
if (displayInteractionXdm) {
console.log('displayInteractionXdm', JSON.stringify(displayInteractionXdm, null, 2));
} else {
console.log('displayInteractionXdm is null');
}
};

const renderTargetOffer = () => {
if (targetProposition?.items) {
if (targetProposition.items[0].format === TARGET_OFFER_TYPE_JSON) {
Expand Down Expand Up @@ -362,6 +398,18 @@ export default () => {
onPress={onPropositionUpdate}
/>
</View>
<View style={{margin: 5}}>
<Button
title="Multiple Offers Displayed"
onPress={multipleOffersDisplayed}
/>
</View>
<View style={{margin: 5}}>
<Button
title="Multiple Offers Generate Display Interaction XDM"
onPress={multipleOffersGenerateDisplayInteractionXdm}
/>
</View>
<Text style={{...styles.welcome, fontSize: 20}}>
SDK Version:: {version}
</Text>
Expand Down
56 changes: 56 additions & 0 deletions packages/optimize/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,62 @@ const decisionScopes = [
Optimize.updatePropositions(decisionScopes, null, null);
```

### Batching display interaction events for multiple Offers:

The Optimize SDK now provides enhanced support for batching display interaction events for multiple Offers. The following APIs are available:

#### displayed

**Syntax**

```typescript
displayed(offers: Array<Offer>)
```

**Example**

```typescript

const propositionsMap: Map<string, Proposition> = await Optimize.getPropositions(decisionScopes);
const offers: Array<Offer> = [];

propositionsMap.forEach((proposition: Proposition) => {
if (proposition && proposition.items && proposition.items.length > 0) {
proposition.items.forEach((offer) => {
offers.push(offer);
});
}
});

Optimize.displayed(offers);
```

#### generateDisplayInteractionXdm

**Syntax**

```typescript
generateDisplayInteractionXdm(offers: Array<Offer>): Promise<Map<string, any>>;
```

**Example**

```typescript

const propositionsMap: Map<string, Proposition> = await Optimize.getPropositions(decisionScopes);
const offers: Array<Offer> = [];

propositionsMap.forEach((proposition: Proposition) => {
if (proposition && proposition.items && proposition.items.length > 0) {
proposition.items.forEach((offer) => {
offers.push(offer);
});
}
});

const displayInteractionXdm = await Optimize.generateDisplayInteractionXdm(offers);
```

---

## Public classes
Expand Down
15 changes: 15 additions & 0 deletions packages/optimize/__tests__/OptimizeTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,4 +278,19 @@ describe('Optimize', () => {
'eyJhY3Rpdml0eUlkIjoieGNvcmU6b2ZmZXItYWN0aXZpdHk6MTExMTExMTExMTExMTExMSIsInBsYWNlbWVudElkIjoieGNvcmU6b2ZmZXItcGxhY2VtZW50OjExMTExMTExMTExMTExMTEiLCJpdGVtQ291bnQiOjEwfQ=='
);
});

it('Test Optimize.displayed', async () => {
const spy = jest.spyOn(NativeModules.AEPOptimize, 'multipleOffersDisplayed');
const offers = [new Offer(offerJson)];
await Optimize.displayed(offers);
expect(spy).toHaveBeenCalledWith(offers);
});

it('Test Optimize.generateDisplayInteractionXdm', async () => {
const spy = jest.spyOn(NativeModules.AEPOptimize, 'multipleOffersGenerateDisplayInteractionXdm');
const offers = [new Offer(offerJson)];
await Optimize.generateDisplayInteractionXdm(offers);
expect(spy).toHaveBeenCalledWith(offers);
});

});
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
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.
*/

package com.adobe.marketing.mobile.reactnative.optimize;

class RCTAEPOptimizeConstants {
static final String UNIQUE_PROPOSITION_ID_KEY = "uniquePropositionId";
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
*/
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;
Expand All @@ -20,8 +21,11 @@
import com.adobe.marketing.mobile.optimize.DecisionScope;
import com.adobe.marketing.mobile.optimize.Offer;
import com.adobe.marketing.mobile.optimize.OfferType;
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;
Expand All @@ -34,15 +38,19 @@
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 <Proposition ID, Proposition>
private final Map<String, OptimizeProposition> propositionCache = new ConcurrentHashMap<>();

public RCTAEPOptimizeModule(ReactApplicationContext reactContext) {
super(reactContext);
Expand Down Expand Up @@ -73,6 +81,9 @@ public void extensionVersion(final Promise promise) {

@ReactMethod
public void clearCachedPropositions() {
// clear the react native cache
clearPropositionsCache();
// clear the native cache
Optimize.clearCachedPropositions();
}

Expand All @@ -97,6 +108,7 @@ public void fail(final AEPOptimizeError adobeError) {

@Override
public void call(final Map<DecisionScope, OptimizeProposition> decisionScopePropositionMap) {
cachePropositionOffers(decisionScopePropositionMap);
Log.d(TAG, "updatePropositions callback success.");
if (successCallback != null) {
final WritableMap response = RCTAEPOptimizeUtil.createCallbackResponse(decisionScopePropositionMap);
Expand All @@ -119,6 +131,7 @@ public void fail(final AdobeError adobeError) {

@Override
public void call(final Map<DecisionScope, OptimizeProposition> decisionScopePropositionMap) {
cachePropositionOffers(decisionScopePropositionMap);
final WritableMap writableMap = new WritableNativeMap();
for (final Map.Entry<DecisionScope, OptimizeProposition> entry : decisionScopePropositionMap.entrySet()) {
writableMap.putMap(entry.getKey().getName(), RCTAEPOptimizeUtil.convertPropositionToWritableMap(entry.getValue()));
Expand All @@ -128,11 +141,72 @@ public void call(final Map<DecisionScope, OptimizeProposition> decisionScopeProp
});
}

private void cachePropositionOffers(final Map<DecisionScope, OptimizeProposition> decisionScopePropositionMap) {
for (final Map.Entry<DecisionScope, OptimizeProposition> entry : decisionScopePropositionMap.entrySet()) {
OptimizeProposition proposition = entry.getValue();
if (proposition == null) {
continue;
}

String activityId = null;
try {
Map<String, Object> activity = proposition.getActivity();
if (activity != null && activity.containsKey("id")) {
activityId = DataReader.getString(activity, "id");
} else {
Map<String, Object> scopeDetails = proposition.getScopeDetails();
if (scopeDetails != null && scopeDetails.containsKey("activity")) {
Map<String, Object> 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<Offer> 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<Offer> nativeOffers = RCTAEPOptimizeUtil.getNativeOffers(offersArray, propositionCache);

if (!nativeOffers.isEmpty()) {
Log.d(TAG, "multipleOffersGenerateDisplayInteractionXdm: calling generateDisplayInteractionXdm for: " + nativeOffers.size() + " offers: " + nativeOffers.toString());
final Map<String, Object> 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<Map<DecisionScope, OptimizeProposition>>() {
@Override
public void call(final Map<DecisionScope, OptimizeProposition> decisionScopePropositionMap) {
cachePropositionOffers(decisionScopePropositionMap);
sendUpdatedPropositionsEvent(decisionScopePropositionMap);
}
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,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) {
Expand Down Expand Up @@ -187,6 +193,46 @@ private static List<Object> convertReadableArrayToList(final ReadableArray reada
return list;
}

static List<Offer> getNativeOffers(final ReadableArray offersArray, Map<String, OptimizeProposition> propositionCache) {
List<Offer> nativeOffers = new ArrayList<>();

if (offersArray == null || offersArray.size() == 0) {
Log.d(TAG, "getNativeOffers: offersArray is null or empty");
return nativeOffers;
}

for (int i = 0; i < offersArray.size(); i++) {
ReadableMap offer = offersArray.getMap(i);
if (offer == null) {
Log.d(TAG, "getNativeOffers: offer is null for index: " + i);
continue;
}

String uniquePropositionId = offer.getString(RCTAEPOptimizeConstants.UNIQUE_PROPOSITION_ID_KEY);
String offerId = offer.getString("id");

if (uniquePropositionId == null || offerId == null) {
Log.d(TAG, "getNativeOffers: uniquePropositionId or offerId is null for offer: " + offer.toString());
continue;
}

OptimizeProposition proposition = propositionCache.get(uniquePropositionId);
if (proposition == null) {
Log.d(TAG, "getNativeOffers: proposition not found in cache for uniquePropositionId: " + uniquePropositionId);
continue;
}

for (Offer propositionOffer : proposition.getOffers()) {
if (propositionOffer.getId().equalsIgnoreCase(offerId)) {
nativeOffers.add(propositionOffer);
break;
}
}
}

return nativeOffers;
}

/**
* Helper method to create callback response
* @param propositionsMap
Expand Down
Loading