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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

***Bug Fixes***

- `Mapp.engage(...)` is now awaitable so singleton-dependent calls can safely run after native initialization; engagement failures reject instead of being logged silently.
- Android: Failed Firebase token registration now rejects with `FCM_REGISTRATION_FAILED` instead of crashing while reading a failed task result.
- Android: All Mapp engage calls run on the main looper. Background Firebase callbacks wait for a bounded engage attempt and safely return failure after SDK errors, timeout, or interruption.
- Android/Expo: Mapp and custom push ownership now remove the SDK v7 Firebase service and remain idempotent when prebuild runs repeatedly or changes mode.
Expand Down
2 changes: 1 addition & 1 deletion MIGRATION_2.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Read [Breaking changes in 2.0.0](BREAKING_CHANGES.md) first to determine which c
| `Mapp.inAppMarkAsRead(templateId, eventId)` | Android no-op. | Android fetches the Mapp Engage 7.1.2 inbox message and updates it to `READ`. |
| `Mapp.inAppMarkAsUnRead(templateId, eventId)` | Android no-op. | Android fetches the inbox message and updates it to `UNREAD`. |
| `Mapp.inAppMarkAsDeleted(templateId, eventId)` | Android no-op. | Android fetches the inbox message and updates it to `DELETED`. |
| `Mapp.engage(...)` | iOS JavaScript called the private native `autoengage` and `engageInapp` methods separately. | All platforms use the public native `engage` entry point. On iOS it still initializes both push and in-app, using `AppoxeeConfig.plist` as the credential source of truth. |
| `Mapp.engage(...)` | iOS JavaScript called the private native `autoengage` and `engageInapp` methods separately, and Android engagement returned before its main-thread task completed. | All platforms use an awaitable native engagement entry point. Await it before calling singleton-dependent APIs. On iOS it still initializes both push and in-app, using `AppoxeeConfig.plist` as the credential source of truth. |
| iOS event listeners | Events emitted before a JavaScript listener was attached were dropped. | Up to 50 cold-start events are buffered and delivered after a listener attaches. Consumers should tolerate receiving an initial queued event. |

For the Android inbox methods, `eventId` remains accepted for source compatibility but Mapp Engage 7.1.2 identifies and fetches the message using `templateId`.
Expand Down
2 changes: 1 addition & 1 deletion Mapp.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ export class Mapp {
server: string,
appID: string,
tenantID: string
) {
): Promise<boolean> {
return RNMappPluginModule.engage(
sdkKey,
googleProjectId,
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,11 @@ const subscription = events.addListener('com.mapp.deep_link_received', event =>
// Route the deep link.
});

Mapp.engage('ANDROID_SDK_KEY', 'FCM_PROJECT_ID', 'EMC', 'APP_ID', 'TENANT_ID');
await Mapp.engage('ANDROID_SDK_KEY', 'FCM_PROJECT_ID', 'EMC', 'APP_ID', 'TENANT_ID');
```

Always await `Mapp.engage(...)` before calling APIs that use the native Mapp singleton. The promise resolves after native engagement and bridge setup complete; use `Mapp.onInitCompletedListener()` or `Mapp.isReady()` when a feature specifically requires the SDK's later ready state.

### Android push ownership

`pushHandling: "mapp"` is the default. It requires `expo.android.googleServicesFile` and retains `com.reactlibrary.MessageService` as the sole normal-priority Mapp FCM callback owner. The config plugin removes the Mapp SDK v7 service (`com.appoxee.shared.MappMessagingService`) from the merged app manifest.
Expand Down Expand Up @@ -160,7 +162,7 @@ Basic usage:
```js
import { Mapp } from 'react-native-mapp-plugin';

Mapp.engage('SDK_KEY', 'FCM_PROJECT_ID', 'EMC', 'APP_ID', 'TENANT_ID');
await Mapp.engage('SDK_KEY', 'FCM_PROJECT_ID', 'EMC', 'APP_ID', 'TENANT_ID');
```

See the [Mapp integration documentation](https://mapp-wiki.atlassian.net/wiki/spaces/MIC/pages/1154875400/React+Native+Integration+for+Mapp+Cloud) for the full JavaScript API and native Mapp configuration values.
12 changes: 8 additions & 4 deletions __tests__/MappBridge.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,10 @@ describe("getAlias", () => {
// ---------------------------------------------------------------------------

describe("engage (Android)", () => {
test("passes all 5 params to native engage", () => {
Mapp.engage("sdkKey", "projectId", "L3", "appId", "tenantId");
test("returns the awaitable native engagement and passes all 5 params", async () => {
native.engage.mockResolvedValueOnce(true);
await expect(Mapp.engage("sdkKey", "projectId", "L3", "appId", "tenantId"))
.resolves.toBe(true);
expect(native.engage).toHaveBeenCalledWith(
"sdkKey", "projectId", "L3", "appId", "tenantId"
);
Expand All @@ -118,8 +120,10 @@ describe("engage (Android)", () => {
describe("engage (iOS)", () => {
beforeEach(() => { platform.OS = "ios"; });

test("uses the generated TurboModule engage method; native iOS reads credentials from the generated plist", () => {
Mapp.engage("sdkKey", "projectId", "L3", "appId", "tenantId");
test("uses the awaitable TurboModule method; native iOS reads credentials from the generated plist", async () => {
native.engage.mockResolvedValueOnce(true);
await expect(Mapp.engage("sdkKey", "projectId", "L3", "appId", "tenantId"))
.resolves.toBe(true);
expect(native.engage).toHaveBeenCalledWith("sdkKey", "projectId", "L3", "appId", "tenantId");
expect(native.autoengage).not.toHaveBeenCalled();
expect(native.engageInapp).not.toHaveBeenCalled();
Expand Down
4 changes: 2 additions & 2 deletions __tests__/MappLogic.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,13 +85,13 @@ describe("convertEventEnum", () => {
// ---------------------------------------------------------------------------

describe("Mapp.js platform dispatch", () => {
test("engage() uses the generated cross-platform TurboModule method", () => {
test("engage() uses the generated awaitable cross-platform TurboModule method", () => {
expect(mappSource).toMatch(/RNMappPluginModule\.engage\(/);
expect(mappSource).not.toMatch(/RNMappPluginModule\.autoengage/);
expect(mappSource).not.toMatch(/RNMappPluginModule\.engageInapp/);
});

test("engage() calls RNMappPluginModule.engage on Android path", () => {
test("engage() calls RNMappPluginModule.engage", () => {
expect(mappSource).toMatch(/RNMappPluginModule\.engage\s*\(/);
});

Expand Down
2 changes: 1 addition & 1 deletion android/.classpath
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-17/"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-21/"/>
<classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/>
<classpathentry kind="output" path="bin/default"/>
</classpath>
2 changes: 1 addition & 1 deletion android/.settings/org.eclipse.buildship.core.prefs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER)
connection.project.dir=
eclipse.preferences.version=1
gradle.user.home=
java.home=/Users/semsudin.tafilovic/.sdkman/candidates/java/11.0.26-tem
java.home=/Users/semsudin.tafilovic/.sdkman/candidates/java/21.0.11-tem
jvm.arguments=
offline.mode=false
override.workspace.settings=true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,54 @@ final class MappEngagementDispatcher {

private MappEngagementDispatcher() {}

interface EngagementCallback {
void onSuccess();
void onFailure(@NonNull Exception error);
}

static void engageAsync(
@NonNull Application application,
@Nullable AppoxeeOptions options,
@Nullable Runnable afterEngage
) {
engageAsync(application, options, afterEngage, null);
}

static void engageAsync(
@NonNull Application application,
@Nullable AppoxeeOptions options,
@Nullable Runnable afterEngage,
@Nullable EngagementCallback callback
) {
Runnable operation = () -> {
if (engageNow(application, options) && afterEngage != null) {
afterEngage.run();
try {
Appoxee.engage(application, options);
if (afterEngage != null) {
afterEngage.run();
}
} catch (Exception error) {
Log.e(TAG, "Mapp initialization failed", error);
if (callback != null) {
callback.onFailure(error);
}
return;
}
if (callback != null) {
callback.onSuccess();
}
};
if (Looper.myLooper() == Looper.getMainLooper()) {
operation.run();
return;
}
if (!new Handler(Looper.getMainLooper()).post(operation)) {
Log.e(TAG, "Unable to post Mapp initialization to the main looper");
IllegalStateException error = new IllegalStateException(
"Unable to post Mapp initialization to the main looper"
);
Log.e(TAG, error.getMessage(), error);
if (callback != null) {
callback.onFailure(error);
}
}
}

Expand Down Expand Up @@ -94,7 +126,7 @@ private static boolean engageNow(
try {
Appoxee.engage(application, options);
return true;
} catch (RuntimeException error) {
} catch (Exception error) {
Log.e(TAG, "Mapp initialization failed", error);
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ public NativeRNMappPluginModuleSpec(ReactApplicationContext reactContext) {

@ReactMethod
@DoNotStrip
public abstract void engage(String sdkKey, String googleProjectId, String server, String appID, String tenantID);
public abstract void engage(String sdkKey, String googleProjectId, String server, String appID, String tenantID, Promise promise);

@ReactMethod
@DoNotStrip
Expand Down
41 changes: 32 additions & 9 deletions android/src/main/java/com/reactlibrary/RNMappPluginModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -384,19 +384,42 @@ public void engage2() {
}

@ReactMethod
public void engage(String sdkKey, String googleProjectId, String server, String appID, String tenantID) {
AppoxeeOptions opt = createOptions(server, sdkKey, appID, tenantID);
opt.setNotificationMode(NotificationMode.BACKGROUND_AND_FOREGROUND);
public void engage(String sdkKey, String googleProjectId, String server, String appID,
String tenantID, Promise promise) {
final AppoxeeOptions opt;
try {
opt = createOptions(server, sdkKey, appID, tenantID);
opt.setNotificationMode(NotificationMode.BACKGROUND_AND_FOREGROUND);
} catch (RuntimeException error) {
promise.reject("MAPP_ENGAGE_INVALID_CONFIGURATION", error.getMessage(), error);
return;
}

MappEngagementDispatcher.engageAsync(Objects.requireNonNull(application), opt, () -> {
Appoxee.instance().subscribe(new AppoxeeObserver() {
@Override
public void onReadyStatusChanged(boolean status, MappResult<DevicePayload> result) {
MappEngagementDispatcher.engageAsync(
Objects.requireNonNull(application),
opt,
this::configureAfterEngage,
new MappEngagementDispatcher.EngagementCallback() {
@Override
public void onSuccess() {
promise.resolve(true);
}

@Override
public void onFailure(@NonNull Exception error) {
promise.reject("MAPP_ENGAGE_FAILED", "Mapp initialization failed", error);
}
}
});
);
}

Appoxee.instance().setPushBroadcast(MyPushBroadcastReceiver.class);
private void configureAfterEngage() {
Appoxee.instance().subscribe(new AppoxeeObserver() {
@Override
public void onReadyStatusChanged(boolean status, MappResult<DevicePayload> result) {
}
});
Appoxee.instance().setPushBroadcast(MyPushBroadcastReceiver.class);
}

@ReactMethod
Expand Down
2 changes: 1 addition & 1 deletion android/src/main/jni/RNMappPlugin-generated.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ static facebook::jsi::Value __hostFunction_NativeRNMappPluginModuleSpecJSI_engag

static facebook::jsi::Value __hostFunction_NativeRNMappPluginModuleSpecJSI_engage(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
static jmethodID cachedMethodId = nullptr;
return static_cast<JavaTurboModule &>(turboModule).invokeJavaMethod(rt, VoidKind, "engage", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V", args, count, cachedMethodId);
return static_cast<JavaTurboModule &>(turboModule).invokeJavaMethod(rt, PromiseKind, "engage", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/facebook/react/bridge/Promise;)V", args, count, cachedMethodId);
}

static facebook::jsi::Value __hostFunction_NativeRNMappPluginModuleSpecJSI_engageTestServer(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,12 +154,12 @@ class JSI_EXPORT NativeRNMappPluginModuleCxxSpec : public TurboModule {
static_assert(
bridging::getParameterCount(&T::engage) == 6,
"Expected engage(...) to have 6 parameters");
bridging::callFromJs<void>(rt, &T::engage, static_cast<NativeRNMappPluginModuleCxxSpec*>(&turboModule)->jsInvoker_, static_cast<T*>(&turboModule),
return bridging::callFromJs<jsi::Value>(rt, &T::engage, static_cast<NativeRNMappPluginModuleCxxSpec*>(&turboModule)->jsInvoker_, static_cast<T*>(&turboModule),
count <= 0 ? throw jsi::JSError(rt, "Expected argument in position 0 to be passed") : args[0].asString(rt),
count <= 1 ? throw jsi::JSError(rt, "Expected argument in position 1 to be passed") : args[1].asString(rt),
count <= 2 ? throw jsi::JSError(rt, "Expected argument in position 2 to be passed") : args[2].asString(rt),
count <= 3 ? throw jsi::JSError(rt, "Expected argument in position 3 to be passed") : args[3].asString(rt),
count <= 4 ? throw jsi::JSError(rt, "Expected argument in position 4 to be passed") : args[4].asString(rt));return jsi::Value::undefined();
count <= 4 ? throw jsi::JSError(rt, "Expected argument in position 4 to be passed") : args[4].asString(rt));
}

static jsi::Value __engageTestServer(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ public void testAllReactMethodSignatures() {
// --- Engage / init ---
assertMethod("engage2");
assertMethod("engage",
String.class, String.class, String.class, String.class, String.class);
String.class, String.class, String.class, String.class, String.class, Promise.class);
assertMethod("engageTestServer",
String.class, String.class, String.class, String.class, String.class, String.class);
assertMethod("onInitCompletedListener", Promise.class);
Expand Down
Loading
Loading