expo-iap 5.3.0 → 5.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.
package/README.md CHANGED
@@ -9,7 +9,7 @@ Expo IAP is a powerful in-app purchase solution for Expo and React Native applic
9
9
 
10
10
  If you're shipping an app with expo-iap, we’d love to hear about it—please share your product and feedback in [expo-iap Q&A Discussions](https://github.com/hyodotdev/openiap/discussions/categories/expo-iap). Community stories help us keep improving the ecosystem.
11
11
 
12
- <a href="https://openiap.dev"><img src="https://raw.githubusercontent.com/hyodotdev/openiap/main/logo.png" alt="Open IAP" height="40" /></a>
12
+ <a href="https://openiap.dev"><img src="https://raw.githubusercontent.com/hyodotdev/openiap/main/logo.webp" alt="Open IAP" height="40" /></a>
13
13
 
14
14
  </div>
15
15
 
@@ -64,7 +64,7 @@ For detailed usage examples and error handling, see the [documentation](https://
64
64
 
65
65
  ## Powered by OpenIAP
66
66
 
67
- <a href="https://openiap.dev"><img src="https://raw.githubusercontent.com/hyodotdev/openiap/main/logo.png" alt="OpenIAP" height="50" /></a>
67
+ <a href="https://openiap.dev"><img src="https://raw.githubusercontent.com/hyodotdev/openiap/main/logo.webp" alt="OpenIAP" height="50" /></a>
68
68
 
69
69
  Expo IAP conforms to the **[OpenIAP specification](https://openiap.dev)** — an open, vendor-neutral interoperability standard for in-app purchases. OpenIAP provides:
70
70
 
@@ -75,6 +75,14 @@ object ExpoIapHelper {
75
75
  }
76
76
  }
77
77
 
78
+ fun parsePurchaseProductQueryType(rawType: String?): ProductQueryType {
79
+ val type = parseProductQueryType(rawType)
80
+ require(type != ProductQueryType.All) {
81
+ "Product type all is only supported for product queries."
82
+ }
83
+ return type
84
+ }
85
+
78
86
  internal fun parseDeepLinkSubscriptionParams(params: Map<String, Any?>): DeepLinkSubscriptionParams {
79
87
  return DeepLinkSubscriptionParams(
80
88
  sku = params["skuAndroid"] as? String,
@@ -87,43 +95,93 @@ object ExpoIapHelper {
87
95
  // before parsing the native request.
88
96
  val effective: Map<String, Any?> =
89
97
  run {
90
- val request = params["request"] as? Map<*, *>
91
- if (request != null) {
92
- val nested = request["google"] as? Map<*, *>
93
- if (nested != null) {
94
- val flat = mutableMapOf<String, Any?>()
95
- // Carry over top-level fields such as the purchase type.
96
- for ((k, v) in params) {
97
- if (k != "request") flat[k] = v
98
- }
99
- // Overlay platform-specific fields
100
- for ((k, v) in nested) {
101
- if (k is String) flat[k] = v
102
- }
103
- flat
104
- } else {
105
- params
98
+ if (params.containsKey("request")) {
99
+ val request = params["request"]
100
+ require(request is Map<*, *>) { "request must be an object" }
101
+ val nested = request["google"]
102
+ require(nested is Map<*, *> && nested.keys.all { it is String }) {
103
+ "request.google must be an object with string keys"
104
+ }
105
+ require(!nested.containsKey("type")) {
106
+ "type must be provided only at the purchase envelope"
107
+ }
108
+ val flat = mutableMapOf<String, Any?>()
109
+ // Carry over top-level fields such as the purchase type.
110
+ for ((k, v) in params) {
111
+ if (k != "request") flat[k] = v
112
+ }
113
+ // Overlay platform-specific fields.
114
+ for ((k, v) in nested) {
115
+ flat[k as String] = v
106
116
  }
117
+ flat
107
118
  } else {
108
119
  params
109
120
  }
110
121
  }
111
122
 
123
+ require(effective["type"] == null || effective["type"] is String) {
124
+ "type must be a string"
125
+ }
126
+ effective["skus"]?.let { rawSkus ->
127
+ require(rawSkus is List<*> && rawSkus.all { it is String && it.isNotBlank() }) {
128
+ "skus must contain only non-empty strings"
129
+ }
130
+ }
131
+ for (key in listOf(
132
+ "obfuscatedAccountId",
133
+ "obfuscatedProfileId",
134
+ "purchaseToken",
135
+ "originalExternalTransactionId",
136
+ "offerToken",
137
+ )) {
138
+ require(effective[key] == null || effective[key] is String) { "$key must be a string" }
139
+ }
140
+ require(effective["isOfferPersonalized"] == null || effective["isOfferPersonalized"] is Boolean) {
141
+ "isOfferPersonalized must be a boolean"
142
+ }
143
+ for (key in listOf("developerBillingOption", "subscriptionProductReplacementParams")) {
144
+ effective[key]?.let { value ->
145
+ require(value is Map<*, *> && value.keys.all { it is String }) {
146
+ "$key must be an object with string keys"
147
+ }
148
+ }
149
+ }
150
+ effective["subscriptionOffers"]?.let { rawOffers ->
151
+ require(rawOffers is List<*>) { "subscriptionOffers must be a list" }
152
+ require(rawOffers.all { rawOffer ->
153
+ val offer = rawOffer as? Map<*, *> ?: return@all false
154
+ val sku = offer["sku"] as? String
155
+ val token = offer["offerToken"] as? String
156
+ offer.keys.all { it is String } && !sku.isNullOrBlank() && !token.isNullOrBlank()
157
+ }) { "subscriptionOffers must contain valid sku and offerToken strings" }
158
+ }
159
+
112
160
  val type = effective["type"] as? String
113
- val skus = (effective["skus"] as? List<*>)?.filterIsInstance<String>() ?: emptyList()
161
+ val purchaseType = parsePurchaseProductQueryType(type)
162
+ val subscriptionOnlyFields = listOf(
163
+ "subscriptionOffers",
164
+ "subscriptionProductReplacementParams",
165
+ "purchaseToken",
166
+ "originalExternalTransactionId",
167
+ )
168
+ require(
169
+ purchaseType != ProductQueryType.InApp ||
170
+ subscriptionOnlyFields.none { effective[it] != null },
171
+ ) { "Subscription options require product type subs" }
172
+ require(purchaseType != ProductQueryType.Subs || effective["offerToken"] == null) {
173
+ "offerToken requires product type in-app"
174
+ }
175
+ val skus = (effective["skus"] as? List<*>)?.map { it as String } ?: emptyList()
114
176
  val obfuscatedAccountId = effective["obfuscatedAccountId"] as? String
115
177
  val obfuscatedProfileId = effective["obfuscatedProfileId"] as? String
116
178
  val isOfferPersonalized = effective["isOfferPersonalized"] as? Boolean ?: false
117
179
  val explicitSubscriptionOffers =
118
- (effective["subscriptionOffers"] as? List<*>)?.mapNotNull { rawOffer ->
119
- val offerMap = rawOffer as? Map<*, *> ?: return@mapNotNull null
120
- val sku = offerMap["sku"] as? String
121
- val offerToken = offerMap["offerToken"] as? String
122
- if (sku.isNullOrEmpty() || offerToken.isNullOrEmpty()) {
123
- null
124
- } else {
125
- AndroidSubscriptionOfferInput(offerToken = offerToken, sku = sku)
126
- }
180
+ (effective["subscriptionOffers"] as? List<*>)?.map { rawOffer ->
181
+ val offerMap = rawOffer as Map<*, *>
182
+ val sku = offerMap["sku"] as String
183
+ val offerToken = offerMap["offerToken"] as String
184
+ AndroidSubscriptionOfferInput(offerToken = offerToken, sku = sku)
127
185
  } ?: emptyList()
128
186
  val purchaseToken = effective["purchaseToken"] as? String
129
187
  val originalExternalTransactionId = effective["originalExternalTransactionId"] as? String
@@ -131,23 +189,13 @@ object ExpoIapHelper {
131
189
  (effective["developerBillingOption"] as? Map<*, *>)?.let { optionMap ->
132
190
  val json =
133
191
  optionMap.entries
134
- .mapNotNull { (key, value) ->
135
- (key as? String)?.let { it to value }
136
- }.toMap()
192
+ .associate { (key, value) -> key as String to value }
137
193
  DeveloperBillingOptionParamsAndroid.fromJson(json)
138
194
  }
139
195
  val subscriptionProductReplacementParams =
140
196
  (effective["subscriptionProductReplacementParams"] as? Map<*, *>)?.let { paramsMap ->
141
- val oldProductId = paramsMap["oldProductId"] as? String
142
- val replacementModeStr = paramsMap["replacementMode"] as? String
143
- if (oldProductId.isNullOrEmpty() || replacementModeStr.isNullOrEmpty()) {
144
- null
145
- } else {
146
- SubscriptionProductReplacementParamsAndroid(
147
- oldProductId = oldProductId,
148
- replacementMode = parseSubscriptionReplacementMode(replacementModeStr),
149
- )
150
- }
197
+ val json = paramsMap.entries.associate { (key, value) -> key as String to value }
198
+ SubscriptionProductReplacementParamsAndroid.fromJson(json)
151
199
  }
152
200
  // offerToken for one-time purchase discounts (Android 8.0+)
153
201
  val offerToken = effective["offerToken"] as? String
@@ -20,6 +20,7 @@ import dev.hyo.openiap.RequestPurchaseResultPurchases
20
20
  import dev.hyo.openiap.RequestSubscriptionAndroidProps
21
21
  import dev.hyo.openiap.RequestSubscriptionPropsByPlatforms
22
22
  import dev.hyo.openiap.VerifyPurchaseGoogleOptions
23
+ import dev.hyo.openiap.VerifyPurchaseHorizonOptions
23
24
  import dev.hyo.openiap.VerifyPurchaseProps
24
25
  import dev.hyo.openiap.VerifyPurchaseWithProviderProps
25
26
  import dev.hyo.openiap.store.OpenIapStore
@@ -73,6 +74,31 @@ internal fun deliverPurchaseRequestFailure(
73
74
  rejectPendingPromises(errorCode, errorEnvelope)
74
75
  }
75
76
 
77
+ @Suppress("UNCHECKED_CAST")
78
+ internal fun verifyPurchasePropsFromMap(params: Map<String, Any?>): VerifyPurchaseProps {
79
+ fun requiredOption(options: Map<String, Any?>, scope: String, field: String): String =
80
+ (options[field] as? String)?.takeIf { it.isNotEmpty() }
81
+ ?: throw IllegalArgumentException("Missing or empty required parameter: $scope.$field")
82
+
83
+ val googleOptions = (params["google"] as? Map<String, Any?>)?.let { options ->
84
+ VerifyPurchaseGoogleOptions(
85
+ sku = requiredOption(options, "google", "sku"),
86
+ accessToken = requiredOption(options, "google", "accessToken"),
87
+ packageName = requiredOption(options, "google", "packageName"),
88
+ purchaseToken = requiredOption(options, "google", "purchaseToken"),
89
+ isSub = options["isSub"] as? Boolean,
90
+ )
91
+ }
92
+ val horizonOptions = (params["horizon"] as? Map<String, Any?>)?.let { options ->
93
+ VerifyPurchaseHorizonOptions(
94
+ sku = requiredOption(options, "horizon", "sku"),
95
+ userId = requiredOption(options, "horizon", "userId"),
96
+ accessToken = requiredOption(options, "horizon", "accessToken"),
97
+ )
98
+ }
99
+ return VerifyPurchaseProps(google = googleOptions, horizon = horizonOptions)
100
+ }
101
+
76
102
  class ExpoIapModule : Module() {
77
103
  companion object {
78
104
  const val TAG = "ExpoIapModule"
@@ -348,7 +374,7 @@ class ExpoIapModule : Module() {
348
374
  val parsedParams = ExpoIapHelper.parseRequestPurchaseParams(params)
349
375
 
350
376
  val productType =
351
- when (ExpoIapHelper.parseProductQueryType(parsedParams.type)) {
377
+ when (ExpoIapHelper.parsePurchaseProductQueryType(parsedParams.type)) {
352
378
  ProductQueryType.Subs -> ProductQueryType.Subs
353
379
  else -> ProductQueryType.InApp
354
380
  }
@@ -497,35 +523,17 @@ class ExpoIapModule : Module() {
497
523
  }
498
524
  }
499
525
 
500
- @Suppress("UNCHECKED_CAST")
501
526
  AsyncFunction("verifyPurchase") { params: Map<String, Any?>, promise: Promise ->
502
- ExpoIapLog.payload("verifyPurchase", params)
527
+ ExpoIapLog.payload(
528
+ "verifyPurchase",
529
+ mapOf(
530
+ "hasGoogle" to (params["google"] != null),
531
+ "hasHorizon" to (params["horizon"] != null),
532
+ ),
533
+ )
503
534
  scope.launch {
504
535
  try {
505
- val googleOptions =
506
- (params["google"] as? Map<String, Any?>)?.let { opts ->
507
- VerifyPurchaseGoogleOptions(
508
- sku =
509
- (opts["sku"] as? String)?.takeIf { it.isNotEmpty() }
510
- ?: throw IllegalArgumentException("Missing or empty required parameter: google.sku"),
511
- accessToken =
512
- (opts["accessToken"] as? String)?.takeIf { it.isNotEmpty() }
513
- ?: throw IllegalArgumentException("Missing or empty required parameter: google.accessToken"),
514
- packageName =
515
- (opts["packageName"] as? String)?.takeIf { it.isNotEmpty() }
516
- ?: throw IllegalArgumentException("Missing or empty required parameter: google.packageName"),
517
- purchaseToken =
518
- (opts["purchaseToken"] as? String)?.takeIf { it.isNotEmpty() }
519
- ?: throw IllegalArgumentException("Missing or empty required parameter: google.purchaseToken"),
520
- isSub = opts["isSub"] as? Boolean,
521
- )
522
- }
523
-
524
- val props =
525
- VerifyPurchaseProps(
526
- google = googleOptions,
527
- )
528
-
536
+ val props = verifyPurchasePropsFromMap(params)
529
537
  val result = openIap.verifyPurchase(props)
530
538
  val resultMap = result.toJson()
531
539
  ExpoIapLog.result("verifyPurchase", resultMap)
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/build/index.js CHANGED
@@ -655,6 +655,25 @@ function normalizeRequestProps(request, platform) {
655
655
  }
656
656
  return request.google;
657
657
  }
658
+ function validateAndroidPurchaseBranchOptions(request, type) {
659
+ if (request == null || typeof request !== 'object')
660
+ return;
661
+ const fields = request;
662
+ const subscriptionOnlyFields = [
663
+ 'subscriptionOffers',
664
+ 'subscriptionProductReplacementParams',
665
+ 'purchaseToken',
666
+ 'originalExternalTransactionId',
667
+ ];
668
+ if ((type === 'in-app' &&
669
+ subscriptionOnlyFields.some((field) => fields[field] != null)) ||
670
+ (type === 'subs' && fields.offerToken != null)) {
671
+ throw createPurchaseError({
672
+ code: ErrorCode.DeveloperError,
673
+ message: 'Invalid request for Android. Purchase options must match the selected product type.',
674
+ });
675
+ }
676
+ }
658
677
  /**
659
678
  * Initiate a purchase or subscription flow. The result is delivered through
660
679
  * `purchaseUpdatedListener` — NOT the return value.
@@ -684,20 +703,29 @@ function normalizeRequestProps(request, platform) {
684
703
  export const requestPurchase = async (args) => {
685
704
  const { request, type } = args;
686
705
  const { canonical, native } = normalizeProductType(type);
706
+ if (canonical === 'all') {
707
+ throw createPurchaseError({
708
+ code: ErrorCode.DeveloperError,
709
+ message: 'Product type all is only supported for product queries.',
710
+ });
711
+ }
687
712
  const isInAppPurchase = canonical === 'in-app';
688
713
  if (Platform.OS === 'ios') {
689
714
  const normalizedRequest = normalizeRequestProps(request, 'ios');
690
715
  if (!normalizedRequest?.sku) {
691
- throw new Error('Invalid request for Apple. The `sku` property is required and must be a string.\n\n' +
692
- 'Expected format:\n' +
693
- ' requestPurchase({\n' +
694
- ' request: {\n' +
695
- ' apple: { sku: "product_id" },\n' +
696
- ' google: { skus: ["product_id"] }\n' +
697
- ' },\n' +
698
- ' type: "in-app"\n' +
699
- ' })\n\n' +
700
- 'See: https://openiap.dev/docs/apis/request-purchase');
716
+ throw createPurchaseError({
717
+ message: 'Invalid request for Apple. The `sku` property is required and must be a string.\n\n' +
718
+ 'Expected format:\n' +
719
+ ' requestPurchase({\n' +
720
+ ' request: {\n' +
721
+ ' apple: { sku: "product_id" },\n' +
722
+ ' google: { skus: ["product_id"] }\n' +
723
+ ' },\n' +
724
+ ' type: "in-app"\n' +
725
+ ' })\n\n' +
726
+ 'See: https://openiap.dev/docs/apis/request-purchase',
727
+ code: ErrorCode.EmptySkuList,
728
+ });
701
729
  }
702
730
  if (canonical !== 'in-app' && canonical !== 'subs') {
703
731
  throw new Error(`Unsupported product type: ${canonical}`);
@@ -722,19 +750,23 @@ export const requestPurchase = async (args) => {
722
750
  return canonical === 'subs' ? [] : null;
723
751
  }
724
752
  if (isAndroidStoreRuntime()) {
753
+ validateAndroidPurchaseBranchOptions(request?.google, canonical);
725
754
  if (isInAppPurchase) {
726
755
  const normalizedRequest = normalizeRequestProps(request, 'android');
727
756
  if (!normalizedRequest?.skus?.length) {
728
- throw new Error('Invalid request for Google. The `skus` property is required and must be a non-empty array.\n\n' +
729
- 'Expected format:\n' +
730
- ' requestPurchase({\n' +
731
- ' request: {\n' +
732
- ' apple: { sku: "product_id" },\n' +
733
- ' google: { skus: ["product_id"] }\n' +
734
- ' },\n' +
735
- ' type: "in-app"\n' +
736
- ' })\n\n' +
737
- 'See: https://openiap.dev/docs/apis/request-purchase');
757
+ throw createPurchaseError({
758
+ message: 'Invalid request for Google. The `skus` property is required and must be a non-empty array.\n\n' +
759
+ 'Expected format:\n' +
760
+ ' requestPurchase({\n' +
761
+ ' request: {\n' +
762
+ ' apple: { sku: "product_id" },\n' +
763
+ ' google: { skus: ["product_id"] }\n' +
764
+ ' },\n' +
765
+ ' type: "in-app"\n' +
766
+ ' })\n\n' +
767
+ 'See: https://openiap.dev/docs/apis/request-purchase',
768
+ code: ErrorCode.EmptySkuList,
769
+ });
738
770
  }
739
771
  const { skus, obfuscatedAccountId, obfuscatedProfileId, isOfferPersonalized, offerToken, developerBillingOption, } = normalizedRequest;
740
772
  const result = (await invokeNativeWithPurchaseError(() => ExpoIapModule.requestPurchase({
@@ -758,16 +790,19 @@ export const requestPurchase = async (args) => {
758
790
  if (canonical === 'subs') {
759
791
  const normalizedRequest = normalizeRequestProps(request, 'android');
760
792
  if (!normalizedRequest?.skus?.length) {
761
- throw new Error('Invalid request for Google. The `skus` property is required and must be a non-empty array.\n\n' +
762
- 'Expected format:\n' +
763
- ' requestPurchase({\n' +
764
- ' request: {\n' +
765
- ' apple: { sku: "subscription_id" },\n' +
766
- ' google: { skus: ["subscription_id"] }\n' +
767
- ' },\n' +
768
- ' type: "subs"\n' +
769
- ' })\n\n' +
770
- 'See: https://openiap.dev/docs/apis/request-purchase');
793
+ throw createPurchaseError({
794
+ message: 'Invalid request for Google. The `skus` property is required and must be a non-empty array.\n\n' +
795
+ 'Expected format:\n' +
796
+ ' requestPurchase({\n' +
797
+ ' request: {\n' +
798
+ ' apple: { sku: "subscription_id" },\n' +
799
+ ' google: { skus: ["subscription_id"] }\n' +
800
+ ' },\n' +
801
+ ' type: "subs"\n' +
802
+ ' })\n\n' +
803
+ 'See: https://openiap.dev/docs/apis/request-purchase',
804
+ code: ErrorCode.EmptySkuList,
805
+ });
771
806
  }
772
807
  const { skus, obfuscatedAccountId, obfuscatedProfileId, isOfferPersonalized, subscriptionOffers, purchaseToken: purchaseTokenInput, originalExternalTransactionId, developerBillingOption, subscriptionProductReplacementParams, } = normalizedRequest;
773
808
  const normalizedOffers = subscriptionOffers ?? [];