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
68 changes: 67 additions & 1 deletion 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 @@ -87,16 +88,33 @@ export default () => {
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<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 @@ -118,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 @@ -329,6 +380,9 @@ export default () => {
<View style={{margin: 5}}>
<Button title="Update Propositions" onPress={updatePropositions} />
</View>
<View style={{margin: 5}}>
<Button title="Test Update Propositions Callback" onPress={testUpdatePropositionsCallback} />
</View>
<View style={{margin: 5}}>
<Button title="Get Propositions" onPress={getPropositions} />
</View>
Expand All @@ -344,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
125 changes: 122 additions & 3 deletions packages/optimize/__tests__/OptimizeTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, Proposition>) => {};
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<string, Proposition>();
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<string, Proposition> | null = null;
const callback = (propositions: Map<string, Proposition>) => {
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<string, Proposition>();
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<string, Proposition> | null = null;
let errorResponse: any = null;

const onSuccess = (propositions: Map<string, Proposition>) => {
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
Expand Down Expand Up @@ -174,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";
}
Loading