react-native-iap 16.3.0 → 16.3.2
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.
- package/README.md +3 -3
- package/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt +111 -15
- package/android/src/main/java/com/margelo/nitro/iap/RnIapLog.kt +5 -1
- package/ios/HybridRnIap.swift +1 -1
- package/ios/RnIapLog.swift +6 -6
- package/lib/module/index.js +51 -6
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.kepler.js +9 -0
- package/lib/module/index.kepler.js.map +1 -1
- package/lib/module/kit-api.js +3 -1
- package/lib/module/kit-api.js.map +1 -1
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -1
- package/lib/module/vega-adapter.js +3 -4
- package/lib/module/vega-adapter.js.map +1 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/index.kepler.d.ts.map +1 -1
- package/lib/typescript/src/kit-api.d.ts +119 -2
- package/lib/typescript/src/kit-api.d.ts.map +1 -1
- package/lib/typescript/src/specs/RnIap.nitro.d.ts +9 -3
- package/lib/typescript/src/specs/RnIap.nitro.d.ts.map +1 -1
- package/lib/typescript/src/types.d.ts +33 -9
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/lib/typescript/src/vega-adapter.d.ts.map +1 -1
- package/nitrogen/generated/android/NitroIap+autolinking.cmake +1 -1
- package/nitrogen/generated/android/c++/JHybridRnIapSpec.cpp +8 -4
- package/nitrogen/generated/android/c++/JHybridRnIapSpec.hpp +1 -1
- package/nitrogen/generated/android/c++/JNitroPurchaseVerificationResultAndroid.hpp +5 -1
- package/nitrogen/generated/android/c++/JNitroPurchaseVerificationResultHorizon.hpp +69 -0
- package/nitrogen/generated/android/c++/JVariant_NitroPurchaseVerificationResultIOS_NitroPurchaseVerificationResultAndroid_NitroPurchaseVerificationResultHorizon.cpp +30 -0
- package/nitrogen/generated/android/c++/{JVariant_NitroPurchaseVerificationResultIOS_NitroPurchaseVerificationResultAndroid.hpp → JVariant_NitroPurchaseVerificationResultIOS_NitroPurchaseVerificationResultAndroid_NitroPurchaseVerificationResultHorizon.hpp} +33 -16
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/HybridRnIapSpec.kt +1 -1
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/NitroPurchaseVerificationResultAndroid.kt +8 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/NitroPurchaseVerificationResultHorizon.kt +61 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/{Variant_NitroPurchaseVerificationResultIOS_NitroPurchaseVerificationResultAndroid.kt → Variant_NitroPurchaseVerificationResultIOS_NitroPurchaseVerificationResultAndroid_NitroPurchaseVerificationResultHorizon.kt} +21 -8
- package/nitrogen/generated/ios/NitroIap-Swift-Cxx-Bridge.cpp +4 -4
- package/nitrogen/generated/ios/NitroIap-Swift-Cxx-Bridge.hpp +43 -34
- package/nitrogen/generated/ios/NitroIap-Swift-Cxx-Umbrella.hpp +3 -0
- package/nitrogen/generated/ios/c++/HybridRnIapSpecSwift.hpp +4 -1
- package/nitrogen/generated/ios/swift/{Func_void_std__variant_NitroPurchaseVerificationResultIOS__NitroPurchaseVerificationResultAndroid_.swift → Func_void_std__variant_NitroPurchaseVerificationResultIOS__NitroPurchaseVerificationResultAndroid__NitroPurchaseVerificationResultHorizon_.swift} +14 -11
- package/nitrogen/generated/ios/swift/HybridRnIapSpec.swift +1 -1
- package/nitrogen/generated/ios/swift/HybridRnIapSpec_cxx.swift +11 -9
- package/nitrogen/generated/ios/swift/NitroPurchaseVerificationResultAndroid.swift +7 -2
- package/nitrogen/generated/ios/swift/NitroPurchaseVerificationResultHorizon.swift +71 -0
- package/nitrogen/generated/ios/swift/{Variant_NitroPurchaseVerificationResultIOS_NitroPurchaseVerificationResultAndroid.swift → Variant_NitroPurchaseVerificationResultIOS_NitroPurchaseVerificationResultAndroid_NitroPurchaseVerificationResultHorizon.swift} +6 -4
- package/nitrogen/generated/shared/c++/HybridRnIapSpec.hpp +4 -1
- package/nitrogen/generated/shared/c++/NitroPurchaseVerificationResultAndroid.hpp +5 -1
- package/nitrogen/generated/shared/c++/NitroPurchaseVerificationResultHorizon.hpp +93 -0
- package/openiap-versions.json +3 -3
- package/package.json +3 -2
- package/src/index.kepler.ts +10 -2
- package/src/index.ts +88 -18
- package/src/kit-api.ts +125 -4
- package/src/specs/RnIap.nitro.ts +12 -2
- package/src/types.ts +34 -9
- package/src/vega-adapter.ts +6 -11
- package/nitrogen/generated/android/c++/JVariant_NitroPurchaseVerificationResultIOS_NitroPurchaseVerificationResultAndroid.cpp +0 -26
package/README.md
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
|
|
6
6
|
[](https://npmjs.org/package/react-native-iap) [](https://npmjs.org/package/react-native-iap) [](https://openiap.dev) [](https://opencollective.com/openiap) [](https://github.com/hyodotdev/openiap/actions/workflows/ci-react-native-iap.yml?query=branch%3Amain) [](https://app.codecov.io/gh/hyodotdev/openiap/tree/main/libraries/react-native-iap) [](https://app.fossa.com/projects/git%2Bgithub.com%2Fhyochan%2Freact-native-iap?ref=badge_shield&issueType=license)
|
|
7
7
|
|
|
8
|
-
**React Native IAP** is a high-performance in-app purchase library using Nitro Modules that **conforms to the [
|
|
8
|
+
**React Native IAP** is a high-performance in-app purchase library using Nitro Modules that **conforms to the [OpenIAP 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.
|
|
10
|
+
<a href="https://openiap.dev"><img src="https://raw.githubusercontent.com/hyodotdev/openiap/main/logo.webp" alt="OpenIAP" 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.
|
|
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
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
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<
|
|
1468
|
+
override fun verifyPurchase(params: NitroPurchaseVerificationParams): Promise<Variant_NitroPurchaseVerificationResultIOS_NitroPurchaseVerificationResultAndroid_NitroPurchaseVerificationResultHorizon> {
|
|
1409
1469
|
return Promise.async {
|
|
1410
1470
|
try {
|
|
1411
|
-
|
|
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
|
-
|
|
1578
|
+
Variant_NitroPurchaseVerificationResultIOS_NitroPurchaseVerificationResultAndroid_NitroPurchaseVerificationResultHorizon.Second(result)
|
|
1483
1579
|
|
|
1484
1580
|
} catch (e: OpenIapException) {
|
|
1485
1581
|
RnIapLog.failure("verifyPurchase", e)
|
|
@@ -67,7 +67,7 @@ internal object RnIapLog {
|
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
|
|
70
|
+
internal fun sanitizeMap(source: Map<*, *>): Map<String, Any?> {
|
|
71
71
|
fun isSensitiveKey(key: String): Boolean {
|
|
72
72
|
val normalized = key.lowercase(Locale.ROOT).filter { it.isLetterOrDigit() }
|
|
73
73
|
return SENSITIVE_KEY_FRAGMENTS.any { normalized.contains(it) } ||
|
|
@@ -77,6 +77,10 @@ internal object RnIapLog {
|
|
|
77
77
|
val sanitized = linkedMapOf<String, Any?>()
|
|
78
78
|
for ((rawKey, rawValue) in source) {
|
|
79
79
|
val key = rawKey as? String ?: continue
|
|
80
|
+
if (rawValue == null || rawValue === JSONObject.NULL) {
|
|
81
|
+
sanitized[key] = JSONObject.NULL
|
|
82
|
+
continue
|
|
83
|
+
}
|
|
80
84
|
if (isSensitiveKey(key)) {
|
|
81
85
|
sanitized[key] = "hidden"
|
|
82
86
|
continue
|
package/ios/HybridRnIap.swift
CHANGED
|
@@ -399,7 +399,7 @@ class HybridRnIap: HybridRnIapSpec {
|
|
|
399
399
|
}
|
|
400
400
|
}
|
|
401
401
|
|
|
402
|
-
func verifyPurchase(params: NitroPurchaseVerificationParams) throws -> Promise<
|
|
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)
|
package/ios/RnIapLog.swift
CHANGED
|
@@ -105,22 +105,22 @@ enum RnIapLog {
|
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
private static func sanitize(_ value: Any?) -> Any? {
|
|
108
|
-
guard let value else { return nil }
|
|
109
|
-
|
|
110
|
-
if let dictionary = value as? [String: Any] {
|
|
111
|
-
return sanitizeDictionary(dictionary)
|
|
112
|
-
}
|
|
108
|
+
guard let value, !(value is NSNull) else { return nil }
|
|
113
109
|
|
|
114
110
|
if let optionalDictionary = value as? [String: Any?] {
|
|
115
111
|
var compact: [String: Any] = [:]
|
|
116
112
|
for (key, optionalValue) in optionalDictionary {
|
|
117
|
-
if let optionalValue {
|
|
113
|
+
if let optionalValue, !(optionalValue is NSNull) {
|
|
118
114
|
compact[key] = optionalValue
|
|
119
115
|
}
|
|
120
116
|
}
|
|
121
117
|
return sanitizeDictionary(compact)
|
|
122
118
|
}
|
|
123
119
|
|
|
120
|
+
if let dictionary = value as? [String: Any] {
|
|
121
|
+
return sanitizeDictionary(dictionary)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
124
|
if let array = value as? [Any] {
|
|
125
125
|
return array.compactMap { sanitize($0) }
|
|
126
126
|
}
|
package/lib/module/index.js
CHANGED
|
@@ -621,7 +621,10 @@ export const fetchProducts = async request => {
|
|
|
621
621
|
} = request;
|
|
622
622
|
try {
|
|
623
623
|
if (!skus?.length) {
|
|
624
|
-
throw
|
|
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
|
|
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
|
-
|
|
1285
|
-
|
|
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 = (
|
|
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,
|