Skip to content

Commit 23c968c

Browse files
Feat- Optimize Enhancements (Callback and Batch Tracking) (#513)
* Optimize - Update Proposition with callback support (#496) * initial commit for update proposition api enhancement * fixed error on clicking update proposition due to null or undefined callback. * removed redundant code from RCTAEPOptimize.m * fixed callback signature in api call in Optimize.ts and updated tests * used separate callback for success and error case as native android sdk calls success and failure separately. * tests fix and removed callbacklog from testapp * fixed iOS bridge code to call both success and error callbacks and test update * fixed android bridge to use AdobeCallbackWithError to provide error callback with AEPoptimize error response * removed error parameter from createCallBackResponse * changed public api signature to pass onSuccess and onError as functions instead of objects * moved createCallbackResponse method from RCTAEPOptimizeModule to RCTAEPOptimizeUtil * sending propositions map directly instead of sending under response key in successCallback * added type for AEPOptimizeError and returned AEpOptimizeError type in errorCallback * fixed index.js * add displayed and generateDisplayInteractionXDM APIs for multiple offers (#508) * add displayed API for multiple offers * add proposition id to Offer class * add ios impl for displayed api * add generateDisplayInteractionXDM and UTs * fix android build errors * use activity id as unique id for caching * update cache populate and clear logic * fix typos * add exception handling for android data reader util * fix ios bridge * add activity and placement fields to util methods * clean up test app * update documentation --------- Co-authored-by: Ishita Gambhir <[email protected]>
1 parent 05045f9 commit 23c968c

File tree

13 files changed

+793
-26
lines changed

13 files changed

+793
-26
lines changed

apps/AEPSampleAppNewArchEnabled/app/OptimizeView.tsx

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
Optimize,
1616
DecisionScope,
1717
Proposition,
18+
Offer,
1819
} from '@adobe/react-native-aepoptimize';
1920
import {WebView} from 'react-native-webview';
2021
import styles from '../styles/styles';
@@ -87,16 +88,33 @@ export default () => {
8788
console.log('Updated Propositions');
8889
};
8990

91+
const testUpdatePropositionsCallback = () => {
92+
console.log('Testing updatePropositions with callback...');
93+
Optimize.updatePropositions(
94+
decisionScopes,
95+
undefined,
96+
undefined,
97+
(response) => {
98+
console.log('Callback received:', response);
99+
},
100+
(error) => {
101+
console.log('Error:', error);
102+
}
103+
);
104+
};
105+
90106
const getPropositions = async () => {
91107
const propositions: Map<string, Proposition> =
92108
await Optimize.getPropositions(decisionScopes);
93-
console.log(propositions);
109+
console.log(propositions.size, ' propositions size');
94110
if (propositions) {
95111
setTextProposition(propositions.get(decisionScopeText.getName()));
96112
setImageProposition(propositions.get(decisionScopeImage.getName()));
97113
setHtmlProposition(propositions.get(decisionScopeHtml.getName()));
98114
setJsonProposition(propositions.get(decisionScopeJson.getName()));
99115
setTargetProposition(propositions.get(decisionScopeTargetMbox.getName()));
116+
const propositionObject = Object.fromEntries(propositions);
117+
console.log('propositions', JSON.stringify(propositionObject, null, 2));
100118
}
101119
};
102120

@@ -118,6 +136,39 @@ export default () => {
118136
},
119137
});
120138

139+
const multipleOffersDisplayed = async () => {
140+
const propositionsMap: Map<string, Proposition> = await Optimize.getPropositions(decisionScopes);
141+
const offers: Array<Offer> = [];
142+
propositionsMap.forEach((proposition: Proposition) => {
143+
if (proposition && proposition.items && proposition.items.length > 0) {
144+
proposition.items.forEach((offer) => {
145+
offers.push(offer);
146+
});
147+
}
148+
});
149+
console.log('offers', offers);
150+
Optimize.displayed(offers);
151+
};
152+
153+
const multipleOffersGenerateDisplayInteractionXdm = async () => {
154+
const propositionsMap: Map<string, Proposition> = await Optimize.getPropositions(decisionScopes);
155+
const offers: Array<Offer> = [];
156+
propositionsMap.forEach((proposition: Proposition) => {
157+
if (proposition && proposition.items && proposition.items.length > 0) {
158+
proposition.items.forEach((offer) => {
159+
offers.push(offer);
160+
});
161+
}
162+
});
163+
console.log('offers', offers);
164+
const displayInteractionXdm = await Optimize.generateDisplayInteractionXdm(offers);
165+
if (displayInteractionXdm) {
166+
console.log('displayInteractionXdm', JSON.stringify(displayInteractionXdm, null, 2));
167+
} else {
168+
console.log('displayInteractionXdm is null');
169+
}
170+
};
171+
121172
const renderTargetOffer = () => {
122173
if (targetProposition?.items) {
123174
if (targetProposition.items[0].format === TARGET_OFFER_TYPE_JSON) {
@@ -329,6 +380,9 @@ export default () => {
329380
<View style={{margin: 5}}>
330381
<Button title="Update Propositions" onPress={updatePropositions} />
331382
</View>
383+
<View style={{margin: 5}}>
384+
<Button title="Test Update Propositions Callback" onPress={testUpdatePropositionsCallback} />
385+
</View>
332386
<View style={{margin: 5}}>
333387
<Button title="Get Propositions" onPress={getPropositions} />
334388
</View>
@@ -344,6 +398,18 @@ export default () => {
344398
onPress={onPropositionUpdate}
345399
/>
346400
</View>
401+
<View style={{margin: 5}}>
402+
<Button
403+
title="Multiple Offers Displayed"
404+
onPress={multipleOffersDisplayed}
405+
/>
406+
</View>
407+
<View style={{margin: 5}}>
408+
<Button
409+
title="Multiple Offers Generate Display Interaction XDM"
410+
onPress={multipleOffersGenerateDisplayInteractionXdm}
411+
/>
412+
</View>
347413
<Text style={{...styles.welcome, fontSize: 20}}>
348414
SDK Version:: {version}
349415
</Text>

packages/optimize/README.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,62 @@ const decisionScopes = [
163163
Optimize.updatePropositions(decisionScopes, null, null);
164164
```
165165

166+
### Batching display interaction events for multiple Offers:
167+
168+
The Optimize SDK now provides enhanced support for batching display interaction events for multiple Offers. The following APIs are available:
169+
170+
#### displayed
171+
172+
**Syntax**
173+
174+
```typescript
175+
displayed(offers: Array<Offer>)
176+
```
177+
178+
**Example**
179+
180+
```typescript
181+
182+
const propositionsMap: Map<string, Proposition> = await Optimize.getPropositions(decisionScopes);
183+
const offers: Array<Offer> = [];
184+
185+
propositionsMap.forEach((proposition: Proposition) => {
186+
if (proposition && proposition.items && proposition.items.length > 0) {
187+
proposition.items.forEach((offer) => {
188+
offers.push(offer);
189+
});
190+
}
191+
});
192+
193+
Optimize.displayed(offers);
194+
```
195+
196+
#### generateDisplayInteractionXdm
197+
198+
**Syntax**
199+
200+
```typescript
201+
generateDisplayInteractionXdm(offers: Array<Offer>): Promise<Map<string, any>>;
202+
```
203+
204+
**Example**
205+
206+
```typescript
207+
208+
const propositionsMap: Map<string, Proposition> = await Optimize.getPropositions(decisionScopes);
209+
const offers: Array<Offer> = [];
210+
211+
propositionsMap.forEach((proposition: Proposition) => {
212+
if (proposition && proposition.items && proposition.items.length > 0) {
213+
proposition.items.forEach((offer) => {
214+
offers.push(offer);
215+
});
216+
}
217+
});
218+
219+
const displayInteractionXdm = await Optimize.generateDisplayInteractionXdm(offers);
220+
```
221+
166222
---
167223

168224
## Public classes

packages/optimize/__tests__/OptimizeTests.ts

Lines changed: 122 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,18 +51,122 @@ describe('Optimize', () => {
5151
});
5252

5353
it('AEPOptimize updateProposition is called', async () => {
54-
const spy = jest.spyOn(NativeModules.AEPOptimize, 'updatePropositions');
55-
let decisionScopes = [new DecisionScope('abcdef')];
54+
let spy = jest.spyOn(NativeModules.AEPOptimize, "updatePropositions");
55+
let decisionScopes = [new DecisionScope("abcdef")];
5656
let xdm = new Map();
5757
let data = new Map();
5858
await Optimize.updatePropositions(decisionScopes, xdm, data);
5959
expect(spy).toHaveBeenCalledWith(
6060
decisionScopes.map((decisionScope) => decisionScope.getName()),
6161
xdm,
62-
data
62+
data,
63+
expect.any(Function),
64+
expect.any(Function)
65+
);
66+
});
67+
68+
it('AEPOptimize updateProposition is called with callback', async () => {
69+
let spy = jest.spyOn(NativeModules.AEPOptimize, "updatePropositions");
70+
let decisionScopes = [new DecisionScope("abcdef")];
71+
let xdm = new Map();
72+
let data = new Map();
73+
const callback = (_propositions: Map<string, Proposition>) => {};
74+
await Optimize.updatePropositions(decisionScopes, xdm, data, callback as any, undefined);
75+
expect(spy).toHaveBeenCalledWith(
76+
decisionScopes.map((decisionScope) => decisionScope.getName()),
77+
xdm,
78+
data,
79+
expect.any(Function),
80+
expect.any(Function)
6381
);
6482
});
6583

84+
it('AEPOptimize updateProposition callback handles successful response', async () => {
85+
const mockResponse = new Map<string, Proposition>();
86+
mockResponse.set('scope1', new Proposition(propositionJson as any));
87+
// Mock the native method to call the callback with mock data
88+
const mockMethod = jest.fn().mockImplementation((...args: any[]) => {
89+
const callback = args[3];
90+
if (typeof callback === 'function') {
91+
callback(mockResponse);
92+
}
93+
});
94+
NativeModules.AEPOptimize.updatePropositions = mockMethod;
95+
let decisionScopes = [new DecisionScope("abcdef")];
96+
let callbackResponse: Map<string, Proposition> | null = null;
97+
const callback = (propositions: Map<string, Proposition>) => {
98+
callbackResponse = propositions;
99+
};
100+
await Optimize.updatePropositions(decisionScopes, undefined, undefined, callback as any, undefined);
101+
expect(callbackResponse).not.toBeNull();
102+
expect(callbackResponse!.get('scope1')).toBeInstanceOf(Proposition);
103+
});
104+
105+
it('AEPOptimize updateProposition callback handles error response', async () => {
106+
// For error, the callback may not be called, or may be called with an empty map or undefined. We'll simulate an empty map.
107+
const mockErrorResponse = new Error('Test error');
108+
// Mock the native method to call the callback with error data
109+
const mockMethod = jest.fn().mockImplementation((...args: any[]) => {
110+
const onError = args[4];
111+
if (typeof onError === 'function') {
112+
onError(mockErrorResponse);
113+
}
114+
});
115+
NativeModules.AEPOptimize.updatePropositions = mockMethod;
116+
let decisionScopes = [new DecisionScope("abcdef")];
117+
let callbackResponse: any = null;
118+
const onError = (error: any) => {
119+
callbackResponse = error;
120+
};
121+
await Optimize.updatePropositions(decisionScopes, undefined, undefined, undefined, onError as any);
122+
expect(callbackResponse).not.toBeNull();
123+
expect(callbackResponse!.message).toBe('Test error');
124+
});
125+
126+
it('AEPOptimize updateProposition calls both success and error callbacks', async () => {
127+
const mockSuccessResponse = new Map<string, Proposition>();
128+
mockSuccessResponse.set('scope1', new Proposition(propositionJson as any));
129+
const mockError = { message: 'Test error', code: 500 };
130+
131+
// Mock the native method to call both callbacks
132+
const mockMethod = jest.fn().mockImplementation((...args: any[]) => {
133+
const onSuccess = args[3];
134+
const onError = args[4];
135+
if (typeof onSuccess === 'function') {
136+
onSuccess(mockSuccessResponse);
137+
}
138+
if (typeof onError === 'function') {
139+
onError(mockError);
140+
}
141+
});
142+
NativeModules.AEPOptimize.updatePropositions = mockMethod;
143+
144+
let successCalled = false;
145+
let errorCalled = false;
146+
let successResponse: Map<string, Proposition> | null = null;
147+
let errorResponse: any = null;
148+
149+
const onSuccess = (propositions: Map<string, Proposition>) => {
150+
successCalled = true;
151+
successResponse = propositions;
152+
};
153+
const onError = (error: any) => {
154+
errorCalled = true;
155+
errorResponse = error;
156+
};
157+
158+
let decisionScopes = [new DecisionScope("abcdef")];
159+
await Optimize.updatePropositions(decisionScopes, undefined, undefined, onSuccess as any, onError as any);
160+
161+
expect(successCalled).toBe(true);
162+
expect(errorCalled).toBe(true);
163+
expect(successResponse).not.toBeNull();
164+
expect(successResponse!.get('scope1')).toBeInstanceOf(Proposition);
165+
expect(errorResponse).toBeDefined();
166+
expect(errorResponse.message).toBe('Test error');
167+
expect(errorResponse.code).toBe(500);
168+
});
169+
66170
it('Test Offer object state', async () => {
67171
const offer = new Offer(offerJson);
68172
//Asserts
@@ -174,4 +278,19 @@ describe('Optimize', () => {
174278
'eyJhY3Rpdml0eUlkIjoieGNvcmU6b2ZmZXItYWN0aXZpdHk6MTExMTExMTExMTExMTExMSIsInBsYWNlbWVudElkIjoieGNvcmU6b2ZmZXItcGxhY2VtZW50OjExMTExMTExMTExMTExMTEiLCJpdGVtQ291bnQiOjEwfQ=='
175279
);
176280
});
281+
282+
it('Test Optimize.displayed', async () => {
283+
const spy = jest.spyOn(NativeModules.AEPOptimize, 'multipleOffersDisplayed');
284+
const offers = [new Offer(offerJson)];
285+
await Optimize.displayed(offers);
286+
expect(spy).toHaveBeenCalledWith(offers);
287+
});
288+
289+
it('Test Optimize.generateDisplayInteractionXdm', async () => {
290+
const spy = jest.spyOn(NativeModules.AEPOptimize, 'multipleOffersGenerateDisplayInteractionXdm');
291+
const offers = [new Offer(offerJson)];
292+
await Optimize.generateDisplayInteractionXdm(offers);
293+
expect(spy).toHaveBeenCalledWith(offers);
294+
});
295+
177296
});
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/*
2+
Copyright 2025 Adobe. All rights reserved.
3+
This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License. You may obtain a copy
5+
of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
Unless required by applicable law or agreed to in writing, software distributed under
7+
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
8+
OF ANY KIND, either express or implied. See the License for the specific language
9+
governing permissions and limitations under the License.
10+
*/
11+
12+
package com.adobe.marketing.mobile.reactnative.optimize;
13+
14+
class RCTAEPOptimizeConstants {
15+
static final String UNIQUE_PROPOSITION_ID_KEY = "uniquePropositionId";
16+
}

0 commit comments

Comments
 (0)