react-native-iap 15.3.5 → 15.4.0-rc.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 (83) hide show
  1. package/README.md +1 -1
  2. package/android/build.gradle +13 -5
  3. package/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt +16 -3
  4. package/ios/HybridRnIap.swift +12 -0
  5. package/lib/module/index.js +93 -75
  6. package/lib/module/index.js.map +1 -1
  7. package/lib/module/index.kepler.js +170 -0
  8. package/lib/module/index.kepler.js.map +1 -0
  9. package/lib/module/types/amazon-devices-kepler/index.d.js +2 -0
  10. package/lib/module/types/amazon-devices-kepler/index.d.js.map +1 -0
  11. package/lib/module/types.js +5 -2
  12. package/lib/module/types.js.map +1 -1
  13. package/lib/module/utils/error.js +98 -15
  14. package/lib/module/utils/error.js.map +1 -1
  15. package/lib/module/utils/type-bridge.js +4 -1
  16. package/lib/module/utils/type-bridge.js.map +1 -1
  17. package/lib/module/vega-adapter.js +981 -0
  18. package/lib/module/vega-adapter.js.map +1 -0
  19. package/lib/module/vega.js +10 -0
  20. package/lib/module/vega.js.map +1 -0
  21. package/lib/module/vega.kepler.js +18 -0
  22. package/lib/module/vega.kepler.js.map +1 -0
  23. package/lib/typescript/plugin/src/withIAP.d.ts +33 -0
  24. package/lib/typescript/plugin/src/withIAP.d.ts.map +1 -1
  25. package/lib/typescript/src/index.d.ts +9 -2
  26. package/lib/typescript/src/index.d.ts.map +1 -1
  27. package/lib/typescript/src/index.kepler.d.ts +55 -0
  28. package/lib/typescript/src/index.kepler.d.ts.map +1 -0
  29. package/lib/typescript/src/specs/RnIap.nitro.d.ts +10 -1
  30. package/lib/typescript/src/specs/RnIap.nitro.d.ts.map +1 -1
  31. package/lib/typescript/src/types.d.ts +16 -3
  32. package/lib/typescript/src/types.d.ts.map +1 -1
  33. package/lib/typescript/src/utils/error.d.ts +1 -1
  34. package/lib/typescript/src/utils/error.d.ts.map +1 -1
  35. package/lib/typescript/src/utils/type-bridge.d.ts.map +1 -1
  36. package/lib/typescript/src/vega-adapter.d.ts +76 -0
  37. package/lib/typescript/src/vega-adapter.d.ts.map +1 -0
  38. package/lib/typescript/src/vega.d.ts +4 -0
  39. package/lib/typescript/src/vega.d.ts.map +1 -0
  40. package/lib/typescript/src/vega.kepler.d.ts +4 -0
  41. package/lib/typescript/src/vega.kepler.d.ts.map +1 -0
  42. package/nitrogen/generated/android/NitroIap+autolinking.cmake +1 -0
  43. package/nitrogen/generated/android/c++/JHybridRnIapSpec.cpp +5 -0
  44. package/nitrogen/generated/android/c++/JIapStore.hpp +3 -0
  45. package/nitrogen/generated/android/c++/JNitroVerifyPurchaseWithIapkitAmazonProps.hpp +71 -0
  46. package/nitrogen/generated/android/c++/JNitroVerifyPurchaseWithIapkitProps.hpp +9 -1
  47. package/nitrogen/generated/android/c++/JNitroVerifyPurchaseWithProviderProps.hpp +4 -0
  48. package/nitrogen/generated/android/c++/JVariant_NullType_NitroVerifyPurchaseWithIapkitAmazonProps.cpp +26 -0
  49. package/nitrogen/generated/android/c++/JVariant_NullType_NitroVerifyPurchaseWithIapkitAmazonProps.hpp +75 -0
  50. package/nitrogen/generated/android/c++/JVariant_NullType_NitroVerifyPurchaseWithIapkitProps.hpp +4 -0
  51. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/IapStore.kt +2 -1
  52. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/NitroVerifyPurchaseWithIapkitAmazonProps.kt +44 -0
  53. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/NitroVerifyPurchaseWithIapkitProps.kt +5 -2
  54. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/Variant_NullType_NitroVerifyPurchaseWithIapkitAmazonProps.kt +53 -0
  55. package/nitrogen/generated/ios/NitroIap-Swift-Cxx-Bridge.hpp +47 -0
  56. package/nitrogen/generated/ios/NitroIap-Swift-Cxx-Umbrella.hpp +3 -0
  57. package/nitrogen/generated/ios/c++/HybridRnIapSpecSwift.hpp +3 -0
  58. package/nitrogen/generated/ios/swift/IapStore.swift +4 -0
  59. package/nitrogen/generated/ios/swift/NitroVerifyPurchaseWithIapkitAmazonProps.swift +103 -0
  60. package/nitrogen/generated/ios/swift/NitroVerifyPurchaseWithIapkitProps.swift +38 -1
  61. package/nitrogen/generated/ios/swift/Variant_NullType_NitroVerifyPurchaseWithIapkitAmazonProps.swift +18 -0
  62. package/nitrogen/generated/shared/c++/IapStore.hpp +4 -0
  63. package/nitrogen/generated/shared/c++/NitroVerifyPurchaseWithIapkitAmazonProps.hpp +94 -0
  64. package/nitrogen/generated/shared/c++/NitroVerifyPurchaseWithIapkitProps.hpp +8 -1
  65. package/openiap-versions.json +2 -2
  66. package/package.json +17 -4
  67. package/plugin/build/withIAP.d.ts +33 -0
  68. package/plugin/build/withIAP.js +109 -42
  69. package/plugin/src/withIAP.ts +185 -55
  70. package/src/index.kepler.ts +311 -0
  71. package/src/index.ts +187 -78
  72. package/src/specs/RnIap.nitro.ts +13 -2
  73. package/src/types/amazon-devices-kepler/index.d.ts +14 -0
  74. package/src/types.ts +17 -3
  75. package/src/utils/error.ts +127 -14
  76. package/src/utils/type-bridge.ts +4 -1
  77. package/src/vega-adapter.ts +1503 -0
  78. package/src/vega.kepler.ts +21 -0
  79. package/src/vega.ts +10 -0
  80. package/android/src/test/java/com/margelo/nitro/iap/ListenerThreadSafetyTest.kt +0 -154
  81. package/android/src/test/java/com/margelo/nitro/iap/OpenIapExceptionTest.kt +0 -93
  82. package/android/src/test/java/com/margelo/nitro/iap/ProductQueryHelpersTest.kt +0 -140
  83. package/plugin/tsconfig.tsbuildinfo +0 -1
package/README.md CHANGED
@@ -105,7 +105,7 @@ Before installing React Native IAP, make sure you have:
105
105
 
106
106
  #### Android Configuration
107
107
 
108
- **Kotlin Version Requirement:** This library requires Kotlin 2.0+. Configure your project's Kotlin version:
108
+ **Kotlin Version Requirement:** This library requires Kotlin 2.2+. Configure your project's Kotlin version:
109
109
 
110
110
  In your root `android/build.gradle`:
111
111
 
@@ -107,8 +107,13 @@ def getExtOrIntegerDefault(name) {
107
107
  return getExtOrDefault(name).toString().toInteger()
108
108
  }
109
109
 
110
- // Read horizonEnabled from gradle.properties, default to false (play)
110
+ // Read store flags from gradle.properties, default to play
111
111
  def horizonEnabled = project.findProperty('horizonEnabled')?.toBoolean() ?: false
112
+ def fireOsEnabled = project.findProperty('fireOsEnabled')?.toBoolean() ?: false
113
+ if (horizonEnabled && fireOsEnabled) {
114
+ throw new GradleException("react-native-iap: horizonEnabled and fireOsEnabled cannot both be true")
115
+ }
116
+ def openiapFlavor = fireOsEnabled ? 'amazon' : (horizonEnabled ? 'horizon' : 'play')
112
117
 
113
118
  def resolveOpenIapGoogleBuildFile() {
114
119
  def candidates = [
@@ -155,9 +160,8 @@ android {
155
160
  // Ship consumer keep rules so Nitro HybridObjects aren't stripped in app release builds
156
161
  consumerProguardFiles 'consumer-rules.pro'
157
162
 
158
- // Use horizonEnabled to determine platform flavor
159
- def flavor = horizonEnabled ? 'horizon' : 'play'
160
- missingDimensionStrategy "platform", flavor
163
+ // Use explicit store flags to determine platform flavor
164
+ missingDimensionStrategy "platform", openiapFlavor
161
165
 
162
166
  externalNativeBuild {
163
167
  cmake {
@@ -251,7 +255,9 @@ dependencies {
251
255
  }
252
256
 
253
257
  // Google Play Services
254
- implementation "com.google.android.gms:play-services-base:$playServicesBaseVersion"
258
+ if (!fireOsEnabled && !horizonEnabled) {
259
+ implementation "com.google.android.gms:play-services-base:$playServicesBaseVersion"
260
+ }
255
261
 
256
262
  // Kotlin coroutines
257
263
  implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
@@ -261,6 +267,8 @@ dependencies {
261
267
  def localGoogleProject = findProject(':openiap')
262
268
  if (localGoogleProject != null) {
263
269
  implementation project(':openiap')
270
+ } else if (fireOsEnabled) {
271
+ implementation "io.github.hyochan.openiap:openiap-google-amazon:${googleVersionString}"
264
272
  } else if (horizonEnabled) {
265
273
  implementation "io.github.hyochan.openiap:openiap-google-horizon:${googleVersionString}"
266
274
  } else {
@@ -43,13 +43,13 @@ import dev.hyo.openiap.ExternalLinkLaunchModeAndroid as OpenIapExternalLinkLaunc
43
43
  import dev.hyo.openiap.ExternalLinkTypeAndroid as OpenIapExternalLinkType
44
44
  import dev.hyo.openiap.listener.OpenIapDeveloperProvidedBillingListener
45
45
  import dev.hyo.openiap.store.OpenIapStore
46
+ import java.util.Locale
46
47
  import kotlin.coroutines.cancellation.CancellationException
47
48
  import kotlinx.coroutines.Dispatchers
48
49
  import kotlinx.coroutines.withContext
49
50
  import kotlinx.coroutines.CompletableDeferred
50
51
  import org.json.JSONArray
51
52
  import org.json.JSONObject
52
- import java.util.Locale
53
53
 
54
54
  /**
55
55
  * Custom exception for OpenIAP errors that only includes the error JSON without stack traces.
@@ -575,7 +575,7 @@ class HybridRnIap : HybridRnIapSpec() {
575
575
  mapOf("type" to androidOptions?.type?.name, "includeSuspended" to includeSuspended)
576
576
  )
577
577
 
578
- val typeName = androidOptions?.type?.name?.lowercase()
578
+ val typeName = androidOptions?.type?.name?.lowercase(java.util.Locale.ROOT)
579
579
  val normalizedType = when (typeName) {
580
580
  "inapp" -> {
581
581
  RnIapLog.warn("getAvailablePurchases received legacy type 'inapp'; forwarding as 'in-app'")
@@ -1279,6 +1279,7 @@ class HybridRnIap : HybridRnIapSpec() {
1279
1279
  dev.hyo.openiap.IapStore.Apple -> IapStore.APPLE
1280
1280
  dev.hyo.openiap.IapStore.Google -> IapStore.GOOGLE
1281
1281
  dev.hyo.openiap.IapStore.Horizon -> IapStore.HORIZON
1282
+ dev.hyo.openiap.IapStore.Amazon -> IapStore.AMAZON
1282
1283
  dev.hyo.openiap.IapStore.Unknown -> IapStore.UNKNOWN
1283
1284
  }
1284
1285
  }
@@ -1469,7 +1470,7 @@ class HybridRnIap : HybridRnIapSpec() {
1469
1470
  return Promise.async {
1470
1471
  try {
1471
1472
  // Convert Nitro enum to string (e.g., IAPKIT -> "iapkit")
1472
- val providerString = params.provider.name.lowercase()
1473
+ val providerString = params.provider.name.lowercase(java.util.Locale.ROOT)
1473
1474
  RnIapLog.payload("verifyPurchaseWithProvider", mapOf("provider" to providerString))
1474
1475
 
1475
1476
  // Build the props map for OpenIAP - use string value for provider
@@ -1482,6 +1483,14 @@ class HybridRnIap : HybridRnIapSpec() {
1482
1483
  (iapkit.google as? Variant_NullType_NitroVerifyPurchaseWithIapkitGoogleProps.Second)?.value?.let { google ->
1483
1484
  iapkitMap["google"] = mapOf("purchaseToken" to google.purchaseToken)
1484
1485
  }
1486
+ (iapkit.amazon as? Variant_NullType_NitroVerifyPurchaseWithIapkitAmazonProps.Second)?.value?.let { amazon ->
1487
+ val amazonMap = mutableMapOf<String, Any?>(
1488
+ "receiptId" to amazon.receiptId
1489
+ )
1490
+ amazon.userId.unwrapString()?.let { amazonMap["userId"] = it }
1491
+ amazon.sandbox.unwrapBool()?.let { amazonMap["sandbox"] = it }
1492
+ iapkitMap["amazon"] = amazonMap
1493
+ }
1485
1494
  (iapkit.apple as? Variant_NullType_NitroVerifyPurchaseWithIapkitAppleProps.Second)?.value?.let { apple ->
1486
1495
  iapkitMap["apple"] = mapOf("jws" to apple.jws)
1487
1496
  }
@@ -1609,6 +1618,7 @@ class HybridRnIap : HybridRnIapSpec() {
1609
1618
  // Alternative Billing (Android)
1610
1619
  // -------------------------------------------------------------------------
1611
1620
 
1621
+ @Suppress("DEPRECATION")
1612
1622
  override fun checkAlternativeBillingAvailabilityAndroid(): Promise<Boolean> {
1613
1623
  return Promise.async {
1614
1624
  RnIapLog.payload("checkAlternativeBillingAvailabilityAndroid", null)
@@ -1626,6 +1636,7 @@ class HybridRnIap : HybridRnIapSpec() {
1626
1636
  }
1627
1637
  }
1628
1638
 
1639
+ @Suppress("DEPRECATION")
1629
1640
  override fun showAlternativeBillingDialogAndroid(): Promise<Boolean> {
1630
1641
  return Promise.async {
1631
1642
  RnIapLog.payload("showAlternativeBillingDialogAndroid", null)
@@ -1647,6 +1658,7 @@ class HybridRnIap : HybridRnIapSpec() {
1647
1658
  }
1648
1659
  }
1649
1660
 
1661
+ @Suppress("DEPRECATION")
1650
1662
  override fun createAlternativeBillingTokenAndroid(sku: Variant_NullType_String?): Promise<Variant_NullType_String> {
1651
1663
  return Promise.async {
1652
1664
  val skuValue = sku.unwrapString()
@@ -2000,6 +2012,7 @@ class HybridRnIap : HybridRnIapSpec() {
2000
2012
  "APPLE" -> IapStore.APPLE
2001
2013
  "GOOGLE" -> IapStore.GOOGLE
2002
2014
  "HORIZON" -> IapStore.HORIZON
2015
+ "AMAZON" -> IapStore.AMAZON
2003
2016
  else -> IapStore.UNKNOWN
2004
2017
  }
2005
2018
  }
@@ -446,6 +446,18 @@ class HybridRnIap: HybridRnIapSpec {
446
446
  if case .second(let google) = iapkit.google {
447
447
  iapkitDict["google"] = ["purchaseToken": google.purchaseToken]
448
448
  }
449
+ if case .second(let amazon) = iapkit.amazon {
450
+ var amazonDict: [String: Any] = [
451
+ "receiptId": amazon.receiptId
452
+ ]
453
+ if case .second(let sandbox) = amazon.sandbox {
454
+ amazonDict["sandbox"] = sandbox
455
+ }
456
+ if case .second(let userId) = amazon.userId {
457
+ amazonDict["userId"] = userId
458
+ }
459
+ iapkitDict["amazon"] = amazonDict
460
+ }
449
461
  propsDict["iapkit"] = iapkitDict
450
462
  }
451
463
  // Use JSONSerialization + JSONDecoder like expo-iap does
@@ -10,11 +10,12 @@ import { NitroModules } from 'react-native-nitro-modules';
10
10
 
11
11
  import { ErrorCode } from "./types.js";
12
12
  import { convertNitroProductToProduct, convertNitroPurchaseToPurchase, convertProductToProductSubscription, validateNitroProduct, validateNitroPurchase, convertNitroSubscriptionStatusToSubscriptionStatusIOS } from "./utils/type-bridge.js";
13
- import { parseErrorStringToJsonObj } from "./utils/error.js";
13
+ import { isUserCancelledError, parseErrorStringToJsonObj } from "./utils/error.js";
14
14
  import { normalizeErrorCodeFromNative, createPurchaseError, DUPLICATE_PURCHASE_CODE } from "./utils/errorMapping.js";
15
15
  import { RnIapConsole } from "./utils/debug.js";
16
16
  import { getSuccessFromPurchaseVariant } from "./utils/purchase.js";
17
17
  import { parseAppTransactionPayload } from "./utils.js";
18
+ import { getVegaIapModule, isVegaOS } from "./vega.js";
18
19
 
19
20
  // ------------------------------
20
21
  // Billing Programs API (Android 8.2.0+)
@@ -28,6 +29,7 @@ import { parseAppTransactionPayload } from "./utils.js";
28
29
 
29
30
  export * from "./types.js";
30
31
  export * from "./utils/error.js";
32
+ export * from "./vega.js";
31
33
  const LEGACY_INAPP_WARNING = "[react-native-iap] `type: 'inapp'` is deprecated and will be removed in a future major version. Use 'in-app' instead.";
32
34
  const toErrorMessage = error => {
33
35
  if (typeof error === 'object' && error !== null && 'message' in error && error.message != null) {
@@ -35,6 +37,13 @@ const toErrorMessage = error => {
35
37
  }
36
38
  return String(error ?? '');
37
39
  };
40
+ const parseErrorAndLogIfNeeded = (message, error) => {
41
+ const parsedError = parseErrorStringToJsonObj(error);
42
+ if (!isUserCancelledError(parsedError)) {
43
+ RnIapConsole.error(message, error);
44
+ }
45
+ return parsedError;
46
+ };
38
47
  const unsupportedPlatformError = () => new Error(`Unsupported platform: ${Platform.OS}`);
39
48
  // ActiveSubscription and PurchaseError types are already exported via 'export * from ./types'
40
49
 
@@ -56,6 +65,10 @@ let iapRef = null;
56
65
  */
57
66
  export const isNitroReady = () => {
58
67
  if (iapRef) return true;
68
+ if (isVegaOS()) {
69
+ iapRef = getVegaIapModule();
70
+ return Boolean(iapRef);
71
+ }
59
72
  try {
60
73
  iapRef = NitroModules.createHybridObject('RnIap');
61
74
  return true;
@@ -86,9 +99,20 @@ export const isMacOS = () => {
86
99
  export const isStandardIOS = () => {
87
100
  return Platform.OS === 'ios' && !isTVOS() && !isMacOS();
88
101
  };
102
+ const isAndroidStoreRuntime = () => {
103
+ return Platform.OS === 'android' || isVegaOS();
104
+ };
89
105
  const IAP = {
90
106
  get instance() {
91
107
  if (iapRef) return iapRef;
108
+ if (isVegaOS()) {
109
+ const vegaModule = getVegaIapModule();
110
+ if (!vegaModule) {
111
+ throw new Error('Amazon Vega IAP module is unavailable. Install @amazon-devices/keplerscript-appstore-iap-lib in the Vega app target and build with the React Native for Vega kepler platform.');
112
+ }
113
+ iapRef = vegaModule;
114
+ return iapRef;
115
+ }
92
116
 
93
117
  // Attempt to create the HybridObject and map common Nitro/JSI readiness errors
94
118
  try {
@@ -500,7 +524,7 @@ function tryAttachSubscriptionBillingIssueNative() {
500
524
  } catch (e) {
501
525
  const msg = toErrorMessage(e);
502
526
  if (msg.includes('Nitro runtime not installed')) {
503
- RnIapConsole.warn('[subscriptionBillingIssueListener] Nitro not ready yet; will retry on next registration after initConnection()');
527
+ RnIapConsole.warn('[subscriptionBillingIssueListener] Nitro not ready yet; will retry after initConnection()');
504
528
  } else {
505
529
  throw e;
506
530
  }
@@ -590,7 +614,7 @@ export const fetchProducts = async request => {
590
614
 
591
615
  // item.type === 'subs' case
592
616
  // For Android, check if subscription items have actual offers
593
- if (Platform.OS === 'android' && item.platform === 'android' && item.type === 'subs') {
617
+ if (isAndroidStoreRuntime() && item.platform === 'android' && item.type === 'subs') {
594
618
  // TypeScript now knows this is ProductSubscriptionAndroid
595
619
  const hasSubscriptionOffers = item.subscriptionOfferDetailsAndroid && Array.isArray(item.subscriptionOfferDetailsAndroid) && item.subscriptionOfferDetailsAndroid.length > 0;
596
620
  RnIapConsole.debug(`[fetchProducts] ${item.id}: type=${item.type}, hasOffers=${hasSubscriptionOffers}`);
@@ -622,8 +646,7 @@ export const fetchProducts = async request => {
622
646
  }
623
647
  return convertedProducts;
624
648
  } catch (error) {
625
- RnIapConsole.error('[fetchProducts] Failed:', error);
626
- const parsedError = parseErrorStringToJsonObj(error);
649
+ const parsedError = parseErrorAndLogIfNeeded('[fetchProducts] Failed:', error);
627
650
  throw createPurchaseError({
628
651
  code: parsedError.code,
629
652
  message: parsedError.message,
@@ -677,9 +700,22 @@ export const getAvailablePurchases = async options => {
677
700
  RnIapConsole.warn(`[getAvailablePurchases] Some purchases failed validation: ${nitroPurchases.length - validPurchases.length} invalid`);
678
701
  }
679
702
  return validPurchases.map(convertNitroPurchaseToPurchase);
680
- } else if (Platform.OS === 'android') {
681
- // For Android, we need to call twice for inapp and subs
703
+ } else if (isAndroidStoreRuntime()) {
682
704
  const includeSuspended = Boolean(options?.includeSuspendedAndroid ?? false);
705
+ if (isVegaOS()) {
706
+ const nitroPurchases = await IAP.instance.getAvailablePurchases({
707
+ android: {
708
+ includeSuspended
709
+ }
710
+ });
711
+ const validPurchases = nitroPurchases.filter(validateNitroPurchase);
712
+ if (validPurchases.length !== nitroPurchases.length) {
713
+ RnIapConsole.warn(`[getAvailablePurchases] Some Vega purchases failed validation: ${nitroPurchases.length - validPurchases.length} invalid`);
714
+ }
715
+ return validPurchases.map(convertNitroPurchaseToPurchase);
716
+ }
717
+
718
+ // For Android Play/Horizon/Fire OS, query in-app items and subscriptions separately.
683
719
  const inappNitroPurchases = await IAP.instance.getAvailablePurchases({
684
720
  android: {
685
721
  type: 'inapp',
@@ -728,8 +764,7 @@ export const getPromotedProductIOS = async () => {
728
764
  const converted = convertNitroProductToProduct(nitroProduct);
729
765
  return converted.platform === 'ios' ? converted : null;
730
766
  } catch (error) {
731
- RnIapConsole.error('[getPromotedProductIOS] Failed:', error);
732
- const parsedError = parseErrorStringToJsonObj(error);
767
+ const parsedError = parseErrorAndLogIfNeeded('[getPromotedProductIOS] Failed:', error);
733
768
  throw createPurchaseError({
734
769
  code: parsedError.code,
735
770
  message: parsedError.message,
@@ -772,8 +807,8 @@ export const getStorefrontIOS = async () => {
772
807
  * @see {@link https://openiap.dev/docs/apis/get-storefront}
773
808
  */
774
809
  export const getStorefront = async () => {
775
- if (Platform.OS !== 'ios' && Platform.OS !== 'android') {
776
- RnIapConsole.warn('[getStorefront] Storefront lookup is only supported on iOS and Android.');
810
+ if (Platform.OS !== 'ios' && !isAndroidStoreRuntime()) {
811
+ RnIapConsole.warn('[getStorefront] Storefront lookup is only supported on iOS, Android, and Vega OS.');
777
812
  return '';
778
813
  }
779
814
  const hasUnifiedMethod = typeof IAP.instance.getStorefront === 'function';
@@ -858,8 +893,7 @@ export const subscriptionStatusIOS = async sku => {
858
893
  if (!Array.isArray(statuses)) return [];
859
894
  return statuses.filter(status => status != null).map(convertNitroSubscriptionStatusToSubscriptionStatusIOS);
860
895
  } catch (error) {
861
- RnIapConsole.error('[subscriptionStatusIOS] Failed:', error);
862
- const parsedError = parseErrorStringToJsonObj(error);
896
+ const parsedError = parseErrorAndLogIfNeeded('[subscriptionStatusIOS] Failed:', error);
863
897
  throw createPurchaseError({
864
898
  code: parsedError.code,
865
899
  message: parsedError.message,
@@ -889,8 +923,7 @@ export const currentEntitlementIOS = async sku => {
889
923
  }
890
924
  return null;
891
925
  } catch (error) {
892
- RnIapConsole.error('[currentEntitlementIOS] Failed:', error);
893
- const parsedError = parseErrorStringToJsonObj(error);
926
+ const parsedError = parseErrorAndLogIfNeeded('[currentEntitlementIOS] Failed:', error);
894
927
  throw createPurchaseError({
895
928
  code: parsedError.code,
896
929
  message: parsedError.message,
@@ -920,8 +953,7 @@ export const latestTransactionIOS = async sku => {
920
953
  }
921
954
  return null;
922
955
  } catch (error) {
923
- RnIapConsole.error('[latestTransactionIOS] Failed:', error);
924
- const parsedError = parseErrorStringToJsonObj(error);
956
+ const parsedError = parseErrorAndLogIfNeeded('[latestTransactionIOS] Failed:', error);
925
957
  throw createPurchaseError({
926
958
  code: parsedError.code,
927
959
  message: parsedError.message,
@@ -946,8 +978,7 @@ export const getPendingTransactionsIOS = async () => {
946
978
  const nitroPurchases = await IAP.instance.getPendingTransactionsIOS();
947
979
  return nitroPurchases.map(convertNitroPurchaseToPurchase).filter(purchase => purchase.platform === 'ios');
948
980
  } catch (error) {
949
- RnIapConsole.error('[getPendingTransactionsIOS] Failed:', error);
950
- const parsedError = parseErrorStringToJsonObj(error);
981
+ const parsedError = parseErrorAndLogIfNeeded('[getPendingTransactionsIOS] Failed:', error);
951
982
  throw createPurchaseError({
952
983
  code: parsedError.code,
953
984
  message: parsedError.message,
@@ -970,8 +1001,7 @@ export const getAllTransactionsIOS = async () => {
970
1001
  const nitroPurchases = await IAP.instance.getAllTransactionsIOS();
971
1002
  return nitroPurchases.map(convertNitroPurchaseToPurchase).filter(purchase => purchase.platform === 'ios');
972
1003
  } catch (error) {
973
- RnIapConsole.error('[getAllTransactionsIOS] Failed:', error);
974
- const parsedError = parseErrorStringToJsonObj(error);
1004
+ const parsedError = parseErrorAndLogIfNeeded('[getAllTransactionsIOS] Failed:', error);
975
1005
  throw createPurchaseError({
976
1006
  code: parsedError.code,
977
1007
  message: parsedError.message,
@@ -996,8 +1026,7 @@ export const showManageSubscriptionsIOS = async () => {
996
1026
  const nitroPurchases = await IAP.instance.showManageSubscriptionsIOS();
997
1027
  return nitroPurchases.map(convertNitroPurchaseToPurchase).filter(purchase => purchase.platform === 'ios');
998
1028
  } catch (error) {
999
- RnIapConsole.error('[showManageSubscriptionsIOS] Failed:', error);
1000
- const parsedError = parseErrorStringToJsonObj(error);
1029
+ const parsedError = parseErrorAndLogIfNeeded('[showManageSubscriptionsIOS] Failed:', error);
1001
1030
  throw createPurchaseError({
1002
1031
  code: parsedError.code,
1003
1032
  message: parsedError.message,
@@ -1022,8 +1051,7 @@ export const isEligibleForIntroOfferIOS = async groupID => {
1022
1051
  try {
1023
1052
  return await IAP.instance.isEligibleForIntroOfferIOS(groupID);
1024
1053
  } catch (error) {
1025
- RnIapConsole.error('[isEligibleForIntroOfferIOS] Failed:', error);
1026
- const parsedError = parseErrorStringToJsonObj(error);
1054
+ const parsedError = parseErrorAndLogIfNeeded('[isEligibleForIntroOfferIOS] Failed:', error);
1027
1055
  throw createPurchaseError({
1028
1056
  code: parsedError.code,
1029
1057
  message: parsedError.message,
@@ -1048,8 +1076,7 @@ export const getReceiptDataIOS = async () => {
1048
1076
  try {
1049
1077
  return await IAP.instance.getReceiptDataIOS();
1050
1078
  } catch (error) {
1051
- RnIapConsole.error('[getReceiptDataIOS] Failed:', error);
1052
- const parsedError = parseErrorStringToJsonObj(error);
1079
+ const parsedError = parseErrorAndLogIfNeeded('[getReceiptDataIOS] Failed:', error);
1053
1080
  throw createPurchaseError({
1054
1081
  code: parsedError.code,
1055
1082
  message: parsedError.message,
@@ -1069,8 +1096,7 @@ export const getReceiptIOS = async () => {
1069
1096
  }
1070
1097
  return await IAP.instance.getReceiptDataIOS();
1071
1098
  } catch (error) {
1072
- RnIapConsole.error('[getReceiptIOS] Failed:', error);
1073
- const parsedError = parseErrorStringToJsonObj(error);
1099
+ const parsedError = parseErrorAndLogIfNeeded('[getReceiptIOS] Failed:', error);
1074
1100
  throw createPurchaseError({
1075
1101
  code: parsedError.code,
1076
1102
  message: parsedError.message,
@@ -1090,8 +1116,7 @@ export const requestReceiptRefreshIOS = async () => {
1090
1116
  }
1091
1117
  return await IAP.instance.getReceiptDataIOS();
1092
1118
  } catch (error) {
1093
- RnIapConsole.error('[requestReceiptRefreshIOS] Failed:', error);
1094
- const parsedError = parseErrorStringToJsonObj(error);
1119
+ const parsedError = parseErrorAndLogIfNeeded('[requestReceiptRefreshIOS] Failed:', error);
1095
1120
  throw createPurchaseError({
1096
1121
  code: parsedError.code,
1097
1122
  message: parsedError.message,
@@ -1116,8 +1141,7 @@ export const isTransactionVerifiedIOS = async sku => {
1116
1141
  try {
1117
1142
  return await IAP.instance.isTransactionVerifiedIOS(sku);
1118
1143
  } catch (error) {
1119
- RnIapConsole.error('[isTransactionVerifiedIOS] Failed:', error);
1120
- const parsedError = parseErrorStringToJsonObj(error);
1144
+ const parsedError = parseErrorAndLogIfNeeded('[isTransactionVerifiedIOS] Failed:', error);
1121
1145
  throw createPurchaseError({
1122
1146
  code: parsedError.code,
1123
1147
  message: parsedError.message,
@@ -1142,8 +1166,7 @@ export const getTransactionJwsIOS = async sku => {
1142
1166
  try {
1143
1167
  return await IAP.instance.getTransactionJwsIOS(sku);
1144
1168
  } catch (error) {
1145
- RnIapConsole.error('[getTransactionJwsIOS] Failed:', error);
1146
- const parsedError = parseErrorStringToJsonObj(error);
1169
+ const parsedError = parseErrorAndLogIfNeeded('[getTransactionJwsIOS] Failed:', error);
1147
1170
  throw createPurchaseError({
1148
1171
  code: parsedError.code,
1149
1172
  message: parsedError.message,
@@ -1178,10 +1201,13 @@ export const getTransactionJwsIOS = async sku => {
1178
1201
  */
1179
1202
  export const initConnection = async config => {
1180
1203
  try {
1181
- return await IAP.instance.initConnection(config);
1204
+ const result = await IAP.instance.initConnection(config);
1205
+ if (subscriptionBillingIssueJsListeners.size > 0) {
1206
+ tryAttachSubscriptionBillingIssueNative();
1207
+ }
1208
+ return result;
1182
1209
  } catch (error) {
1183
- RnIapConsole.error('Failed to initialize IAP connection:', error);
1184
- const parsedError = parseErrorStringToJsonObj(error);
1210
+ const parsedError = parseErrorAndLogIfNeeded('Failed to initialize IAP connection:', error);
1185
1211
  throw createPurchaseError({
1186
1212
  code: parsedError.code,
1187
1213
  message: parsedError.message,
@@ -1203,8 +1229,7 @@ export const endConnection = async () => {
1203
1229
  resetListenerState();
1204
1230
  return result;
1205
1231
  } catch (error) {
1206
- RnIapConsole.error('Failed to end IAP connection:', error);
1207
- const parsedError = parseErrorStringToJsonObj(error);
1232
+ const parsedError = parseErrorAndLogIfNeeded('Failed to end IAP connection:', error);
1208
1233
  throw createPurchaseError({
1209
1234
  code: parsedError.code,
1210
1235
  message: parsedError.message,
@@ -1229,8 +1254,7 @@ export const restorePurchases = async () => {
1229
1254
  onlyIncludeActiveItemsIOS: true
1230
1255
  });
1231
1256
  } catch (error) {
1232
- RnIapConsole.error('Failed to restore purchases:', error);
1233
- const parsedError = parseErrorStringToJsonObj(error);
1257
+ const parsedError = parseErrorAndLogIfNeeded('Failed to restore purchases:', error);
1234
1258
  throw createPurchaseError({
1235
1259
  code: parsedError.code,
1236
1260
  message: parsedError.message,
@@ -1284,7 +1308,7 @@ export const requestPurchase = async request => {
1284
1308
  if (!iosRequest?.sku) {
1285
1309
  throw new Error('Invalid request for iOS. The `sku` property is required.');
1286
1310
  }
1287
- } else if (Platform.OS === 'android') {
1311
+ } else if (isAndroidStoreRuntime()) {
1288
1312
  // Support both 'google' (recommended) and 'android' (deprecated) fields
1289
1313
  const androidRequest = perPlatformRequest.google ?? perPlatformRequest.android;
1290
1314
  if (!androidRequest?.skus?.length) {
@@ -1338,7 +1362,7 @@ export const requestPurchase = async request => {
1338
1362
 
1339
1363
  // Support both 'google' (recommended) and 'android' (deprecated) fields
1340
1364
  const androidRequestSource = perPlatformRequest.google ?? perPlatformRequest.android;
1341
- if (Platform.OS === 'android' && androidRequestSource) {
1365
+ if (isAndroidStoreRuntime() && androidRequestSource) {
1342
1366
  const androidRequest = isSubs ? androidRequestSource : androidRequestSource;
1343
1367
  const androidPayload = {
1344
1368
  skus: androidRequest.skus
@@ -1380,8 +1404,7 @@ export const requestPurchase = async request => {
1380
1404
  }
1381
1405
  return await IAP.instance.requestPurchase(unifiedRequest);
1382
1406
  } catch (error) {
1383
- RnIapConsole.error('Failed to request purchase:', error);
1384
- const parsedError = parseErrorStringToJsonObj(error);
1407
+ const parsedError = parseErrorAndLogIfNeeded('Failed to request purchase:', error);
1385
1408
  throw createPurchaseError({
1386
1409
  code: parsedError.code,
1387
1410
  message: parsedError.message,
@@ -1432,7 +1455,7 @@ export const finishTransaction = async args => {
1432
1455
  transactionId: purchase.id
1433
1456
  }
1434
1457
  };
1435
- } else if (Platform.OS === 'android') {
1458
+ } else if (isAndroidStoreRuntime()) {
1436
1459
  const token = purchase.purchaseToken ?? undefined;
1437
1460
  if (!token) {
1438
1461
  throw new Error('purchaseToken required to finish Android transaction');
@@ -1482,8 +1505,8 @@ export const finishTransaction = async args => {
1482
1505
  */
1483
1506
  export const acknowledgePurchaseAndroid = async purchaseToken => {
1484
1507
  try {
1485
- if (Platform.OS !== 'android') {
1486
- throw new Error('acknowledgePurchaseAndroid is only available on Android');
1508
+ if (!isAndroidStoreRuntime()) {
1509
+ throw new Error('acknowledgePurchaseAndroid is only available on Android and Vega OS');
1487
1510
  }
1488
1511
  const result = await IAP.instance.finishTransaction({
1489
1512
  android: {
@@ -1493,8 +1516,7 @@ export const acknowledgePurchaseAndroid = async purchaseToken => {
1493
1516
  });
1494
1517
  return getSuccessFromPurchaseVariant(result, 'acknowledgePurchaseAndroid');
1495
1518
  } catch (error) {
1496
- RnIapConsole.error('Failed to acknowledge purchase Android:', error);
1497
- const parsedError = parseErrorStringToJsonObj(error);
1519
+ const parsedError = parseErrorAndLogIfNeeded('Failed to acknowledge purchase Android:', error);
1498
1520
  throw createPurchaseError({
1499
1521
  code: parsedError.code,
1500
1522
  message: parsedError.message,
@@ -1518,8 +1540,8 @@ export const acknowledgePurchaseAndroid = async purchaseToken => {
1518
1540
  */
1519
1541
  export const consumePurchaseAndroid = async purchaseToken => {
1520
1542
  try {
1521
- if (Platform.OS !== 'android') {
1522
- throw new Error('consumePurchaseAndroid is only available on Android');
1543
+ if (!isAndroidStoreRuntime()) {
1544
+ throw new Error('consumePurchaseAndroid is only available on Android and Vega OS');
1523
1545
  }
1524
1546
  const result = await IAP.instance.finishTransaction({
1525
1547
  android: {
@@ -1529,8 +1551,7 @@ export const consumePurchaseAndroid = async purchaseToken => {
1529
1551
  });
1530
1552
  return getSuccessFromPurchaseVariant(result, 'consumePurchaseAndroid');
1531
1553
  } catch (error) {
1532
- RnIapConsole.error('Failed to consume purchase Android:', error);
1533
- const parsedError = parseErrorStringToJsonObj(error);
1554
+ const parsedError = parseErrorAndLogIfNeeded('Failed to consume purchase Android:', error);
1534
1555
  throw createPurchaseError({
1535
1556
  code: parsedError.code,
1536
1557
  message: parsedError.message,
@@ -1656,8 +1677,7 @@ export const validateReceipt = async options => {
1656
1677
  return result;
1657
1678
  }
1658
1679
  } catch (error) {
1659
- RnIapConsole.error('[validateReceipt] Failed:', error);
1660
- const parsedError = parseErrorStringToJsonObj(error);
1680
+ const parsedError = parseErrorAndLogIfNeeded('[validateReceipt] Failed:', error);
1661
1681
  throw createPurchaseError({
1662
1682
  code: parsedError.code,
1663
1683
  message: parsedError.message,
@@ -1713,8 +1733,14 @@ export const validateReceiptIOS = async options => {
1713
1733
  * provider: 'iapkit',
1714
1734
  * iapkit: {
1715
1735
  * apiKey: 'your-api-key',
1716
- * apple: { jws: purchase.purchaseToken },
1717
- * google: { purchaseToken: purchase.purchaseToken },
1736
+ * // Choose exactly one store payload.
1737
+ * // apple: { jws: purchase.purchaseToken },
1738
+ * // google: { purchaseToken: purchase.purchaseToken },
1739
+ * amazon: {
1740
+ * userId: amazonUserId,
1741
+ * receiptId: purchase.purchaseToken,
1742
+ * sandbox: __DEV__,
1743
+ * },
1718
1744
  * },
1719
1745
  * });
1720
1746
  * ```
@@ -1744,8 +1770,7 @@ export const verifyPurchaseWithProvider = async options => {
1744
1770
  errors: result.errors ?? null
1745
1771
  };
1746
1772
  } catch (error) {
1747
- RnIapConsole.error('[verifyPurchaseWithProvider] Failed:', error);
1748
- const parsedError = parseErrorStringToJsonObj(error);
1773
+ const parsedError = parseErrorAndLogIfNeeded('[verifyPurchaseWithProvider] Failed:', error);
1749
1774
  throw createPurchaseError({
1750
1775
  code: parsedError.code,
1751
1776
  message: parsedError.message,
@@ -1770,8 +1795,7 @@ export const syncIOS = async () => {
1770
1795
  const result = await IAP.instance.syncIOS();
1771
1796
  return Boolean(result);
1772
1797
  } catch (error) {
1773
- RnIapConsole.error('[syncIOS] Failed:', error);
1774
- const parsedError = parseErrorStringToJsonObj(error);
1798
+ const parsedError = parseErrorAndLogIfNeeded('[syncIOS] Failed:', error);
1775
1799
  throw createPurchaseError({
1776
1800
  code: parsedError.code,
1777
1801
  message: parsedError.message,
@@ -1796,8 +1820,7 @@ export const presentCodeRedemptionSheetIOS = async () => {
1796
1820
  const result = await IAP.instance.presentCodeRedemptionSheetIOS();
1797
1821
  return Boolean(result);
1798
1822
  } catch (error) {
1799
- RnIapConsole.error('[presentCodeRedemptionSheetIOS] Failed:', error);
1800
- const parsedError = parseErrorStringToJsonObj(error);
1823
+ const parsedError = parseErrorAndLogIfNeeded('[presentCodeRedemptionSheetIOS] Failed:', error);
1801
1824
  throw createPurchaseError({
1802
1825
  code: parsedError.code,
1803
1826
  message: parsedError.message,
@@ -1846,8 +1869,7 @@ export const requestPurchaseOnPromotedProductIOS = async () => {
1846
1869
  }
1847
1870
  return true;
1848
1871
  } catch (error) {
1849
- RnIapConsole.error('[requestPurchaseOnPromotedProductIOS] Failed:', error);
1850
- const parsedError = parseErrorStringToJsonObj(error);
1872
+ const parsedError = parseErrorAndLogIfNeeded('[requestPurchaseOnPromotedProductIOS] Failed:', error);
1851
1873
  throw createPurchaseError({
1852
1874
  code: parsedError.code,
1853
1875
  message: parsedError.message,
@@ -1872,8 +1894,7 @@ export const clearTransactionIOS = async () => {
1872
1894
  await IAP.instance.clearTransactionIOS();
1873
1895
  return true;
1874
1896
  } catch (error) {
1875
- RnIapConsole.error('[clearTransactionIOS] Failed:', error);
1876
- const parsedError = parseErrorStringToJsonObj(error);
1897
+ const parsedError = parseErrorAndLogIfNeeded('[clearTransactionIOS] Failed:', error);
1877
1898
  throw createPurchaseError({
1878
1899
  code: parsedError.code,
1879
1900
  message: parsedError.message,
@@ -1899,8 +1920,7 @@ export const beginRefundRequestIOS = async sku => {
1899
1920
  const status = await IAP.instance.beginRefundRequestIOS(sku);
1900
1921
  return status ?? null;
1901
1922
  } catch (error) {
1902
- RnIapConsole.error('[beginRefundRequestIOS] Failed:', error);
1903
- const parsedError = parseErrorStringToJsonObj(error);
1923
+ const parsedError = parseErrorAndLogIfNeeded('[beginRefundRequestIOS] Failed:', error);
1904
1924
  throw createPurchaseError({
1905
1925
  code: parsedError.code,
1906
1926
  message: parsedError.message,
@@ -1946,8 +1966,7 @@ export const deepLinkToSubscriptionsIOS = async () => {
1946
1966
  await IAP.instance.showManageSubscriptionsIOS();
1947
1967
  return true;
1948
1968
  } catch (error) {
1949
- RnIapConsole.error('[deepLinkToSubscriptionsIOS] Failed:', error);
1950
- const parsedError = parseErrorStringToJsonObj(error);
1969
+ const parsedError = parseErrorAndLogIfNeeded('[deepLinkToSubscriptionsIOS] Failed:', error);
1951
1970
  throw createPurchaseError({
1952
1971
  code: parsedError.code,
1953
1972
  message: parsedError.message,
@@ -2015,8 +2034,7 @@ export const getActiveSubscriptions = async subscriptionIds => {
2015
2034
  RnIapConsole.error('IAP connection not initialized:', error);
2016
2035
  throw error;
2017
2036
  }
2018
- RnIapConsole.error('Failed to get active subscriptions:', error);
2019
- const parsedError = parseErrorStringToJsonObj(error);
2037
+ const parsedError = parseErrorAndLogIfNeeded('Failed to get active subscriptions:', error);
2020
2038
  throw createPurchaseError({
2021
2039
  code: parsedError.code,
2022
2040
  message: parsedError.message,