expo-iap 3.1.35 → 3.1.37

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 (46) hide show
  1. package/CLAUDE.md +2 -3
  2. package/android/src/main/java/expo/modules/iap/ExpoIapModule.kt +71 -9
  3. package/build/index.d.ts +38 -0
  4. package/build/index.d.ts.map +1 -1
  5. package/build/index.js +48 -0
  6. package/build/index.js.map +1 -1
  7. package/build/modules/android.d.ts +4 -2
  8. package/build/modules/android.d.ts.map +1 -1
  9. package/build/modules/android.js +2 -0
  10. package/build/modules/android.js.map +1 -1
  11. package/build/modules/ios.d.ts.map +1 -1
  12. package/build/modules/ios.js +2 -1
  13. package/build/modules/ios.js.map +1 -1
  14. package/build/types.d.ts +109 -51
  15. package/build/types.d.ts.map +1 -1
  16. package/build/types.js +3 -0
  17. package/build/types.js.map +1 -1
  18. package/build/useIAP.d.ts +5 -2
  19. package/build/useIAP.d.ts.map +1 -1
  20. package/build/useIAP.js +9 -1
  21. package/build/useIAP.js.map +1 -1
  22. package/build/utils/errorMapping.d.ts.map +1 -1
  23. package/build/utils/errorMapping.js +3 -0
  24. package/build/utils/errorMapping.js.map +1 -1
  25. package/coverage/clover.xml +185 -176
  26. package/coverage/coverage-final.json +4 -4
  27. package/coverage/lcov-report/index.html +18 -18
  28. package/coverage/lcov-report/src/index.html +18 -18
  29. package/coverage/lcov-report/src/index.ts.html +178 -10
  30. package/coverage/lcov-report/src/modules/android.ts.html +10 -4
  31. package/coverage/lcov-report/src/modules/index.html +1 -1
  32. package/coverage/lcov-report/src/modules/ios.ts.html +10 -13
  33. package/coverage/lcov-report/src/utils/debug.ts.html +1 -1
  34. package/coverage/lcov-report/src/utils/errorMapping.ts.html +18 -3
  35. package/coverage/lcov-report/src/utils/index.html +1 -1
  36. package/coverage/lcov.info +338 -319
  37. package/ios/ExpoIapHelper.swift +18 -0
  38. package/ios/ExpoIapModule.swift +56 -15
  39. package/openiap-versions.json +3 -3
  40. package/package.json +1 -1
  41. package/src/index.ts +56 -0
  42. package/src/modules/android.ts +4 -2
  43. package/src/modules/ios.ts +7 -8
  44. package/src/types.ts +126 -57
  45. package/src/useIAP.ts +27 -5
  46. package/src/utils/errorMapping.ts +5 -0
@@ -1,6 +1,24 @@
1
+ import ExpoModulesCore
1
2
  import Foundation
2
3
  import OpenIAP
3
4
 
5
+ /// Exception wrapper for PurchaseError that preserves OpenIAP error codes
6
+ /// This ensures consistent error format between try-catch and onPurchaseError callback
7
+ class IapException: GenericException<(code: String, message: String, productId: String?)> {
8
+ override var code: String { param.code }
9
+ override var reason: String { param.message }
10
+
11
+ var productId: String? { param.productId }
12
+
13
+ static func from(_ error: PurchaseError) -> IapException {
14
+ let payload = OpenIapSerialization.encode(error)
15
+ let code = payload["code"] as? String ?? "unknown"
16
+ let message = payload["message"] as? String ?? error.localizedDescription
17
+ let productId = payload["productId"] as? String
18
+ return IapException((code: code, message: message, productId: productId))
19
+ }
20
+ }
21
+
4
22
  enum ExpoIapHelper {
5
23
  private static var listeners: [Subscription] = []
6
24
 
@@ -13,9 +13,9 @@ public final class ExpoIapModule: Module {
13
13
  nonisolated public func definition() -> ModuleDefinition {
14
14
  Name("ExpoIap")
15
15
 
16
- Constants([
17
- "ERROR_CODES": OpenIapSerialization.errorCodes()
18
- ])
16
+ Constant("ERROR_CODES") {
17
+ OpenIapSerialization.errorCodes()
18
+ }
19
19
 
20
20
  Events(
21
21
  OpenIapEvent.purchaseUpdated.rawValue,
@@ -61,10 +61,8 @@ public final class ExpoIapModule: Module {
61
61
 
62
62
  AsyncFunction("requestPurchase") { (payload: [String: Any]) async throws -> Any? in
63
63
  ExpoIapLog.payload("requestPurchase", payload: payload)
64
- print("🔍 [ExpoIap] Raw payload useAlternativeBilling: \(payload["useAlternativeBilling"] ?? "nil")")
65
64
  try await ExpoIapHelper.ensureConnection(isInitialized: self.isInitialized)
66
65
  let props = try ExpoIapHelper.decodeRequestPurchaseProps(from: payload)
67
- print("🔍 [ExpoIap] Decoded props useAlternativeBilling: \(props.useAlternativeBilling ?? false)")
68
66
 
69
67
  do {
70
68
  guard let result = try await OpenIapModule.shared.requestPurchase(props) else {
@@ -86,10 +84,11 @@ public final class ExpoIapModule: Module {
86
84
  }
87
85
  } catch let error as PurchaseError {
88
86
  ExpoIapLog.failure("requestPurchase", error: error)
89
- throw error
87
+ throw IapException.from(error)
90
88
  } catch {
91
89
  ExpoIapLog.failure("requestPurchase", error: error)
92
- throw PurchaseError.make(code: .purchaseError, message: error.localizedDescription)
90
+ throw IapException.from(
91
+ PurchaseError.make(code: .purchaseError, message: error.localizedDescription))
93
92
  }
94
93
  }
95
94
 
@@ -189,18 +188,60 @@ public final class ExpoIapModule: Module {
189
188
  ExpoIapLog.payload("validateReceiptIOS", payload: ["sku": sku])
190
189
  try await ExpoIapHelper.ensureConnection(isInitialized: self.isInitialized)
191
190
  do {
192
- let props = try OpenIapSerialization.receiptValidationProps(from: ["sku": sku])
193
- let result = try await OpenIapModule.shared.validateReceiptIOS(props)
191
+ let props = try OpenIapSerialization.verifyPurchaseProps(from: ["sku": sku])
192
+ let result = try await OpenIapModule.shared.verifyPurchase(props)
194
193
  var payload = OpenIapSerialization.encode(result)
195
- payload["purchaseToken"] = result.jwsRepresentation
194
+
195
+ // Extract jwsRepresentation from the result
196
+ if case .verifyPurchaseResultIos(let iosResult) = result {
197
+ payload["purchaseToken"] = iosResult.jwsRepresentation
198
+ }
199
+
196
200
  let sanitized = ExpoIapHelper.sanitizeDictionary(payload)
197
201
  ExpoIapLog.result("validateReceiptIOS", value: sanitized)
198
202
  return sanitized
199
203
  } catch let error as PurchaseError {
200
204
  ExpoIapLog.failure("validateReceiptIOS", error: error)
201
- throw error
205
+ throw IapException.from(error)
202
206
  } catch {
203
207
  ExpoIapLog.failure("validateReceiptIOS", error: error)
208
+ throw IapException.from(PurchaseError.make(code: .receiptFailed))
209
+ }
210
+ }
211
+
212
+ AsyncFunction("verifyPurchase") { (params: [String: Any]) async throws -> [String: Any] in
213
+ ExpoIapLog.payload("verifyPurchase", payload: params)
214
+ try await ExpoIapHelper.ensureConnection(isInitialized: self.isInitialized)
215
+ do {
216
+ let props = try OpenIapSerialization.verifyPurchaseProps(from: params)
217
+ let result = try await OpenIapModule.shared.verifyPurchase(props)
218
+ let sanitized = ExpoIapHelper.sanitizeDictionary(OpenIapSerialization.encode(result))
219
+ ExpoIapLog.result("verifyPurchase", value: sanitized)
220
+ return sanitized
221
+ } catch let error as PurchaseError {
222
+ ExpoIapLog.failure("verifyPurchase", error: error)
223
+ throw error
224
+ } catch {
225
+ ExpoIapLog.failure("verifyPurchase", error: error)
226
+ throw PurchaseError.make(code: .receiptFailed)
227
+ }
228
+ }
229
+
230
+ AsyncFunction("verifyPurchaseWithProvider") { (params: [String: Any]) async throws -> [String: Any] in
231
+ ExpoIapLog.payload("verifyPurchaseWithProvider", payload: params)
232
+ try await ExpoIapHelper.ensureConnection(isInitialized: self.isInitialized)
233
+ do {
234
+ let jsonData = try JSONSerialization.data(withJSONObject: params)
235
+ let props = try JSONDecoder().decode(VerifyPurchaseWithProviderProps.self, from: jsonData)
236
+ let result = try await OpenIapModule.shared.verifyPurchaseWithProvider(props)
237
+ let sanitized = ExpoIapHelper.sanitizeDictionary(OpenIapSerialization.encode(result))
238
+ ExpoIapLog.result("verifyPurchaseWithProvider", value: sanitized)
239
+ return sanitized
240
+ } catch let error as PurchaseError {
241
+ ExpoIapLog.failure("verifyPurchaseWithProvider", error: error)
242
+ throw error
243
+ } catch {
244
+ ExpoIapLog.failure("verifyPurchaseWithProvider", error: error)
204
245
  throw PurchaseError.make(code: .receiptFailed)
205
246
  }
206
247
  }
@@ -312,10 +353,10 @@ public final class ExpoIapModule: Module {
312
353
  return nil
313
354
  } catch let error as PurchaseError {
314
355
  ExpoIapLog.failure("currentEntitlementIOS", error: error)
315
- throw error
356
+ throw IapException.from(error)
316
357
  } catch {
317
358
  ExpoIapLog.failure("currentEntitlementIOS", error: error)
318
- throw PurchaseError.make(code: .skuNotFound, productId: sku)
359
+ throw IapException.from(PurchaseError.make(code: .skuNotFound, productId: sku))
319
360
  }
320
361
  }
321
362
 
@@ -332,10 +373,10 @@ public final class ExpoIapModule: Module {
332
373
  return nil
333
374
  } catch let error as PurchaseError {
334
375
  ExpoIapLog.failure("latestTransactionIOS", error: error)
335
- throw error
376
+ throw IapException.from(error)
336
377
  } catch {
337
378
  ExpoIapLog.failure("latestTransactionIOS", error: error)
338
- throw PurchaseError.make(code: .skuNotFound, productId: sku)
379
+ throw IapException.from(PurchaseError.make(code: .skuNotFound, productId: sku))
339
380
  }
340
381
  }
341
382
 
@@ -1,5 +1,5 @@
1
1
  {
2
- "apple": "1.2.39",
3
- "google": "1.3.7",
4
- "gql": "1.2.5"
2
+ "apple": "1.2.41",
3
+ "google": "1.3.8",
4
+ "gql": "1.2.7"
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-iap",
3
- "version": "3.1.35",
3
+ "version": "3.1.37",
4
4
  "description": "In App Purchase module in Expo",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
package/src/index.ts CHANGED
@@ -708,6 +708,8 @@ export const deepLinkToSubscriptions: MutationField<
708
708
  * For production apps, always validate receipts on your secure server:
709
709
  * - iOS: Send receipt data to Apple's verification endpoint from your server
710
710
  * - Android: Use Google Play Developer API with service account credentials
711
+ *
712
+ * @deprecated Use verifyPurchase instead
711
713
  */
712
714
  export const validateReceipt: MutationField<'validateReceipt'> = async (
713
715
  options,
@@ -741,6 +743,60 @@ export const validateReceipt: MutationField<'validateReceipt'> = async (
741
743
  throw new Error('Platform not supported');
742
744
  };
743
745
 
746
+ /**
747
+ * Verify purchase with the configured providers
748
+ *
749
+ * This function uses the native OpenIAP verifyPurchase implementation
750
+ * which validates purchases using platform-specific methods.
751
+ *
752
+ * @param options - Receipt validation options containing the SKU
753
+ * @returns Promise resolving to receipt validation result
754
+ */
755
+ export const verifyPurchase: MutationField<'verifyPurchase'> = async (
756
+ options,
757
+ ) => {
758
+ if (Platform.OS === 'ios' || Platform.OS === 'android') {
759
+ return ExpoIapModule.verifyPurchase(options);
760
+ }
761
+
762
+ throw new Error(`Unsupported platform: ${Platform.OS}`);
763
+ };
764
+
765
+ /**
766
+ * Verify purchase with a specific provider (e.g., IAPKit)
767
+ *
768
+ * This function allows you to verify purchases using external verification
769
+ * services like IAPKit, which provide additional validation and security.
770
+ *
771
+ * @param options - Verification options including provider and credentials
772
+ * @returns Promise resolving to provider-specific verification result
773
+ *
774
+ * @example
775
+ * ```typescript
776
+ * const result = await verifyPurchaseWithProvider({
777
+ * provider: 'iapkit',
778
+ * iapkit: {
779
+ * apiKey: 'your-api-key',
780
+ * apple: {
781
+ * jws: purchase.purchaseToken // JWS from purchase
782
+ * },
783
+ * google: {
784
+ * purchaseToken: purchase.purchaseToken
785
+ * }
786
+ * }
787
+ * });
788
+ * ```
789
+ */
790
+ export const verifyPurchaseWithProvider: MutationField<
791
+ 'verifyPurchaseWithProvider'
792
+ > = async (options) => {
793
+ if (Platform.OS === 'ios' || Platform.OS === 'android') {
794
+ return ExpoIapModule.verifyPurchaseWithProvider(options);
795
+ }
796
+
797
+ throw new Error(`Unsupported platform: ${Platform.OS}`);
798
+ };
799
+
744
800
  export * from './useIAP';
745
801
  export {
746
802
  ErrorCodeUtils,
@@ -8,7 +8,7 @@ import ExpoIapModule from '../ExpoIapModule';
8
8
  import type {
9
9
  DeepLinkOptions,
10
10
  MutationField,
11
- ReceiptValidationResultAndroid,
11
+ VerifyPurchaseResultAndroid,
12
12
  } from '../types';
13
13
 
14
14
  type NativeAndroidModule = {
@@ -80,6 +80,8 @@ export const deepLinkToSubscriptionsAndroid = async (
80
80
  * Validate receipt for Android. NOTE: This method is here for debugging purposes only. Including
81
81
  * your access token in the binary you ship to users is potentially dangerous.
82
82
  * Use server side validation instead for your production builds
83
+ *
84
+ * @deprecated Use verifyPurchase instead
83
85
  * @param {Object} params - The parameters object
84
86
  * @param {string} params.packageName - package name of your app.
85
87
  * @param {string} params.productId - product id for your in app product.
@@ -100,7 +102,7 @@ export const validateReceiptAndroid = async ({
100
102
  productToken: string;
101
103
  accessToken: string;
102
104
  isSub?: boolean;
103
- }): Promise<ReceiptValidationResultAndroid> => {
105
+ }): Promise<VerifyPurchaseResultAndroid> => {
104
106
  const type = isSub ? 'subscriptions' : 'products';
105
107
 
106
108
  const url =
@@ -13,8 +13,8 @@ import type {
13
13
  Purchase,
14
14
  PurchaseIOS,
15
15
  QueryField,
16
- ReceiptValidationProps,
17
- ReceiptValidationResultIOS,
16
+ VerifyPurchaseProps,
17
+ VerifyPurchaseResultIOS,
18
18
  SubscriptionStatusIOS,
19
19
  } from '../types';
20
20
  import type {PurchaseError} from '../utils/errorMapping';
@@ -226,7 +226,8 @@ export const getTransactionJwsIOS: QueryField<'getTransactionJwsIOS'> = async (
226
226
  * NOTE: For proper security, Apple recommends verifying receipts on your server using
227
227
  * the verifyReceipt endpoint rather than relying solely on client-side verification.
228
228
  *
229
- * @param {string} sku The product's SKU (on iOS)
229
+ * @deprecated Use verifyPurchase instead
230
+ * @param props The product's SKU or verification props
230
231
  * @returns {Promise<{
231
232
  * isValid: boolean;
232
233
  * receiptData: string;
@@ -234,11 +235,9 @@ export const getTransactionJwsIOS: QueryField<'getTransactionJwsIOS'> = async (
234
235
  * latestTransaction?: Purchase;
235
236
  * }>}
236
237
  */
237
- const validateReceiptIOSImpl = async (
238
- props: ReceiptValidationProps | string,
239
- ) => {
238
+ const validateReceiptIOSImpl = async (props: VerifyPurchaseProps | string) => {
240
239
  const sku =
241
- typeof props === 'string' ? props : (props as ReceiptValidationProps)?.sku;
240
+ typeof props === 'string' ? props : (props as VerifyPurchaseProps)?.sku;
242
241
 
243
242
  if (!sku) {
244
243
  throw new Error('validateReceiptIOS requires a SKU');
@@ -246,7 +245,7 @@ const validateReceiptIOSImpl = async (
246
245
 
247
246
  return (await ExpoIapModule.validateReceiptIOS(
248
247
  sku,
249
- )) as ReceiptValidationResultIOS;
248
+ )) as VerifyPurchaseResultIOS;
250
249
  };
251
250
 
252
251
  export const validateReceiptIOS =
package/src/types.ts CHANGED
@@ -28,6 +28,11 @@ export interface ActiveSubscription {
28
28
  renewalInfoIOS?: (RenewalInfoIOS | null);
29
29
  transactionDate: number;
30
30
  transactionId: string;
31
+ /**
32
+ * @deprecated iOS only - use daysUntilExpirationIOS instead.
33
+ * Whether the subscription will expire soon (within 7 days).
34
+ * Consider using daysUntilExpirationIOS for more precise control.
35
+ */
31
36
  willExpireSoon?: (boolean | null);
32
37
  }
33
38
 
@@ -131,6 +136,9 @@ export enum ErrorCode {
131
136
  NotPrepared = 'not-prepared',
132
137
  Pending = 'pending',
133
138
  PurchaseError = 'purchase-error',
139
+ PurchaseVerificationFailed = 'purchase-verification-failed',
140
+ PurchaseVerificationFinishFailed = 'purchase-verification-finish-failed',
141
+ PurchaseVerificationFinished = 'purchase-verification-finished',
134
142
  QueryProduct = 'query-product',
135
143
  ReceiptFailed = 'receipt-failed',
136
144
  ReceiptFinished = 'receipt-finished',
@@ -172,6 +180,11 @@ export type IapEvent = 'purchase-updated' | 'purchase-error' | 'promoted-product
172
180
 
173
181
  export type IapPlatform = 'ios' | 'android';
174
182
 
183
+ /** Unified purchase states from IAPKit verification response. */
184
+ export type IapkitPurchaseState = 'entitled' | 'pending-acknowledgment' | 'pending' | 'canceled' | 'expired' | 'ready-to-consume' | 'consumed' | 'unknown' | 'inauthentic';
185
+
186
+ export type IapkitStore = 'apple' | 'google';
187
+
175
188
  /** Connection initialization configuration */
176
189
  export interface InitConnectionConfig {
177
190
  /**
@@ -241,8 +254,15 @@ export interface Mutation {
241
254
  showManageSubscriptionsIOS: Promise<PurchaseIOS[]>;
242
255
  /** Force a StoreKit sync for transactions (iOS 15+) */
243
256
  syncIOS: Promise<boolean>;
244
- /** Validate purchase receipts with the configured providers */
245
- validateReceipt: Promise<ReceiptValidationResult>;
257
+ /**
258
+ * Validate purchase receipts with the configured providers
259
+ * @deprecated Use verifyPurchase
260
+ */
261
+ validateReceipt: Promise<VerifyPurchaseResult>;
262
+ /** Verify purchases with the configured providers */
263
+ verifyPurchase: Promise<VerifyPurchaseResult>;
264
+ /** Verify purchases with a specific provider (e.g., IAPKit) */
265
+ verifyPurchaseWithProvider: Promise<VerifyPurchaseWithProviderResult>;
246
266
  }
247
267
 
248
268
 
@@ -282,7 +302,11 @@ export type MutationRequestPurchaseArgs =
282
302
  };
283
303
 
284
304
 
285
- export type MutationValidateReceiptArgs = ReceiptValidationProps;
305
+ export type MutationValidateReceiptArgs = VerifyPurchaseProps;
306
+
307
+ export type MutationVerifyPurchaseArgs = VerifyPurchaseProps;
308
+
309
+ export type MutationVerifyPurchaseWithProviderArgs = VerifyPurchaseWithProviderProps;
286
310
 
287
311
  export type PaymentModeIOS = 'empty' | 'free-trial' | 'pay-as-you-go' | 'pay-up-front';
288
312
 
@@ -330,11 +354,11 @@ export interface ProductCommon {
330
354
  displayName?: (string | null);
331
355
  displayPrice: string;
332
356
  id: string;
333
- platform: IapPlatform;
357
+ platform: 'android' | 'ios';
334
358
  price?: (number | null);
335
359
  title: string;
336
- type: ProductType;
337
- }
360
+ type: 'in-app' | 'subs';
361
+ }
338
362
 
339
363
  export interface ProductIOS extends ProductCommon {
340
364
  currency: string;
@@ -522,6 +546,8 @@ export interface PurchaseOptions {
522
546
 
523
547
  export type PurchaseState = 'pending' | 'purchased' | 'failed' | 'restored' | 'deferred' | 'unknown';
524
548
 
549
+ export type PurchaseVerificationProvider = 'iapkit';
550
+
525
551
  export interface Query {
526
552
  /** Check if external purchase notice sheet can be presented (iOS 18.2+) */
527
553
  canPresentExternalPurchaseNoticeIOS: Promise<boolean>;
@@ -560,8 +586,11 @@ export interface Query {
560
586
  latestTransactionIOS?: Promise<(PurchaseIOS | null)>;
561
587
  /** Get StoreKit 2 subscription status details (iOS 15+) */
562
588
  subscriptionStatusIOS: Promise<SubscriptionStatusIOS[]>;
563
- /** Validate a receipt for a specific product */
564
- validateReceiptIOS: Promise<ReceiptValidationResultIOS>;
589
+ /**
590
+ * Validate a receipt for a specific product
591
+ * @deprecated Use verifyPurchase
592
+ */
593
+ validateReceiptIOS: Promise<VerifyPurchaseResultIOS>;
565
594
  }
566
595
 
567
596
 
@@ -586,55 +615,7 @@ export type QueryLatestTransactionIosArgs = string;
586
615
 
587
616
  export type QuerySubscriptionStatusIosArgs = string;
588
617
 
589
- export type QueryValidateReceiptIosArgs = ReceiptValidationProps;
590
-
591
- export interface ReceiptValidationAndroidOptions {
592
- accessToken: string;
593
- isSub?: (boolean | null);
594
- packageName: string;
595
- productToken: string;
596
- }
597
-
598
- export interface ReceiptValidationProps {
599
- /** Android-specific validation options */
600
- androidOptions?: (ReceiptValidationAndroidOptions | null);
601
- /** Product SKU to validate */
602
- sku: string;
603
- }
604
-
605
- export type ReceiptValidationResult = ReceiptValidationResultAndroid | ReceiptValidationResultIOS;
606
-
607
- export interface ReceiptValidationResultAndroid {
608
- autoRenewing: boolean;
609
- betaProduct: boolean;
610
- cancelDate?: (number | null);
611
- cancelReason?: (string | null);
612
- deferredDate?: (number | null);
613
- deferredSku?: (string | null);
614
- freeTrialEndDate: number;
615
- gracePeriodEndDate: number;
616
- parentProductId: string;
617
- productId: string;
618
- productType: string;
619
- purchaseDate: number;
620
- quantity: number;
621
- receiptId: string;
622
- renewalDate: number;
623
- term: string;
624
- termSku: string;
625
- testTransaction: boolean;
626
- }
627
-
628
- export interface ReceiptValidationResultIOS {
629
- /** Whether the receipt is valid */
630
- isValid: boolean;
631
- /** JWS representation */
632
- jwsRepresentation: string;
633
- /** Latest transaction if available */
634
- latestTransaction?: (Purchase | null);
635
- /** Receipt data string */
636
- receiptData: string;
637
- }
618
+ export type QueryValidateReceiptIosArgs = VerifyPurchaseProps;
638
619
 
639
620
  export interface RefundResultIOS {
640
621
  message?: (string | null);
@@ -769,6 +750,33 @@ export interface RequestSubscriptionPropsByPlatforms {
769
750
  ios?: (RequestSubscriptionIosProps | null);
770
751
  }
771
752
 
753
+ export interface RequestVerifyPurchaseWithIapkitAppleProps {
754
+ /** The JWS token returned with the purchase response. */
755
+ jws: string;
756
+ }
757
+
758
+ export interface RequestVerifyPurchaseWithIapkitGoogleProps {
759
+ /** The token provided to the user's device when the product or subscription was purchased. */
760
+ purchaseToken: string;
761
+ }
762
+
763
+ export interface RequestVerifyPurchaseWithIapkitProps {
764
+ /** API key used for the Authorization header (Bearer {apiKey}). */
765
+ apiKey?: (string | null);
766
+ /** Apple verification parameters. */
767
+ apple?: (RequestVerifyPurchaseWithIapkitAppleProps | null);
768
+ /** Google verification parameters. */
769
+ google?: (RequestVerifyPurchaseWithIapkitGoogleProps | null);
770
+ }
771
+
772
+ export interface RequestVerifyPurchaseWithIapkitResult {
773
+ /** Whether the purchase is valid (not falsified). */
774
+ isValid: boolean;
775
+ /** The current state of the purchase. */
776
+ state: IapkitPurchaseState;
777
+ store: IapkitStore;
778
+ }
779
+
772
780
  export interface Subscription {
773
781
  /** Fires when the App Store surfaces a promoted product (iOS only) */
774
782
  promotedProductIOS: string;
@@ -826,6 +834,65 @@ export interface UserChoiceBillingDetails {
826
834
  products: string[];
827
835
  }
828
836
 
837
+ export interface VerifyPurchaseAndroidOptions {
838
+ accessToken: string;
839
+ isSub?: (boolean | null);
840
+ packageName: string;
841
+ productToken: string;
842
+ }
843
+
844
+ export interface VerifyPurchaseProps {
845
+ /** Android-specific validation options */
846
+ androidOptions?: (VerifyPurchaseAndroidOptions | null);
847
+ /** Product SKU to validate */
848
+ sku: string;
849
+ }
850
+
851
+ export type VerifyPurchaseResult = VerifyPurchaseResultAndroid | VerifyPurchaseResultIOS;
852
+
853
+ export interface VerifyPurchaseResultAndroid {
854
+ autoRenewing: boolean;
855
+ betaProduct: boolean;
856
+ cancelDate?: (number | null);
857
+ cancelReason?: (string | null);
858
+ deferredDate?: (number | null);
859
+ deferredSku?: (string | null);
860
+ freeTrialEndDate: number;
861
+ gracePeriodEndDate: number;
862
+ parentProductId: string;
863
+ productId: string;
864
+ productType: string;
865
+ purchaseDate: number;
866
+ quantity: number;
867
+ receiptId: string;
868
+ renewalDate: number;
869
+ term: string;
870
+ termSku: string;
871
+ testTransaction: boolean;
872
+ }
873
+
874
+ export interface VerifyPurchaseResultIOS {
875
+ /** Whether the receipt is valid */
876
+ isValid: boolean;
877
+ /** JWS representation */
878
+ jwsRepresentation: string;
879
+ /** Latest transaction if available */
880
+ latestTransaction?: (Purchase | null);
881
+ /** Receipt data string */
882
+ receiptData: string;
883
+ }
884
+
885
+ export interface VerifyPurchaseWithProviderProps {
886
+ iapkit?: (RequestVerifyPurchaseWithIapkitProps | null);
887
+ provider: PurchaseVerificationProvider;
888
+ }
889
+
890
+ export interface VerifyPurchaseWithProviderResult {
891
+ /** IAPKit verification results (can include Apple and Google entries) */
892
+ iapkit: RequestVerifyPurchaseWithIapkitResult[];
893
+ provider: PurchaseVerificationProvider;
894
+ }
895
+
829
896
  export type VoidResult = void;
830
897
 
831
898
  // -- Query helper types (auto-generated)
@@ -884,6 +951,8 @@ export type MutationArgsMap = {
884
951
  showManageSubscriptionsIOS: never;
885
952
  syncIOS: never;
886
953
  validateReceipt: MutationValidateReceiptArgs;
954
+ verifyPurchase: MutationVerifyPurchaseArgs;
955
+ verifyPurchaseWithProvider: MutationVerifyPurchaseWithProviderArgs;
887
956
  };
888
957
 
889
958
  export type MutationField<K extends keyof Mutation> =
package/src/useIAP.ts CHANGED
@@ -15,6 +15,8 @@ import {
15
15
  requestPurchase as requestPurchaseInternal,
16
16
  fetchProducts,
17
17
  validateReceipt as validateReceiptInternal,
18
+ verifyPurchase as verifyPurchaseInternal,
19
+ verifyPurchaseWithProvider as verifyPurchaseWithProviderInternal,
18
20
  getActiveSubscriptions,
19
21
  hasActiveSubscriptions,
20
22
  type ActiveSubscription,
@@ -41,8 +43,10 @@ import type {
41
43
  Purchase,
42
44
  MutationRequestPurchaseArgs,
43
45
  PurchaseInput,
44
- ReceiptValidationProps,
45
- ReceiptValidationResult,
46
+ VerifyPurchaseProps,
47
+ VerifyPurchaseResult,
48
+ VerifyPurchaseWithProviderProps,
49
+ VerifyPurchaseWithProviderResult,
46
50
  ProductAndroid,
47
51
  ProductSubscriptionIOS,
48
52
  } from './types';
@@ -77,9 +81,14 @@ type UseIap = {
77
81
  requestPurchase: (
78
82
  params: MutationRequestPurchaseArgs,
79
83
  ) => ReturnType<typeof requestPurchaseInternal>;
84
+ /** @deprecated Use verifyPurchase instead */
80
85
  validateReceipt: (
81
- props: ReceiptValidationProps,
82
- ) => Promise<ReceiptValidationResult>;
86
+ props: VerifyPurchaseProps,
87
+ ) => Promise<VerifyPurchaseResult>;
88
+ verifyPurchase: (props: VerifyPurchaseProps) => Promise<VerifyPurchaseResult>;
89
+ verifyPurchaseWithProvider: (
90
+ props: VerifyPurchaseWithProviderProps,
91
+ ) => Promise<VerifyPurchaseWithProviderResult>;
83
92
  restorePurchases: () => Promise<void>;
84
93
  getPromotedProductIOS: () => Promise<Product | null>;
85
94
  requestPurchaseOnPromotedProductIOS: () => Promise<boolean>;
@@ -383,10 +392,21 @@ export function useIAP(options?: UseIAPOptions): UseIap {
383
392
  }
384
393
  }, []);
385
394
 
386
- const validateReceipt = useCallback(async (props: ReceiptValidationProps) => {
395
+ const validateReceipt = useCallback(async (props: VerifyPurchaseProps) => {
387
396
  return validateReceiptInternal(props);
388
397
  }, []);
389
398
 
399
+ const verifyPurchase = useCallback(async (props: VerifyPurchaseProps) => {
400
+ return verifyPurchaseInternal(props);
401
+ }, []);
402
+
403
+ const verifyPurchaseWithProvider = useCallback(
404
+ async (props: VerifyPurchaseWithProviderProps) => {
405
+ return verifyPurchaseWithProviderInternal(props);
406
+ },
407
+ [],
408
+ );
409
+
390
410
  const initIapWithSubscriptions = useCallback(async (): Promise<void> => {
391
411
  // CRITICAL: Register listeners BEFORE initConnection to avoid race condition
392
412
  // Events might fire immediately after initConnection, so listeners must be ready
@@ -481,6 +501,8 @@ export function useIAP(options?: UseIAPOptions): UseIap {
481
501
  fetchProducts: fetchProductsInternal,
482
502
  requestPurchase: requestPurchaseWithReset,
483
503
  validateReceipt,
504
+ verifyPurchase,
505
+ verifyPurchaseWithProvider,
484
506
  restorePurchases: restorePurchasesInternal,
485
507
  // internal getters kept for hook state management
486
508
  getPromotedProductIOS,
@@ -80,6 +80,11 @@ const COMMON_ERROR_CODE_MAP: Record<ErrorCode, string> = {
80
80
  [ErrorCode.BillingUnavailable]: ErrorCode.BillingUnavailable,
81
81
  [ErrorCode.FeatureNotSupported]: ErrorCode.FeatureNotSupported,
82
82
  [ErrorCode.EmptySkuList]: ErrorCode.EmptySkuList,
83
+ [ErrorCode.PurchaseVerificationFailed]: ErrorCode.PurchaseVerificationFailed,
84
+ [ErrorCode.PurchaseVerificationFinishFailed]:
85
+ ErrorCode.PurchaseVerificationFinishFailed,
86
+ [ErrorCode.PurchaseVerificationFinished]:
87
+ ErrorCode.PurchaseVerificationFinished,
83
88
  };
84
89
 
85
90
  export const ErrorCodeMapping = {