adobe / adobe/aepsdk-react-native

[Messaging] Android `updatePropositionsForSurfaces` takes no Promise, so awaiting it resolves before the fetch completes

Open
#589 1 comment 0 reactions 0 assignees View on GitHub
bug triage-required
Dominant language
TypeScript
Stars
24
Forks
40
Avg merge
4d 11h
Merged PRs (30d)
2

Description

### Prerequisites

- [x] This is not a Security Disclosure, otherwise please follow the guidelines in [Security Policy](https://github.com/adobe/aepsdk-react-native/security/policy).
- [x] I have searched in this repository's issues to see if it has already been reported.
- [x] I have updated to the latest released version of the SDK and the issue still persists.

### Bug summary

`Messaging.updatePropositionsForSurfaces()` is typed `Promise` and documented as dispatching a fetch of propositions from remote. On iOS it settles when that fetch completes. On Android the `@ReactMethod` accepts no `Promise` and calls the fire-and-forget SDK overload, so the promise settles on the next microtask — long before the Edge round trip.

This breaks the documented refresh-then-read pattern on Android. `getPropositionsForSurfaces` only ever returns what is already cached and never goes to the network itself, so a read that follows an awaited refresh observes the *pre-refresh* cache. On a cold cache it returns nothing, and an audience-targeted surface renders its empty state even though the guest has content waiting.

### Environment

`npx react-native info` is unavailable in this project (Expo bare workflow without `@react-native-community/cli`), so the below is `npx expo-env-info` plus the Adobe SDK versions.

System:
OS: macOS 26.5.2
Shell: 5.9 - /bin/zsh
Binaries:
Node: 26.7.0
Yarn: 1.22.22
npm: 11.19.0
Java: openjdk 26.0.2
Managers:
CocoaPods: 1.16.2
SDKs:
iOS SDK:
Platforms: DriverKit 25.5, iOS 26.5, macOS 26.5, tvOS 26.5, visionOS 26.5, watchOS 26.5
IDEs:
Xcode: 26.6/17F113
npmPackages:
expo: 56.0.12
expo-router: 56.2.11
react: 19.2.3
react-native: 0.85.3
Expo Workflow: bare

Adobe packages:
"@adobe/react-native-aepmessaging": "7.4.0" (latest release; also present in 7.3.0)
"@adobe/react-native-aepcore": "7.0.0"
"@adobe/react-native-aepedgeidentity": "7.0.0"
"@adobe/react-native-aepassurance": "7.0.0"

Android native:
com.adobe.marketing.mobile:sdk-bom:3.+ -> com.adobe.marketing.mobile:messaging 3.10.0
newArchEnabled: true
hermesEnabled: true

iOS native:
AEPMessaging (CocoaPods), hermes

### Steps to reproduce

1. Ensure the proposition cache is cold for a surface — a fresh install, or any surface not yet fetched during this app launch.
2. On Android, await the refresh and then read the surface:

```ts
await Messaging.updatePropositionsForSurfaces([surface]);
const result = await Messaging.getPropositionsForSurfaces([surface]);
console.log(Object.keys(result)); // []
```

3. Run the identical code on iOS, where `result` contains the surface.
4. On Android, retry the same read a few seconds later (or on a second screen focus) and the surface is now populated, confirming the data was on its way rather than absent.

### Current behavior

On Android the `await` returns before the network response, so the subsequent read sees an empty cache.

`android/src/main/java/com/adobe/marketing/mobile/reactnative/messaging/RCTAEPMessagingModule.java:194`:

```java
@ReactMethod
public void updatePropositionsForSurfaces(ReadableArray surfaces) {
Messaging.updatePropositionsForSurfaces(
RCTAEPMessagingUtil.convertSurfaces(surfaces));
propositionItemByUuid.clear();
}
```

A `void` `@ReactMethod` settles on the next microtask, and the cache clear runs synchronously rather than on completion.

iOS, in the same release, bridges the SDK's completion handler — `ios/src/RCTAEPMessaging.swift:123`:

```swift
Messaging.updatePropositionsForSurfaces(mapped) { success in
if success {
self.propositionByUuid.removeAll()
resolve(nil)
} else {
reject("Unable to update propositions for surfaces", nil, nil)
}
}
```

The shared JS layer is the same on both platforms and is typed as a promise, so the declared contract is met on one platform only — `src/Messaging.ts:373`:

```ts
static async updatePropositionsForSurfaces(
surfaces: string[]
): Promise {
return await RCTAEPMessaging.updatePropositionsForSurfaces(surfaces);
}
```

Worth noting that the native module spec in the same file (`src/Messaging.ts:43`) types the method as returning `void`:

```ts
updatePropositionsForSurfaces: (surfaces: string[]) => void;
```

so the iOS promise support added in 7.4.0 isn't reflected in the types either — the `Promise` on the public API is currently only accurate on iOS, and only by accident of the `async` wrapper.

### Expected behavior

Awaiting `updatePropositionsForSurfaces` should settle once the fetch has been processed on both platforms, so that a subsequent `getPropositionsForSurfaces` observes the refreshed cache.

The Android SDK already exposes what the bridge needs: `updatePropositionsForSurfaces(List, AdobeCallback)`. Giving the `@ReactMethod` a trailing `Promise`, settling from the callback, and moving the cache clear into the success branch would match the existing iOS resolve/reject semantics:

```java
@ReactMethod
public void updatePropositionsForSurfaces(ReadableArray surfaces,
final Promise promise) {
Messaging.updatePropositionsForSurfaces(
RCTAEPMessagingUtil.convertSurfaces(surfaces),
new AdobeCallback() {
@Override
public void call(final Boolean success) {
if (Boolean.TRUE.equals(success)) {
propositionItemByUuid.clear();
promise.resolve(null);
} else {
promise.reject("updatePropositionsForSurfaces",
"Unable to update propositions for surfaces");
}
}
});
}
```

No JavaScript change is required, since React Native appends the promise as the trailing argument. Updating the native module spec's return type to `Promise` would keep the types honest.

### Anything else?

In a production React Native app this presented as an audience-targeted screen showing its empty state to signed-in users who did have content waiting. It is easy to misdiagnose, because the same screen works correctly on iOS and works on Android as soon as anything warms the cache — so it reproduces reliably only on a genuinely cold surface.

Because the promise currently resolves rather than rejects, there is also no error to observe: the failure is silent, and the only symptom is an empty surface.

We are carrying the change above as a `patch-package` patch against 7.4.0 and would much rather drop it. Happy to open a PR if that's useful.

Contributor guide

Open the contributing guide

Research direction

Start with android/src/main/java/com/adobe/marketing/mobile/reactnative/messaging/RCTAEPMessagingModule.java:194 and compare it with ios/src/RCTAEPMessaging.swift:123 and the native module spec in src/Messaging.ts:43. Reproduce the cold-cache refresh-then-read sequence on Android, then verify that awaiting the refresh observes the populated surface and follows the existing iOS resolve/reject behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
java, react-native, swift, typescript
Domain
mobile
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
78/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.