react-native-mapp-plugin 2.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,4 +1,19 @@
1
- ## Version 2.0.0 (unreleased)
1
+ ## Version 2.0.1 (2026/09/04)
2
+
3
+ ***Bug Fixes***
4
+
5
+ - iOS: `Mapp.onInitCompletedListener()` now returns a `Promise<boolean>` instead of `null`.
6
+ - iOS: When the SDK is already initialized, `Mapp.onInitCompletedListener()` resolves immediately with `true`.
7
+ - iOS: When the SDK is still initializing, `Mapp.onInitCompletedListener()` waits for the existing `com.mapp.init` native event and removes its one-time listener after resolving.
8
+ - Android: Preserved the existing native `AppoxeeObserver` implementation and behavior of `Mapp.onInitCompletedListener()`.
9
+ - Android/iOS: Preserved the public `Mapp.onInitCompletedListener(): Promise<boolean>` API while providing consistent initialization-waiting behavior on both platforms.
10
+
11
+ ***Tests***
12
+
13
+ - Added bridge coverage for both iOS readiness paths: already initialized and waiting for the native initialization event.
14
+ - Updated platform-dispatch coverage to verify that Android continues to use the native listener and iOS uses its existing initialization event.
15
+
16
+ ## Version 2.0.0 (2026/09/03)
2
17
 
3
18
  ***Breaking Changes***
4
19
 
@@ -10,6 +25,7 @@
10
25
 
11
26
  ***Bug Fixes***
12
27
 
28
+ - `Mapp.engage(...)` is now awaitable so singleton-dependent calls can safely run after native initialization; engagement failures reject instead of being logged silently.
13
29
  - Android: Failed Firebase token registration now rejects with `FCM_REGISTRATION_FAILED` instead of crashing while reading a failed task result.
14
30
  - 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.
15
31
  - Android/Expo: Mapp and custom push ownership now remove the SDK v7 Firebase service and remain idempotent when prebuild runs repeatedly or changes mode.
package/MIGRATION_2.0.md CHANGED
@@ -31,7 +31,7 @@ Read [Breaking changes in 2.0.0](BREAKING_CHANGES.md) first to determine which c
31
31
  | `Mapp.inAppMarkAsRead(templateId, eventId)` | Android no-op. | Android fetches the Mapp Engage 7.1.2 inbox message and updates it to `READ`. |
32
32
  | `Mapp.inAppMarkAsUnRead(templateId, eventId)` | Android no-op. | Android fetches the inbox message and updates it to `UNREAD`. |
33
33
  | `Mapp.inAppMarkAsDeleted(templateId, eventId)` | Android no-op. | Android fetches the inbox message and updates it to `DELETED`. |
34
- | `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. |
34
+ | `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. |
35
35
  | 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. |
36
36
 
37
37
  For the Android inbox methods, `eventId` remains accepted for source compatibility but Mapp Engage 7.1.2 identifies and fetches the message using `templateId`.
package/Mapp.js CHANGED
@@ -127,7 +127,7 @@ export class Mapp {
127
127
  server: string,
128
128
  appID: string,
129
129
  tenantID: string
130
- ) {
130
+ ): Promise<boolean> {
131
131
  return RNMappPluginModule.engage(
132
132
  sdkKey,
133
133
  googleProjectId,
@@ -163,10 +163,22 @@ export class Mapp {
163
163
  * @return {Promise.<boolean>} A promise with the result.
164
164
  */
165
165
  static onInitCompletedListener(): Promise<boolean> {
166
- if (Platform.OS == "android") {
166
+ if (Platform.OS === "android") {
167
167
  return RNMappPluginModule.onInitCompletedListener();
168
168
  }
169
- return null;
169
+
170
+ return RNMappPluginModule.isReady().then((ready) => {
171
+ if (ready) {
172
+ return true;
173
+ }
174
+
175
+ return new Promise((resolve) => {
176
+ const subscription = EventEmitter.addListener(IOS_INIT, () => {
177
+ subscription.remove();
178
+ resolve(true);
179
+ });
180
+ });
181
+ });
170
182
  }
171
183
 
172
184
  /**
package/README.md CHANGED
@@ -88,9 +88,11 @@ const subscription = events.addListener('com.mapp.deep_link_received', event =>
88
88
  // Route the deep link.
89
89
  });
90
90
 
91
- Mapp.engage('ANDROID_SDK_KEY', 'FCM_PROJECT_ID', 'EMC', 'APP_ID', 'TENANT_ID');
91
+ await Mapp.engage('ANDROID_SDK_KEY', 'FCM_PROJECT_ID', 'EMC', 'APP_ID', 'TENANT_ID');
92
92
  ```
93
93
 
94
+ 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.
95
+
94
96
  ### Android push ownership
95
97
 
96
98
  `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.
@@ -160,7 +162,7 @@ Basic usage:
160
162
  ```js
161
163
  import { Mapp } from 'react-native-mapp-plugin';
162
164
 
163
- Mapp.engage('SDK_KEY', 'FCM_PROJECT_ID', 'EMC', 'APP_ID', 'TENANT_ID');
165
+ await Mapp.engage('SDK_KEY', 'FCM_PROJECT_ID', 'EMC', 'APP_ID', 'TENANT_ID');
164
166
  ```
165
167
 
166
168
  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.
@@ -22,14 +22,40 @@ final class MappEngagementDispatcher {
22
22
 
23
23
  private MappEngagementDispatcher() {}
24
24
 
25
+ interface EngagementCallback {
26
+ void onSuccess();
27
+ void onFailure(@NonNull Exception error);
28
+ }
29
+
25
30
  static void engageAsync(
26
31
  @NonNull Application application,
27
32
  @Nullable AppoxeeOptions options,
28
33
  @Nullable Runnable afterEngage
34
+ ) {
35
+ engageAsync(application, options, afterEngage, null);
36
+ }
37
+
38
+ static void engageAsync(
39
+ @NonNull Application application,
40
+ @Nullable AppoxeeOptions options,
41
+ @Nullable Runnable afterEngage,
42
+ @Nullable EngagementCallback callback
29
43
  ) {
30
44
  Runnable operation = () -> {
31
- if (engageNow(application, options) && afterEngage != null) {
32
- afterEngage.run();
45
+ try {
46
+ Appoxee.engage(application, options);
47
+ if (afterEngage != null) {
48
+ afterEngage.run();
49
+ }
50
+ } catch (Exception error) {
51
+ Log.e(TAG, "Mapp initialization failed", error);
52
+ if (callback != null) {
53
+ callback.onFailure(error);
54
+ }
55
+ return;
56
+ }
57
+ if (callback != null) {
58
+ callback.onSuccess();
33
59
  }
34
60
  };
35
61
  if (Looper.myLooper() == Looper.getMainLooper()) {
@@ -37,7 +63,13 @@ final class MappEngagementDispatcher {
37
63
  return;
38
64
  }
39
65
  if (!new Handler(Looper.getMainLooper()).post(operation)) {
40
- Log.e(TAG, "Unable to post Mapp initialization to the main looper");
66
+ IllegalStateException error = new IllegalStateException(
67
+ "Unable to post Mapp initialization to the main looper"
68
+ );
69
+ Log.e(TAG, error.getMessage(), error);
70
+ if (callback != null) {
71
+ callback.onFailure(error);
72
+ }
41
73
  }
42
74
  }
43
75
 
@@ -94,7 +126,7 @@ final class MappEngagementDispatcher {
94
126
  try {
95
127
  Appoxee.engage(application, options);
96
128
  return true;
97
- } catch (RuntimeException error) {
129
+ } catch (Exception error) {
98
130
  Log.e(TAG, "Mapp initialization failed", error);
99
131
  return false;
100
132
  }
@@ -76,7 +76,7 @@ public abstract class NativeRNMappPluginModuleSpec extends ReactContextBaseJavaM
76
76
 
77
77
  @ReactMethod
78
78
  @DoNotStrip
79
- public abstract void engage(String sdkKey, String googleProjectId, String server, String appID, String tenantID);
79
+ public abstract void engage(String sdkKey, String googleProjectId, String server, String appID, String tenantID, Promise promise);
80
80
 
81
81
  @ReactMethod
82
82
  @DoNotStrip
@@ -384,19 +384,42 @@ public class RNMappPluginModule extends NativeRNMappPluginModuleSpec implements
384
384
  }
385
385
 
386
386
  @ReactMethod
387
- public void engage(String sdkKey, String googleProjectId, String server, String appID, String tenantID) {
388
- AppoxeeOptions opt = createOptions(server, sdkKey, appID, tenantID);
389
- opt.setNotificationMode(NotificationMode.BACKGROUND_AND_FOREGROUND);
387
+ public void engage(String sdkKey, String googleProjectId, String server, String appID,
388
+ String tenantID, Promise promise) {
389
+ final AppoxeeOptions opt;
390
+ try {
391
+ opt = createOptions(server, sdkKey, appID, tenantID);
392
+ opt.setNotificationMode(NotificationMode.BACKGROUND_AND_FOREGROUND);
393
+ } catch (RuntimeException error) {
394
+ promise.reject("MAPP_ENGAGE_INVALID_CONFIGURATION", error.getMessage(), error);
395
+ return;
396
+ }
390
397
 
391
- MappEngagementDispatcher.engageAsync(Objects.requireNonNull(application), opt, () -> {
392
- Appoxee.instance().subscribe(new AppoxeeObserver() {
393
- @Override
394
- public void onReadyStatusChanged(boolean status, MappResult<DevicePayload> result) {
398
+ MappEngagementDispatcher.engageAsync(
399
+ Objects.requireNonNull(application),
400
+ opt,
401
+ this::configureAfterEngage,
402
+ new MappEngagementDispatcher.EngagementCallback() {
403
+ @Override
404
+ public void onSuccess() {
405
+ promise.resolve(true);
406
+ }
407
+
408
+ @Override
409
+ public void onFailure(@NonNull Exception error) {
410
+ promise.reject("MAPP_ENGAGE_FAILED", "Mapp initialization failed", error);
411
+ }
395
412
  }
396
- });
413
+ );
414
+ }
397
415
 
398
- Appoxee.instance().setPushBroadcast(MyPushBroadcastReceiver.class);
416
+ private void configureAfterEngage() {
417
+ Appoxee.instance().subscribe(new AppoxeeObserver() {
418
+ @Override
419
+ public void onReadyStatusChanged(boolean status, MappResult<DevicePayload> result) {
420
+ }
399
421
  });
422
+ Appoxee.instance().setPushBroadcast(MyPushBroadcastReceiver.class);
400
423
  }
401
424
 
402
425
  @ReactMethod
@@ -64,7 +64,7 @@ static facebook::jsi::Value __hostFunction_NativeRNMappPluginModuleSpecJSI_engag
64
64
 
65
65
  static facebook::jsi::Value __hostFunction_NativeRNMappPluginModuleSpecJSI_engage(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
66
66
  static jmethodID cachedMethodId = nullptr;
67
- 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);
67
+ 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);
68
68
  }
69
69
 
70
70
  static facebook::jsi::Value __hostFunction_NativeRNMappPluginModuleSpecJSI_engageTestServer(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
@@ -154,12 +154,12 @@ private:
154
154
  static_assert(
155
155
  bridging::getParameterCount(&T::engage) == 6,
156
156
  "Expected engage(...) to have 6 parameters");
157
- bridging::callFromJs<void>(rt, &T::engage, static_cast<NativeRNMappPluginModuleCxxSpec*>(&turboModule)->jsInvoker_, static_cast<T*>(&turboModule),
157
+ return bridging::callFromJs<jsi::Value>(rt, &T::engage, static_cast<NativeRNMappPluginModuleCxxSpec*>(&turboModule)->jsInvoker_, static_cast<T*>(&turboModule),
158
158
  count <= 0 ? throw jsi::JSError(rt, "Expected argument in position 0 to be passed") : args[0].asString(rt),
159
159
  count <= 1 ? throw jsi::JSError(rt, "Expected argument in position 1 to be passed") : args[1].asString(rt),
160
160
  count <= 2 ? throw jsi::JSError(rt, "Expected argument in position 2 to be passed") : args[2].asString(rt),
161
161
  count <= 3 ? throw jsi::JSError(rt, "Expected argument in position 3 to be passed") : args[3].asString(rt),
162
- count <= 4 ? throw jsi::JSError(rt, "Expected argument in position 4 to be passed") : args[4].asString(rt));return jsi::Value::undefined();
162
+ count <= 4 ? throw jsi::JSError(rt, "Expected argument in position 4 to be passed") : args[4].asString(rt));
163
163
  }
164
164
 
165
165
  static jsi::Value __engageTestServer(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
@@ -94,7 +94,7 @@ RCT_EXPORT_METHOD(getToken:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRej
94
94
  RCT_EXPORT_METHOD(engage2) {}
95
95
 
96
96
  RCT_EXPORT_METHOD(engageTestServer:(NSString *)cepUrl sdkKey:(NSString *)sdkKey googleProjectId:(NSString *)projectId server:(NSString *)server appID:(NSString *)appID tenantID:(NSString *)tenantID) {
97
- [self engage:sdkKey googleProjectId:projectId server:server appID:appID tenantID:tenantID];
97
+ [self performEngage:sdkKey googleProjectId:projectId server:server appID:appID tenantID:tenantID];
98
98
  }
99
99
 
100
100
  RCT_EXPORT_METHOD(onInitCompletedListener:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
@@ -131,13 +131,33 @@ RCT_EXPORT_METHOD(removeAndroidListeners:(NSInteger)count) {}
131
131
 
132
132
  #pragma mark Exported methods - Notifications
133
133
 
134
- RCT_EXPORT_METHOD(engage: (NSString *)sdkKey googleProjectId: (NSString *)projectId server:(NSString *)server appID:(NSString *)appID tenantID:(NSString *)tenantID) {
134
+ - (void)performEngage:(NSString *)sdkKey
135
+ googleProjectId:(NSString *)projectId
136
+ server:(NSString *)server
137
+ appID:(NSString *)appID
138
+ tenantID:(NSString *)tenantID {
135
139
  SERVER serv = [self getServerKeyFor:server];
136
140
  [[Appoxee shared] engageAndAutoIntegrateWithLaunchOptions:nil andDelegate:[RNMappEventEmmiter shared] with:serv];
137
141
  [[Appoxee shared] addObserver: [RNMappEventEmmiter shared] forKeyPath:@"isReady" options:NSKeyValueObservingOptionNew context:nil];
138
142
  [[AppoxeeInapp shared] engageWithDelegate:[RNMappEventEmmiter shared] with:[self getInappServerKeyFor:server]];
139
143
  }
140
144
 
145
+ RCT_EXPORT_METHOD(engage:(NSString *)sdkKey
146
+ googleProjectId:(NSString *)projectId
147
+ server:(NSString *)server
148
+ appID:(NSString *)appID
149
+ tenantID:(NSString *)tenantID
150
+ resolve:(RCTPromiseResolveBlock)resolve
151
+ reject:(RCTPromiseRejectBlock)reject) {
152
+ @try {
153
+ [self performEngage:sdkKey googleProjectId:projectId server:server appID:appID tenantID:tenantID];
154
+ resolve(@YES);
155
+ } @catch (NSException *exception) {
156
+ NSString *message = exception.reason ?: @"Mapp initialization failed";
157
+ reject(@"MAPP_ENGAGE_FAILED", message, nil);
158
+ }
159
+ }
160
+
141
161
  RCT_EXPORT_METHOD(getAlias:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
142
162
  [[Appoxee shared] getDeviceAliasWithCompletionHandler:^(NSError * _Nullable appoxeeError, id _Nullable data) {
143
163
  if (appoxeeError == nil && data != nil) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-mapp-plugin",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "Mapp SDK for React Native.",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -99,7 +99,12 @@
99
99
  ],
100
100
  "transform": {
101
101
  "^.+\\.js$": "babel-jest",
102
- "^.+\\.ts$": ["ts-jest", { "tsconfig": "plugin/tsconfig.json" }]
102
+ "^.+\\.ts$": [
103
+ "ts-jest",
104
+ {
105
+ "tsconfig": "plugin/tsconfig.json"
106
+ }
107
+ ]
103
108
  },
104
109
  "moduleNameMapper": {
105
110
  "^react-native$": "<rootDir>/__mocks__/react-native.js"
@@ -16,7 +16,7 @@ export interface Spec extends TurboModule {
16
16
  getAlias(): Promise<string>;
17
17
  /** @deprecated Use engage(...). */
18
18
  engage2(): void;
19
- engage(sdkKey: string, googleProjectId: string, server: string, appID: string, tenantID: string): void;
19
+ engage(sdkKey: string, googleProjectId: string, server: string, appID: string, tenantID: string): Promise<boolean>;
20
20
  engageTestServer(cepURl: string, sdkKey: string, googleProjectId: string, server: string, appID: string, tenantID: string): void;
21
21
  onInitCompletedListener(): Promise<boolean>;
22
22
  isReady(): Promise<boolean>;