react-native-iap 16.4.1 → 16.5.0

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 (63) hide show
  1. package/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt +200 -142
  2. package/android/src/main/java/com/margelo/nitro/iap/TokenizedListenerRegistry.kt +0 -1
  3. package/ios/HybridRnIap.swift +832 -165
  4. package/ios/RnIapHelper.swift +1 -1
  5. package/lib/module/hooks/useIAP.js +48 -32
  6. package/lib/module/hooks/useIAP.js.map +1 -1
  7. package/lib/module/index.js +206 -152
  8. package/lib/module/index.js.map +1 -1
  9. package/lib/module/index.kepler.js +1 -0
  10. package/lib/module/index.kepler.js.map +1 -1
  11. package/lib/module/types.js +1 -0
  12. package/lib/module/types.js.map +1 -1
  13. package/lib/module/utils/error.js +5 -4
  14. package/lib/module/utils/error.js.map +1 -1
  15. package/lib/module/vega-adapter.js +4 -1
  16. package/lib/module/vega-adapter.js.map +1 -1
  17. package/lib/typescript/src/hooks/useIAP.d.ts +31 -15
  18. package/lib/typescript/src/hooks/useIAP.d.ts.map +1 -1
  19. package/lib/typescript/src/index.d.ts +28 -14
  20. package/lib/typescript/src/index.d.ts.map +1 -1
  21. package/lib/typescript/src/index.kepler.d.ts.map +1 -1
  22. package/lib/typescript/src/specs/RnIap.nitro.d.ts +10 -0
  23. package/lib/typescript/src/specs/RnIap.nitro.d.ts.map +1 -1
  24. package/lib/typescript/src/types.d.ts +11 -2
  25. package/lib/typescript/src/types.d.ts.map +1 -1
  26. package/lib/typescript/src/utils/error.d.ts.map +1 -1
  27. package/lib/typescript/src/vega-adapter.d.ts.map +1 -1
  28. package/nitrogen/generated/android/NitroIap+autolinking.cmake +1 -0
  29. package/nitrogen/generated/android/c++/JHybridRnIapSpec.cpp +9 -0
  30. package/nitrogen/generated/android/c++/JNitroPurchaseRequest.hpp +7 -1
  31. package/nitrogen/generated/android/c++/JNitroPurchaseRequestType.hpp +58 -0
  32. package/nitrogen/generated/android/c++/JNitroVerifyPurchaseWithIapkitHorizonProps.hpp +66 -0
  33. package/nitrogen/generated/android/c++/JNitroVerifyPurchaseWithIapkitProps.hpp +8 -1
  34. package/nitrogen/generated/android/c++/JNitroVerifyPurchaseWithProviderProps.hpp +3 -0
  35. package/nitrogen/generated/android/c++/JVariant_NullType_NitroVerifyPurchaseWithIapkitHorizonProps.cpp +26 -0
  36. package/nitrogen/generated/android/c++/JVariant_NullType_NitroVerifyPurchaseWithIapkitHorizonProps.hpp +74 -0
  37. package/nitrogen/generated/android/c++/JVariant_NullType_NitroVerifyPurchaseWithIapkitProps.hpp +3 -0
  38. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/NitroPurchaseRequest.kt +8 -3
  39. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/NitroPurchaseRequestType.kt +23 -0
  40. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/NitroVerifyPurchaseWithIapkitHorizonProps.kt +56 -0
  41. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/NitroVerifyPurchaseWithIapkitProps.kt +7 -2
  42. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/Variant_NullType_NitroVerifyPurchaseWithIapkitHorizonProps.kt +62 -0
  43. package/nitrogen/generated/ios/NitroIap-Swift-Cxx-Bridge.hpp +65 -0
  44. package/nitrogen/generated/ios/NitroIap-Swift-Cxx-Umbrella.hpp +6 -0
  45. package/nitrogen/generated/ios/c++/HybridRnIapSpecSwift.hpp +6 -0
  46. package/nitrogen/generated/ios/swift/NitroPurchaseRequest.swift +13 -2
  47. package/nitrogen/generated/ios/swift/NitroPurchaseRequestType.swift +40 -0
  48. package/nitrogen/generated/ios/swift/NitroVerifyPurchaseWithIapkitHorizonProps.swift +66 -0
  49. package/nitrogen/generated/ios/swift/NitroVerifyPurchaseWithIapkitProps.swift +38 -1
  50. package/nitrogen/generated/ios/swift/Variant_NullType_NitroVerifyPurchaseWithIapkitHorizonProps.swift +30 -0
  51. package/nitrogen/generated/shared/c++/NitroPurchaseRequest.hpp +9 -2
  52. package/nitrogen/generated/shared/c++/NitroPurchaseRequestType.hpp +76 -0
  53. package/nitrogen/generated/shared/c++/NitroVerifyPurchaseWithIapkitHorizonProps.hpp +90 -0
  54. package/nitrogen/generated/shared/c++/NitroVerifyPurchaseWithIapkitProps.hpp +8 -1
  55. package/openiap-versions.json +3 -3
  56. package/package.json +1 -1
  57. package/src/hooks/useIAP.ts +96 -58
  58. package/src/index.kepler.ts +1 -0
  59. package/src/index.ts +248 -195
  60. package/src/specs/RnIap.nitro.ts +12 -0
  61. package/src/types.ts +12 -2
  62. package/src/utils/error.ts +7 -3
  63. package/src/vega-adapter.ts +9 -5
@@ -78,13 +78,53 @@ class OpenIapException(private val errorJson: String, cause: Throwable? = null)
78
78
  }
79
79
  }
80
80
 
81
+ internal fun parseOpenIapError(err: Throwable): OpenIapError {
82
+ var cause: Throwable? = err
83
+ while (cause != null) {
84
+ if (cause is OpenIapError) return cause
85
+ cause = cause.cause
86
+ }
87
+
88
+ cause = err
89
+ while (cause != null) {
90
+ val message = cause.message ?: ""
91
+ when {
92
+ message.contains("not prepared", ignoreCase = true) ||
93
+ message.contains("not initialized", ignoreCase = true) -> return OpenIapError.NotPrepared
94
+ message.contains("developer error", ignoreCase = true) ||
95
+ message.contains("activity not available", ignoreCase = true) -> return OpenIapError.DeveloperError()
96
+ message.contains("network", ignoreCase = true) -> return OpenIapError.NetworkError
97
+ message.contains("service unavailable", ignoreCase = true) ||
98
+ message.contains("billing unavailable", ignoreCase = true) -> return OpenIapError.ServiceUnavailable()
99
+ }
100
+ cause = cause.cause
101
+ }
102
+ return OpenIapError.ServiceUnavailable()
103
+ }
104
+
105
+ internal fun normalizeAvailablePurchasesType(type: NitroAvailablePurchasesAndroidType?): String? =
106
+ when (type) {
107
+ NitroAvailablePurchasesAndroidType.IN_APP -> "in-app"
108
+ NitroAvailablePurchasesAndroidType.SUBS -> "subs"
109
+ null -> null
110
+ }
111
+
112
+ internal fun rejectDisconnectedPurchase(
113
+ isInitialized: Boolean,
114
+ sendError: (OpenIapError) -> Unit,
115
+ ): Boolean {
116
+ if (isInitialized) return false
117
+ sendError(OpenIapError.NotPrepared)
118
+ return true
119
+ }
120
+
81
121
  internal suspend fun endRnConnectionWithCleanup(
82
122
  endConnection: suspend () -> Boolean,
83
123
  cleanup: () -> Unit,
84
- ): Boolean = try {
85
- endConnection()
86
- } finally {
124
+ ): Boolean {
125
+ val result = endConnection()
87
126
  cleanup()
127
+ return result
88
128
  }
89
129
 
90
130
  /**
@@ -139,6 +179,17 @@ internal fun requireMatchingPurchaseOptions(
139
179
  }
140
180
  }
141
181
 
182
+ internal inline fun <T> catchNonCancellation(
183
+ block: () -> T,
184
+ onFailure: (Exception) -> T,
185
+ ): T = try {
186
+ block()
187
+ } catch (error: CancellationException) {
188
+ throw error
189
+ } catch (error: Exception) {
190
+ onFailure(error)
191
+ }
192
+
142
193
  /**
143
194
  * Bound for the pending purchase-event queues, matching expo-iap's
144
195
  * ExpoIapHelper.MAX_BUFFERED_EVENTS. Oldest entries are dropped on overflow.
@@ -183,6 +234,7 @@ class HybridRnIap : HybridRnIapSpec() {
183
234
  private val developerProvidedBillingListenersAndroid = mutableListOf<(DeveloperProvidedBillingDetailsAndroid) -> Unit>()
184
235
  private val subscriptionBillingIssueListeners = mutableListOf<(NitroPurchase) -> Unit>()
185
236
  private var listenersAttached = false
237
+ @Volatile
186
238
  private var isInitialized = false
187
239
  private val connectionLifecycleQueue = ConnectionLifecycleQueue()
188
240
 
@@ -206,15 +258,10 @@ class HybridRnIap : HybridRnIapSpec() {
206
258
  private fun Variant_NullType_Double?.unwrapDouble(): Double? = (this as? Variant_NullType_Double.Second)?.value
207
259
  private fun Variant_NullType_Boolean?.unwrapBool(): Boolean? = (this as? Variant_NullType_Boolean.Second)?.value
208
260
 
209
- private suspend fun ensureConnection() {
210
- initConnection(null as Variant_NullType_InitConnectionConfig?).await()
211
- }
212
-
213
261
  override fun initConnection(config: Variant_NullType_InitConnectionConfig?): Promise<Boolean> {
214
262
  val configValue = (config as? Variant_NullType_InitConnectionConfig.Second)?.value
215
263
  val performInit: suspend () -> Boolean = initOperation@{
216
264
  RnIapLog.payload("initConnection", configValue)
217
- if (isInitialized) return@initOperation true
218
265
 
219
266
  // CRITICAL: Set Activity BEFORE calling initConnection
220
267
  // Horizon SDK needs Activity to initialize OVRPlatform with proper returnComponent
@@ -236,7 +283,7 @@ class HybridRnIap : HybridRnIapSpec() {
236
283
  }
237
284
  } catch (err: CancellationException) {
238
285
  throw err
239
- } catch (err: Throwable) {
286
+ } catch (err: Exception) {
240
287
  val error = OpenIapError.InitConnection
241
288
  val errorMessage = err.message ?: err.javaClass.name
242
289
  RnIapLog.failure("initConnection.setActivity", err)
@@ -337,7 +384,7 @@ class HybridRnIap : HybridRnIapSpec() {
337
384
  }
338
385
  } catch (err: CancellationException) {
339
386
  throw err
340
- } catch (err: Throwable) {
387
+ } catch (err: Exception) {
341
388
  listenersAttached = false
342
389
  val error = OpenIapError.InitConnection
343
390
  val errorMessage = err.message ?: err.javaClass.name
@@ -374,7 +421,7 @@ class HybridRnIap : HybridRnIapSpec() {
374
421
  }
375
422
  } catch (err: CancellationException) {
376
423
  throw err
377
- } catch (err: Throwable) {
424
+ } catch (err: Exception) {
378
425
  val error = OpenIapError.InitConnection
379
426
  RnIapLog.failure("initConnection.native", err)
380
427
  throw OpenIapException(
@@ -470,7 +517,6 @@ class HybridRnIap : HybridRnIapSpec() {
470
517
  throw OpenIapException(toErrorJson(OpenIapError.EmptySkuList))
471
518
  }
472
519
 
473
- ensureConnection()
474
520
 
475
521
  val queryType = parseProductQueryType(type)
476
522
  val skusList = skus.toList()
@@ -545,9 +591,15 @@ class HybridRnIap : HybridRnIapSpec() {
545
591
  return@async defaultResult
546
592
  }
547
593
 
594
+ if (rejectDisconnectedPurchase(isInitialized) { error ->
595
+ RnIapLog.warn("requestPurchase called before initConnection")
596
+ sendPurchaseError(toErrorResult(error))
597
+ }) {
598
+ return@async defaultResult
599
+ }
600
+
548
601
  var reachedOpenIapRequest = false
549
602
  try {
550
- ensureConnection()
551
603
 
552
604
  // Ensure Activity is available for purchase flow
553
605
  val activity = withContext(Dispatchers.Main) {
@@ -565,29 +617,36 @@ class HybridRnIap : HybridRnIapSpec() {
565
617
  openIap.setActivity(activity)
566
618
  }
567
619
 
568
- val missingSkus = androidRequest.skus.filterNot { productTypeBySku.containsKey(it) }
569
- if (missingSkus.isNotEmpty()) {
620
+ val queryType = request.type?.let { requestType ->
621
+ when (requestType) {
622
+ NitroPurchaseRequestType.SUBS -> ProductQueryType.Subs
623
+ NitroPurchaseRequestType.IN_APP -> ProductQueryType.InApp
624
+ }
625
+ } ?: run {
626
+ val missingSkus = androidRequest.skus.filterNot { productTypeBySku.containsKey(it) }
570
627
  missingSkus.forEach { sku ->
571
- RnIapLog.warn("requestPurchase missing cached type for $sku; attempting fetch")
572
- val fetched = runCatching {
573
- openIap.fetchProducts(
574
- ProductRequest(listOf(sku), ProductQueryType.All)
575
- ).productsOrEmpty()
576
- }.getOrElse { error ->
577
- RnIapLog.failure("requestPurchase.fetchMissing", error)
578
- emptyList()
579
- }
628
+ RnIapLog.warn("requestPurchase missing type hint for $sku; attempting fetch")
629
+ val fetched = catchNonCancellation(
630
+ block = {
631
+ openIap.fetchProducts(
632
+ ProductRequest(listOf(sku), ProductQueryType.All)
633
+ ).productsOrEmpty()
634
+ },
635
+ onFailure = { error ->
636
+ if (error is OpenIapError) throw error
637
+ RnIapLog.failure("requestPurchase.fetchMissing", error)
638
+ emptyList()
639
+ },
640
+ )
580
641
  fetched.firstOrNull()?.let { productTypeBySku[it.id] = it.type.rawValue }
581
642
  if (!productTypeBySku.containsKey(sku)) {
582
643
  sendPurchaseError(toErrorResult(OpenIapError.SkuNotFound(sku)))
583
644
  return@async defaultResult
584
645
  }
585
646
  }
647
+ parseProductQueryType(productTypeBySku[androidRequest.skus.first()] ?: "in-app")
586
648
  }
587
649
 
588
- val typeHint = androidRequest.skus.firstOrNull()?.let { productTypeBySku[it] } ?: "in-app"
589
- val queryType = parseProductQueryType(typeHint)
590
-
591
650
  try {
592
651
  requireMatchingPurchaseOptions(
593
652
  type = queryType,
@@ -697,6 +756,12 @@ class HybridRnIap : HybridRnIapSpec() {
697
756
  defaultResult
698
757
  } catch (e: CancellationException) {
699
758
  throw e
759
+ } catch (e: OpenIapError) {
760
+ RnIapLog.failure("requestPurchase", e)
761
+ if (!reachedOpenIapRequest) {
762
+ sendPurchaseError(toErrorResult(error = e, debugMessage = e.message))
763
+ }
764
+ defaultResult
700
765
  } catch (e: Exception) {
701
766
  RnIapLog.failure("requestPurchase", e)
702
767
  if (!reachedOpenIapRequest) {
@@ -716,54 +781,53 @@ class HybridRnIap : HybridRnIapSpec() {
716
781
  // Purchase history methods (Unified)
717
782
  override fun getAvailablePurchases(options: NitroAvailablePurchasesOptions?): Promise<Array<NitroPurchase>> {
718
783
  return Promise.async {
719
- val androidOptions = (options?.android as? Variant_NullType_NitroAvailablePurchasesAndroidOptions.Second)?.value
720
- ensureConnection()
784
+ try {
785
+ val androidOptions = (options?.android as? Variant_NullType_NitroAvailablePurchasesAndroidOptions.Second)?.value
721
786
 
722
- val includeSuspended = androidOptions?.includeSuspended.unwrapBool() ?: false
787
+ val includeSuspended = androidOptions?.includeSuspended.unwrapBool() ?: false
723
788
 
724
- RnIapLog.payload(
725
- "getAvailablePurchases",
726
- mapOf("type" to androidOptions?.type?.name, "includeSuspended" to includeSuspended)
727
- )
789
+ RnIapLog.payload(
790
+ "getAvailablePurchases",
791
+ mapOf("type" to androidOptions?.type?.name, "includeSuspended" to includeSuspended)
792
+ )
728
793
 
729
- val typeName = androidOptions?.type?.name?.lowercase(java.util.Locale.ROOT)
730
- val normalizedType = when (typeName) {
731
- "in-app", "subs" -> typeName
732
- else -> null
733
- }
794
+ val normalizedType = normalizeAvailablePurchasesType(androidOptions?.type)
734
795
 
735
- // Create PurchaseOptions with includeSuspendedAndroid
736
- val purchaseOptions = dev.hyo.openiap.PurchaseOptions(
737
- includeSuspendedAndroid = includeSuspended
738
- )
796
+ // Create PurchaseOptions with includeSuspendedAndroid
797
+ val purchaseOptions = dev.hyo.openiap.PurchaseOptions(
798
+ includeSuspendedAndroid = includeSuspended
799
+ )
739
800
 
740
- val result: List<OpenIapPurchase> = if (normalizedType != null) {
741
- val typeEnum = parseProductQueryType(normalizedType)
742
- RnIapLog.payload(
743
- "getAvailablePurchases.native",
744
- mapOf("type" to typeEnum.rawValue, "includeSuspended" to includeSuspended)
801
+ val result: List<OpenIapPurchase> = if (normalizedType != null) {
802
+ val typeEnum = parseProductQueryType(normalizedType)
803
+ RnIapLog.payload(
804
+ "getAvailablePurchases.native",
805
+ mapOf("type" to typeEnum.rawValue, "includeSuspended" to includeSuspended)
806
+ )
807
+ // Note: getAvailableItems doesn't accept PurchaseOptions
808
+ // includeSuspended only applies when fetching all types
809
+ openIap.getAvailableItems(typeEnum)
810
+ } else {
811
+ RnIapLog.payload("getAvailablePurchases.native", mapOf("type" to "all", "includeSuspended" to includeSuspended))
812
+ openIap.getAvailablePurchases(purchaseOptions)
813
+ }
814
+ RnIapLog.result(
815
+ "getAvailablePurchases",
816
+ mapOf(
817
+ "purchaseCount" to result.size,
818
+ "productIds" to result.map { it.productId }
819
+ )
745
820
  )
746
- // Note: getAvailableItems doesn't accept PurchaseOptions
747
- // includeSuspended only applies when fetching all types
748
- openIap.getAvailableItems(typeEnum)
749
- } else {
750
- RnIapLog.payload("getAvailablePurchases.native", mapOf("type" to "all", "includeSuspended" to includeSuspended))
751
- openIap.getAvailablePurchases(purchaseOptions)
821
+ result.map { convertToNitroPurchase(it) }.toTypedArray()
822
+ } catch (e: OpenIapError) {
823
+ RnIapLog.failure("getAvailablePurchases", e)
824
+ throw OpenIapException(toErrorJson(e), e)
752
825
  }
753
- RnIapLog.result(
754
- "getAvailablePurchases",
755
- mapOf(
756
- "purchaseCount" to result.size,
757
- "productIds" to result.map { it.productId }
758
- )
759
- )
760
- result.map { convertToNitroPurchase(it) }.toTypedArray()
761
826
  }
762
827
  }
763
828
 
764
829
  override fun getActiveSubscriptions(subscriptionIds: Array<String>?): Promise<Array<NitroActiveSubscription>> {
765
830
  return Promise.async {
766
- ensureConnection()
767
831
 
768
832
  RnIapLog.payload(
769
833
  "getActiveSubscriptions",
@@ -771,7 +835,6 @@ class HybridRnIap : HybridRnIapSpec() {
771
835
  )
772
836
 
773
837
  try {
774
- // Use OpenIapModule's native getActiveSubscriptions method
775
838
  RnIapLog.payload("getActiveSubscriptions.native", mapOf("type" to "subs"))
776
839
  val activeSubscriptions = openIap.getActiveSubscriptions(subscriptionIds?.toList())
777
840
 
@@ -801,6 +864,11 @@ class HybridRnIap : HybridRnIapSpec() {
801
864
  )
802
865
 
803
866
  nitroSubscriptions.toTypedArray()
867
+ } catch (e: CancellationException) {
868
+ throw e
869
+ } catch (e: OpenIapError) {
870
+ RnIapLog.failure("getActiveSubscriptions", e)
871
+ throw OpenIapException(toErrorJson(e))
804
872
  } catch (e: Exception) {
805
873
  RnIapLog.failure("getActiveSubscriptions", e)
806
874
  val error = OpenIapError.ServiceUnavailable()
@@ -817,7 +885,6 @@ class HybridRnIap : HybridRnIapSpec() {
817
885
 
818
886
  override fun hasActiveSubscriptions(subscriptionIds: Array<String>?): Promise<Boolean> {
819
887
  return Promise.async {
820
- ensureConnection()
821
888
 
822
889
  RnIapLog.payload(
823
890
  "hasActiveSubscriptions",
@@ -828,6 +895,11 @@ class HybridRnIap : HybridRnIapSpec() {
828
895
  val hasActive = openIap.hasActiveSubscriptions(subscriptionIds?.toList())
829
896
  RnIapLog.result("hasActiveSubscriptions", hasActive)
830
897
  hasActive
898
+ } catch (e: CancellationException) {
899
+ throw e
900
+ } catch (e: OpenIapError) {
901
+ RnIapLog.failure("hasActiveSubscriptions", e)
902
+ throw OpenIapException(toErrorJson(e))
831
903
  } catch (e: Exception) {
832
904
  RnIapLog.failure("hasActiveSubscriptions", e)
833
905
  val error = OpenIapError.ServiceUnavailable()
@@ -877,27 +949,6 @@ class HybridRnIap : HybridRnIapSpec() {
877
949
  )
878
950
  }
879
951
 
880
- // Ensure connection; if it fails, return an error result instead of throwing
881
- try {
882
- ensureConnection()
883
- } catch (e: Exception) {
884
- val err = OpenIapError.InitConnection
885
- return@async Variant_Boolean_NitroPurchaseResult.Second(
886
- NitroPurchaseResult(
887
- responseCode = -1.0,
888
- debugMessage = e.message,
889
- code = OpenIapError.toCode(err),
890
- message = e.message?.takeIf { it.isNotBlank() } ?: err.message,
891
- purchaseToken = purchaseToken,
892
- productId = null,
893
- productIds = null,
894
- productType = null,
895
- isEmptyProductList = null,
896
- subResponseCodeAndroid = null
897
- )
898
- )
899
- }
900
-
901
952
  try {
902
953
  if (isConsumable) {
903
954
  openIap.consumePurchaseAndroid(purchaseToken)
@@ -920,6 +971,24 @@ class HybridRnIap : HybridRnIapSpec() {
920
971
  )
921
972
  RnIapLog.result("finishTransaction", mapOf("success" to true))
922
973
  result
974
+ } catch (e: CancellationException) {
975
+ throw e
976
+ } catch (e: OpenIapError) {
977
+ RnIapLog.failure("finishTransaction", e)
978
+ Variant_Boolean_NitroPurchaseResult.Second(
979
+ NitroPurchaseResult(
980
+ responseCode = -1.0,
981
+ debugMessage = e.message,
982
+ code = OpenIapError.toCode(e),
983
+ message = e.message,
984
+ purchaseToken = purchaseToken,
985
+ productId = null,
986
+ productIds = null,
987
+ productType = null,
988
+ isEmptyProductList = null,
989
+ subResponseCodeAndroid = null
990
+ )
991
+ )
923
992
  } catch (e: Exception) {
924
993
  val err = OpenIapError.BillingError()
925
994
  RnIapLog.failure("finishTransaction", e)
@@ -944,7 +1013,6 @@ class HybridRnIap : HybridRnIapSpec() {
944
1013
  override fun getStorefront(): Promise<String> {
945
1014
  return Promise.async {
946
1015
  try {
947
- ensureConnection()
948
1016
  RnIapLog.payload("getStorefront", null)
949
1017
  val value = openIap.getStorefront()
950
1018
  if (value.isBlank()) {
@@ -954,6 +1022,8 @@ class HybridRnIap : HybridRnIapSpec() {
954
1022
  }
955
1023
  RnIapLog.result("getStorefront", value)
956
1024
  value
1025
+ } catch (e: CancellationException) {
1026
+ throw e
957
1027
  } catch (e: OpenIapException) {
958
1028
  RnIapLog.failure("getStorefront", e)
959
1029
  throw e
@@ -1400,7 +1470,6 @@ class HybridRnIap : HybridRnIapSpec() {
1400
1470
  override fun deepLinkToSubscriptionsAndroid(options: NitroDeepLinkOptionsAndroid): Promise<Unit> {
1401
1471
  return Promise.async {
1402
1472
  try {
1403
- ensureConnection()
1404
1473
  OpenIapDeepLinkOptions(
1405
1474
  skuAndroid = options.skuAndroid.unwrapString(),
1406
1475
  packageNameAndroid = options.packageNameAndroid.unwrapString()
@@ -1568,6 +1637,8 @@ class HybridRnIap : HybridRnIapSpec() {
1568
1637
 
1569
1638
  Variant_NitroPurchaseVerificationResultIOS_NitroPurchaseVerificationResultAndroid_NitroPurchaseVerificationResultHorizon.Second(result)
1570
1639
 
1640
+ } catch (e: CancellationException) {
1641
+ throw e
1571
1642
  } catch (e: OpenIapException) {
1572
1643
  RnIapLog.failure("verifyPurchase", e)
1573
1644
  throw e
@@ -1607,6 +1678,11 @@ class HybridRnIap : HybridRnIapSpec() {
1607
1678
  (iapkit.google as? Variant_NullType_NitroVerifyPurchaseWithIapkitGoogleProps.Second)?.value?.let { google ->
1608
1679
  iapkitMap["google"] = mapOf("purchaseToken" to google.purchaseToken)
1609
1680
  }
1681
+ (iapkit.horizon as? Variant_NullType_NitroVerifyPurchaseWithIapkitHorizonProps.Second)?.value?.let { horizon ->
1682
+ val horizonMap = mutableMapOf<String, Any?>("sku" to horizon.sku)
1683
+ horizon.userId.unwrapString()?.let { horizonMap["userId"] = it }
1684
+ iapkitMap["horizon"] = horizonMap
1685
+ }
1610
1686
  (iapkit.amazon as? Variant_NullType_NitroVerifyPurchaseWithIapkitAmazonProps.Second)?.value?.let { amazon ->
1611
1687
  val amazonMap = mutableMapOf<String, Any?>(
1612
1688
  "receiptId" to amazon.receiptId
@@ -1673,6 +1749,8 @@ class HybridRnIap : HybridRnIapSpec() {
1673
1749
  errors = nitroErrors?.let { Variant_NullType_Array_NitroVerifyPurchaseWithProviderError_.Second(it) },
1674
1750
  provider = mapPurchaseVerificationProvider(result.provider.rawValue)
1675
1751
  )
1752
+ } catch (e: CancellationException) {
1753
+ throw e
1676
1754
  } catch (e: Exception) {
1677
1755
  RnIapLog.failure("verifyPurchaseWithProvider", e)
1678
1756
  val error = OpenIapError.VerificationFailed
@@ -1764,9 +1842,7 @@ class HybridRnIap : HybridRnIapSpec() {
1764
1842
  }
1765
1843
 
1766
1844
  override fun removeUserChoiceBillingListenerAndroid(listener: (UserChoiceBillingDetails) -> Unit) {
1767
- synchronized(userChoiceBillingListenersAndroid) {
1768
- userChoiceBillingListenersAndroid.remove(listener)
1769
- }
1845
+ removeSingletonBridgeListener(userChoiceBillingListenersAndroid, listener)
1770
1846
  }
1771
1847
 
1772
1848
  private fun sendUserChoiceBilling(details: UserChoiceBillingDetails) {
@@ -1782,9 +1858,7 @@ class HybridRnIap : HybridRnIapSpec() {
1782
1858
  }
1783
1859
 
1784
1860
  override fun removeDeveloperProvidedBillingListenerAndroid(listener: (DeveloperProvidedBillingDetailsAndroid) -> Unit) {
1785
- synchronized(developerProvidedBillingListenersAndroid) {
1786
- developerProvidedBillingListenersAndroid.remove(listener)
1787
- }
1861
+ removeSingletonBridgeListener(developerProvidedBillingListenersAndroid, listener)
1788
1862
  }
1789
1863
 
1790
1864
  private fun sendDeveloperProvidedBilling(details: DeveloperProvidedBillingDetailsAndroid) {
@@ -1813,9 +1887,7 @@ class HybridRnIap : HybridRnIapSpec() {
1813
1887
 
1814
1888
  override fun removeSubscriptionBillingIssueListener(listener: (purchase: NitroPurchase) -> Unit) {
1815
1889
  synchronized(subscriptionBillingIssueAttachLock) {
1816
- synchronized(subscriptionBillingIssueListeners) {
1817
- subscriptionBillingIssueListeners.remove(listener)
1818
- }
1890
+ removeSingletonBridgeListener(subscriptionBillingIssueListeners, listener)
1819
1891
  }
1820
1892
  }
1821
1893
 
@@ -1865,21 +1937,22 @@ class HybridRnIap : HybridRnIapSpec() {
1865
1937
 
1866
1938
  override fun enableBillingProgramAndroid(program: BillingProgramAndroid) {
1867
1939
  RnIapLog.payload("enableBillingProgramAndroid", mapOf("program" to program.name))
1868
- try {
1869
- val openIapProgram = mapBillingProgram(program)
1870
- openIapStore.enableBillingProgram(openIapProgram)
1871
- RnIapLog.result("enableBillingProgramAndroid", true)
1872
- } catch (err: Throwable) {
1873
- RnIapLog.failure("enableBillingProgramAndroid", err)
1874
- // enableBillingProgram is void, so we just log the error
1875
- }
1940
+ catchNonCancellation(
1941
+ block = {
1942
+ val openIapProgram = mapBillingProgram(program)
1943
+ openIapStore.enableBillingProgram(openIapProgram)
1944
+ RnIapLog.result("enableBillingProgramAndroid", true)
1945
+ },
1946
+ onFailure = { err ->
1947
+ RnIapLog.failure("enableBillingProgramAndroid", err)
1948
+ },
1949
+ )
1876
1950
  }
1877
1951
 
1878
1952
  override fun isBillingProgramAvailableAndroid(program: BillingProgramAndroid): Promise<NitroBillingProgramAvailabilityResultAndroid> {
1879
1953
  return Promise.async {
1880
1954
  RnIapLog.payload("isBillingProgramAvailableAndroid", mapOf("program" to program.name))
1881
1955
  try {
1882
- ensureConnection()
1883
1956
  val openIapProgram = mapBillingProgram(program)
1884
1957
  val result = openIapStore.isBillingProgramAvailable(openIapProgram)
1885
1958
  val nitroResult = NitroBillingProgramAvailabilityResultAndroid(
@@ -1890,7 +1963,9 @@ class HybridRnIap : HybridRnIapSpec() {
1890
1963
  )
1891
1964
  RnIapLog.result("isBillingProgramAvailableAndroid", mapOf("isAvailable" to result.isAvailable))
1892
1965
  nitroResult
1893
- } catch (err: Throwable) {
1966
+ } catch (err: CancellationException) {
1967
+ throw err
1968
+ } catch (err: Exception) {
1894
1969
  RnIapLog.failure("isBillingProgramAvailableAndroid", err)
1895
1970
  val errorType = parseOpenIapError(err)
1896
1971
  throw OpenIapException(toErrorJson(errorType, debugMessage = err.message), err)
@@ -1906,7 +1981,6 @@ class HybridRnIap : HybridRnIapSpec() {
1906
1981
  "userLocale" to params.userLocale.unwrapString()
1907
1982
  ))
1908
1983
  try {
1909
- ensureConnection()
1910
1984
  val result = openIapStore.getBillingChoiceInfo(
1911
1985
  OpenIapGetBillingChoiceInfoParams(
1912
1986
  billingProgram = mapBillingProgram(params.billingProgram),
@@ -1920,7 +1994,9 @@ class HybridRnIap : HybridRnIapSpec() {
1920
1994
  )
1921
1995
  RnIapLog.result("getBillingChoiceInfoAndroid", mapOf("hasImageUrl" to result.playBillingChoiceImageUrl.isNotBlank()))
1922
1996
  nitroResult
1923
- } catch (err: Throwable) {
1997
+ } catch (err: CancellationException) {
1998
+ throw err
1999
+ } catch (err: Exception) {
1924
2000
  RnIapLog.failure("getBillingChoiceInfoAndroid", err)
1925
2001
  val errorType = parseOpenIapError(err)
1926
2002
  throw OpenIapException(toErrorJson(errorType, debugMessage = err.message), err)
@@ -1938,7 +2014,6 @@ class HybridRnIap : HybridRnIapSpec() {
1938
2014
  mapOf("program" to program.name, "developerBillingType" to developerBillingType?.name)
1939
2015
  )
1940
2016
  try {
1941
- ensureConnection()
1942
2017
  val openIapProgram = mapBillingProgram(program)
1943
2018
  val result = openIapStore.createBillingProgramReportingDetails(
1944
2019
  openIapProgram,
@@ -1950,7 +2025,9 @@ class HybridRnIap : HybridRnIapSpec() {
1950
2025
  )
1951
2026
  RnIapLog.result("createBillingProgramReportingDetailsAndroid", mapOf("hasToken" to true))
1952
2027
  nitroResult
1953
- } catch (err: Throwable) {
2028
+ } catch (err: CancellationException) {
2029
+ throw err
2030
+ } catch (err: Exception) {
1954
2031
  RnIapLog.failure("createBillingProgramReportingDetailsAndroid", err)
1955
2032
  val errorType = parseOpenIapError(err)
1956
2033
  throw OpenIapException(toErrorJson(errorType, debugMessage = err.message), err)
@@ -1964,7 +2041,6 @@ class HybridRnIap : HybridRnIapSpec() {
1964
2041
  return Promise.async {
1965
2042
  RnIapLog.payload("showBillingProgramInformationDialogAndroid", mapOf("program" to params.billingProgram.name))
1966
2043
  try {
1967
- ensureConnection()
1968
2044
  val activity = withContext(Dispatchers.Main) {
1969
2045
  runCatching { context.currentActivity }.getOrNull()
1970
2046
  } ?: throw OpenIapException(toErrorJson(OpenIapError.DeveloperError(), debugMessage = "Activity not available"))
@@ -1983,7 +2059,9 @@ class HybridRnIap : HybridRnIapSpec() {
1983
2059
  debugMessage = result.debugMessage.wrapVariant(),
1984
2060
  subResponseCode = mapSubResponseCode(result.subResponseCode)
1985
2061
  )
1986
- } catch (err: Throwable) {
2062
+ } catch (err: CancellationException) {
2063
+ throw err
2064
+ } catch (err: Exception) {
1987
2065
  RnIapLog.failure("showBillingProgramInformationDialogAndroid", err)
1988
2066
  val errorType = parseOpenIapError(err)
1989
2067
  throw OpenIapException(toErrorJson(errorType, debugMessage = err.message), err)
@@ -1999,7 +2077,6 @@ class HybridRnIap : HybridRnIapSpec() {
1999
2077
  val categories = messageParams?.categories?.asSecondOrNull()
2000
2078
  RnIapLog.payload("showInAppMessagesAndroid", mapOf("categories" to categories?.map { it.name }))
2001
2079
  try {
2002
- ensureConnection()
2003
2080
  val activity = withContext(Dispatchers.Main) {
2004
2081
  runCatching { context.currentActivity }.getOrNull()
2005
2082
  } ?: throw OpenIapException(toErrorJson(OpenIapError.DeveloperError(), debugMessage = "Activity not available"))
@@ -2018,7 +2095,9 @@ class HybridRnIap : HybridRnIapSpec() {
2018
2095
  responseCode = mapInAppMessageResponseCode(result.responseCode),
2019
2096
  purchaseToken = result.purchaseToken.wrapVariant()
2020
2097
  )
2021
- } catch (err: Throwable) {
2098
+ } catch (err: CancellationException) {
2099
+ throw err
2100
+ } catch (err: Exception) {
2022
2101
  RnIapLog.failure("showInAppMessagesAndroid", err)
2023
2102
  val errorType = parseOpenIapError(err)
2024
2103
  throw OpenIapException(toErrorJson(errorType, debugMessage = err.message), err)
@@ -2035,7 +2114,6 @@ class HybridRnIap : HybridRnIapSpec() {
2035
2114
  "linkUri" to params.linkUri
2036
2115
  ))
2037
2116
  try {
2038
- ensureConnection()
2039
2117
 
2040
2118
  val activity = withContext(Dispatchers.Main) {
2041
2119
  runCatching { context.currentActivity }.getOrNull()
@@ -2054,7 +2132,9 @@ class HybridRnIap : HybridRnIapSpec() {
2054
2132
  }
2055
2133
  RnIapLog.result("launchExternalLinkAndroid", result)
2056
2134
  result
2057
- } catch (err: Throwable) {
2135
+ } catch (err: CancellationException) {
2136
+ throw err
2137
+ } catch (err: Exception) {
2058
2138
  RnIapLog.failure("launchExternalLinkAndroid", err)
2059
2139
  val errorType = parseOpenIapError(err)
2060
2140
  throw OpenIapException(toErrorJson(errorType, debugMessage = err.message))
@@ -2074,7 +2154,9 @@ class HybridRnIap : HybridRnIapSpec() {
2074
2154
  val result = handler()
2075
2155
  RnIapLog.result("openRedeemOfferCodeAndroid", result)
2076
2156
  result
2077
- } catch (err: Throwable) {
2157
+ } catch (err: CancellationException) {
2158
+ throw err
2159
+ } catch (err: Exception) {
2078
2160
  RnIapLog.failure("openRedeemOfferCodeAndroid", err)
2079
2161
  val errorType = parseOpenIapError(err)
2080
2162
  throw OpenIapException(toErrorJson(errorType, debugMessage = err.message), err)
@@ -2230,30 +2312,6 @@ class HybridRnIap : HybridRnIapSpec() {
2230
2312
  }
2231
2313
  }
2232
2314
 
2233
- // ---------------------------------------------------------------------
2234
- // OpenIAP error helpers: unify error codes/messages from library
2235
- // ---------------------------------------------------------------------
2236
- private fun parseOpenIapError(err: Throwable): OpenIapError {
2237
- // Try to extract OpenIapError from the exception chain
2238
- var cause: Throwable? = err
2239
- while (cause != null) {
2240
- val message = cause.message ?: ""
2241
- // Check if message contains OpenIAP error patterns
2242
- when {
2243
- message.contains("not prepared", ignoreCase = true) ||
2244
- message.contains("not initialized", ignoreCase = true) -> return OpenIapError.NotPrepared
2245
- message.contains("developer error", ignoreCase = true) ||
2246
- message.contains("activity not available", ignoreCase = true) -> return OpenIapError.DeveloperError()
2247
- message.contains("network", ignoreCase = true) -> return OpenIapError.NetworkError
2248
- message.contains("service unavailable", ignoreCase = true) ||
2249
- message.contains("billing unavailable", ignoreCase = true) -> return OpenIapError.ServiceUnavailable()
2250
- }
2251
- cause = cause.cause
2252
- }
2253
- // Default to ServiceUnavailable if we can't determine the error type
2254
- return OpenIapError.ServiceUnavailable()
2255
- }
2256
-
2257
2315
  private fun toErrorJson(
2258
2316
  error: OpenIapError,
2259
2317
  productId: String? = null,
@@ -35,6 +35,5 @@ internal class TokenizedListenerRegistry<T> {
35
35
  @Synchronized
36
36
  fun clear() {
37
37
  registrations.clear()
38
- nextToken = 1.0
39
38
  }
40
39
  }