Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
3e0b377
initial commit for update proposition api enhancement
akhiljain1907 Jun 25, 2025
4403ddf
fixed error on clicking update proposition due to null or undefined c…
akhiljain1907 Jun 26, 2025
2c7b026
removed redundant code from RCTAEPOptimize.m
akhiljain1907 Jul 2, 2025
2a12ff8
fixed callback signature in api call in Optimize.ts and updated tests
akhiljain1907 Jul 2, 2025
dbfad8b
used separate callback for success and error case as native android s…
akhiljain1907 Jul 16, 2025
50640c4
tests fix and removed callbacklog from testapp
akhiljain1907 Jul 16, 2025
8a3cae0
fixed iOS bridge code to call both success and error callbacks and te…
akhiljain1907 Jul 16, 2025
263f03d
fixed android bridge to use AdobeCallbackWithError to provide error c…
akhiljain1907 Jul 24, 2025
43831fc
Merge branch 'staging' into feat/updateProposition
akhiljain1907 Jul 24, 2025
5b0999f
removed error parameter from createCallBackResponse
akhiljain1907 Jul 24, 2025
4fdeac0
Merge branch 'feat/updateProposition' of github.com:akhiljain1907/aep…
akhiljain1907 Jul 24, 2025
b814931
changed public api signature to pass onSuccess and onError as functio…
akhiljain1907 Jul 28, 2025
fa3ca69
moved createCallbackResponse method from RCTAEPOptimizeModule to RCTA…
akhiljain1907 Jul 28, 2025
f096057
sending propositions map directly instead of sending under response k…
akhiljain1907 Jul 28, 2025
28cccbc
added type for AEPOptimizeError and returned AEpOptimizeError type in…
akhiljain1907 Jul 31, 2025
4a3527c
fixed index.js
akhiljain1907 Jul 31, 2025
b8d8e44
Merge staging into feat/updatePropositions
akhiljain1907 Aug 8, 2025
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
20 changes: 19 additions & 1 deletion apps/AEPSampleAppNewArchEnabled/app/OptimizeView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export default () => {
decisionScopeImage,
decisionScopeHtml,
decisionScopeJson,
decisionScopeTargetMbox,
decisionScopeTargetMbox
];

const optimizeExtensionVersion = async () => {
Expand All @@ -87,6 +87,21 @@ 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);
Expand Down Expand Up @@ -329,6 +344,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 Down
110 changes: 107 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
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@

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;
Expand All @@ -28,11 +30,14 @@
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.Collections;
import java.util.List;
import java.util.Map;
import androidx.annotation.Nullable;

public class RCTAEPOptimizeModule extends ReactContextBaseJavaModule {

Expand Down Expand Up @@ -72,12 +77,34 @@ public void 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<DecisionScope> decisionScopeList = RCTAEPOptimizeUtil.createDecisionScopes(decisionScopesArray);

Map<String, Object> mapXdm = xdm != null ? RCTAEPOptimizeUtil.convertReadableMapToMap(xdm) : Collections.<String, Object>emptyMap();
Map<String, Object> mapData = data != null ? RCTAEPOptimizeUtil.convertReadableMapToMap(data) : Collections.<String, Object>emptyMap();
Optimize.updatePropositions(decisionScopeList, mapXdm, mapData);

Optimize.updatePropositions(decisionScopeList, mapXdm, mapData, new AdobeCallbackWithOptimizeError<Map<DecisionScope, OptimizeProposition>>() {
@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<DecisionScope, OptimizeProposition> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -185,4 +186,44 @@ private static List<Object> convertReadableArrayToList(final ReadableArray reada
}
return list;
}

/**
* Helper method to create callback response
* @param propositionsMap
* @return WritableMap
*/
static WritableMap createCallbackResponse(final Map<DecisionScope, OptimizeProposition> propositionsMap) {
final WritableMap propositionsWritableMap = new WritableNativeMap();

if (propositionsMap != null && !propositionsMap.isEmpty()) {
for (final Map.Entry<DecisionScope, OptimizeProposition> entry : propositionsMap.entrySet()) {
propositionsWritableMap.putMap(entry.getKey().getName(), RCTAEPOptimizeUtil.convertPropositionToWritableMap(entry.getValue()));
}
}

return propositionsWritableMap;
}

/**
* Converts an AEPOptimizeError to a WritableMap for React Native error callback
*/
static WritableMap convertAEPOptimizeErrorToWritableMap(final AEPOptimizeError error) {
final WritableMap errorMap = new WritableNativeMap();
if (error == null) {
return errorMap;
}
errorMap.putString("type", error.getType());
if (error.getStatus() != null) {
errorMap.putInt("status", error.getStatus());
}
errorMap.putString("title", error.getTitle());
errorMap.putString("detail", error.getDetail());
if (error.getReport() != null && error.getReport() instanceof Map) {
errorMap.putMap("report", convertMapToWritableMap((Map<String, Object>) error.getReport()));
}
if (error.getAdobeError() != null) {
errorMap.putString("aepError", error.getAdobeError().getErrorName());
}
return errorMap;
}
}
90 changes: 80 additions & 10 deletions packages/optimize/ios/src/RCTAEPOptimize.m
Original file line number Diff line number Diff line change
Expand Up @@ -50,19 +50,47 @@ - (dispatch_queue_t)methodQueue {
[AEPMobileOptimize clearCachedPropositions];
}

RCT_EXPORT_METHOD(updatePropositions
: (NSArray<NSString *> *)decisionsScopes xdm
: (NSDictionary<NSString *, id> *)xdm data
: (NSDictionary<NSString *, id> *)data) {
// Helper method to handle proposition dictionary creation
- (NSDictionary<NSString *, NSDictionary<NSString *, id> *> *)createPropositionDictionary:(NSDictionary<AEPDecisionScope *, AEPOptimizeProposition *> *)decisionScopePropositionDict {
NSMutableDictionary<NSString *, NSDictionary<NSString *, id> *> *propositionDictionary = [[NSMutableDictionary alloc] initWithCapacity:decisionScopePropositionDict.count];

for (AEPDecisionScope *key in decisionScopePropositionDict) {
AEPOptimizeProposition *proposition = decisionScopePropositionDict[key];
if (proposition) {
[propositionDictionary setValue:[self convertPropositionToDict:proposition] forKey:key.name];
}
}
return propositionDictionary;
}

[AEPLog traceWithLabel:TAG message:@"updatePropositions is called."];
NSArray<AEPDecisionScope *> *decisionScopesArray =
[self createDecisionScopesArray:decisionsScopes];
[AEPMobileOptimize updatePropositions:decisionScopesArray
withXdm:xdm
andData:data];
// Unified method that handles both callback and non-callback cases
RCT_EXPORT_METHOD(updatePropositions:(NSArray<NSString *> *)decisionScopesArray
withXdm:(NSDictionary *)xdm
andData:(NSDictionary *)data
successCallback:(RCTResponseSenderBlock)successCallback
errorCallback:(RCTResponseSenderBlock)errorCallback) {
[AEPLog traceWithLabel:TAG message:@"updatePropositions is called."];
NSArray<AEPDecisionScope *> *scopes = [self createDecisionScopesArray:decisionScopesArray];
[AEPMobileOptimize updatePropositions:scopes
withXdm:xdm
andData:data
completion:^(NSDictionary<AEPDecisionScope *, AEPOptimizeProposition *> *decisionScopePropositionDict, NSError *error) {
if (error) {
NSDictionary *errorDict = [self convertNSErrorToOptimizeErrorDict:error];
if (errorCallback != nil) {
errorCallback(@[errorDict]);
}
}
if (decisionScopePropositionDict) {
NSDictionary *propositions = [self createCallbackResponse:decisionScopePropositionDict];
if (successCallback != nil) {
successCallback(@[propositions]);
}
}
}];
}


RCT_EXPORT_METHOD(getPropositions
: (NSArray<NSString *> *)decisionScopes resolver
: (RCTPromiseResolveBlock)resolve rejector
Expand Down Expand Up @@ -294,6 +322,48 @@ - (NSString *)convertOfferTypeToString:(AEPOfferType)offerType {
}
}

// Helper to convert NSError to a structured error dictionary for JS
- (NSDictionary *)convertNSErrorToOptimizeErrorDict:(NSError *)error {
if (!error) return @{};
NSMutableDictionary *errorDict = [NSMutableDictionary dictionary];

// Log for debugging
NSLog(@"[AEPOptimize] NSError.domain: %@", error.domain);
NSLog(@"[AEPOptimize] NSError.code: %ld", (long)error.code);
NSLog(@"[AEPOptimize] NSError.userInfo: %@", error.userInfo);
NSLog(@"[AEPOptimize] NSError.localizedDescription: %@", error.localizedDescription);

// Extract AEPOptimizeError properties from userInfo (matches Android structure)
NSDictionary *userInfo = error.userInfo;

errorDict[@"type"] = userInfo[@"type"] ?: @"";
errorDict[@"status"] = userInfo[@"status"] ?: @(error.code);
errorDict[@"title"] = userInfo[@"title"] ?: @"";
errorDict[@"detail"] = userInfo[@"detail"] ?: @"";
errorDict[@"report"] = userInfo[@"report"] ?: @{};

// Handle aepError - check for both nil and NSNull
id aepErrorValue = userInfo[@"aepError"];
if (aepErrorValue && aepErrorValue != [NSNull null]) {
errorDict[@"aepError"] = aepErrorValue;
} else {
errorDict[@"aepError"] = @"general.unexpected";
}

return errorDict;
}

// Helper method to create standardized response for callbacks
- (NSDictionary *)createCallbackResponse:(NSDictionary<AEPDecisionScope *, AEPOptimizeProposition *> *)decisionScopePropositionDict {

if (decisionScopePropositionDict && [decisionScopePropositionDict count] > 0) {
// Return the propositions map directly
return [self createPropositionDictionary:decisionScopePropositionDict];
}

return @{};
}

- (void)handleError:(NSError *)error rejecter:(RCTPromiseRejectBlock)reject {
if (!error || !reject) {
return;
Expand Down
Loading