react-native-nami-sdk 3.3.3 → 3.3.4

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.
@@ -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.3"
89
- amazonImplementation "com.namiml:sdk-amazon:3.3.3"
88
+ playImplementation "com.namiml:sdk-android:3.3.4"
89
+ amazonImplementation "com.namiml:sdk-amazon:3.3.4"
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"
@@ -3,23 +3,24 @@ package com.namiml.reactnative
3
3
  import android.app.Activity
4
4
  import android.net.Uri
5
5
  import android.util.Log
6
+ import androidx.core.net.toUri
6
7
  import com.facebook.react.bridge.*
7
8
  import com.facebook.react.module.annotations.ReactModule
8
9
  import com.facebook.react.modules.core.DeviceEventManagerModule
9
10
  import com.facebook.react.turbomodule.core.interfaces.TurboModule
10
11
  import com.namiml.billing.NamiPurchase
12
+ import com.namiml.campaign.LaunchCampaignError
11
13
  import com.namiml.campaign.LaunchCampaignResult
12
14
  import com.namiml.campaign.NamiCampaign
13
15
  import com.namiml.campaign.NamiCampaignManager
14
16
  import com.namiml.paywall.model.NamiPaywallEvent
15
17
  import com.namiml.paywall.model.PaywallLaunchContext
16
- import androidx.core.net.toUri
17
18
 
18
19
  @ReactModule(name = NamiCampaignManagerBridgeModule.NAME)
19
20
  class NamiCampaignManagerBridgeModule internal constructor(
20
- private val reactContext: ReactApplicationContext
21
- ) : ReactContextBaseJavaModule(reactContext), TurboModule {
22
-
21
+ private val reactContext: ReactApplicationContext,
22
+ ) : ReactContextBaseJavaModule(reactContext),
23
+ TurboModule {
23
24
  companion object {
24
25
  const val NAME = "RNNamiCampaignManager"
25
26
  const val CAMPAIGN_ID = "campaignId"
@@ -42,10 +43,7 @@ class NamiCampaignManagerBridgeModule internal constructor(
42
43
  const val NAMI_PAYWALL_EVENT = "NamiPaywallEvent"
43
44
  }
44
45
 
45
-
46
- override fun getName(): String {
47
- return NAME
48
- }
46
+ override fun getName(): String = NAME
49
47
 
50
48
  private fun campaignToReadableMap(campaign: NamiCampaign): ReadableMap {
51
49
  val readableMap = Arguments.createMap()
@@ -94,7 +92,37 @@ class NamiCampaignManagerBridgeModule internal constructor(
94
92
  val keyIterator = attr.keySetIterator()
95
93
  while (keyIterator.hasNextKey()) {
96
94
  val key = keyIterator.nextKey()
97
- customAttributes[key] = attr.getString(key) ?: ""
95
+ try {
96
+ // Handle different data types and store with proper types
97
+ val value: Any =
98
+ when (attr.getType(key)) {
99
+ ReadableType.String -> attr.getString(key) ?: ""
100
+ ReadableType.Boolean -> attr.getBoolean(key)
101
+ ReadableType.Number -> {
102
+ // Try to get as int first, then as double
103
+ try {
104
+ attr.getInt(key)
105
+ } catch (e: Exception) {
106
+ attr.getDouble(key)
107
+ }
108
+ }
109
+ ReadableType.Null -> ""
110
+ ReadableType.Array -> attr.getArray(key)?.toArrayList() ?: emptyList<Any>()
111
+ ReadableType.Map -> attr.getMap(key)?.toHashMap() ?: emptyMap<String, Any>()
112
+ }
113
+ customAttributes[key] = value
114
+ } catch (e: Exception) {
115
+ // Log the error and try to store the value as a string representation
116
+ Log.w(NAME, "Error parsing customAttribute '$key': ${e.message}. Attempting string conversion.")
117
+ try {
118
+ // Fallback: try to get the dynamic value and convert to string
119
+ val dynamicValue = attr.getDynamic(key)
120
+ customAttributes[key] = dynamicValue.asString() ?: ""
121
+ } catch (fallbackError: Exception) {
122
+ // If all else fails, log and skip this attribute
123
+ Log.e(NAME, "Failed to parse customAttribute '$key': ${fallbackError.message}. Skipping.")
124
+ }
125
+ }
98
126
  }
99
127
  Log.d(NAME, "customAttributes $customAttributes")
100
128
  }
@@ -120,8 +148,7 @@ class NamiCampaignManagerBridgeModule internal constructor(
120
148
 
121
149
  if (theActivity != null) {
122
150
  reactApplicationContext.runOnUiQueueThread {
123
- val paywallActionCallback = {
124
- paywallEvent: NamiPaywallEvent ->
151
+ val paywallActionCallback = { paywallEvent: NamiPaywallEvent ->
125
152
  handlePaywallCallback(
126
153
  paywallEvent,
127
154
  actionCallback,
@@ -184,7 +211,13 @@ class NamiCampaignManagerBridgeModule internal constructor(
184
211
  putString("id", paywallEvent.sku?.id ?: "")
185
212
  putString("skuId", paywallEvent.sku?.skuId ?: "")
186
213
  putString("name", paywallEvent.sku?.name ?: "")
187
- putString("type", paywallEvent.sku?.type.toString().lowercase())
214
+ putString(
215
+ "type",
216
+ paywallEvent.sku
217
+ ?.type
218
+ .toString()
219
+ .lowercase(),
220
+ )
188
221
  }
189
222
  resultMap.putMap(SKU, skuMap)
190
223
  }
@@ -222,8 +255,8 @@ class NamiCampaignManagerBridgeModule internal constructor(
222
255
  emitEvent(NAMI_PAYWALL_EVENT, resultMap)
223
256
  }
224
257
 
225
- private fun createPurchaseArray(purchases: List<NamiPurchase>?): WritableArray {
226
- return WritableNativeArray().apply {
258
+ private fun createPurchaseArray(purchases: List<NamiPurchase>?): WritableArray =
259
+ WritableNativeArray().apply {
227
260
  purchases?.forEach { purchase ->
228
261
  try {
229
262
  pushMap(purchase.toPurchaseDict())
@@ -232,7 +265,6 @@ class NamiCampaignManagerBridgeModule internal constructor(
232
265
  }
233
266
  }
234
267
  }
235
- }
236
268
 
237
269
  private fun emitEvent(
238
270
  event: String,
@@ -246,17 +278,46 @@ class NamiCampaignManagerBridgeModule internal constructor(
246
278
  }
247
279
  }
248
280
 
281
+ /**
282
+ * Maps Android LaunchCampaignError enum values to iOS-compatible error codes
283
+ * to ensure consistent error handling across platforms in React Native.
284
+ */
285
+ private fun mapErrorCodeToIOS(androidError: LaunchCampaignError): Int =
286
+ when (androidError) {
287
+ LaunchCampaignError.DEFAULT_CAMPAIGN_NOT_FOUND -> 0
288
+ LaunchCampaignError.LABELED_CAMPAIGN_NOT_FOUND -> 1
289
+ LaunchCampaignError.CAMPAIGN_DATA_NOT_FOUND -> 2
290
+ LaunchCampaignError.PAYWALL_ALREADY_DISPLAYED -> 3
291
+ LaunchCampaignError.SDK_NOT_INITIALIZED -> 4
292
+ LaunchCampaignError.URL_CAMPAIGN_NOT_FOUND -> 6
293
+ LaunchCampaignError.PRODUCT_GROUPS_NOT_FOUND -> 8
294
+ }
295
+
249
296
  private fun handleResult(
250
297
  result: LaunchCampaignResult,
251
298
  resultCallback: Callback,
252
299
  ) {
253
- val resultMap = Arguments.createMap()
254
300
  when (result) {
255
301
  is LaunchCampaignResult.Success -> {
256
302
  resultCallback.invoke(true)
257
303
  }
258
304
  is LaunchCampaignResult.Failure -> {
259
- resultCallback.invoke(false, "${result.error}")
305
+ val error = result.error
306
+ val errorInfo =
307
+ if (error is LaunchCampaignError) {
308
+ Arguments.createMap().apply {
309
+ putInt("code", mapErrorCodeToIOS(error))
310
+ putString("domain", "LaunchCampaignError")
311
+ putString("message", error.errorMessage.ifEmpty { error.toString() })
312
+ }
313
+ } else {
314
+ Arguments.createMap().apply {
315
+ putInt("code", -1)
316
+ putString("domain", "Unknown")
317
+ putString("message", error.toString())
318
+ }
319
+ }
320
+ resultCallback.invoke(false, errorInfo)
260
321
  }
261
322
  }
262
323
  }
@@ -279,13 +340,35 @@ class NamiCampaignManagerBridgeModule internal constructor(
279
340
  val isCampaignAvailable =
280
341
  when {
281
342
  campaignSource == null -> NamiCampaignManager.isCampaignAvailable()
282
- campaignSource.toUri().scheme != null -> NamiCampaignManager.isCampaignAvailable(
283
- campaignSource.toUri())
343
+ campaignSource.toUri().scheme != null ->
344
+ NamiCampaignManager.isCampaignAvailable(campaignSource.toUri())
284
345
  else -> NamiCampaignManager.isCampaignAvailable(campaignSource)
285
346
  }
286
347
  promise.resolve(isCampaignAvailable)
287
348
  }
288
349
 
350
+ @ReactMethod
351
+ fun isFlow(
352
+ label: String?,
353
+ withUrl: String?,
354
+ promise: Promise,
355
+ ) {
356
+ try {
357
+ val uri = if (!withUrl.isNullOrEmpty()) {
358
+ val parsedUri = Uri.parse(withUrl)
359
+ if (parsedUri.scheme.isNullOrEmpty()) {
360
+ promise.reject("ISFLOW_ERROR", "Invalid URL format: missing scheme", null)
361
+ return
362
+ }
363
+ parsedUri
364
+ } else null
365
+ val result = NamiCampaignManager.isFlow(label = label, uri = uri)
366
+ promise.resolve(result)
367
+ } catch (e: Exception) {
368
+ promise.reject("ISFLOW_ERROR", "Failed to check if campaign is flow: ${e.message}", e)
369
+ }
370
+ }
371
+
289
372
  @ReactMethod
290
373
  fun refresh(promise: Promise) {
291
374
  NamiCampaignManager.refresh { campaigns ->
@@ -52,7 +52,11 @@ class NamiFlowManagerBridgeModule internal constructor(
52
52
  @ReactMethod
53
53
  fun resume() {
54
54
  Handler(Looper.getMainLooper()).postDelayed({
55
- NamiFlowManager.resume()
55
+ if (reactApplicationContext.hasCurrentActivity()) {
56
+ NamiFlowManager.resume(reactApplicationContext.currentActivity)
57
+ } else {
58
+ NamiFlowManager.resume()
59
+ }
56
60
  }, 100L)
57
61
  }
58
62
 
@@ -71,6 +71,7 @@ export interface Spec extends TurboModule {
71
71
  value?: string;
72
72
  }[]>;
73
73
  isCampaignAvailable(source?: string): Promise<boolean>;
74
+ isFlow(label?: string | null, withUrl?: string | null): Promise<boolean>;
74
75
  refresh(): Promise<{
75
76
  id?: string;
76
77
  rule?: string;
@@ -64,7 +64,7 @@ export interface Spec extends TurboModule {
64
64
  isHidden(): Promise<boolean>;
65
65
  isPaywallOpen(): Promise<boolean>;
66
66
  buySkuCancel(): void;
67
- setProductDetails(productDetails: string, allowOffers?: boolean): void;
67
+ setProductDetails(productDetails: string, allowOffers: boolean): void;
68
68
  setAppSuppliedVideoDetails(url: string, name?: string): void;
69
69
  allowUserInteraction(allowed: boolean): void;
70
70
  }
@@ -17,6 +17,7 @@ export declare const NamiCampaignManager: {
17
17
  value?: string;
18
18
  }[]>;
19
19
  isCampaignAvailable: (campaignName: string | null) => Promise<boolean>;
20
+ isFlow: (label?: string | null, withUrl?: string | null) => Promise<boolean>;
20
21
  refresh: () => Promise<{
21
22
  id?: string;
22
23
  rule?: string;
@@ -24,7 +24,7 @@ export declare const NamiPaywallManager: {
24
24
  isHidden: () => Promise<boolean>;
25
25
  isPaywallOpen: () => Promise<boolean>;
26
26
  buySkuCancel: () => void;
27
- setProductDetails: (productDetails: string, allowOffers?: boolean) => void;
27
+ setProductDetails: (productDetails: string, allowOffers: boolean) => void;
28
28
  setAppSuppliedVideoDetails: (url: string, name?: string) => void;
29
29
  allowUserInteraction: (allowed: boolean) => void;
30
30
  };
@@ -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.3";
5
+ export declare const NAMI_REACT_NATIVE_VERSION = "3.3.4";
@@ -16,6 +16,8 @@ RCT_EXTERN_METHOD(allCampaigns:(RCTPromiseResolveBlock)resolve rejecter:(RCTProm
16
16
 
17
17
  RCT_EXTERN_METHOD(isCampaignAvailable:(nullable NSString *)campaignSource resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
18
18
 
19
+ RCT_EXTERN_METHOD(isFlow:(nullable NSString *)label withUrl:(nullable NSString *)withUrl resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
20
+
19
21
  RCT_EXTERN_METHOD(refresh:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
20
22
 
21
23
  RCT_EXTERN_METHOD(registerAvailableCampaignsHandler)
@@ -289,6 +289,22 @@ class RNNamiCampaignManager: RCTEventEmitter {
289
289
  resolve(isCampaignAvailable)
290
290
  }
291
291
 
292
+ @objc(isFlow:withUrl:resolver:rejecter:)
293
+ func isFlow(
294
+ label: String?,
295
+ withUrl: String?,
296
+ resolve: @escaping RCTPromiseResolveBlock,
297
+ reject _: @escaping RCTPromiseRejectBlock
298
+ ) {
299
+ var url: URL?
300
+ if let withUrl = withUrl {
301
+ url = URL(string: withUrl)
302
+ }
303
+
304
+ let result = NamiCampaignManager.isFlow(label: label, url: url)
305
+ resolve(result)
306
+ }
307
+
292
308
  @objc(refresh:rejecter:)
293
309
  func refresh(resolve: @escaping RCTPromiseResolveBlock, reject _: @escaping RCTPromiseRejectBlock) {
294
310
  NamiCampaignManager.refresh { campaigns in
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "react-native-nami-sdk",
3
- "version": "3.3.3",
4
- "description": "React Native SDK for Nami - No-code paywall management with A/B testing.",
3
+ "version": "3.3.4",
4
+ "description": "React Native SDK for Nami - No-code paywall and onboarding flows with A/B testing.",
5
5
  "main": "index.ts",
6
6
  "types": "dist/index.d.ts",
7
7
  "codegenConfig": {
@@ -35,7 +35,6 @@
35
35
  "keywords": [
36
36
  "in-app-purchase",
37
37
  "ios",
38
- "ipados",
39
38
  "tvos",
40
39
  "paywall",
41
40
  "react-native",
@@ -43,7 +42,8 @@
43
42
  "subscriptions",
44
43
  "iap",
45
44
  "play-billing",
46
- "payments"
45
+ "payments",
46
+ "onboarding"
47
47
  ],
48
48
  "author": {
49
49
  "username": "hellonami",
@@ -52,12 +52,8 @@
52
52
  },
53
53
  "contributors": [
54
54
  {
55
- "name": "Francisco Pena",
56
- "email": "francisco.pena@namiml.com"
57
- },
58
- {
59
- "name": "Kendall Gelner",
60
- "email": "kendall.gelner@namiml.com"
55
+ "name": "Dan Burcaw",
56
+ "email": "dan.burcaw@namiml.com"
61
57
  }
62
58
  ],
63
59
  "license": "SEE LICENSE FILE",
@@ -70,11 +66,6 @@
70
66
  "react": ">=18",
71
67
  "react-native": ">=0.73"
72
68
  },
73
- "peerDependenciesMeta": {
74
- "react-native": {
75
- "optional": true
76
- }
77
- },
78
69
  "devDependencies": {
79
70
  "@react-native/eslint-config": "^0.80.0",
80
71
  "@types/jest": "^29.5.2",
@@ -21,7 +21,7 @@ Pod::Spec.new do |s|
21
21
  s.requires_arc = true
22
22
  s.swift_version = '5.0' # or your supported version
23
23
 
24
- s.dependency 'Nami', '3.3.3'
24
+ s.dependency 'Nami', '3.3.4.1'
25
25
  s.dependency 'React'
26
26
 
27
27
  pod_target_xcconfig = {
@@ -78,6 +78,8 @@ export interface Spec extends TurboModule {
78
78
 
79
79
  isCampaignAvailable(source?: string): Promise<boolean>;
80
80
 
81
+ isFlow(label?: string | null, withUrl?: string | null): Promise<boolean>;
82
+
81
83
  refresh(): Promise<
82
84
  {
83
85
  id?: string;
@@ -76,7 +76,7 @@ export interface Spec extends TurboModule {
76
76
 
77
77
  buySkuCancel(): void;
78
78
 
79
- setProductDetails(productDetails: string, allowOffers?: boolean): void;
79
+ setProductDetails(productDetails: string, allowOffers: boolean): void;
80
80
 
81
81
  setAppSuppliedVideoDetails(url: string, name?: string): void;
82
82
 
@@ -97,6 +97,10 @@ export const NamiCampaignManager = {
97
97
  );
98
98
  },
99
99
 
100
+ isFlow: async (label?: string | null, withUrl?: string | null) => {
101
+ return await RNNamiCampaignManager.isFlow(label, withUrl);
102
+ },
103
+
100
104
  refresh: async () => {
101
105
  return await RNNamiCampaignManager.refresh();
102
106
  },
@@ -106,7 +106,7 @@ export const NamiPaywallManager = {
106
106
  RNNamiPaywallManager.buySkuCancel();
107
107
  },
108
108
 
109
- setProductDetails: (productDetails: string, allowOffers?: boolean): void => {
109
+ setProductDetails: (productDetails: string, allowOffers: boolean): void => {
110
110
  RNNamiPaywallManager.setProductDetails(productDetails, allowOffers);
111
111
  },
112
112
 
package/src/version.ts CHANGED
@@ -2,4 +2,4 @@
2
2
  * Auto-generated file. Do not edit manually.
3
3
  * React Native Nami SDK version.
4
4
  */
5
- export const NAMI_REACT_NATIVE_VERSION = '3.3.3';
5
+ export const NAMI_REACT_NATIVE_VERSION = '3.3.4';