react-native-iap 15.2.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 (95) hide show
  1. package/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt +117 -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 +33 -0
  5. package/lib/module/hooks/useIAP.js.map +1 -1
  6. package/lib/module/hooks/useWebhookEvents.js +113 -0
  7. package/lib/module/hooks/useWebhookEvents.js.map +1 -0
  8. package/lib/module/index.js +331 -131
  9. package/lib/module/index.js.map +1 -1
  10. package/lib/module/kit-api.js +161 -0
  11. package/lib/module/kit-api.js.map +1 -0
  12. package/lib/module/types.js +16 -0
  13. package/lib/module/types.js.map +1 -1
  14. package/lib/module/utils/error.js.map +1 -1
  15. package/lib/module/utils/errorMapping.js +6 -0
  16. package/lib/module/utils/errorMapping.js.map +1 -1
  17. package/lib/module/webhook-client.js +164 -0
  18. package/lib/module/webhook-client.js.map +1 -0
  19. package/lib/typescript/plugin/src/withIAP.d.ts +1 -1
  20. package/lib/typescript/src/hooks/useIAP.d.ts +162 -2
  21. package/lib/typescript/src/hooks/useIAP.d.ts.map +1 -1
  22. package/lib/typescript/src/hooks/useWebhookEvents.d.ts +55 -0
  23. package/lib/typescript/src/hooks/useWebhookEvents.d.ts.map +1 -0
  24. package/lib/typescript/src/index.d.ts +282 -129
  25. package/lib/typescript/src/index.d.ts.map +1 -1
  26. package/lib/typescript/src/kit-api.d.ts +54 -0
  27. package/lib/typescript/src/kit-api.d.ts.map +1 -0
  28. package/lib/typescript/src/specs/RnIap.nitro.d.ts +7 -0
  29. package/lib/typescript/src/specs/RnIap.nitro.d.ts.map +1 -1
  30. package/lib/typescript/src/types.d.ts +304 -74
  31. package/lib/typescript/src/types.d.ts.map +1 -1
  32. package/lib/typescript/src/utils/error.d.ts +3 -0
  33. package/lib/typescript/src/utils/error.d.ts.map +1 -1
  34. package/lib/typescript/src/utils/errorMapping.d.ts +6 -0
  35. package/lib/typescript/src/utils/errorMapping.d.ts.map +1 -1
  36. package/lib/typescript/src/webhook-client.d.ts +82 -0
  37. package/lib/typescript/src/webhook-client.d.ts.map +1 -0
  38. package/nitrogen/generated/android/NitroIap+autolinking.cmake +3 -0
  39. package/nitrogen/generated/android/c++/JAdvancedCommerceInfoIOS.hpp +118 -0
  40. package/nitrogen/generated/android/c++/JAdvancedCommerceItemDetailsIOS.hpp +62 -0
  41. package/nitrogen/generated/android/c++/JAdvancedCommerceItemIOS.hpp +78 -0
  42. package/nitrogen/generated/android/c++/JAdvancedCommerceRefundIOS.hpp +62 -0
  43. package/nitrogen/generated/android/c++/JHybridRnIapSpec.cpp +44 -0
  44. package/nitrogen/generated/android/c++/JHybridRnIapSpec.hpp +1 -0
  45. package/nitrogen/generated/android/c++/JPurchase.hpp +11 -0
  46. package/nitrogen/generated/android/c++/JPurchaseIOS.hpp +16 -1
  47. package/nitrogen/generated/android/c++/JRequestPurchaseResult.hpp +11 -0
  48. package/nitrogen/generated/android/c++/JVariant_NullType_AdvancedCommerceInfoIOS.cpp +26 -0
  49. package/nitrogen/generated/android/c++/JVariant_NullType_AdvancedCommerceInfoIOS.hpp +84 -0
  50. package/nitrogen/generated/android/c++/JVariant_NullType_AdvancedCommerceItemDetailsIOS.cpp +26 -0
  51. package/nitrogen/generated/android/c++/JVariant_NullType_AdvancedCommerceItemDetailsIOS.hpp +74 -0
  52. package/nitrogen/generated/android/c++/JVariant_NullType_Array_AdvancedCommerceRefundIOS_.cpp +35 -0
  53. package/nitrogen/generated/android/c++/JVariant_NullType_Array_AdvancedCommerceRefundIOS_.hpp +84 -0
  54. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/AdvancedCommerceInfoIOS.kt +59 -0
  55. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/AdvancedCommerceItemDetailsIOS.kt +38 -0
  56. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/AdvancedCommerceItemIOS.kt +44 -0
  57. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/AdvancedCommerceRefundIOS.kt +38 -0
  58. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/HybridRnIapSpec.kt +4 -0
  59. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/PurchaseIOS.kt +5 -2
  60. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/Variant_NullType_AdvancedCommerceInfoIOS.kt +53 -0
  61. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/Variant_NullType_AdvancedCommerceItemDetailsIOS.kt +53 -0
  62. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/Variant_NullType_Array_AdvancedCommerceRefundIOS_.kt +53 -0
  63. package/nitrogen/generated/ios/NitroIap-Swift-Cxx-Bridge.hpp +166 -0
  64. package/nitrogen/generated/ios/NitroIap-Swift-Cxx-Umbrella.hpp +12 -0
  65. package/nitrogen/generated/ios/c++/HybridRnIapSpecSwift.hpp +20 -0
  66. package/nitrogen/generated/ios/swift/AdvancedCommerceInfoIOS.swift +294 -0
  67. package/nitrogen/generated/ios/swift/AdvancedCommerceItemDetailsIOS.swift +61 -0
  68. package/nitrogen/generated/ios/swift/AdvancedCommerceItemIOS.swift +141 -0
  69. package/nitrogen/generated/ios/swift/AdvancedCommerceRefundIOS.swift +61 -0
  70. package/nitrogen/generated/ios/swift/HybridRnIapSpec.swift +1 -0
  71. package/nitrogen/generated/ios/swift/HybridRnIapSpec_cxx.swift +25 -0
  72. package/nitrogen/generated/ios/swift/PurchaseIOS.swift +39 -2
  73. package/nitrogen/generated/ios/swift/Variant_NullType_AdvancedCommerceInfoIOS.swift +18 -0
  74. package/nitrogen/generated/ios/swift/Variant_NullType_AdvancedCommerceItemDetailsIOS.swift +18 -0
  75. package/nitrogen/generated/ios/swift/Variant_NullType__AdvancedCommerceRefundIOS_.swift +18 -0
  76. package/nitrogen/generated/shared/c++/AdvancedCommerceInfoIOS.hpp +117 -0
  77. package/nitrogen/generated/shared/c++/AdvancedCommerceItemDetailsIOS.hpp +86 -0
  78. package/nitrogen/generated/shared/c++/AdvancedCommerceItemIOS.hpp +99 -0
  79. package/nitrogen/generated/shared/c++/AdvancedCommerceRefundIOS.hpp +86 -0
  80. package/nitrogen/generated/shared/c++/HybridRnIapSpec.cpp +1 -0
  81. package/nitrogen/generated/shared/c++/HybridRnIapSpec.hpp +1 -0
  82. package/nitrogen/generated/shared/c++/PurchaseIOS.hpp +9 -2
  83. package/openiap-versions.json +3 -3
  84. package/package.json +1 -1
  85. package/plugin/build/withIAP.d.ts +1 -1
  86. package/plugin/src/withIAP.ts +1 -1
  87. package/src/hooks/useIAP.ts +162 -2
  88. package/src/hooks/useWebhookEvents.ts +180 -0
  89. package/src/index.ts +348 -130
  90. package/src/kit-api.ts +225 -0
  91. package/src/specs/RnIap.nitro.ts +8 -0
  92. package/src/types.ts +314 -74
  93. package/src/utils/error.ts +3 -0
  94. package/src/utils/errorMapping.ts +12 -0
  95. 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
@@ -134,7 +134,7 @@ class HybridRnIap : HybridRnIapSpec() {
134
134
  } catch (err: CancellationException) {
135
135
  throw err
136
136
  } catch (err: Throwable) {
137
- val error = OpenIAPError.InitConnection
137
+ val error = OpenIapError.InitConnection
138
138
  val errorMessage = err.message ?: err.javaClass.name
139
139
  RnIapLog.failure("initConnection.setActivity", err)
140
140
  throw OpenIapException(
@@ -173,22 +173,14 @@ class HybridRnIap : HybridRnIapSpec() {
173
173
  }.onFailure { RnIapLog.failure("purchaseUpdatedListener", it) }
174
174
  })
175
175
  openIap.addPurchaseErrorListener(OpenIapPurchaseErrorListener { e ->
176
- val code = OpenIAPError.toCode(e)
177
- val message = e.message ?: OpenIAPError.defaultMessage(code)
176
+ val code = OpenIapError.toCode(e)
177
+ val message = e.message ?: OpenIapError.defaultMessage(code)
178
178
  runCatching {
179
179
  RnIapLog.result(
180
180
  "purchaseErrorListener",
181
181
  mapOf("code" to code, "message" to message)
182
182
  )
183
- sendPurchaseError(
184
- NitroPurchaseResult(
185
- responseCode = -1.0,
186
- debugMessage = null,
187
- code = code,
188
- message = message,
189
- purchaseToken = null
190
- )
191
- )
183
+ sendPurchaseError(toErrorResult(e))
192
184
  }.onFailure { RnIapLog.failure("purchaseErrorListener", it) }
193
185
  })
194
186
  openIap.addUserChoiceBillingListener(OpenIapUserChoiceBillingListener { details ->
@@ -223,7 +215,7 @@ class HybridRnIap : HybridRnIapSpec() {
223
215
  throw err
224
216
  } catch (err: Throwable) {
225
217
  listenersAttached = false
226
- val error = OpenIAPError.InitConnection
218
+ val error = OpenIapError.InitConnection
227
219
  val errorMessage = err.message ?: err.javaClass.name
228
220
  RnIapLog.failure("initConnection.listeners", err)
229
221
  val wrapped = OpenIapException(
@@ -267,7 +259,7 @@ class HybridRnIap : HybridRnIapSpec() {
267
259
  openIap.initConnection(openIapConfig)
268
260
  }
269
261
  } catch (err: Throwable) {
270
- val error = OpenIAPError.InitConnection
262
+ val error = OpenIapError.InitConnection
271
263
  RnIapLog.failure("initConnection.native", err)
272
264
  throw OpenIapException(
273
265
  toErrorJson(
@@ -278,7 +270,7 @@ class HybridRnIap : HybridRnIapSpec() {
278
270
  )
279
271
  }
280
272
  if (!ok) {
281
- val error = OpenIAPError.InitConnection
273
+ val error = OpenIapError.InitConnection
282
274
  RnIapLog.failure("initConnection.native", Exception(error.message))
283
275
  throw OpenIapException(
284
276
  toErrorJson(
@@ -335,7 +327,7 @@ class HybridRnIap : HybridRnIapSpec() {
335
327
  )
336
328
 
337
329
  if (skus.isEmpty()) {
338
- throw OpenIapException(toErrorJson(OpenIAPError.EmptySkuList))
330
+ throw OpenIapException(toErrorJson(OpenIapError.EmptySkuList))
339
331
  }
340
332
 
341
333
  ensureConnection()
@@ -343,46 +335,46 @@ class HybridRnIap : HybridRnIapSpec() {
343
335
  val queryType = parseProductQueryType(type)
344
336
  val skusList = skus.toList()
345
337
 
346
- val products: List<ProductCommon> = when (queryType) {
347
- ProductQueryType.All -> {
348
- // Fetch both InApp and Subs products
349
- val byId = mutableMapOf<String, ProductCommon>()
350
-
351
- 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 -> {
352
361
  RnIapLog.payload(
353
362
  "fetchProducts.native",
354
- mapOf("skus" to skusList, "type" to kind.rawValue)
363
+ mapOf("skus" to skusList, "type" to queryType.rawValue)
355
364
  )
356
- val fetched = openIap.fetchProducts(ProductRequest(skusList, kind)).productsOrEmpty()
365
+ val fetched = openIap.fetchProducts(ProductRequest(skusList, queryType)).productsOrEmpty()
357
366
  RnIapLog.result(
358
367
  "fetchProducts.native",
359
368
  fetched.map { mapOf("id" to it.id, "type" to it.type.rawValue) }
360
369
  )
361
370
 
362
- // Collect products by ID (no duplicates possible in Play Billing)
363
- fetched.forEach { product ->
364
- byId.putIfAbsent(product.id, product)
365
- }
371
+ // Preserve input order for non-All queries
372
+ val byId = fetched.associateBy { it.id }
373
+ skusList.mapNotNull { byId[it] }
366
374
  }
367
-
368
- // Return products in the same order as input skusList
369
- skusList.mapNotNull { byId[it] }
370
- }
371
- else -> {
372
- RnIapLog.payload(
373
- "fetchProducts.native",
374
- mapOf("skus" to skusList, "type" to queryType.rawValue)
375
- )
376
- val fetched = openIap.fetchProducts(ProductRequest(skusList, queryType)).productsOrEmpty()
377
- RnIapLog.result(
378
- "fetchProducts.native",
379
- fetched.map { mapOf("id" to it.id, "type" to it.type.rawValue) }
380
- )
381
-
382
- // Preserve input order for non-All queries
383
- val byId = fetched.associateBy { it.id }
384
- skusList.mapNotNull { byId[it] }
385
375
  }
376
+ } catch (e: OpenIapError) {
377
+ throw OpenIapException(toErrorJson(e))
386
378
  }
387
379
 
388
380
  products.forEach { p -> productTypeBySku[p.id] = p.type.rawValue }
@@ -414,13 +406,13 @@ class HybridRnIap : HybridRnIapSpec() {
414
406
 
415
407
  if (androidRequest == null) {
416
408
  RnIapLog.warn("requestPurchase called without android payload")
417
- sendPurchaseError(toErrorResult(OpenIAPError.DeveloperError()))
409
+ sendPurchaseError(toErrorResult(OpenIapError.DeveloperError()))
418
410
  return@async defaultResult
419
411
  }
420
412
 
421
413
  if (androidRequest.skus.isEmpty()) {
422
414
  RnIapLog.warn("requestPurchase received empty SKU list")
423
- sendPurchaseError(toErrorResult(OpenIAPError.EmptySkuList))
415
+ sendPurchaseError(toErrorResult(OpenIapError.EmptySkuList))
424
416
  return@async defaultResult
425
417
  }
426
418
 
@@ -435,7 +427,7 @@ class HybridRnIap : HybridRnIapSpec() {
435
427
 
436
428
  if (activity == null) {
437
429
  RnIapLog.warn("requestPurchase: Activity is null - cannot start purchase flow")
438
- sendPurchaseError(toErrorResult(OpenIAPError.MissingCurrentActivity))
430
+ sendPurchaseError(toErrorResult(OpenIapError.MissingCurrentActivity))
439
431
  return@async defaultResult
440
432
  }
441
433
 
@@ -457,7 +449,7 @@ class HybridRnIap : HybridRnIapSpec() {
457
449
  }
458
450
  fetched.firstOrNull()?.let { productTypeBySku[it.id] = it.type.rawValue }
459
451
  if (!productTypeBySku.containsKey(sku)) {
460
- sendPurchaseError(toErrorResult(OpenIAPError.SkuNotFound(sku)))
452
+ sendPurchaseError(toErrorResult(OpenIapError.SkuNotFound(sku)))
461
453
  return@async defaultResult
462
454
  }
463
455
  }
@@ -552,7 +544,7 @@ class HybridRnIap : HybridRnIapSpec() {
552
544
  RnIapLog.failure("requestPurchase", e)
553
545
  sendPurchaseError(
554
546
  toErrorResult(
555
- error = OpenIAPError.PurchaseFailed(),
547
+ error = OpenIapError.PurchaseFailed(),
556
548
  debugMessage = e.message,
557
549
  messageOverride = e.message
558
550
  )
@@ -654,7 +646,7 @@ class HybridRnIap : HybridRnIapSpec() {
654
646
  nitroSubscriptions.toTypedArray()
655
647
  } catch (e: Exception) {
656
648
  RnIapLog.failure("getActiveSubscriptions", e)
657
- val error = OpenIAPError.ServiceUnavailable()
649
+ val error = OpenIapError.ServiceUnavailable()
658
650
  throw OpenIapException(
659
651
  toErrorJson(
660
652
  error = error,
@@ -681,7 +673,7 @@ class HybridRnIap : HybridRnIapSpec() {
681
673
  hasActive
682
674
  } catch (e: Exception) {
683
675
  RnIapLog.failure("hasActiveSubscriptions", e)
684
- val error = OpenIAPError.ServiceUnavailable()
676
+ val error = OpenIapError.ServiceUnavailable()
685
677
  throw OpenIapException(
686
678
  toErrorJson(
687
679
  error = error,
@@ -716,7 +708,7 @@ class HybridRnIap : HybridRnIapSpec() {
716
708
  NitroPurchaseResult(
717
709
  responseCode = -1.0,
718
710
  debugMessage = "Missing purchaseToken",
719
- code = OpenIAPError.toCode(OpenIAPError.DeveloperError()),
711
+ code = OpenIapError.toCode(OpenIapError.DeveloperError()),
720
712
  message = "Missing purchaseToken",
721
713
  purchaseToken = null
722
714
  )
@@ -727,12 +719,12 @@ class HybridRnIap : HybridRnIapSpec() {
727
719
  try {
728
720
  ensureConnection()
729
721
  } catch (e: Exception) {
730
- val err = OpenIAPError.InitConnection
722
+ val err = OpenIapError.InitConnection
731
723
  return@async Variant_Boolean_NitroPurchaseResult.Second(
732
724
  NitroPurchaseResult(
733
725
  responseCode = -1.0,
734
726
  debugMessage = e.message,
735
- code = OpenIAPError.toCode(err),
727
+ code = OpenIapError.toCode(err),
736
728
  message = e.message?.takeIf { it.isNotBlank() } ?: err.message,
737
729
  purchaseToken = purchaseToken
738
730
  )
@@ -757,13 +749,13 @@ class HybridRnIap : HybridRnIapSpec() {
757
749
  RnIapLog.result("finishTransaction", mapOf("success" to true))
758
750
  result
759
751
  } catch (e: Exception) {
760
- val err = OpenIAPError.BillingError()
752
+ val err = OpenIapError.BillingError()
761
753
  RnIapLog.failure("finishTransaction", e)
762
754
  Variant_Boolean_NitroPurchaseResult.Second(
763
755
  NitroPurchaseResult(
764
756
  responseCode = -1.0,
765
757
  debugMessage = e.message,
766
- code = OpenIAPError.toCode(err),
758
+ code = OpenIapError.toCode(err),
767
759
  message = e.message?.takeIf { it.isNotBlank() } ?: err.message,
768
760
  purchaseToken = null
769
761
  )
@@ -1279,14 +1271,14 @@ class HybridRnIap : HybridRnIapSpec() {
1279
1271
  // iOS-specific method - not supported on Android
1280
1272
  override fun getStorefrontIOS(): Promise<String> {
1281
1273
  return Promise.async {
1282
- throw OpenIapException(toErrorJson(OpenIAPError.FeatureNotSupported()))
1274
+ throw OpenIapException(toErrorJson(OpenIapError.FeatureNotSupported()))
1283
1275
  }
1284
1276
  }
1285
1277
 
1286
1278
  // iOS-specific method - not supported on Android
1287
1279
  override fun getAppTransactionIOS(): Promise<Variant_NullType_String> {
1288
1280
  return Promise.async {
1289
- throw OpenIapException(toErrorJson(OpenIAPError.FeatureNotSupported()))
1281
+ throw OpenIapException(toErrorJson(OpenIapError.FeatureNotSupported()))
1290
1282
  }
1291
1283
  }
1292
1284
 
@@ -1373,7 +1365,7 @@ class HybridRnIap : HybridRnIapSpec() {
1373
1365
  try {
1374
1366
  // For Android, we need the google options to be provided (new platform-specific structure)
1375
1367
  val nitroGoogleOptions = (params.google as? Variant_NullType_NitroReceiptValidationGoogleOptions.Second)?.value
1376
- ?: throw OpenIapException(toErrorJson(OpenIAPError.DeveloperError(), debugMessage = "Missing required parameter: google options"))
1368
+ ?: throw OpenIapException(toErrorJson(OpenIapError.DeveloperError(), debugMessage = "Missing required parameter: google options"))
1377
1369
 
1378
1370
  // Validate required google fields
1379
1371
  val validations = mapOf(
@@ -1384,7 +1376,7 @@ class HybridRnIap : HybridRnIapSpec() {
1384
1376
  )
1385
1377
  for ((name, value) in validations) {
1386
1378
  if (value.isEmpty()) {
1387
- 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"))
1388
1380
  }
1389
1381
  }
1390
1382
 
@@ -1412,7 +1404,7 @@ class HybridRnIap : HybridRnIapSpec() {
1412
1404
 
1413
1405
  // Cast to Android result type (on Android, verifyPurchase returns VerifyPurchaseResultAndroid)
1414
1406
  val androidResult = verifyResult as? VerifyPurchaseResultAndroid
1415
- ?: throw OpenIapException(toErrorJson(OpenIAPError.InvalidPurchaseVerification, debugMessage = "Unexpected result type from verifyPurchase"))
1407
+ ?: throw OpenIapException(toErrorJson(OpenIapError.InvalidPurchaseVerification, debugMessage = "Unexpected result type from verifyPurchase"))
1416
1408
 
1417
1409
  // Convert OpenIAP result to Nitro result
1418
1410
  val result = NitroReceiptValidationResultAndroid(
@@ -1444,7 +1436,7 @@ class HybridRnIap : HybridRnIapSpec() {
1444
1436
  } catch (e: Exception) {
1445
1437
  RnIapLog.failure("validateReceipt", e)
1446
1438
  val debugMessage = e.message
1447
- val error = OpenIAPError.InvalidPurchaseVerification
1439
+ val error = OpenIapError.InvalidPurchaseVerification
1448
1440
  throw OpenIapException(
1449
1441
  toErrorJson(
1450
1442
  error = error,
@@ -1509,7 +1501,7 @@ class HybridRnIap : HybridRnIapSpec() {
1509
1501
  )
1510
1502
  } catch (e: Exception) {
1511
1503
  RnIapLog.failure("verifyPurchaseWithProvider", e)
1512
- val error = OpenIAPError.VerificationFailed
1504
+ val error = OpenIapError.VerificationFailed
1513
1505
  throw OpenIapException(
1514
1506
  toErrorJson(
1515
1507
  error = error,
@@ -1524,31 +1516,37 @@ class HybridRnIap : HybridRnIapSpec() {
1524
1516
  // iOS-specific methods - Not applicable on Android, return appropriate defaults
1525
1517
  override fun subscriptionStatusIOS(sku: String): Promise<Variant_NullType_Array_NitroSubscriptionStatus_> {
1526
1518
  return Promise.async {
1527
- throw OpenIapException(toErrorJson(OpenIAPError.FeatureNotSupported()))
1519
+ throw OpenIapException(toErrorJson(OpenIapError.FeatureNotSupported()))
1528
1520
  }
1529
1521
  }
1530
1522
 
1531
1523
  override fun currentEntitlementIOS(sku: String): Promise<Variant_NullType_NitroPurchase> {
1532
1524
  return Promise.async {
1533
- throw OpenIapException(toErrorJson(OpenIAPError.FeatureNotSupported()))
1525
+ throw OpenIapException(toErrorJson(OpenIapError.FeatureNotSupported()))
1534
1526
  }
1535
1527
  }
1536
1528
 
1537
1529
  override fun latestTransactionIOS(sku: String): Promise<Variant_NullType_NitroPurchase> {
1538
1530
  return Promise.async {
1539
- throw OpenIapException(toErrorJson(OpenIAPError.FeatureNotSupported()))
1531
+ throw OpenIapException(toErrorJson(OpenIapError.FeatureNotSupported()))
1540
1532
  }
1541
1533
  }
1542
1534
 
1543
1535
  override fun getPendingTransactionsIOS(): Promise<Array<NitroPurchase>> {
1544
1536
  return Promise.async {
1545
- throw OpenIapException(toErrorJson(OpenIAPError.FeatureNotSupported()))
1537
+ throw OpenIapException(toErrorJson(OpenIapError.FeatureNotSupported()))
1546
1538
  }
1547
1539
  }
1548
1540
 
1541
+ override fun getAllTransactionsIOS(): Promise<Array<NitroPurchase>> {
1542
+ return Promise.async {
1543
+ throw OpenIapException(toErrorJson(OpenIapError.FeatureNotSupported()))
1544
+ }
1545
+ }
1546
+
1549
1547
  override fun syncIOS(): Promise<Boolean> {
1550
1548
  return Promise.async {
1551
- throw OpenIapException(toErrorJson(OpenIAPError.FeatureNotSupported()))
1549
+ throw OpenIapException(toErrorJson(OpenIapError.FeatureNotSupported()))
1552
1550
  }
1553
1551
  }
1554
1552
 
@@ -1556,37 +1554,37 @@ class HybridRnIap : HybridRnIapSpec() {
1556
1554
 
1557
1555
  override fun isEligibleForIntroOfferIOS(groupID: String): Promise<Boolean> {
1558
1556
  return Promise.async {
1559
- throw OpenIapException(toErrorJson(OpenIAPError.FeatureNotSupported()))
1557
+ throw OpenIapException(toErrorJson(OpenIapError.FeatureNotSupported()))
1560
1558
  }
1561
1559
  }
1562
1560
 
1563
1561
  override fun getReceiptDataIOS(): Promise<String> {
1564
1562
  return Promise.async {
1565
- throw OpenIapException(toErrorJson(OpenIAPError.FeatureNotSupported()))
1563
+ throw OpenIapException(toErrorJson(OpenIapError.FeatureNotSupported()))
1566
1564
  }
1567
1565
  }
1568
1566
 
1569
1567
  override fun getReceiptIOS(): Promise<String> {
1570
1568
  return Promise.async {
1571
- throw OpenIapException(toErrorJson(OpenIAPError.FeatureNotSupported()))
1569
+ throw OpenIapException(toErrorJson(OpenIapError.FeatureNotSupported()))
1572
1570
  }
1573
1571
  }
1574
1572
 
1575
1573
  override fun requestReceiptRefreshIOS(): Promise<String> {
1576
1574
  return Promise.async {
1577
- throw OpenIapException(toErrorJson(OpenIAPError.FeatureNotSupported()))
1575
+ throw OpenIapException(toErrorJson(OpenIapError.FeatureNotSupported()))
1578
1576
  }
1579
1577
  }
1580
1578
 
1581
1579
  override fun isTransactionVerifiedIOS(sku: String): Promise<Boolean> {
1582
1580
  return Promise.async {
1583
- throw OpenIapException(toErrorJson(OpenIAPError.FeatureNotSupported()))
1581
+ throw OpenIapException(toErrorJson(OpenIapError.FeatureNotSupported()))
1584
1582
  }
1585
1583
  }
1586
1584
 
1587
1585
  override fun getTransactionJwsIOS(sku: String): Promise<Variant_NullType_String> {
1588
1586
  return Promise.async {
1589
- throw OpenIapException(toErrorJson(OpenIAPError.FeatureNotSupported()))
1587
+ throw OpenIapException(toErrorJson(OpenIapError.FeatureNotSupported()))
1590
1588
  }
1591
1589
  }
1592
1590
 
@@ -1616,7 +1614,7 @@ class HybridRnIap : HybridRnIapSpec() {
1616
1614
  RnIapLog.payload("showAlternativeBillingDialogAndroid", null)
1617
1615
  try {
1618
1616
  val activity = context.currentActivity
1619
- ?: throw OpenIapException(toErrorJson(OpenIAPError.DeveloperError(), debugMessage = "Activity not available"))
1617
+ ?: throw OpenIapException(toErrorJson(OpenIapError.DeveloperError(), debugMessage = "Activity not available"))
1620
1618
 
1621
1619
  val userAccepted = withContext(Dispatchers.Main) {
1622
1620
  openIap.setActivity(activity)
@@ -1813,7 +1811,7 @@ class HybridRnIap : HybridRnIapSpec() {
1813
1811
 
1814
1812
  val activity = withContext(Dispatchers.Main) {
1815
1813
  runCatching { context.currentActivity }.getOrNull()
1816
- } ?: throw OpenIapException(toErrorJson(OpenIAPError.DeveloperError(), debugMessage = "Activity not available"))
1814
+ } ?: throw OpenIapException(toErrorJson(OpenIapError.DeveloperError(), debugMessage = "Activity not available"))
1817
1815
 
1818
1816
  val openIapParams = OpenIapLaunchExternalLinkParams(
1819
1817
  billingProgram = mapBillingProgram(params.billingProgram),
@@ -1868,96 +1866,98 @@ class HybridRnIap : HybridRnIapSpec() {
1868
1866
 
1869
1867
  override fun canPresentExternalPurchaseNoticeIOS(): Promise<Boolean> {
1870
1868
  return Promise.async {
1871
- throw OpenIapException(toErrorJson(OpenIAPError.FeatureNotSupported()))
1869
+ throw OpenIapException(toErrorJson(OpenIapError.FeatureNotSupported()))
1872
1870
  }
1873
1871
  }
1874
1872
 
1875
1873
  override fun presentExternalPurchaseNoticeSheetIOS(): Promise<ExternalPurchaseNoticeResultIOS> {
1876
1874
  return Promise.async {
1877
- throw OpenIapException(toErrorJson(OpenIAPError.FeatureNotSupported()))
1875
+ throw OpenIapException(toErrorJson(OpenIapError.FeatureNotSupported()))
1878
1876
  }
1879
1877
  }
1880
1878
 
1881
1879
  override fun presentExternalPurchaseLinkIOS(url: String): Promise<ExternalPurchaseLinkResultIOS> {
1882
1880
  return Promise.async {
1883
- throw OpenIapException(toErrorJson(OpenIAPError.FeatureNotSupported()))
1881
+ throw OpenIapException(toErrorJson(OpenIapError.FeatureNotSupported()))
1884
1882
  }
1885
1883
  }
1886
1884
 
1887
1885
  // ExternalPurchaseCustomLink (iOS 18.1+) - iOS only stubs
1888
1886
  override fun isEligibleForExternalPurchaseCustomLinkIOS(): Promise<Boolean> {
1889
1887
  return Promise.async {
1890
- throw OpenIapException(toErrorJson(OpenIAPError.FeatureNotSupported()))
1888
+ throw OpenIapException(toErrorJson(OpenIapError.FeatureNotSupported()))
1891
1889
  }
1892
1890
  }
1893
1891
 
1894
1892
  override fun getExternalPurchaseCustomLinkTokenIOS(tokenType: ExternalPurchaseCustomLinkTokenTypeIOS): Promise<ExternalPurchaseCustomLinkTokenResultIOS> {
1895
1893
  return Promise.async {
1896
- throw OpenIapException(toErrorJson(OpenIAPError.FeatureNotSupported()))
1894
+ throw OpenIapException(toErrorJson(OpenIapError.FeatureNotSupported()))
1897
1895
  }
1898
1896
  }
1899
1897
 
1900
1898
  override fun showExternalPurchaseCustomLinkNoticeIOS(noticeType: ExternalPurchaseCustomLinkNoticeTypeIOS): Promise<ExternalPurchaseCustomLinkNoticeResultIOS> {
1901
1899
  return Promise.async {
1902
- throw OpenIapException(toErrorJson(OpenIAPError.FeatureNotSupported()))
1900
+ throw OpenIapException(toErrorJson(OpenIapError.FeatureNotSupported()))
1903
1901
  }
1904
1902
  }
1905
1903
 
1906
1904
  // ---------------------------------------------------------------------
1907
1905
  // OpenIAP error helpers: unify error codes/messages from library
1908
1906
  // ---------------------------------------------------------------------
1909
- private fun parseOpenIapError(err: Throwable): OpenIAPError {
1910
- // Try to extract OpenIAPError from the exception chain
1907
+ private fun parseOpenIapError(err: Throwable): OpenIapError {
1908
+ // Try to extract OpenIapError from the exception chain
1911
1909
  var cause: Throwable? = err
1912
1910
  while (cause != null) {
1913
1911
  val message = cause.message ?: ""
1914
1912
  // Check if message contains OpenIAP error patterns
1915
1913
  when {
1916
1914
  message.contains("not prepared", ignoreCase = true) ||
1917
- message.contains("not initialized", ignoreCase = true) -> return OpenIAPError.NotPrepared
1915
+ message.contains("not initialized", ignoreCase = true) -> return OpenIapError.NotPrepared
1918
1916
  message.contains("developer error", ignoreCase = true) ||
1919
- message.contains("activity not available", ignoreCase = true) -> return OpenIAPError.DeveloperError()
1920
- 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
1921
1919
  message.contains("service unavailable", ignoreCase = true) ||
1922
- message.contains("billing unavailable", ignoreCase = true) -> return OpenIAPError.ServiceUnavailable()
1920
+ message.contains("billing unavailable", ignoreCase = true) -> return OpenIapError.ServiceUnavailable()
1923
1921
  }
1924
1922
  cause = cause.cause
1925
1923
  }
1926
1924
  // Default to ServiceUnavailable if we can't determine the error type
1927
- return OpenIAPError.ServiceUnavailable()
1925
+ return OpenIapError.ServiceUnavailable()
1928
1926
  }
1929
1927
 
1930
1928
  private fun toErrorJson(
1931
- error: OpenIAPError,
1929
+ error: OpenIapError,
1932
1930
  productId: String? = null,
1933
1931
  debugMessage: String? = null,
1934
1932
  messageOverride: String? = null
1935
1933
  ): String {
1936
- val code = OpenIAPError.Companion.toCode(error)
1934
+ val code = OpenIapError.Companion.toCode(error)
1937
1935
  val message = messageOverride?.takeIf { it.isNotBlank() }
1938
1936
  ?: error.message?.takeIf { it.isNotBlank() }
1939
- ?: OpenIAPError.Companion.defaultMessage(code)
1940
-
1941
- 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?>(
1942
1945
  "code" to code,
1943
1946
  "message" to message
1944
1947
  )
1945
1948
 
1946
- errorMap["responseCode"] = -1
1947
- 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 }
1948
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 }
1949
1958
 
1950
1959
  return try {
1951
- val jsonPairs = errorMap.map { (key, value) ->
1952
- val valueStr = when (value) {
1953
- is String -> "\"${value.replace("\"", "\\\"")}\""
1954
- is Number -> value.toString()
1955
- is Boolean -> value.toString()
1956
- else -> "\"$value\""
1957
- }
1958
- "\"$key\":$valueStr"
1959
- }
1960
- "{${jsonPairs.joinToString(",")}}"
1960
+ JSONObject(errorMap).toString()
1961
1961
  } catch (e: Exception) {
1962
1962
  "$code: $message"
1963
1963
  }
@@ -2011,18 +2011,21 @@ class HybridRnIap : HybridRnIapSpec() {
2011
2011
  }
2012
2012
 
2013
2013
  private fun toErrorResult(
2014
- error: OpenIAPError,
2014
+ error: OpenIapError,
2015
2015
  productId: String? = null,
2016
2016
  debugMessage: String? = null,
2017
2017
  messageOverride: String? = null
2018
2018
  ): NitroPurchaseResult {
2019
- val code = OpenIAPError.Companion.toCode(error)
2019
+ val code = OpenIapError.Companion.toCode(error)
2020
2020
  val message = messageOverride?.takeIf { it.isNotBlank() }
2021
2021
  ?: error.message?.takeIf { it.isNotBlank() }
2022
- ?: 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
2023
2026
  return NitroPurchaseResult(
2024
- responseCode = -1.0,
2025
- debugMessage = debugMessage ?: error.message,
2027
+ responseCode = responseCode ?: -1.0,
2028
+ debugMessage = debugMessage ?: diagnosticMessage ?: error.message,
2026
2029
  code = code,
2027
2030
  message = message,
2028
2031
  purchaseToken = null
@@ -0,0 +1,42 @@
1
+ package com.margelo.nitro.iap
2
+
3
+ import dev.hyo.openiap.ProductCommon
4
+ import dev.hyo.openiap.ProductQueryType
5
+ import kotlin.coroutines.cancellation.CancellationException
6
+ import kotlinx.coroutines.async
7
+ import kotlinx.coroutines.coroutineScope
8
+
9
+ internal suspend fun collectAllQueryProducts(
10
+ skusList: List<String>,
11
+ fetchKind: suspend (ProductQueryType) -> List<ProductCommon>,
12
+ onFailure: (ProductQueryType, Throwable) -> Unit = { _, _ -> },
13
+ ): List<ProductCommon> = coroutineScope {
14
+ val byId = linkedMapOf<String, ProductCommon>()
15
+ var firstFailure: Throwable? = null
16
+
17
+ val queries = listOf(ProductQueryType.InApp, ProductQueryType.Subs).map { kind ->
18
+ kind to async {
19
+ runCatching {
20
+ fetchKind(kind)
21
+ }
22
+ }
23
+ }
24
+
25
+ queries.forEach { (kind, query) ->
26
+ query.await().onSuccess { fetched ->
27
+ fetched.forEach { product ->
28
+ byId.putIfAbsent(product.id, product)
29
+ }
30
+ }.onFailure { error ->
31
+ if (error is CancellationException) throw error
32
+ onFailure(kind, error)
33
+ if (firstFailure == null) firstFailure = error
34
+ }
35
+ }
36
+
37
+ if (byId.isEmpty()) {
38
+ firstFailure?.let { throw it }
39
+ }
40
+
41
+ skusList.mapNotNull { byId[it] }
42
+ }