react-native-nami-sdk 3.3.2 → 3.3.3-2

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.
Files changed (37) hide show
  1. package/android/build.gradle +2 -2
  2. package/android/src/main/AndroidManifest.xml +14 -1
  3. package/android/src/main/java/com/namiml/reactnative/NamiBridgePackage.java +15 -0
  4. package/android/src/main/java/com/namiml/reactnative/NamiFlowManagerBridge.kt +22 -17
  5. package/android/src/main/java/com/namiml/reactnative/NamiOverlayControlBridge.kt +171 -0
  6. package/android/src/main/java/com/namiml/reactnative/ReactOverlayActivity.kt +29 -0
  7. package/android/src/main/res/values/styles.xml +10 -0
  8. package/dist/index.d.ts +1 -0
  9. package/dist/specs/NativeNamiFlowManager.d.ts +1 -0
  10. package/dist/specs/NativeNamiOverlayControl.d.ts +9 -0
  11. package/dist/src/NamiOverlayControl.d.ts +9 -0
  12. package/dist/src/NamiOverlayHost.d.ts +1 -0
  13. package/dist/src/overlay.d.ts +4 -0
  14. package/dist/src/registerOverlay.d.ts +1 -0
  15. package/dist/src/version.d.ts +1 -1
  16. package/index.ts +1 -0
  17. package/ios/Nami.swift +21 -8
  18. package/ios/NamiCampaignManagerBridge.m +3 -2
  19. package/ios/NamiCampaignManagerBridge.swift +16 -11
  20. package/ios/NamiCustomerManager.m +3 -2
  21. package/ios/NamiCustomerManager.swift +44 -21
  22. package/ios/NamiEntitlementManagerBridge.m +3 -2
  23. package/ios/NamiEntitlementManagerBridge.swift +14 -12
  24. package/ios/NamiFlowManagerBridge.m +1 -1
  25. package/ios/NamiFlowManagerBridge.swift +17 -22
  26. package/ios/NamiOverlayControlBridge.m +17 -0
  27. package/ios/NamiOverlayControlBridge.swift +132 -0
  28. package/ios/NamiPaywallManagerBridge.m +2 -2
  29. package/ios/NamiPaywallManagerBridge.swift +19 -20
  30. package/ios/NamiPurchaseManagerBridge.m +2 -2
  31. package/ios/NamiPurchaseManagerBridge.swift +17 -8
  32. package/package.json +10 -3
  33. package/react-native-nami-sdk.podspec +19 -9
  34. package/specs/NativeNamiFlowManager.ts +1 -0
  35. package/specs/NativeNamiOverlayControl.ts +9 -0
  36. package/src/NamiOverlayControl.tsx +48 -0
  37. package/src/version.ts +1 -1
@@ -85,8 +85,8 @@ dependencies {
85
85
  implementation fileTree(dir: 'libs', include: ['*.jar'])
86
86
  implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
87
87
 
88
- playImplementation "com.namiml:sdk-android:3.3.2.2"
89
- amazonImplementation "com.namiml:sdk-amazon:3.3.2.2"
88
+ playImplementation "com.namiml:sdk-android:3.3.3.1"
89
+ amazonImplementation "com.namiml:sdk-amazon:3.3.3.1"
90
90
 
91
91
  implementation "com.facebook.react:react-native:+" // From node_modules
92
92
  coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.0.4"
@@ -1,3 +1,16 @@
1
- <manifest package="com.namiml.reactnative">
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
2
+ package="com.namiml.reactnative">
2
3
 
4
+
5
+ <application>
6
+ <activity
7
+ android:name=".ReactOverlayActivity"
8
+ android:theme="@style/Theme.TransparentReact"
9
+ android:exported="false"
10
+ android:launchMode="singleTop"
11
+ android:taskAffinity=""
12
+ android:noHistory="true"
13
+ android:excludeFromRecents="true"
14
+ android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode" />
15
+ </application>
3
16
  </manifest>
@@ -29,6 +29,8 @@ public class NamiBridgePackage extends TurboReactPackage {
29
29
  case NamiPaywallManagerBridgeModule.NAME -> new NamiPaywallManagerBridgeModule(context);
30
30
  case NamiPurchaseManagerBridgeModule.NAME ->
31
31
  new NamiPurchaseManagerBridgeModule(context);
32
+ case NamiOverlayControlBridgeModule.NAME ->
33
+ new NamiOverlayControlBridgeModule(context);
32
34
  default -> null;
33
35
  };
34
36
  }
@@ -129,6 +131,19 @@ public class NamiBridgePackage extends TurboReactPackage {
129
131
  BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
130
132
  )
131
133
  );
134
+
135
+ moduleInfos.put(
136
+ NamiOverlayControlBridgeModule.NAME,
137
+ new ReactModuleInfo(
138
+ NamiOverlayControlBridgeModule.NAME,
139
+ NamiOverlayControlBridgeModule.NAME,
140
+ false,
141
+ false,
142
+ true,
143
+ false,
144
+ BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
145
+ )
146
+ );
132
147
  return moduleInfos;
133
148
  };
134
149
  }
@@ -4,39 +4,40 @@ import android.os.Handler
4
4
  import android.os.Looper
5
5
  import android.util.Log
6
6
  import com.facebook.react.bridge.*
7
- import com.facebook.react.modules.core.DeviceEventManagerModule
8
7
  import com.facebook.react.module.annotations.ReactModule
8
+ import com.facebook.react.modules.core.DeviceEventManagerModule
9
9
  import com.facebook.react.turbomodule.core.interfaces.TurboModule
10
10
  import com.namiml.flow.NamiFlowManager
11
11
 
12
12
  @ReactModule(name = NamiFlowManagerBridgeModule.NAME)
13
13
  class NamiFlowManagerBridgeModule internal constructor(
14
- reactContext: ReactApplicationContext
15
- ) : ReactContextBaseJavaModule(reactContext), TurboModule {
16
-
14
+ reactContext: ReactApplicationContext,
15
+ ) : ReactContextBaseJavaModule(reactContext),
16
+ TurboModule {
17
17
  companion object {
18
18
  const val NAME = "RNNamiFlowManager"
19
19
  }
20
20
 
21
- override fun getName(): String {
22
- return NAME
23
- }
21
+ override fun getName(): String = NAME
24
22
 
25
23
  @ReactMethod
26
24
  fun registerStepHandoff() {
27
25
  NamiFlowManager.registerStepHandoff { handoffTag, handoffData ->
28
- val payload = Arguments.createMap().apply {
29
- putString("handoffTag", handoffTag)
30
- if (handoffData != null) {
31
- try {
32
- val map = Arguments.makeNativeMap(handoffData)
33
- putMap("handoffData", map)
34
- } catch (e: Exception) {
35
- Log.d(NAME, "Failed to convert handoffData to NativeMap: ${e.localizedMessage}")
26
+ val payload =
27
+ Arguments.createMap().apply {
28
+ putString("handoffTag", handoffTag)
29
+ if (handoffData != null) {
30
+ try {
31
+ putMap("handoffData", Arguments.makeNativeMap(handoffData))
32
+ } catch (e: Exception) {
33
+ Log.d(NAME, "Failed to convert handoffData to NativeMap: ${e.localizedMessage}")
34
+ }
36
35
  }
37
36
  }
37
+
38
+ reactApplicationContext.runOnUiQueueThread {
39
+ sendEvent("Handoff", payload)
38
40
  }
39
- sendEvent("Handoff", payload)
40
41
  }
41
42
  }
42
43
 
@@ -74,7 +75,10 @@ class NamiFlowManagerBridgeModule internal constructor(
74
75
  promise.resolve(NamiFlowManager.isFlowOpen())
75
76
  }
76
77
 
77
- private fun sendEvent(eventName: String, params: WritableMap?) {
78
+ private fun sendEvent(
79
+ eventName: String,
80
+ params: WritableMap?,
81
+ ) {
78
82
  reactApplicationContext
79
83
  .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
80
84
  .emit(eventName, params)
@@ -82,5 +86,6 @@ class NamiFlowManagerBridgeModule internal constructor(
82
86
 
83
87
  // Required for RN EventEmitter support
84
88
  @ReactMethod fun addListener(eventName: String?) {}
89
+
85
90
  @ReactMethod fun removeListeners(count: Int?) {}
86
91
  }
@@ -0,0 +1,171 @@
1
+ package com.namiml.reactnative
2
+
3
+ import android.app.Activity
4
+ import android.content.Intent
5
+ import android.os.Build
6
+ import android.util.Log
7
+ import com.facebook.react.bridge.*
8
+ import com.facebook.react.modules.core.DeviceEventManagerModule
9
+ import com.facebook.react.module.annotations.ReactModule
10
+ import com.facebook.react.turbomodule.core.interfaces.TurboModule
11
+ import com.facebook.react.bridge.UiThreadUtil
12
+
13
+ @ReactModule(name = NamiOverlayControlBridgeModule.NAME)
14
+ class NamiOverlayControlBridgeModule(private val ctx: ReactApplicationContext)
15
+ : ReactContextBaseJavaModule(ctx), TurboModule {
16
+
17
+ companion object {
18
+ const val NAME = "RNNamiOverlayControl"
19
+ var currentOverlayActivity: ReactOverlayActivity? = null
20
+ var lastValidActivity: Activity? = null
21
+ }
22
+
23
+ override fun getName() = NAME
24
+
25
+ @ReactMethod
26
+ fun presentOverlay(promise: Promise) {
27
+ var theActivity: Activity? = null
28
+ if (reactApplicationContext.hasCurrentActivity()) {
29
+ theActivity = reactApplicationContext.currentActivity
30
+
31
+ // Check if it's the overlay activity that's finishing
32
+ if (theActivity is ReactOverlayActivity) {
33
+ if (theActivity.isFinishing || theActivity.isDestroyed) {
34
+ theActivity = lastValidActivity
35
+ if (theActivity == null || theActivity.isFinishing || theActivity.isDestroyed) {
36
+ promise.reject("NO_ACTIVITY", "No valid activity available")
37
+ return
38
+ }
39
+ } else {
40
+ lastValidActivity = theActivity
41
+ }
42
+ } else {
43
+ lastValidActivity = theActivity
44
+ }
45
+ } else {
46
+ theActivity = lastValidActivity
47
+ if (theActivity == null || theActivity.isFinishing || theActivity.isDestroyed) {
48
+ promise.reject("NO_ACTIVITY", "No valid activity available")
49
+ return
50
+ }
51
+ }
52
+
53
+ if (theActivity == null) {
54
+ promise.reject("NO_ACTIVITY", "No activity available")
55
+ return
56
+ }
57
+
58
+ startOverlayActivity(theActivity, promise)
59
+ }
60
+
61
+ private fun startOverlayActivity(activity: Activity, promise: Promise) {
62
+ try {
63
+ UiThreadUtil.runOnUiThread {
64
+ try {
65
+ val intent = Intent(activity, ReactOverlayActivity::class.java)
66
+ intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
67
+ activity.startActivity(intent)
68
+ // Use modern transition API for Android 14+ (API 34+)
69
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
70
+ activity.overrideActivityTransition(Activity.OVERRIDE_TRANSITION_OPEN, 0, 0)
71
+ } else {
72
+ @Suppress("DEPRECATION")
73
+ activity.overridePendingTransition(0, 0)
74
+ }
75
+
76
+ // Emit ready event after a short delay to ensure activity is started
77
+ android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
78
+ try {
79
+ if (ctx.catalystInstance != null) {
80
+ ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
81
+ .emit("NamiOverlayReady", null)
82
+ }
83
+ } catch (e: Exception) {
84
+ Log.e(NAME, "Failed to emit NamiOverlayReady: ${e.message}")
85
+ }
86
+ }, 100)
87
+
88
+ promise.resolve(null)
89
+ } catch (uiError: Exception) {
90
+ Log.e(NAME, "Error in UI thread: ${uiError.message}", uiError)
91
+ promise.reject("UI_THREAD_ERROR", "Failed in UI thread: ${uiError.message}", uiError)
92
+ }
93
+ }
94
+ } catch (e: Exception) {
95
+ Log.e(NAME, "Error presenting overlay: ${e.message}", e)
96
+ promise.reject("PRESENT_OVERLAY_ERROR", "Failed to present overlay: ${e.message}", e)
97
+ }
98
+ }
99
+
100
+ @ReactMethod
101
+ fun addListener(eventName: String?) {
102
+ // Required for NativeEventEmitter - no-op since we emit events directly
103
+ }
104
+
105
+ @ReactMethod
106
+ fun removeListeners(count: Int?) {
107
+ // Required for NativeEventEmitter - no-op since we emit events directly
108
+ }
109
+
110
+ @ReactMethod
111
+ fun finishOverlay(result: ReadableMap?, promise: Promise) {
112
+ try {
113
+ // Emit result to main app listeners
114
+ val payload: WritableMap = Arguments.createMap().apply {
115
+ if (result != null) {
116
+ merge(result)
117
+ }
118
+ }
119
+ try {
120
+ if (ctx.catalystInstance != null) {
121
+ ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
122
+ .emit("NamiOverlayResult", payload)
123
+ }
124
+ } catch (e: Exception) {
125
+ Log.e(NAME, "Failed to emit NamiOverlayResult: ${e.message}")
126
+ }
127
+
128
+ UiThreadUtil.runOnUiThread {
129
+ // Try to finish the tracked overlay activity first
130
+ currentOverlayActivity?.let { overlay ->
131
+ overlay.finish()
132
+ // Use modern transition API for Android 14+ (API 34+)
133
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
134
+ overlay.overrideActivityTransition(Activity.OVERRIDE_TRANSITION_CLOSE, 0, 0)
135
+ } else {
136
+ @Suppress("DEPRECATION")
137
+ overlay.overridePendingTransition(0, 0)
138
+ }
139
+ currentOverlayActivity = null
140
+
141
+ // Wait for activity to actually finish before resolving promise
142
+ android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
143
+ promise.resolve(null)
144
+ }, 100)
145
+ } ?: run {
146
+ // Fallback to current activity if it's an overlay
147
+ (ctx.currentActivity as? ReactOverlayActivity)?.apply {
148
+ finish()
149
+ // Use modern transition API for Android 14+ (API 34+)
150
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
151
+ overrideActivityTransition(Activity.OVERRIDE_TRANSITION_CLOSE, 0, 0)
152
+ } else {
153
+ @Suppress("DEPRECATION")
154
+ overridePendingTransition(0, 0)
155
+ }
156
+
157
+ // Wait for activity to actually finish before resolving promise
158
+ android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
159
+ promise.resolve(null)
160
+ }, 100)
161
+ } ?: run {
162
+ promise.resolve(null)
163
+ }
164
+ }
165
+ }
166
+ } catch (e: Exception) {
167
+ Log.e(NAME, "Failed to finish overlay: ${e.message}", e)
168
+ promise.reject("FINISH_OVERLAY_ERROR", "Failed to finish overlay", e)
169
+ }
170
+ }
171
+ }
@@ -0,0 +1,29 @@
1
+ package com.namiml.reactnative
2
+
3
+ import android.os.Bundle
4
+ import com.facebook.react.ReactActivity
5
+
6
+ class ReactOverlayActivity : ReactActivity() {
7
+ override fun getMainComponentName(): String = "NamiOverlayHost"
8
+
9
+ override fun onCreate(savedInstanceState: Bundle?) {
10
+ super.onCreate(savedInstanceState)
11
+ overridePendingTransition(0, 0)
12
+
13
+ // Register this activity with the bridge module
14
+ NamiOverlayControlBridgeModule.currentOverlayActivity = this
15
+ }
16
+
17
+ override fun onBackPressed() {
18
+ finish()
19
+ overridePendingTransition(0, 0)
20
+ }
21
+
22
+ override fun onDestroy() {
23
+ super.onDestroy()
24
+ // Clear the reference when activity is destroyed
25
+ if (NamiOverlayControlBridgeModule.currentOverlayActivity == this) {
26
+ NamiOverlayControlBridgeModule.currentOverlayActivity = null
27
+ }
28
+ }
29
+ }
@@ -0,0 +1,10 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <resources>
3
+ <style name="Theme.TransparentReact" parent="Theme.MaterialComponents.DayNight.NoActionBar">
4
+ <item name="android:windowIsTranslucent">true</item>
5
+ <item name="android:windowBackground">@android:color/transparent</item>
6
+ <item name="android:windowNoTitle">true</item>
7
+ <item name="android:statusBarColor">@android:color/transparent</item>
8
+ <item name="android:navigationBarColor">@android:color/transparent</item>
9
+ </style>
10
+ </resources>
package/dist/index.d.ts CHANGED
@@ -5,4 +5,5 @@ export { NamiEntitlementManager } from './src/NamiEntitlementManager';
5
5
  export { NamiPurchaseManager } from './src/NamiPurchaseManager';
6
6
  export { NamiPaywallManager } from './src/NamiPaywallManager';
7
7
  export { NamiFlowManager } from './src/NamiFlowManager';
8
+ export { NamiOverlayControl } from './src/NamiOverlayControl';
8
9
  export * from './src/types';
@@ -6,6 +6,7 @@ export interface Spec extends TurboModule {
6
6
  resume(): void;
7
7
  pause(): void;
8
8
  registerEventHandler(): void;
9
+ purchaseSuccess(): void;
9
10
  }
10
11
  declare const _default: Spec;
11
12
  export default _default;
@@ -0,0 +1,9 @@
1
+ import type { TurboModule } from 'react-native';
2
+ export interface Spec extends TurboModule {
3
+ presentOverlay(): Promise<void>;
4
+ finishOverlay(result?: {
5
+ [key: string]: unknown;
6
+ } | null): Promise<void>;
7
+ }
8
+ declare const _default: Spec;
9
+ export default _default;
@@ -0,0 +1,9 @@
1
+ import { NativeEventEmitter } from 'react-native';
2
+ export declare const NamiOverlayControl: {
3
+ emitter: NativeEventEmitter;
4
+ presentOverlay(): Promise<void>;
5
+ finishOverlay(result?: any): Promise<void>;
6
+ onOverlayReady(handler: () => void): () => void;
7
+ onOverlayResult(handler: (result: any) => void): () => void;
8
+ };
9
+ export default function NamiOverlayHost(): any;
@@ -0,0 +1 @@
1
+ export default function NamiOverlayHost(): null;
@@ -0,0 +1,4 @@
1
+ export declare function presentOverlay(): Promise<void>;
2
+ export declare function finishOverlay(result?: any): Promise<void>;
3
+ export declare function onOverlayReady(handler: () => void): () => void;
4
+ export declare function onOverlayResult(handler: (result: any) => void): () => void;
@@ -0,0 +1 @@
1
+ export {};
@@ -2,4 +2,4 @@
2
2
  * Auto-generated file. Do not edit manually.
3
3
  * React Native Nami SDK version.
4
4
  */
5
- export declare const NAMI_REACT_NATIVE_VERSION = "3.3.2";
5
+ export declare const NAMI_REACT_NATIVE_VERSION = "3.3.3-2";
package/index.ts CHANGED
@@ -5,4 +5,5 @@ export { NamiEntitlementManager } from './src/NamiEntitlementManager';
5
5
  export { NamiPurchaseManager } from './src/NamiPurchaseManager';
6
6
  export { NamiPaywallManager } from './src/NamiPaywallManager';
7
7
  export { NamiFlowManager } from './src/NamiFlowManager';
8
+ export { NamiOverlayControl } from './src/NamiOverlayControl';
8
9
  export * from './src/types';
package/ios/Nami.swift CHANGED
@@ -8,10 +8,6 @@ import Foundation
8
8
  import NamiApple
9
9
  import React
10
10
 
11
- // #if RCT_NEW_ARCH_ENABLED
12
- // extension RNNami: RCTTurboModule {}
13
- // #endif
14
-
15
11
  @objc(RNNami)
16
12
  class RNNami: NSObject {
17
13
  static func moduleName() -> String! {
@@ -63,20 +59,37 @@ class RNNami: NSObject {
63
59
  config.initialConfig = initialConfig
64
60
  }
65
61
 
62
+ var didCallBack = false
66
63
  Nami.configure(with: config) { sdkConfigured in
67
- resolve(["success": sdkConfigured])
64
+ didCallBack = true
65
+ NSLog("RNNami: configure() completion called, sdkConfigured: \(sdkConfigured)")
66
+ DispatchQueue.main.async {
67
+ resolve(["success": sdkConfigured])
68
+ }
69
+ }
70
+
71
+ DispatchQueue.main.asyncAfter(deadline: .now() + 10) {
72
+ if !didCallBack {
73
+ NSLog("RNNami: configure() completion NEVER CALLED, reporting failure.")
74
+ resolve(["success": false])
75
+ }
68
76
  }
69
77
  }
70
78
 
71
79
  @objc
72
- func sdkConfigured(_ resolve: RCTPromiseResolveBlock, rejecter _: RCTPromiseRejectBlock) {
73
- resolve(Nami.sdkConfigured())
80
+ func sdkConfigured(_ resolve: @escaping RCTPromiseResolveBlock, rejecter _: RCTPromiseRejectBlock) {
81
+ let sdkConfigured = Nami.sdkConfigured()
82
+ DispatchQueue.main.async {
83
+ resolve(sdkConfigured)
84
+ }
74
85
  }
75
86
 
76
87
  @objc(sdkVersion:rejecter:)
77
88
  func sdkVersion(resolve: @escaping RCTPromiseResolveBlock, reject _: @escaping RCTPromiseRejectBlock) {
78
89
  let version = Nami.sdkVersion()
79
- resolve(version)
90
+ DispatchQueue.main.async {
91
+ resolve(version)
92
+ }
80
93
  }
81
94
 
82
95
  func isNewArchitectureEnabled() -> Bool {
@@ -6,8 +6,9 @@
6
6
  //
7
7
 
8
8
  #import <React/RCTBridgeModule.h>
9
+ #import <React/RCTEventEmitter.h>
9
10
 
10
- @interface RCT_EXTERN_MODULE(RNNamiCampaignManager, NSObject)
11
+ @interface RCT_EXTERN_MODULE(RNNamiCampaignManager, RCTEventEmitter)
11
12
 
12
13
  RCT_EXTERN_METHOD(launch:(nullable NSString *)label withUrl:(nullable NSString *)withUrl context:(nullable NSDictionary *)context completion:(RCTResponseSenderBlock)callback paywallCompletion:(RCTResponseSenderBlock)paywallCallback);
13
14
 
@@ -20,7 +21,7 @@ RCT_EXTERN_METHOD(refresh:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRe
20
21
  RCT_EXTERN_METHOD(registerAvailableCampaignsHandler)
21
22
 
22
23
  + (BOOL)requiresMainQueueSetup {
23
- return NO;
24
+ return YES;
24
25
  }
25
26
 
26
27
  @end
@@ -10,17 +10,26 @@ import NamiApple
10
10
  import os
11
11
  import React
12
12
 
13
- // #if RCT_NEW_ARCH_ENABLED
14
- // extension RNNamiCampaignManager: RCTTurboModule {}
15
- // #endif
16
-
17
13
  @objc(RNNamiCampaignManager)
18
14
  class RNNamiCampaignManager: RCTEventEmitter {
19
15
  public static var shared: RNNamiCampaignManager?
20
16
 
21
17
  override init() {
22
18
  super.init()
23
- RNNamiCampaignManager.shared = self
19
+ }
20
+
21
+ override class func requiresMainQueueSetup() -> Bool { true }
22
+
23
+ private var hasListeners = false
24
+ override func startObserving() { hasListeners = true }
25
+ override func stopObserving() { hasListeners = false }
26
+
27
+ private func safeSend(withName name: String, body: Any?) {
28
+ guard hasListeners else {
29
+ print("[RNNamiCampaignManager] Warning: no listeners, so event not being sent to JS.")
30
+ return
31
+ } // optional but avoids warnings
32
+ sendEvent(withName: name, body: body)
24
33
  }
25
34
 
26
35
  override func supportedEvents() -> [String]! {
@@ -138,9 +147,7 @@ class RNNamiCampaignManager: RCTEventEmitter {
138
147
  "timeSpentOnPaywall": paywallEvent.timeSpentOnPaywall,
139
148
  ]
140
149
 
141
- DispatchQueue.main.async {
142
- RNNamiCampaignManager.shared?.sendEvent(withName: "NamiPaywallEvent", body: payload)
143
- }
150
+ safeSend(withName: "NamiPaywallEvent", body: payload)
144
151
  }
145
152
 
146
153
  func handleLaunch(callback: RCTResponseSenderBlock?, success: Bool, error: Error?) {
@@ -294,9 +301,7 @@ class RNNamiCampaignManager: RCTEventEmitter {
294
301
  func registerForAvailableCampaigns() {
295
302
  NamiCampaignManager.registerAvailableCampaignsHandler { availableCampaigns in
296
303
  let dictionaries = availableCampaigns.map { campaign in self.campaignInToDictionary(campaign) }
297
- DispatchQueue.main.async {
298
- RNNamiCampaignManager.shared?.sendEvent(withName: "AvailableCampaignsChanged", body: dictionaries)
299
- }
304
+ self.safeSend(withName: "AvailableCampaignsChanged", body: dictionaries)
300
305
  }
301
306
  }
302
307
  }
@@ -6,8 +6,9 @@
6
6
  //
7
7
 
8
8
  #import <React/RCTBridgeModule.h>
9
+ #import <React/RCTEventEmitter.h>
9
10
 
10
- @interface RCT_EXTERN_MODULE(RNNamiCustomerManager, NSObject)
11
+ @interface RCT_EXTERN_MODULE(RNNamiCustomerManager, RCTEventEmitter)
11
12
 
12
13
  RCT_EXTERN_METHOD(setCustomerAttribute:(NSString *)key value:(NSString *)value)
13
14
 
@@ -43,7 +44,7 @@ RCT_EXTERN_METHOD(registerAccountStateHandler)
43
44
 
44
45
 
45
46
  + (BOOL)requiresMainQueueSetup {
46
- return NO;
47
+ return YES;
47
48
  }
48
49
 
49
50
  @end