react-native-iap 15.1.0 → 15.2.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.
Files changed (96) hide show
  1. package/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt +171 -114
  2. package/android/src/main/java/com/margelo/nitro/iap/ProductQueryHelpers.kt +42 -0
  3. package/android/src/test/java/com/margelo/nitro/iap/ProductQueryHelpersTest.kt +140 -0
  4. package/ios/HybridRnIap.swift +72 -1
  5. package/lib/module/hooks/useIAP.js +11 -1
  6. package/lib/module/hooks/useIAP.js.map +1 -1
  7. package/lib/module/hooks/useWebhookEvents.js +113 -0
  8. package/lib/module/hooks/useWebhookEvents.js.map +1 -0
  9. package/lib/module/index.js +405 -131
  10. package/lib/module/index.js.map +1 -1
  11. package/lib/module/kit-api.js +161 -0
  12. package/lib/module/kit-api.js.map +1 -0
  13. package/lib/module/types.js +16 -0
  14. package/lib/module/types.js.map +1 -1
  15. package/lib/module/utils/error.js.map +1 -1
  16. package/lib/module/utils/errorMapping.js +6 -0
  17. package/lib/module/utils/errorMapping.js.map +1 -1
  18. package/lib/module/webhook-client.js +164 -0
  19. package/lib/module/webhook-client.js.map +1 -0
  20. package/lib/typescript/plugin/src/withIAP.d.ts +1 -1
  21. package/lib/typescript/src/hooks/useIAP.d.ts +172 -2
  22. package/lib/typescript/src/hooks/useIAP.d.ts.map +1 -1
  23. package/lib/typescript/src/hooks/useWebhookEvents.d.ts +55 -0
  24. package/lib/typescript/src/hooks/useWebhookEvents.d.ts.map +1 -0
  25. package/lib/typescript/src/index.d.ts +283 -129
  26. package/lib/typescript/src/index.d.ts.map +1 -1
  27. package/lib/typescript/src/kit-api.d.ts +54 -0
  28. package/lib/typescript/src/kit-api.d.ts.map +1 -0
  29. package/lib/typescript/src/specs/RnIap.nitro.d.ts +24 -0
  30. package/lib/typescript/src/specs/RnIap.nitro.d.ts.map +1 -1
  31. package/lib/typescript/src/types.d.ts +320 -75
  32. package/lib/typescript/src/types.d.ts.map +1 -1
  33. package/lib/typescript/src/utils/error.d.ts +3 -0
  34. package/lib/typescript/src/utils/error.d.ts.map +1 -1
  35. package/lib/typescript/src/utils/errorMapping.d.ts +6 -0
  36. package/lib/typescript/src/utils/errorMapping.d.ts.map +1 -1
  37. package/lib/typescript/src/webhook-client.d.ts +82 -0
  38. package/lib/typescript/src/webhook-client.d.ts.map +1 -0
  39. package/nitrogen/generated/android/NitroIap+autolinking.cmake +3 -0
  40. package/nitrogen/generated/android/c++/JAdvancedCommerceInfoIOS.hpp +118 -0
  41. package/nitrogen/generated/android/c++/JAdvancedCommerceItemDetailsIOS.hpp +62 -0
  42. package/nitrogen/generated/android/c++/JAdvancedCommerceItemIOS.hpp +78 -0
  43. package/nitrogen/generated/android/c++/JAdvancedCommerceRefundIOS.hpp +62 -0
  44. package/nitrogen/generated/android/c++/JHybridRnIapSpec.cpp +52 -0
  45. package/nitrogen/generated/android/c++/JHybridRnIapSpec.hpp +3 -0
  46. package/nitrogen/generated/android/c++/JPurchase.hpp +11 -0
  47. package/nitrogen/generated/android/c++/JPurchaseIOS.hpp +16 -1
  48. package/nitrogen/generated/android/c++/JRequestPurchaseResult.hpp +11 -0
  49. package/nitrogen/generated/android/c++/JVariant_NullType_AdvancedCommerceInfoIOS.cpp +26 -0
  50. package/nitrogen/generated/android/c++/JVariant_NullType_AdvancedCommerceInfoIOS.hpp +84 -0
  51. package/nitrogen/generated/android/c++/JVariant_NullType_AdvancedCommerceItemDetailsIOS.cpp +26 -0
  52. package/nitrogen/generated/android/c++/JVariant_NullType_AdvancedCommerceItemDetailsIOS.hpp +74 -0
  53. package/nitrogen/generated/android/c++/JVariant_NullType_Array_AdvancedCommerceRefundIOS_.cpp +35 -0
  54. package/nitrogen/generated/android/c++/JVariant_NullType_Array_AdvancedCommerceRefundIOS_.hpp +84 -0
  55. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/AdvancedCommerceInfoIOS.kt +59 -0
  56. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/AdvancedCommerceItemDetailsIOS.kt +38 -0
  57. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/AdvancedCommerceItemIOS.kt +44 -0
  58. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/AdvancedCommerceRefundIOS.kt +38 -0
  59. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/HybridRnIapSpec.kt +22 -0
  60. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/PurchaseIOS.kt +5 -2
  61. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/Variant_NullType_AdvancedCommerceInfoIOS.kt +53 -0
  62. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/Variant_NullType_AdvancedCommerceItemDetailsIOS.kt +53 -0
  63. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/Variant_NullType_Array_AdvancedCommerceRefundIOS_.kt +53 -0
  64. package/nitrogen/generated/ios/NitroIap-Swift-Cxx-Bridge.hpp +166 -0
  65. package/nitrogen/generated/ios/NitroIap-Swift-Cxx-Umbrella.hpp +12 -0
  66. package/nitrogen/generated/ios/c++/HybridRnIapSpecSwift.hpp +32 -0
  67. package/nitrogen/generated/ios/swift/AdvancedCommerceInfoIOS.swift +294 -0
  68. package/nitrogen/generated/ios/swift/AdvancedCommerceItemDetailsIOS.swift +61 -0
  69. package/nitrogen/generated/ios/swift/AdvancedCommerceItemIOS.swift +141 -0
  70. package/nitrogen/generated/ios/swift/AdvancedCommerceRefundIOS.swift +61 -0
  71. package/nitrogen/generated/ios/swift/HybridRnIapSpec.swift +3 -0
  72. package/nitrogen/generated/ios/swift/HybridRnIapSpec_cxx.swift +57 -0
  73. package/nitrogen/generated/ios/swift/PurchaseIOS.swift +39 -2
  74. package/nitrogen/generated/ios/swift/Variant_NullType_AdvancedCommerceInfoIOS.swift +18 -0
  75. package/nitrogen/generated/ios/swift/Variant_NullType_AdvancedCommerceItemDetailsIOS.swift +18 -0
  76. package/nitrogen/generated/ios/swift/Variant_NullType__AdvancedCommerceRefundIOS_.swift +18 -0
  77. package/nitrogen/generated/shared/c++/AdvancedCommerceInfoIOS.hpp +117 -0
  78. package/nitrogen/generated/shared/c++/AdvancedCommerceItemDetailsIOS.hpp +86 -0
  79. package/nitrogen/generated/shared/c++/AdvancedCommerceItemIOS.hpp +99 -0
  80. package/nitrogen/generated/shared/c++/AdvancedCommerceRefundIOS.hpp +86 -0
  81. package/nitrogen/generated/shared/c++/HybridRnIapSpec.cpp +3 -0
  82. package/nitrogen/generated/shared/c++/HybridRnIapSpec.hpp +3 -0
  83. package/nitrogen/generated/shared/c++/PurchaseIOS.hpp +9 -2
  84. package/openiap-versions.json +3 -3
  85. package/package.json +1 -1
  86. package/plugin/build/withIAP.d.ts +1 -1
  87. package/plugin/src/withIAP.ts +1 -1
  88. package/src/hooks/useIAP.ts +185 -2
  89. package/src/hooks/useWebhookEvents.ts +180 -0
  90. package/src/index.ts +440 -130
  91. package/src/kit-api.ts +225 -0
  92. package/src/specs/RnIap.nitro.ts +31 -0
  93. package/src/types.ts +330 -75
  94. package/src/utils/error.ts +3 -0
  95. package/src/utils/errorMapping.ts +12 -0
  96. package/src/webhook-client.ts +312 -0
@@ -10,7 +10,7 @@ import dev.hyo.openiap.FetchProductsResult
10
10
  import dev.hyo.openiap.FetchProductsResultAll
11
11
  import dev.hyo.openiap.FetchProductsResultProducts
12
12
  import dev.hyo.openiap.FetchProductsResultSubscriptions
13
- import dev.hyo.openiap.OpenIapError as OpenIAPError
13
+ import dev.hyo.openiap.OpenIapError
14
14
  import dev.hyo.openiap.OpenIapModule
15
15
  import dev.hyo.openiap.ProductAndroid
16
16
  import dev.hyo.openiap.ProductQueryType
@@ -84,6 +84,7 @@ class HybridRnIap : HybridRnIapSpec() {
84
84
  private val promotedProductListenersIOS = mutableListOf<(NitroProduct) -> Unit>()
85
85
  private val userChoiceBillingListenersAndroid = mutableListOf<(UserChoiceBillingDetails) -> Unit>()
86
86
  private val developerProvidedBillingListenersAndroid = mutableListOf<(DeveloperProvidedBillingDetailsAndroid) -> Unit>()
87
+ private val subscriptionBillingIssueListeners = mutableListOf<(NitroPurchase) -> Unit>()
87
88
  private var listenersAttached = false
88
89
  private var isInitialized = false
89
90
  private var initDeferred: CompletableDeferred<Boolean>? = null
@@ -133,7 +134,7 @@ class HybridRnIap : HybridRnIapSpec() {
133
134
  } catch (err: CancellationException) {
134
135
  throw err
135
136
  } catch (err: Throwable) {
136
- val error = OpenIAPError.InitConnection
137
+ val error = OpenIapError.InitConnection
137
138
  val errorMessage = err.message ?: err.javaClass.name
138
139
  RnIapLog.failure("initConnection.setActivity", err)
139
140
  throw OpenIapException(
@@ -172,22 +173,14 @@ class HybridRnIap : HybridRnIapSpec() {
172
173
  }.onFailure { RnIapLog.failure("purchaseUpdatedListener", it) }
173
174
  })
174
175
  openIap.addPurchaseErrorListener(OpenIapPurchaseErrorListener { e ->
175
- val code = OpenIAPError.toCode(e)
176
- val message = e.message ?: OpenIAPError.defaultMessage(code)
176
+ val code = OpenIapError.toCode(e)
177
+ val message = e.message ?: OpenIapError.defaultMessage(code)
177
178
  runCatching {
178
179
  RnIapLog.result(
179
180
  "purchaseErrorListener",
180
181
  mapOf("code" to code, "message" to message)
181
182
  )
182
- sendPurchaseError(
183
- NitroPurchaseResult(
184
- responseCode = -1.0,
185
- debugMessage = null,
186
- code = code,
187
- message = message,
188
- purchaseToken = null
189
- )
190
- )
183
+ sendPurchaseError(toErrorResult(e))
191
184
  }.onFailure { RnIapLog.failure("purchaseErrorListener", it) }
192
185
  })
193
186
  openIap.addUserChoiceBillingListener(OpenIapUserChoiceBillingListener { details ->
@@ -222,7 +215,7 @@ class HybridRnIap : HybridRnIapSpec() {
222
215
  throw err
223
216
  } catch (err: Throwable) {
224
217
  listenersAttached = false
225
- val error = OpenIAPError.InitConnection
218
+ val error = OpenIapError.InitConnection
226
219
  val errorMessage = err.message ?: err.javaClass.name
227
220
  RnIapLog.failure("initConnection.listeners", err)
228
221
  val wrapped = OpenIapException(
@@ -266,7 +259,7 @@ class HybridRnIap : HybridRnIapSpec() {
266
259
  openIap.initConnection(openIapConfig)
267
260
  }
268
261
  } catch (err: Throwable) {
269
- val error = OpenIAPError.InitConnection
262
+ val error = OpenIapError.InitConnection
270
263
  RnIapLog.failure("initConnection.native", err)
271
264
  throw OpenIapException(
272
265
  toErrorJson(
@@ -277,7 +270,7 @@ class HybridRnIap : HybridRnIapSpec() {
277
270
  )
278
271
  }
279
272
  if (!ok) {
280
- val error = OpenIAPError.InitConnection
273
+ val error = OpenIapError.InitConnection
281
274
  RnIapLog.failure("initConnection.native", Exception(error.message))
282
275
  throw OpenIapException(
283
276
  toErrorJson(
@@ -314,6 +307,8 @@ class HybridRnIap : HybridRnIapSpec() {
314
307
  promotedProductListenersIOS.clear()
315
308
  synchronized(userChoiceBillingListenersAndroid) { userChoiceBillingListenersAndroid.clear() }
316
309
  synchronized(developerProvidedBillingListenersAndroid) { developerProvidedBillingListenersAndroid.clear() }
310
+ synchronized(subscriptionBillingIssueListeners) { subscriptionBillingIssueListeners.clear() }
311
+ detachSubscriptionBillingIssueIfNeeded()
317
312
  initDeferred = null
318
313
  RnIapLog.result("endConnection", true)
319
314
  true
@@ -332,7 +327,7 @@ class HybridRnIap : HybridRnIapSpec() {
332
327
  )
333
328
 
334
329
  if (skus.isEmpty()) {
335
- throw OpenIapException(toErrorJson(OpenIAPError.EmptySkuList))
330
+ throw OpenIapException(toErrorJson(OpenIapError.EmptySkuList))
336
331
  }
337
332
 
338
333
  ensureConnection()
@@ -340,46 +335,46 @@ class HybridRnIap : HybridRnIapSpec() {
340
335
  val queryType = parseProductQueryType(type)
341
336
  val skusList = skus.toList()
342
337
 
343
- val products: List<ProductCommon> = when (queryType) {
344
- ProductQueryType.All -> {
345
- // Fetch both InApp and Subs products
346
- val byId = mutableMapOf<String, ProductCommon>()
347
-
348
- listOf(ProductQueryType.InApp, ProductQueryType.Subs).forEach { kind ->
338
+ val products: List<ProductCommon> = try {
339
+ when (queryType) {
340
+ ProductQueryType.All -> {
341
+ collectAllQueryProducts(
342
+ skusList = skusList,
343
+ fetchKind = { kind ->
344
+ RnIapLog.payload(
345
+ "fetchProducts.native",
346
+ mapOf("skus" to skusList, "type" to kind.rawValue)
347
+ )
348
+ val fetched = openIap.fetchProducts(ProductRequest(skusList, kind)).productsOrEmpty()
349
+ RnIapLog.result(
350
+ "fetchProducts.native",
351
+ fetched.map { mapOf("id" to it.id, "type" to it.type.rawValue) }
352
+ )
353
+ fetched
354
+ },
355
+ onFailure = { kind, error ->
356
+ RnIapLog.failure("fetchProducts.native[${kind.rawValue}]", error)
357
+ },
358
+ )
359
+ }
360
+ else -> {
349
361
  RnIapLog.payload(
350
362
  "fetchProducts.native",
351
- mapOf("skus" to skusList, "type" to kind.rawValue)
363
+ mapOf("skus" to skusList, "type" to queryType.rawValue)
352
364
  )
353
- val fetched = openIap.fetchProducts(ProductRequest(skusList, kind)).productsOrEmpty()
365
+ val fetched = openIap.fetchProducts(ProductRequest(skusList, queryType)).productsOrEmpty()
354
366
  RnIapLog.result(
355
367
  "fetchProducts.native",
356
368
  fetched.map { mapOf("id" to it.id, "type" to it.type.rawValue) }
357
369
  )
358
370
 
359
- // Collect products by ID (no duplicates possible in Play Billing)
360
- fetched.forEach { product ->
361
- byId.putIfAbsent(product.id, product)
362
- }
371
+ // Preserve input order for non-All queries
372
+ val byId = fetched.associateBy { it.id }
373
+ skusList.mapNotNull { byId[it] }
363
374
  }
364
-
365
- // Return products in the same order as input skusList
366
- skusList.mapNotNull { byId[it] }
367
- }
368
- else -> {
369
- RnIapLog.payload(
370
- "fetchProducts.native",
371
- mapOf("skus" to skusList, "type" to queryType.rawValue)
372
- )
373
- val fetched = openIap.fetchProducts(ProductRequest(skusList, queryType)).productsOrEmpty()
374
- RnIapLog.result(
375
- "fetchProducts.native",
376
- fetched.map { mapOf("id" to it.id, "type" to it.type.rawValue) }
377
- )
378
-
379
- // Preserve input order for non-All queries
380
- val byId = fetched.associateBy { it.id }
381
- skusList.mapNotNull { byId[it] }
382
375
  }
376
+ } catch (e: OpenIapError) {
377
+ throw OpenIapException(toErrorJson(e))
383
378
  }
384
379
 
385
380
  products.forEach { p -> productTypeBySku[p.id] = p.type.rawValue }
@@ -411,13 +406,13 @@ class HybridRnIap : HybridRnIapSpec() {
411
406
 
412
407
  if (androidRequest == null) {
413
408
  RnIapLog.warn("requestPurchase called without android payload")
414
- sendPurchaseError(toErrorResult(OpenIAPError.DeveloperError()))
409
+ sendPurchaseError(toErrorResult(OpenIapError.DeveloperError()))
415
410
  return@async defaultResult
416
411
  }
417
412
 
418
413
  if (androidRequest.skus.isEmpty()) {
419
414
  RnIapLog.warn("requestPurchase received empty SKU list")
420
- sendPurchaseError(toErrorResult(OpenIAPError.EmptySkuList))
415
+ sendPurchaseError(toErrorResult(OpenIapError.EmptySkuList))
421
416
  return@async defaultResult
422
417
  }
423
418
 
@@ -432,7 +427,7 @@ class HybridRnIap : HybridRnIapSpec() {
432
427
 
433
428
  if (activity == null) {
434
429
  RnIapLog.warn("requestPurchase: Activity is null - cannot start purchase flow")
435
- sendPurchaseError(toErrorResult(OpenIAPError.MissingCurrentActivity))
430
+ sendPurchaseError(toErrorResult(OpenIapError.MissingCurrentActivity))
436
431
  return@async defaultResult
437
432
  }
438
433
 
@@ -454,7 +449,7 @@ class HybridRnIap : HybridRnIapSpec() {
454
449
  }
455
450
  fetched.firstOrNull()?.let { productTypeBySku[it.id] = it.type.rawValue }
456
451
  if (!productTypeBySku.containsKey(sku)) {
457
- sendPurchaseError(toErrorResult(OpenIAPError.SkuNotFound(sku)))
452
+ sendPurchaseError(toErrorResult(OpenIapError.SkuNotFound(sku)))
458
453
  return@async defaultResult
459
454
  }
460
455
  }
@@ -549,7 +544,7 @@ class HybridRnIap : HybridRnIapSpec() {
549
544
  RnIapLog.failure("requestPurchase", e)
550
545
  sendPurchaseError(
551
546
  toErrorResult(
552
- error = OpenIAPError.PurchaseFailed(),
547
+ error = OpenIapError.PurchaseFailed(),
553
548
  debugMessage = e.message,
554
549
  messageOverride = e.message
555
550
  )
@@ -651,7 +646,7 @@ class HybridRnIap : HybridRnIapSpec() {
651
646
  nitroSubscriptions.toTypedArray()
652
647
  } catch (e: Exception) {
653
648
  RnIapLog.failure("getActiveSubscriptions", e)
654
- val error = OpenIAPError.ServiceUnavailable()
649
+ val error = OpenIapError.ServiceUnavailable()
655
650
  throw OpenIapException(
656
651
  toErrorJson(
657
652
  error = error,
@@ -678,7 +673,7 @@ class HybridRnIap : HybridRnIapSpec() {
678
673
  hasActive
679
674
  } catch (e: Exception) {
680
675
  RnIapLog.failure("hasActiveSubscriptions", e)
681
- val error = OpenIAPError.ServiceUnavailable()
676
+ val error = OpenIapError.ServiceUnavailable()
682
677
  throw OpenIapException(
683
678
  toErrorJson(
684
679
  error = error,
@@ -713,7 +708,7 @@ class HybridRnIap : HybridRnIapSpec() {
713
708
  NitroPurchaseResult(
714
709
  responseCode = -1.0,
715
710
  debugMessage = "Missing purchaseToken",
716
- code = OpenIAPError.toCode(OpenIAPError.DeveloperError()),
711
+ code = OpenIapError.toCode(OpenIapError.DeveloperError()),
717
712
  message = "Missing purchaseToken",
718
713
  purchaseToken = null
719
714
  )
@@ -724,12 +719,12 @@ class HybridRnIap : HybridRnIapSpec() {
724
719
  try {
725
720
  ensureConnection()
726
721
  } catch (e: Exception) {
727
- val err = OpenIAPError.InitConnection
722
+ val err = OpenIapError.InitConnection
728
723
  return@async Variant_Boolean_NitroPurchaseResult.Second(
729
724
  NitroPurchaseResult(
730
725
  responseCode = -1.0,
731
726
  debugMessage = e.message,
732
- code = OpenIAPError.toCode(err),
727
+ code = OpenIapError.toCode(err),
733
728
  message = e.message?.takeIf { it.isNotBlank() } ?: err.message,
734
729
  purchaseToken = purchaseToken
735
730
  )
@@ -754,13 +749,13 @@ class HybridRnIap : HybridRnIapSpec() {
754
749
  RnIapLog.result("finishTransaction", mapOf("success" to true))
755
750
  result
756
751
  } catch (e: Exception) {
757
- val err = OpenIAPError.BillingError()
752
+ val err = OpenIapError.BillingError()
758
753
  RnIapLog.failure("finishTransaction", e)
759
754
  Variant_Boolean_NitroPurchaseResult.Second(
760
755
  NitroPurchaseResult(
761
756
  responseCode = -1.0,
762
757
  debugMessage = e.message,
763
- code = OpenIAPError.toCode(err),
758
+ code = OpenIapError.toCode(err),
764
759
  message = e.message?.takeIf { it.isNotBlank() } ?: err.message,
765
760
  purchaseToken = null
766
761
  )
@@ -1276,14 +1271,14 @@ class HybridRnIap : HybridRnIapSpec() {
1276
1271
  // iOS-specific method - not supported on Android
1277
1272
  override fun getStorefrontIOS(): Promise<String> {
1278
1273
  return Promise.async {
1279
- throw OpenIapException(toErrorJson(OpenIAPError.FeatureNotSupported()))
1274
+ throw OpenIapException(toErrorJson(OpenIapError.FeatureNotSupported()))
1280
1275
  }
1281
1276
  }
1282
1277
 
1283
1278
  // iOS-specific method - not supported on Android
1284
1279
  override fun getAppTransactionIOS(): Promise<Variant_NullType_String> {
1285
1280
  return Promise.async {
1286
- throw OpenIapException(toErrorJson(OpenIAPError.FeatureNotSupported()))
1281
+ throw OpenIapException(toErrorJson(OpenIapError.FeatureNotSupported()))
1287
1282
  }
1288
1283
  }
1289
1284
 
@@ -1370,7 +1365,7 @@ class HybridRnIap : HybridRnIapSpec() {
1370
1365
  try {
1371
1366
  // For Android, we need the google options to be provided (new platform-specific structure)
1372
1367
  val nitroGoogleOptions = (params.google as? Variant_NullType_NitroReceiptValidationGoogleOptions.Second)?.value
1373
- ?: throw OpenIapException(toErrorJson(OpenIAPError.DeveloperError(), debugMessage = "Missing required parameter: google options"))
1368
+ ?: throw OpenIapException(toErrorJson(OpenIapError.DeveloperError(), debugMessage = "Missing required parameter: google options"))
1374
1369
 
1375
1370
  // Validate required google fields
1376
1371
  val validations = mapOf(
@@ -1381,7 +1376,7 @@ class HybridRnIap : HybridRnIapSpec() {
1381
1376
  )
1382
1377
  for ((name, value) in validations) {
1383
1378
  if (value.isEmpty()) {
1384
- throw OpenIapException(toErrorJson(OpenIAPError.DeveloperError(), debugMessage = "Missing or empty required parameter: $name"))
1379
+ throw OpenIapException(toErrorJson(OpenIapError.DeveloperError(), debugMessage = "Missing or empty required parameter: $name"))
1385
1380
  }
1386
1381
  }
1387
1382
 
@@ -1409,7 +1404,7 @@ class HybridRnIap : HybridRnIapSpec() {
1409
1404
 
1410
1405
  // Cast to Android result type (on Android, verifyPurchase returns VerifyPurchaseResultAndroid)
1411
1406
  val androidResult = verifyResult as? VerifyPurchaseResultAndroid
1412
- ?: throw OpenIapException(toErrorJson(OpenIAPError.InvalidPurchaseVerification, debugMessage = "Unexpected result type from verifyPurchase"))
1407
+ ?: throw OpenIapException(toErrorJson(OpenIapError.InvalidPurchaseVerification, debugMessage = "Unexpected result type from verifyPurchase"))
1413
1408
 
1414
1409
  // Convert OpenIAP result to Nitro result
1415
1410
  val result = NitroReceiptValidationResultAndroid(
@@ -1441,7 +1436,7 @@ class HybridRnIap : HybridRnIapSpec() {
1441
1436
  } catch (e: Exception) {
1442
1437
  RnIapLog.failure("validateReceipt", e)
1443
1438
  val debugMessage = e.message
1444
- val error = OpenIAPError.InvalidPurchaseVerification
1439
+ val error = OpenIapError.InvalidPurchaseVerification
1445
1440
  throw OpenIapException(
1446
1441
  toErrorJson(
1447
1442
  error = error,
@@ -1506,7 +1501,7 @@ class HybridRnIap : HybridRnIapSpec() {
1506
1501
  )
1507
1502
  } catch (e: Exception) {
1508
1503
  RnIapLog.failure("verifyPurchaseWithProvider", e)
1509
- val error = OpenIAPError.VerificationFailed
1504
+ val error = OpenIapError.VerificationFailed
1510
1505
  throw OpenIapException(
1511
1506
  toErrorJson(
1512
1507
  error = error,
@@ -1521,31 +1516,37 @@ class HybridRnIap : HybridRnIapSpec() {
1521
1516
  // iOS-specific methods - Not applicable on Android, return appropriate defaults
1522
1517
  override fun subscriptionStatusIOS(sku: String): Promise<Variant_NullType_Array_NitroSubscriptionStatus_> {
1523
1518
  return Promise.async {
1524
- throw OpenIapException(toErrorJson(OpenIAPError.FeatureNotSupported()))
1519
+ throw OpenIapException(toErrorJson(OpenIapError.FeatureNotSupported()))
1525
1520
  }
1526
1521
  }
1527
1522
 
1528
1523
  override fun currentEntitlementIOS(sku: String): Promise<Variant_NullType_NitroPurchase> {
1529
1524
  return Promise.async {
1530
- throw OpenIapException(toErrorJson(OpenIAPError.FeatureNotSupported()))
1525
+ throw OpenIapException(toErrorJson(OpenIapError.FeatureNotSupported()))
1531
1526
  }
1532
1527
  }
1533
1528
 
1534
1529
  override fun latestTransactionIOS(sku: String): Promise<Variant_NullType_NitroPurchase> {
1535
1530
  return Promise.async {
1536
- throw OpenIapException(toErrorJson(OpenIAPError.FeatureNotSupported()))
1531
+ throw OpenIapException(toErrorJson(OpenIapError.FeatureNotSupported()))
1537
1532
  }
1538
1533
  }
1539
1534
 
1540
1535
  override fun getPendingTransactionsIOS(): Promise<Array<NitroPurchase>> {
1541
1536
  return Promise.async {
1542
- throw OpenIapException(toErrorJson(OpenIAPError.FeatureNotSupported()))
1537
+ throw OpenIapException(toErrorJson(OpenIapError.FeatureNotSupported()))
1543
1538
  }
1544
1539
  }
1545
1540
 
1541
+ override fun getAllTransactionsIOS(): Promise<Array<NitroPurchase>> {
1542
+ return Promise.async {
1543
+ throw OpenIapException(toErrorJson(OpenIapError.FeatureNotSupported()))
1544
+ }
1545
+ }
1546
+
1546
1547
  override fun syncIOS(): Promise<Boolean> {
1547
1548
  return Promise.async {
1548
- throw OpenIapException(toErrorJson(OpenIAPError.FeatureNotSupported()))
1549
+ throw OpenIapException(toErrorJson(OpenIapError.FeatureNotSupported()))
1549
1550
  }
1550
1551
  }
1551
1552
 
@@ -1553,37 +1554,37 @@ class HybridRnIap : HybridRnIapSpec() {
1553
1554
 
1554
1555
  override fun isEligibleForIntroOfferIOS(groupID: String): Promise<Boolean> {
1555
1556
  return Promise.async {
1556
- throw OpenIapException(toErrorJson(OpenIAPError.FeatureNotSupported()))
1557
+ throw OpenIapException(toErrorJson(OpenIapError.FeatureNotSupported()))
1557
1558
  }
1558
1559
  }
1559
1560
 
1560
1561
  override fun getReceiptDataIOS(): Promise<String> {
1561
1562
  return Promise.async {
1562
- throw OpenIapException(toErrorJson(OpenIAPError.FeatureNotSupported()))
1563
+ throw OpenIapException(toErrorJson(OpenIapError.FeatureNotSupported()))
1563
1564
  }
1564
1565
  }
1565
1566
 
1566
1567
  override fun getReceiptIOS(): Promise<String> {
1567
1568
  return Promise.async {
1568
- throw OpenIapException(toErrorJson(OpenIAPError.FeatureNotSupported()))
1569
+ throw OpenIapException(toErrorJson(OpenIapError.FeatureNotSupported()))
1569
1570
  }
1570
1571
  }
1571
1572
 
1572
1573
  override fun requestReceiptRefreshIOS(): Promise<String> {
1573
1574
  return Promise.async {
1574
- throw OpenIapException(toErrorJson(OpenIAPError.FeatureNotSupported()))
1575
+ throw OpenIapException(toErrorJson(OpenIapError.FeatureNotSupported()))
1575
1576
  }
1576
1577
  }
1577
1578
 
1578
1579
  override fun isTransactionVerifiedIOS(sku: String): Promise<Boolean> {
1579
1580
  return Promise.async {
1580
- throw OpenIapException(toErrorJson(OpenIAPError.FeatureNotSupported()))
1581
+ throw OpenIapException(toErrorJson(OpenIapError.FeatureNotSupported()))
1581
1582
  }
1582
1583
  }
1583
1584
 
1584
1585
  override fun getTransactionJwsIOS(sku: String): Promise<Variant_NullType_String> {
1585
1586
  return Promise.async {
1586
- throw OpenIapException(toErrorJson(OpenIAPError.FeatureNotSupported()))
1587
+ throw OpenIapException(toErrorJson(OpenIapError.FeatureNotSupported()))
1587
1588
  }
1588
1589
  }
1589
1590
 
@@ -1613,7 +1614,7 @@ class HybridRnIap : HybridRnIapSpec() {
1613
1614
  RnIapLog.payload("showAlternativeBillingDialogAndroid", null)
1614
1615
  try {
1615
1616
  val activity = context.currentActivity
1616
- ?: throw OpenIapException(toErrorJson(OpenIAPError.DeveloperError(), debugMessage = "Activity not available"))
1617
+ ?: throw OpenIapException(toErrorJson(OpenIapError.DeveloperError(), debugMessage = "Activity not available"))
1617
1618
 
1618
1619
  val userAccepted = withContext(Dispatchers.Main) {
1619
1620
  openIap.setActivity(activity)
@@ -1685,6 +1686,57 @@ class HybridRnIap : HybridRnIapSpec() {
1685
1686
  snapshot.forEach { it(details) }
1686
1687
  }
1687
1688
 
1689
+ // -------------------------------------------------------------------------
1690
+ // Subscription billing-issue listener (cross-platform event)
1691
+ // Source: Play Billing 8.1+ Purchase.isSuspended detection inside openiap-google.
1692
+ // -------------------------------------------------------------------------
1693
+
1694
+ @Volatile
1695
+ private var subscriptionBillingIssueAttached = false
1696
+ private val subscriptionBillingIssueAttachLock = Any()
1697
+ private var subscriptionBillingIssueNativeListener: dev.hyo.openiap.listener.OpenIapSubscriptionBillingIssueListener? = null
1698
+
1699
+ override fun addSubscriptionBillingIssueListener(listener: (purchase: NitroPurchase) -> Unit) {
1700
+ synchronized(subscriptionBillingIssueListeners) {
1701
+ subscriptionBillingIssueListeners.add(listener)
1702
+ }
1703
+ attachSubscriptionBillingIssueIfNeeded()
1704
+ }
1705
+
1706
+ override fun removeSubscriptionBillingIssueListener(listener: (purchase: NitroPurchase) -> Unit) {
1707
+ synchronized(subscriptionBillingIssueListeners) {
1708
+ subscriptionBillingIssueListeners.remove(listener)
1709
+ }
1710
+ }
1711
+
1712
+ private fun attachSubscriptionBillingIssueIfNeeded() {
1713
+ synchronized(subscriptionBillingIssueAttachLock) {
1714
+ if (subscriptionBillingIssueAttached) return
1715
+ val nativeListener = dev.hyo.openiap.listener.OpenIapSubscriptionBillingIssueListener { purchase ->
1716
+ runCatching {
1717
+ val nitro = convertToNitroPurchase(purchase)
1718
+ val snapshot = synchronized(subscriptionBillingIssueListeners) {
1719
+ ArrayList(subscriptionBillingIssueListeners)
1720
+ }
1721
+ snapshot.forEach { it(nitro) }
1722
+ }.onFailure { RnIapLog.failure("subscriptionBillingIssueListener", it) }
1723
+ }
1724
+ openIap.addSubscriptionBillingIssueListener(nativeListener)
1725
+ subscriptionBillingIssueNativeListener = nativeListener
1726
+ subscriptionBillingIssueAttached = true
1727
+ }
1728
+ }
1729
+
1730
+ private fun detachSubscriptionBillingIssueIfNeeded() {
1731
+ synchronized(subscriptionBillingIssueAttachLock) {
1732
+ subscriptionBillingIssueNativeListener?.let {
1733
+ openIap.removeSubscriptionBillingIssueListener(it)
1734
+ }
1735
+ subscriptionBillingIssueNativeListener = null
1736
+ subscriptionBillingIssueAttached = false
1737
+ }
1738
+ }
1739
+
1688
1740
  // -------------------------------------------------------------------------
1689
1741
  // Billing Programs API (Android 8.2.0+)
1690
1742
  // -------------------------------------------------------------------------
@@ -1759,7 +1811,7 @@ class HybridRnIap : HybridRnIapSpec() {
1759
1811
 
1760
1812
  val activity = withContext(Dispatchers.Main) {
1761
1813
  runCatching { context.currentActivity }.getOrNull()
1762
- } ?: throw OpenIapException(toErrorJson(OpenIAPError.DeveloperError(), debugMessage = "Activity not available"))
1814
+ } ?: throw OpenIapException(toErrorJson(OpenIapError.DeveloperError(), debugMessage = "Activity not available"))
1763
1815
 
1764
1816
  val openIapParams = OpenIapLaunchExternalLinkParams(
1765
1817
  billingProgram = mapBillingProgram(params.billingProgram),
@@ -1814,96 +1866,98 @@ class HybridRnIap : HybridRnIapSpec() {
1814
1866
 
1815
1867
  override fun canPresentExternalPurchaseNoticeIOS(): Promise<Boolean> {
1816
1868
  return Promise.async {
1817
- throw OpenIapException(toErrorJson(OpenIAPError.FeatureNotSupported()))
1869
+ throw OpenIapException(toErrorJson(OpenIapError.FeatureNotSupported()))
1818
1870
  }
1819
1871
  }
1820
1872
 
1821
1873
  override fun presentExternalPurchaseNoticeSheetIOS(): Promise<ExternalPurchaseNoticeResultIOS> {
1822
1874
  return Promise.async {
1823
- throw OpenIapException(toErrorJson(OpenIAPError.FeatureNotSupported()))
1875
+ throw OpenIapException(toErrorJson(OpenIapError.FeatureNotSupported()))
1824
1876
  }
1825
1877
  }
1826
1878
 
1827
1879
  override fun presentExternalPurchaseLinkIOS(url: String): Promise<ExternalPurchaseLinkResultIOS> {
1828
1880
  return Promise.async {
1829
- throw OpenIapException(toErrorJson(OpenIAPError.FeatureNotSupported()))
1881
+ throw OpenIapException(toErrorJson(OpenIapError.FeatureNotSupported()))
1830
1882
  }
1831
1883
  }
1832
1884
 
1833
1885
  // ExternalPurchaseCustomLink (iOS 18.1+) - iOS only stubs
1834
1886
  override fun isEligibleForExternalPurchaseCustomLinkIOS(): Promise<Boolean> {
1835
1887
  return Promise.async {
1836
- throw OpenIapException(toErrorJson(OpenIAPError.FeatureNotSupported()))
1888
+ throw OpenIapException(toErrorJson(OpenIapError.FeatureNotSupported()))
1837
1889
  }
1838
1890
  }
1839
1891
 
1840
1892
  override fun getExternalPurchaseCustomLinkTokenIOS(tokenType: ExternalPurchaseCustomLinkTokenTypeIOS): Promise<ExternalPurchaseCustomLinkTokenResultIOS> {
1841
1893
  return Promise.async {
1842
- throw OpenIapException(toErrorJson(OpenIAPError.FeatureNotSupported()))
1894
+ throw OpenIapException(toErrorJson(OpenIapError.FeatureNotSupported()))
1843
1895
  }
1844
1896
  }
1845
1897
 
1846
1898
  override fun showExternalPurchaseCustomLinkNoticeIOS(noticeType: ExternalPurchaseCustomLinkNoticeTypeIOS): Promise<ExternalPurchaseCustomLinkNoticeResultIOS> {
1847
1899
  return Promise.async {
1848
- throw OpenIapException(toErrorJson(OpenIAPError.FeatureNotSupported()))
1900
+ throw OpenIapException(toErrorJson(OpenIapError.FeatureNotSupported()))
1849
1901
  }
1850
1902
  }
1851
1903
 
1852
1904
  // ---------------------------------------------------------------------
1853
1905
  // OpenIAP error helpers: unify error codes/messages from library
1854
1906
  // ---------------------------------------------------------------------
1855
- private fun parseOpenIapError(err: Throwable): OpenIAPError {
1856
- // Try to extract OpenIAPError from the exception chain
1907
+ private fun parseOpenIapError(err: Throwable): OpenIapError {
1908
+ // Try to extract OpenIapError from the exception chain
1857
1909
  var cause: Throwable? = err
1858
1910
  while (cause != null) {
1859
1911
  val message = cause.message ?: ""
1860
1912
  // Check if message contains OpenIAP error patterns
1861
1913
  when {
1862
1914
  message.contains("not prepared", ignoreCase = true) ||
1863
- message.contains("not initialized", ignoreCase = true) -> return OpenIAPError.NotPrepared
1915
+ message.contains("not initialized", ignoreCase = true) -> return OpenIapError.NotPrepared
1864
1916
  message.contains("developer error", ignoreCase = true) ||
1865
- message.contains("activity not available", ignoreCase = true) -> return OpenIAPError.DeveloperError()
1866
- message.contains("network", ignoreCase = true) -> return OpenIAPError.NetworkError
1917
+ message.contains("activity not available", ignoreCase = true) -> return OpenIapError.DeveloperError()
1918
+ message.contains("network", ignoreCase = true) -> return OpenIapError.NetworkError
1867
1919
  message.contains("service unavailable", ignoreCase = true) ||
1868
- message.contains("billing unavailable", ignoreCase = true) -> return OpenIAPError.ServiceUnavailable()
1920
+ message.contains("billing unavailable", ignoreCase = true) -> return OpenIapError.ServiceUnavailable()
1869
1921
  }
1870
1922
  cause = cause.cause
1871
1923
  }
1872
1924
  // Default to ServiceUnavailable if we can't determine the error type
1873
- return OpenIAPError.ServiceUnavailable()
1925
+ return OpenIapError.ServiceUnavailable()
1874
1926
  }
1875
1927
 
1876
1928
  private fun toErrorJson(
1877
- error: OpenIAPError,
1929
+ error: OpenIapError,
1878
1930
  productId: String? = null,
1879
1931
  debugMessage: String? = null,
1880
1932
  messageOverride: String? = null
1881
1933
  ): String {
1882
- val code = OpenIAPError.Companion.toCode(error)
1934
+ val code = OpenIapError.Companion.toCode(error)
1883
1935
  val message = messageOverride?.takeIf { it.isNotBlank() }
1884
1936
  ?: error.message?.takeIf { it.isNotBlank() }
1885
- ?: OpenIAPError.Companion.defaultMessage(code)
1886
-
1887
- val errorMap = mutableMapOf<String, Any>(
1937
+ ?: OpenIapError.Companion.defaultMessage(code)
1938
+ val diagnostics = error.toJSON()
1939
+ val responseCode = (diagnostics["responseCode"] as? Number)?.toInt()
1940
+ val productIds = diagnostics["productIds"] as? List<*>
1941
+ val productType = diagnostics["productType"] as? String
1942
+ val isEmptyProductList = diagnostics["isEmptyProductList"] as? Boolean
1943
+
1944
+ val errorMap = mutableMapOf<String, Any?>(
1888
1945
  "code" to code,
1889
1946
  "message" to message
1890
1947
  )
1891
1948
 
1892
- errorMap["responseCode"] = -1
1893
- debugMessage?.let { errorMap["debugMessage"] = it } ?: error.message?.let { errorMap["debugMessage"] = it }
1949
+ errorMap["responseCode"] = responseCode ?: -1
1950
+ debugMessage
1951
+ ?.let { errorMap["debugMessage"] = it }
1952
+ ?: (diagnostics["debugMessage"] as? String)?.let { errorMap["debugMessage"] = it }
1953
+ ?: error.message?.let { errorMap["debugMessage"] = it }
1894
1954
  productId?.let { errorMap["productId"] = it }
1955
+ if (!productIds.isNullOrEmpty()) errorMap["productIds"] = productIds
1956
+ productType?.let { errorMap["productType"] = it }
1957
+ isEmptyProductList?.let { errorMap["isEmptyProductList"] = it }
1895
1958
 
1896
1959
  return try {
1897
- val jsonPairs = errorMap.map { (key, value) ->
1898
- val valueStr = when (value) {
1899
- is String -> "\"${value.replace("\"", "\\\"")}\""
1900
- is Number -> value.toString()
1901
- is Boolean -> value.toString()
1902
- else -> "\"$value\""
1903
- }
1904
- "\"$key\":$valueStr"
1905
- }
1906
- "{${jsonPairs.joinToString(",")}}"
1960
+ JSONObject(errorMap).toString()
1907
1961
  } catch (e: Exception) {
1908
1962
  "$code: $message"
1909
1963
  }
@@ -1957,18 +2011,21 @@ class HybridRnIap : HybridRnIapSpec() {
1957
2011
  }
1958
2012
 
1959
2013
  private fun toErrorResult(
1960
- error: OpenIAPError,
2014
+ error: OpenIapError,
1961
2015
  productId: String? = null,
1962
2016
  debugMessage: String? = null,
1963
2017
  messageOverride: String? = null
1964
2018
  ): NitroPurchaseResult {
1965
- val code = OpenIAPError.Companion.toCode(error)
2019
+ val code = OpenIapError.Companion.toCode(error)
1966
2020
  val message = messageOverride?.takeIf { it.isNotBlank() }
1967
2021
  ?: error.message?.takeIf { it.isNotBlank() }
1968
- ?: OpenIAPError.Companion.defaultMessage(code)
2022
+ ?: OpenIapError.Companion.defaultMessage(code)
2023
+ val diagnostics = error.toJSON()
2024
+ val responseCode = (diagnostics["responseCode"] as? Number)?.toDouble()
2025
+ val diagnosticMessage = diagnostics["debugMessage"] as? String
1969
2026
  return NitroPurchaseResult(
1970
- responseCode = -1.0,
1971
- debugMessage = debugMessage ?: error.message,
2027
+ responseCode = responseCode ?: -1.0,
2028
+ debugMessage = debugMessage ?: diagnosticMessage ?: error.message,
1972
2029
  code = code,
1973
2030
  message = message,
1974
2031
  purchaseToken = null