react-native-iap 16.2.4 → 16.3.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 (65) hide show
  1. package/README.md +2 -2
  2. package/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt +115 -15
  3. package/ios/HybridRnIap.swift +5 -1
  4. package/lib/module/index.js +57 -7
  5. package/lib/module/index.js.map +1 -1
  6. package/lib/module/index.kepler.js +9 -0
  7. package/lib/module/index.kepler.js.map +1 -1
  8. package/lib/module/kit-api.js +3 -1
  9. package/lib/module/kit-api.js.map +1 -1
  10. package/lib/module/types.js +2 -0
  11. package/lib/module/types.js.map +1 -1
  12. package/lib/module/vega-adapter.js +9 -0
  13. package/lib/module/vega-adapter.js.map +1 -1
  14. package/lib/typescript/src/index.d.ts +3 -1
  15. package/lib/typescript/src/index.d.ts.map +1 -1
  16. package/lib/typescript/src/index.kepler.d.ts.map +1 -1
  17. package/lib/typescript/src/kit-api.d.ts +119 -2
  18. package/lib/typescript/src/kit-api.d.ts.map +1 -1
  19. package/lib/typescript/src/specs/RnIap.nitro.d.ts +13 -3
  20. package/lib/typescript/src/specs/RnIap.nitro.d.ts.map +1 -1
  21. package/lib/typescript/src/types.d.ts +44 -9
  22. package/lib/typescript/src/types.d.ts.map +1 -1
  23. package/lib/typescript/src/vega-adapter.d.ts.map +1 -1
  24. package/nitrogen/generated/android/NitroIap+autolinking.cmake +1 -1
  25. package/nitrogen/generated/android/c++/JHybridRnIapSpec.cpp +8 -4
  26. package/nitrogen/generated/android/c++/JHybridRnIapSpec.hpp +1 -1
  27. package/nitrogen/generated/android/c++/JNitroPurchaseVerificationResultAndroid.hpp +5 -1
  28. package/nitrogen/generated/android/c++/JNitroPurchaseVerificationResultHorizon.hpp +69 -0
  29. package/nitrogen/generated/android/c++/JNitroVerifyPurchaseWithIapkitAmazonProps.hpp +5 -1
  30. package/nitrogen/generated/android/c++/JNitroVerifyPurchaseWithIapkitResult.hpp +5 -1
  31. package/nitrogen/generated/android/c++/JVariant_NitroPurchaseVerificationResultIOS_NitroPurchaseVerificationResultAndroid_NitroPurchaseVerificationResultHorizon.cpp +30 -0
  32. package/nitrogen/generated/android/c++/{JVariant_NitroPurchaseVerificationResultIOS_NitroPurchaseVerificationResultAndroid.hpp → JVariant_NitroPurchaseVerificationResultIOS_NitroPurchaseVerificationResultAndroid_NitroPurchaseVerificationResultHorizon.hpp} +33 -16
  33. package/nitrogen/generated/android/c++/JVariant_NullType_NitroVerifyPurchaseWithIapkitAmazonProps.hpp +1 -1
  34. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/HybridRnIapSpec.kt +1 -1
  35. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/NitroPurchaseVerificationResultAndroid.kt +8 -3
  36. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/NitroPurchaseVerificationResultHorizon.kt +61 -0
  37. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/NitroVerifyPurchaseWithIapkitAmazonProps.kt +8 -3
  38. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/NitroVerifyPurchaseWithIapkitResult.kt +7 -2
  39. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/{Variant_NitroPurchaseVerificationResultIOS_NitroPurchaseVerificationResultAndroid.kt → Variant_NitroPurchaseVerificationResultIOS_NitroPurchaseVerificationResultAndroid_NitroPurchaseVerificationResultHorizon.kt} +21 -8
  40. package/nitrogen/generated/ios/NitroIap-Swift-Cxx-Bridge.cpp +4 -4
  41. package/nitrogen/generated/ios/NitroIap-Swift-Cxx-Bridge.hpp +43 -34
  42. package/nitrogen/generated/ios/NitroIap-Swift-Cxx-Umbrella.hpp +3 -0
  43. package/nitrogen/generated/ios/c++/HybridRnIapSpecSwift.hpp +4 -1
  44. package/nitrogen/generated/ios/swift/{Func_void_std__variant_NitroPurchaseVerificationResultIOS__NitroPurchaseVerificationResultAndroid_.swift → Func_void_std__variant_NitroPurchaseVerificationResultIOS__NitroPurchaseVerificationResultAndroid__NitroPurchaseVerificationResultHorizon_.swift} +14 -11
  45. package/nitrogen/generated/ios/swift/HybridRnIapSpec.swift +1 -1
  46. package/nitrogen/generated/ios/swift/HybridRnIapSpec_cxx.swift +11 -9
  47. package/nitrogen/generated/ios/swift/NitroPurchaseVerificationResultAndroid.swift +7 -2
  48. package/nitrogen/generated/ios/swift/NitroPurchaseVerificationResultHorizon.swift +71 -0
  49. package/nitrogen/generated/ios/swift/NitroVerifyPurchaseWithIapkitAmazonProps.swift +39 -2
  50. package/nitrogen/generated/ios/swift/NitroVerifyPurchaseWithIapkitResult.swift +38 -1
  51. package/nitrogen/generated/ios/swift/{Variant_NitroPurchaseVerificationResultIOS_NitroPurchaseVerificationResultAndroid.swift → Variant_NitroPurchaseVerificationResultIOS_NitroPurchaseVerificationResultAndroid_NitroPurchaseVerificationResultHorizon.swift} +6 -4
  52. package/nitrogen/generated/shared/c++/HybridRnIapSpec.hpp +4 -1
  53. package/nitrogen/generated/shared/c++/NitroPurchaseVerificationResultAndroid.hpp +5 -1
  54. package/nitrogen/generated/shared/c++/NitroPurchaseVerificationResultHorizon.hpp +93 -0
  55. package/nitrogen/generated/shared/c++/NitroVerifyPurchaseWithIapkitAmazonProps.hpp +6 -2
  56. package/nitrogen/generated/shared/c++/NitroVerifyPurchaseWithIapkitResult.hpp +5 -1
  57. package/openiap-versions.json +3 -3
  58. package/package.json +1 -1
  59. package/src/index.kepler.ts +10 -2
  60. package/src/index.ts +94 -19
  61. package/src/kit-api.ts +125 -4
  62. package/src/specs/RnIap.nitro.ts +16 -2
  63. package/src/types.ts +45 -9
  64. package/src/vega-adapter.ts +10 -0
  65. package/nitrogen/generated/android/c++/JVariant_NitroPurchaseVerificationResultIOS_NitroPurchaseVerificationResultAndroid.cpp +0 -26
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  **React Native IAP** is a high-performance in-app purchase library using Nitro Modules that **conforms to the [Open IAP specification](https://openiap.dev)**. It provides a unified API for handling in-app purchases across iOS and Android platforms with comprehensive error handling and modern TypeScript support.
9
9
 
10
- <a href="https://openiap.dev"><img src="https://raw.githubusercontent.com/hyodotdev/openiap/main/logo.png" alt="Open IAP" height="40" /></a>
10
+ <a href="https://openiap.dev"><img src="https://raw.githubusercontent.com/hyodotdev/openiap/main/logo.webp" alt="Open IAP" height="40" /></a>
11
11
  </div>
12
12
 
13
13
  ## 📚 Documentation
@@ -137,7 +137,7 @@ Quick links:
137
137
 
138
138
  ## Powered by OpenIAP
139
139
 
140
- <a href="https://openiap.dev"><img src="https://raw.githubusercontent.com/hyodotdev/openiap/main/logo.png" alt="OpenIAP" height="50" /></a>
140
+ <a href="https://openiap.dev"><img src="https://raw.githubusercontent.com/hyodotdev/openiap/main/logo.webp" alt="OpenIAP" height="50" /></a>
141
141
 
142
142
  React Native IAP conforms to the **[OpenIAP specification](https://openiap.dev)** — an open, vendor-neutral interoperability standard for in-app purchases. OpenIAP provides:
143
143
 
@@ -4,7 +4,7 @@ import com.facebook.react.bridge.ReactApplicationContext
4
4
  import com.margelo.nitro.NitroModules
5
5
  import com.margelo.nitro.core.NullType
6
6
  import com.margelo.nitro.core.Promise
7
- import dev.hyo.openiap.AndroidSubscriptionOfferInput
7
+ import dev.hyo.openiap.AndroidSubscriptionOfferInput as OpenIapAndroidSubscriptionOfferInput
8
8
  import dev.hyo.openiap.DeepLinkOptions as OpenIapDeepLinkOptions
9
9
  import dev.hyo.openiap.FetchProductsResult
10
10
  import dev.hyo.openiap.FetchProductsResultAll
@@ -31,8 +31,10 @@ import dev.hyo.openiap.SubResponseCodeAndroid as OpenIapSubResponseCodeAndroid
31
31
  import dev.hyo.openiap.SubscriptionProductReplacementParamsAndroid as OpenIapSubscriptionProductReplacementParams
32
32
  import dev.hyo.openiap.SubscriptionReplacementModeAndroid as OpenIapSubscriptionReplacementMode
33
33
  import dev.hyo.openiap.VerifyPurchaseGoogleOptions
34
+ import dev.hyo.openiap.VerifyPurchaseHorizonOptions
34
35
  import dev.hyo.openiap.VerifyPurchaseProps
35
36
  import dev.hyo.openiap.VerifyPurchaseResultAndroid
37
+ import dev.hyo.openiap.VerifyPurchaseResultHorizon
36
38
  import dev.hyo.openiap.InitConnectionConfig as OpenIapInitConnectionConfig
37
39
  import dev.hyo.openiap.listener.OpenIapPurchaseErrorListener
38
40
  import dev.hyo.openiap.listener.OpenIapPurchaseUpdateListener
@@ -99,6 +101,44 @@ internal fun <T> removeSingletonBridgeListener(
99
101
  }
100
102
  }
101
103
 
104
+ internal fun requireValidSubscriptionOffers(
105
+ offers: Array<AndroidSubscriptionOfferInput>?,
106
+ ): List<OpenIapAndroidSubscriptionOfferInput> {
107
+ require(offers.orEmpty().all { it.sku.isNotBlank() && it.offerToken.isNotBlank() }) {
108
+ "Every subscription offer must include non-empty sku and offerToken strings"
109
+ }
110
+ return offers.orEmpty().map { offer ->
111
+ OpenIapAndroidSubscriptionOfferInput(
112
+ sku = offer.sku,
113
+ offerToken = offer.offerToken,
114
+ )
115
+ }
116
+ }
117
+
118
+ internal fun requireMatchingPurchaseOptions(
119
+ type: ProductQueryType,
120
+ hasSubscriptionOffers: Boolean,
121
+ hasSubscriptionReplacementParams: Boolean,
122
+ purchaseToken: String?,
123
+ originalExternalTransactionId: String?,
124
+ offerToken: String?,
125
+ ) {
126
+ when (type) {
127
+ ProductQueryType.InApp -> require(
128
+ !hasSubscriptionOffers &&
129
+ !hasSubscriptionReplacementParams &&
130
+ purchaseToken == null &&
131
+ originalExternalTransactionId == null,
132
+ ) { "Subscription options require product type subs" }
133
+ ProductQueryType.Subs -> require(offerToken == null) {
134
+ "offerToken requires product type in-app"
135
+ }
136
+ ProductQueryType.All -> throw IllegalArgumentException(
137
+ "Product type all is only supported for product queries"
138
+ )
139
+ }
140
+ }
141
+
102
142
  /**
103
143
  * Bound for the pending purchase-event queues, matching expo-iap's
104
144
  * ExpoIapHelper.MAX_BUFFERED_EVENTS. Oldest entries are dropped on overflow.
@@ -555,17 +595,37 @@ class HybridRnIap : HybridRnIapSpec() {
555
595
  val typeHint = androidRequest.skus.firstOrNull()?.let { productTypeBySku[it] } ?: "in-app"
556
596
  val queryType = parseProductQueryType(typeHint)
557
597
 
558
- val subscriptionOffers = (androidRequest.subscriptionOffers as? Variant_NullType_Array_AndroidSubscriptionOfferInput_.Second)?.value
559
- ?.mapNotNull { offer ->
560
- val sku = offer.sku
561
- val token = offer.offerToken
562
- if (sku.isBlank() || token.isBlank()) {
563
- null
564
- } else {
565
- AndroidSubscriptionOfferInput(sku = sku, offerToken = token)
566
- }
567
- }
568
- ?: emptyList()
598
+ try {
599
+ requireMatchingPurchaseOptions(
600
+ type = queryType,
601
+ hasSubscriptionOffers = androidRequest.subscriptionOffers is
602
+ Variant_NullType_Array_AndroidSubscriptionOfferInput_.Second,
603
+ hasSubscriptionReplacementParams =
604
+ androidRequest.subscriptionProductReplacementParams is
605
+ Variant_NullType_SubscriptionProductReplacementParamsAndroid.Second,
606
+ purchaseToken = androidRequest.purchaseToken.unwrapString(),
607
+ originalExternalTransactionId =
608
+ androidRequest.originalExternalTransactionId.unwrapString(),
609
+ offerToken = androidRequest.offerToken.unwrapString(),
610
+ )
611
+ } catch (error: IllegalArgumentException) {
612
+ sendPurchaseError(
613
+ toErrorResult(OpenIapError.DeveloperError(error.message))
614
+ )
615
+ return@async defaultResult
616
+ }
617
+
618
+ val subscriptionOffers = try {
619
+ requireValidSubscriptionOffers(
620
+ (androidRequest.subscriptionOffers as?
621
+ Variant_NullType_Array_AndroidSubscriptionOfferInput_.Second)?.value,
622
+ )
623
+ } catch (error: IllegalArgumentException) {
624
+ sendPurchaseError(
625
+ toErrorResult(OpenIapError.DeveloperError(error.message))
626
+ )
627
+ return@async defaultResult
628
+ }
569
629
  val normalizedOffers = subscriptionOffers.takeIf { it.isNotEmpty() }
570
630
  val developerBillingOption =
571
631
  (androidRequest.developerBillingOption as?
@@ -1405,10 +1465,45 @@ class HybridRnIap : HybridRnIapSpec() {
1405
1465
  }
1406
1466
 
1407
1467
  // Receipt validation - calls OpenIAP's verifyPurchase
1408
- override fun verifyPurchase(params: NitroPurchaseVerificationParams): Promise<Variant_NitroPurchaseVerificationResultIOS_NitroPurchaseVerificationResultAndroid> {
1468
+ override fun verifyPurchase(params: NitroPurchaseVerificationParams): Promise<Variant_NitroPurchaseVerificationResultIOS_NitroPurchaseVerificationResultAndroid_NitroPurchaseVerificationResultHorizon> {
1409
1469
  return Promise.async {
1410
1470
  try {
1411
- // For Android, we need the google options to be provided (new platform-specific structure)
1471
+ val nitroHorizonOptions =
1472
+ (params.horizon as? Variant_NullType_NitroPurchaseVerificationHorizonOptions.Second)?.value
1473
+ if (nitroHorizonOptions != null) {
1474
+ val validations = mapOf(
1475
+ "horizon.sku" to nitroHorizonOptions.sku,
1476
+ "horizon.userId" to nitroHorizonOptions.userId,
1477
+ "horizon.accessToken" to nitroHorizonOptions.accessToken
1478
+ )
1479
+ for ((name, value) in validations) {
1480
+ if (value.isEmpty()) {
1481
+ throw OpenIapException(toErrorJson(OpenIapError.DeveloperError(), debugMessage = "Missing or empty required parameter: $name"))
1482
+ }
1483
+ }
1484
+
1485
+ RnIapLog.payload("verifyPurchase", mapOf(
1486
+ "store" to "horizon",
1487
+ "sku" to nitroHorizonOptions.sku
1488
+ ))
1489
+ val props = VerifyPurchaseProps(
1490
+ horizon = VerifyPurchaseHorizonOptions(
1491
+ sku = nitroHorizonOptions.sku,
1492
+ userId = nitroHorizonOptions.userId,
1493
+ accessToken = nitroHorizonOptions.accessToken
1494
+ )
1495
+ )
1496
+ val horizonResult = openIap.verifyPurchase(props) as? VerifyPurchaseResultHorizon
1497
+ ?: throw OpenIapException(toErrorJson(OpenIapError.InvalidPurchaseVerification, debugMessage = "Unexpected Horizon result type from verifyPurchase"))
1498
+ @Suppress("DEPRECATION")
1499
+ val result = NitroPurchaseVerificationResultHorizon(
1500
+ isValid = horizonResult.isValid,
1501
+ grantTime = horizonResult.grantTime.wrapVariant(),
1502
+ success = horizonResult.success
1503
+ )
1504
+ return@async Variant_NitroPurchaseVerificationResultIOS_NitroPurchaseVerificationResultAndroid_NitroPurchaseVerificationResultHorizon.Third(result)
1505
+ }
1506
+
1412
1507
  val nitroGoogleOptions = (params.google as? Variant_NullType_NitroPurchaseVerificationGoogleOptions.Second)?.value
1413
1508
  ?: throw OpenIapException(toErrorJson(OpenIapError.DeveloperError(), debugMessage = "Missing required parameter: google options"))
1414
1509
 
@@ -1459,6 +1554,7 @@ class HybridRnIap : HybridRnIapSpec() {
1459
1554
 
1460
1555
  // Convert OpenIAP result to Nitro result
1461
1556
  val result = NitroPurchaseVerificationResultAndroid(
1557
+ isValid = androidResult.isValid,
1462
1558
  autoRenewing = androidResult.autoRenewing,
1463
1559
  betaProduct = androidResult.betaProduct,
1464
1560
  cancelDate = androidResult.cancelDate.wrapVariant(),
@@ -1479,7 +1575,7 @@ class HybridRnIap : HybridRnIapSpec() {
1479
1575
  testTransaction = androidResult.testTransaction
1480
1576
  )
1481
1577
 
1482
- Variant_NitroPurchaseVerificationResultIOS_NitroPurchaseVerificationResultAndroid.Second(result)
1578
+ Variant_NitroPurchaseVerificationResultIOS_NitroPurchaseVerificationResultAndroid_NitroPurchaseVerificationResultHorizon.Second(result)
1483
1579
 
1484
1580
  } catch (e: OpenIapException) {
1485
1581
  RnIapLog.failure("verifyPurchase", e)
@@ -1524,6 +1620,9 @@ class HybridRnIap : HybridRnIapSpec() {
1524
1620
  val amazonMap = mutableMapOf<String, Any?>(
1525
1621
  "receiptId" to amazon.receiptId
1526
1622
  )
1623
+ amazon.expectedProductId.unwrapString()?.let {
1624
+ amazonMap["expectedProductId"] = it
1625
+ }
1527
1626
  amazon.userId.unwrapString()?.let { amazonMap["userId"] = it }
1528
1627
  amazon.sandbox.unwrapBool()?.let { amazonMap["sandbox"] = it }
1529
1628
  iapkitMap["amazon"] = amazonMap
@@ -1558,6 +1657,7 @@ class HybridRnIap : HybridRnIapSpec() {
1558
1657
  clientPayload = clientPayload?.let {
1559
1658
  Variant_NullType_NitroIapkitProductClientPayload.Second(it)
1560
1659
  },
1660
+ environment = item.environment?.let { Variant_NullType_String.Second(it) },
1561
1661
  isValid = item.isValid,
1562
1662
  productId = item.productId?.let { Variant_NullType_String.Second(it) },
1563
1663
  // Use rawValue ("pending-acknowledgment"), not the Kotlin
@@ -399,7 +399,7 @@ class HybridRnIap: HybridRnIapSpec {
399
399
  }
400
400
  }
401
401
 
402
- func verifyPurchase(params: NitroPurchaseVerificationParams) throws -> Promise<Variant_NitroPurchaseVerificationResultIOS_NitroPurchaseVerificationResultAndroid> {
402
+ func verifyPurchase(params: NitroPurchaseVerificationParams) throws -> Promise<Variant_NitroPurchaseVerificationResultIOS_NitroPurchaseVerificationResultAndroid_NitroPurchaseVerificationResultHorizon> {
403
403
  return Promise.async {
404
404
  do {
405
405
  // Extract SKU from apple options (new platform-specific structure)
@@ -475,6 +475,9 @@ class HybridRnIap: HybridRnIapSpec {
475
475
  var amazonDict: [String: Any] = [
476
476
  "receiptId": amazon.receiptId
477
477
  ]
478
+ if case .second(let expectedProductId) = amazon.expectedProductId {
479
+ amazonDict["expectedProductId"] = expectedProductId
480
+ }
478
481
  if case .second(let sandbox) = amazon.sandbox {
479
482
  amazonDict["sandbox"] = sandbox
480
483
  }
@@ -506,6 +509,7 @@ class HybridRnIap: HybridRnIapSpec {
506
509
  }
507
510
  nitroIapkitResult = NitroVerifyPurchaseWithIapkitResult(
508
511
  clientPayload: clientPayload.map { .second($0) },
512
+ environment: RnIapHelper.wrapString(item.environment),
509
513
  isValid: item.isValid,
510
514
  productId: RnIapHelper.wrapString(item.productId),
511
515
  state: IapkitPurchaseState(fromString: item.state.rawValue) ?? .unknown,
@@ -621,7 +621,10 @@ export const fetchProducts = async request => {
621
621
  } = request;
622
622
  try {
623
623
  if (!skus?.length) {
624
- throw new Error('No SKUs provided');
624
+ throw createPurchaseError({
625
+ message: 'No SKUs provided',
626
+ code: ErrorCode.EmptySkuList
627
+ });
625
628
  }
626
629
  const normalizedType = normalizeProductQueryType(type);
627
630
  const fetchAndConvert = async nitroType => {
@@ -1269,6 +1272,12 @@ export const requestPurchase = async request => {
1269
1272
  type
1270
1273
  } = request;
1271
1274
  const normalizedType = normalizeProductQueryType(type ?? 'in-app');
1275
+ if (normalizedType === 'all') {
1276
+ throw createPurchaseError({
1277
+ code: ErrorCode.DeveloperError,
1278
+ message: 'Product type all is only supported for product queries.'
1279
+ });
1280
+ }
1272
1281
  const isSubs = isSubscriptionQuery(normalizedType);
1273
1282
  const perPlatformRequest = platformRequest;
1274
1283
  if (!perPlatformRequest) {
@@ -1278,11 +1287,18 @@ export const requestPurchase = async request => {
1278
1287
  const androidRequestSource = isAndroidStoreRuntime() ? perPlatformRequest.google : undefined;
1279
1288
  if (Platform.OS === 'ios') {
1280
1289
  if (!iosRequestSource?.sku) {
1281
- throw new Error('Invalid request for iOS. The `sku` property is required.');
1290
+ throw createPurchaseError({
1291
+ message: 'Invalid request for iOS. The `sku` property is required.',
1292
+ code: ErrorCode.EmptySkuList
1293
+ });
1282
1294
  }
1283
1295
  } else if (isAndroidStoreRuntime()) {
1284
- if (!androidRequestSource?.skus?.length) {
1285
- throw new Error('Invalid request for Android. The `skus` property is required and must be a non-empty array.');
1296
+ const skus = androidRequestSource?.skus;
1297
+ if (!Array.isArray(skus) || skus.length === 0 || skus.some(sku => typeof sku !== 'string' || sku.trim() === '')) {
1298
+ throw createPurchaseError({
1299
+ message: 'Invalid request for Android. The `skus` property must contain only non-empty strings.',
1300
+ code: ErrorCode.EmptySkuList
1301
+ });
1286
1302
  }
1287
1303
  } else {
1288
1304
  throw unsupportedPlatformError();
@@ -1327,6 +1343,14 @@ export const requestPurchase = async request => {
1327
1343
  unifiedRequest.apple = iosPayload;
1328
1344
  }
1329
1345
  if (isAndroidStoreRuntime() && androidRequestSource) {
1346
+ const rawAndroidRequest = androidRequestSource;
1347
+ const subscriptionOnlyFields = ['subscriptionOffers', 'subscriptionProductReplacementParams', 'purchaseToken', 'originalExternalTransactionId'];
1348
+ if (!isSubs && subscriptionOnlyFields.some(field => rawAndroidRequest[field] != null) || isSubs && rawAndroidRequest.offerToken != null) {
1349
+ throw createPurchaseError({
1350
+ message: 'Invalid request for Android. Purchase options must match the selected product type.',
1351
+ code: ErrorCode.DeveloperError
1352
+ });
1353
+ }
1330
1354
  const androidRequest = isSubs ? androidRequestSource : androidRequestSource;
1331
1355
  const androidPayload = {
1332
1356
  skus: androidRequest.skus
@@ -1353,6 +1377,19 @@ export const requestPurchase = async request => {
1353
1377
  }
1354
1378
  if (isSubs) {
1355
1379
  const subsRequest = androidRequest;
1380
+ const subscriptionOffers = subsRequest.subscriptionOffers;
1381
+ if (subscriptionOffers != null && (!Array.isArray(subscriptionOffers) || subscriptionOffers.some(offer => {
1382
+ if (offer == null || typeof offer !== 'object') {
1383
+ return true;
1384
+ }
1385
+ const candidate = offer;
1386
+ return typeof candidate.sku !== 'string' || candidate.sku.trim() === '' || typeof candidate.offerToken !== 'string' || candidate.offerToken.trim() === '';
1387
+ }))) {
1388
+ throw createPurchaseError({
1389
+ message: 'Invalid request for Android. Every subscription offer must include non-empty `sku` and `offerToken` strings.',
1390
+ code: ErrorCode.DeveloperError
1391
+ });
1392
+ }
1356
1393
  if (subsRequest.purchaseToken) {
1357
1394
  androidPayload.purchaseToken = subsRequest.purchaseToken;
1358
1395
  }
@@ -1362,7 +1399,7 @@ export const requestPurchase = async request => {
1362
1399
  if (subsRequest.subscriptionProductReplacementParams) {
1363
1400
  androidPayload.subscriptionProductReplacementParams = subsRequest.subscriptionProductReplacementParams;
1364
1401
  }
1365
- androidPayload.subscriptionOffers = (subsRequest.subscriptionOffers ?? []).filter(offer => offer != null).map(offer => ({
1402
+ androidPayload.subscriptionOffers = (subscriptionOffers ?? []).map(offer => ({
1366
1403
  sku: offer.sku,
1367
1404
  offerToken: offer.offerToken
1368
1405
  }));
@@ -1642,10 +1679,18 @@ export const verifyPurchase = async options => {
1642
1679
  latestTransaction: iosResult.latestTransaction ? convertNitroPurchaseToPurchase(iosResult.latestTransaction) : undefined
1643
1680
  };
1644
1681
  return result;
1682
+ } else if (params.horizon !== null) {
1683
+ const horizonResult = nitroResult;
1684
+ const result = {
1685
+ isValid: horizonResult.isValid,
1686
+ grantTime: horizonResult.grantTime,
1687
+ success: horizonResult.success
1688
+ };
1689
+ return result;
1645
1690
  } else {
1646
- // Android
1647
1691
  const androidResult = nitroResult;
1648
1692
  const result = {
1693
+ isValid: androidResult.isValid,
1649
1694
  autoRenewing: androidResult.autoRenewing,
1650
1695
  betaProduct: androidResult.betaProduct,
1651
1696
  cancelDate: androidResult.cancelDate,
@@ -1697,9 +1742,11 @@ export const verifyPurchase = async options => {
1697
1742
  * // apple: { jws: purchase.purchaseToken },
1698
1743
  * // google: { purchaseToken: purchase.purchaseToken },
1699
1744
  * amazon: {
1745
+ * expectedProductId: purchase.productId,
1700
1746
  * userId: amazonUserId,
1701
1747
  * receiptId: purchase.purchaseToken,
1702
- * sandbox: __DEV__,
1748
+ * // Enable only for App Tester after the IAPKit project opt-in.
1749
+ * sandbox: amazonSandboxEnabled,
1703
1750
  * },
1704
1751
  * },
1705
1752
  * });
@@ -1726,6 +1773,9 @@ export const verifyPurchaseWithProvider = async options => {
1726
1773
  ...(result.iapkit.clientPayload == null ? {} : {
1727
1774
  clientPayload: result.iapkit.clientPayload
1728
1775
  }),
1776
+ ...(result.iapkit.environment == null ? {} : {
1777
+ environment: result.iapkit.environment
1778
+ }),
1729
1779
  isValid: result.iapkit.isValid,
1730
1780
  ...(result.iapkit.productId == null ? {} : {
1731
1781
  productId: result.iapkit.productId