expo-iap 3.0.7 → 3.1.0

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 (52) hide show
  1. package/CLAUDE.md +14 -2
  2. package/CONTRIBUTING.md +19 -0
  3. package/README.md +18 -6
  4. package/android/build.gradle +24 -1
  5. package/android/src/main/java/expo/modules/iap/ExpoIapLog.kt +69 -0
  6. package/android/src/main/java/expo/modules/iap/ExpoIapModule.kt +190 -59
  7. package/build/index.d.ts +32 -111
  8. package/build/index.d.ts.map +1 -1
  9. package/build/index.js +198 -243
  10. package/build/index.js.map +1 -1
  11. package/build/modules/android.d.ts +7 -12
  12. package/build/modules/android.d.ts.map +1 -1
  13. package/build/modules/android.js +15 -12
  14. package/build/modules/android.js.map +1 -1
  15. package/build/modules/ios.d.ts +35 -36
  16. package/build/modules/ios.d.ts.map +1 -1
  17. package/build/modules/ios.js +101 -35
  18. package/build/modules/ios.js.map +1 -1
  19. package/build/types.d.ts +107 -82
  20. package/build/types.d.ts.map +1 -1
  21. package/build/types.js +1 -0
  22. package/build/types.js.map +1 -1
  23. package/build/useIAP.d.ts +7 -12
  24. package/build/useIAP.d.ts.map +1 -1
  25. package/build/useIAP.js +49 -23
  26. package/build/useIAP.js.map +1 -1
  27. package/build/utils/errorMapping.d.ts +32 -23
  28. package/build/utils/errorMapping.d.ts.map +1 -1
  29. package/build/utils/errorMapping.js +117 -22
  30. package/build/utils/errorMapping.js.map +1 -1
  31. package/ios/ExpoIap.podspec +3 -2
  32. package/ios/ExpoIapHelper.swift +96 -0
  33. package/ios/ExpoIapLog.swift +127 -0
  34. package/ios/ExpoIapModule.swift +218 -340
  35. package/openiap-versions.json +5 -0
  36. package/package.json +2 -2
  37. package/plugin/build/withIAP.js +6 -4
  38. package/plugin/src/withIAP.ts +14 -4
  39. package/scripts/update-types.mjs +20 -1
  40. package/src/index.ts +280 -356
  41. package/src/modules/android.ts +25 -23
  42. package/src/modules/ios.ts +138 -48
  43. package/src/types.ts +139 -91
  44. package/src/useIAP.ts +91 -58
  45. package/src/utils/errorMapping.ts +203 -23
  46. package/.copilot-instructions.md +0 -321
  47. package/.cursorrules +0 -321
  48. package/build/purchase-error.d.ts +0 -67
  49. package/build/purchase-error.d.ts.map +0 -1
  50. package/build/purchase-error.js +0 -166
  51. package/build/purchase-error.js.map +0 -1
  52. package/src/purchase-error.ts +0 -265
package/src/types.ts CHANGED
@@ -126,68 +126,54 @@ export enum ErrorCode {
126
126
  UserError = 'USER_ERROR'
127
127
  }
128
128
 
129
- export interface FetchProductsResult {
130
- products?: (Product[] | null);
131
- subscriptions?: (ProductSubscription[] | null);
132
- }
129
+ export type FetchProductsResult = Product[] | ProductSubscription[] | null;
133
130
 
134
- export type IapEvent = 'promoted-product-ios' | 'purchase-error' | 'purchase-updated';
131
+ export type IapEvent = 'purchase-updated' | 'purchase-error' | 'promoted-product-ios';
135
132
 
136
- export type IapPlatform = 'android' | 'ios';
133
+ export type IapPlatform = 'ios' | 'android';
137
134
 
138
135
  export interface Mutation {
139
136
  /** Acknowledge a non-consumable purchase or subscription */
140
- acknowledgePurchaseAndroid: Promise<VoidResult>;
137
+ acknowledgePurchaseAndroid: Promise<boolean>;
141
138
  /** Initiate a refund request for a product (iOS 15+) */
142
- beginRefundRequestIOS: Promise<RefundResultIOS>;
139
+ beginRefundRequestIOS?: Promise<(string | null)>;
143
140
  /** Clear pending transactions from the StoreKit payment queue */
144
- clearTransactionIOS: Promise<VoidResult>;
141
+ clearTransactionIOS: Promise<boolean>;
145
142
  /** Consume a purchase token so it can be repurchased */
146
- consumePurchaseAndroid: Promise<VoidResult>;
143
+ consumePurchaseAndroid: Promise<boolean>;
147
144
  /** Open the native subscription management surface */
148
- deepLinkToSubscriptions: Promise<VoidResult>;
145
+ deepLinkToSubscriptions: Promise<void>;
149
146
  /** Close the platform billing connection */
150
147
  endConnection: Promise<boolean>;
151
148
  /** Finish a transaction after validating receipts */
152
- finishTransaction: Promise<VoidResult>;
149
+ finishTransaction: Promise<void>;
153
150
  /** Establish the platform billing connection */
154
151
  initConnection: Promise<boolean>;
155
152
  /** Present the App Store code redemption sheet */
156
- presentCodeRedemptionSheetIOS: Promise<VoidResult>;
153
+ presentCodeRedemptionSheetIOS: Promise<boolean>;
157
154
  /** Initiate a purchase flow; rely on events for final state */
158
- requestPurchase?: Promise<(RequestPurchaseResult | null)>;
155
+ requestPurchase?: Promise<(Purchase | Purchase[] | null)>;
159
156
  /** Purchase the promoted product surfaced by the App Store */
160
- requestPurchaseOnPromotedProductIOS: Promise<PurchaseIOS>;
157
+ requestPurchaseOnPromotedProductIOS: Promise<boolean>;
161
158
  /** Restore completed purchases across platforms */
162
- restorePurchases: Promise<VoidResult>;
159
+ restorePurchases: Promise<void>;
163
160
  /** Open subscription management UI and return changed purchases (iOS 15+) */
164
161
  showManageSubscriptionsIOS: Promise<PurchaseIOS[]>;
165
162
  /** Force a StoreKit sync for transactions (iOS 15+) */
166
- syncIOS: Promise<VoidResult>;
163
+ syncIOS: Promise<boolean>;
167
164
  /** Validate purchase receipts with the configured providers */
168
165
  validateReceipt: Promise<ReceiptValidationResult>;
169
166
  }
170
167
 
171
168
 
172
- export interface MutationAcknowledgePurchaseAndroidArgs {
173
- purchaseToken: string;
174
- }
175
-
176
-
177
- export interface MutationBeginRefundRequestIosArgs {
178
- sku: string;
179
- }
180
-
181
169
 
182
- export interface MutationConsumePurchaseAndroidArgs {
183
- purchaseToken: string;
184
- }
170
+ export type MutationAcknowledgePurchaseAndroidArgs = string;
185
171
 
172
+ export type MutationBeginRefundRequestIosArgs = string;
186
173
 
187
- export interface MutationDeepLinkToSubscriptionsArgs {
188
- options?: (DeepLinkOptions | null);
189
- }
174
+ export type MutationConsumePurchaseAndroidArgs = string;
190
175
 
176
+ export type MutationDeepLinkToSubscriptionsArgs = (DeepLinkOptions | null) | undefined;
191
177
 
192
178
  export interface MutationFinishTransactionArgs {
193
179
  isConsumable?: (boolean | null);
@@ -195,14 +181,20 @@ export interface MutationFinishTransactionArgs {
195
181
  }
196
182
 
197
183
 
198
- export interface MutationRequestPurchaseArgs {
199
- params: RequestPurchaseProps;
200
- }
184
+ export type MutationRequestPurchaseArgs =
185
+ | {
186
+ /** Per-platform purchase request props */
187
+ request: RequestPurchasePropsByPlatforms;
188
+ type: 'in-app';
189
+ }
190
+ | {
191
+ /** Per-platform subscription request props */
192
+ request: RequestSubscriptionPropsByPlatforms;
193
+ type: 'subs';
194
+ };
201
195
 
202
196
 
203
- export interface MutationValidateReceiptArgs {
204
- options: ReceiptValidationProps;
205
- }
197
+ export type MutationValidateReceiptArgs = ReceiptValidationProps;
206
198
 
207
199
  export type PaymentModeIOS = 'empty' | 'free-trial' | 'pay-as-you-go' | 'pay-up-front';
208
200
 
@@ -274,7 +266,7 @@ export interface ProductIOS extends ProductCommon {
274
266
  typeIOS: ProductTypeIOS;
275
267
  }
276
268
 
277
- export type ProductQueryType = 'all' | 'in-app' | 'subs';
269
+ export type ProductQueryType = 'in-app' | 'subs' | 'all';
278
270
 
279
271
  export interface ProductRequest {
280
272
  skus: string[];
@@ -335,7 +327,7 @@ export interface ProductSubscriptionIOS extends ProductCommon {
335
327
 
336
328
  export type ProductType = 'in-app' | 'subs';
337
329
 
338
- export type ProductTypeIOS = 'auto-renewable-subscription' | 'consumable' | 'non-consumable' | 'non-renewing-subscription';
330
+ export type ProductTypeIOS = 'consumable' | 'non-consumable' | 'auto-renewable-subscription' | 'non-renewing-subscription';
339
331
 
340
332
  export type Purchase = PurchaseAndroid | PurchaseIOS;
341
333
 
@@ -357,6 +349,7 @@ export interface PurchaseAndroid extends PurchaseCommon {
357
349
  quantity: number;
358
350
  signatureAndroid?: (string | null);
359
351
  transactionDate: number;
352
+ transactionId?: (string | null);
360
353
  }
361
354
 
362
355
  export interface PurchaseCommon {
@@ -407,6 +400,7 @@ export interface PurchaseIOS extends PurchaseCommon {
407
400
  storefrontCountryCodeIOS?: (string | null);
408
401
  subscriptionGroupIdIOS?: (string | null);
409
402
  transactionDate: number;
403
+ transactionId: string;
410
404
  transactionReasonIOS?: (string | null);
411
405
  webOrderLineItemIdIOS?: (string | null);
412
406
  }
@@ -436,13 +430,13 @@ export interface PurchaseOptions {
436
430
  onlyIncludeActiveItemsIOS?: (boolean | null);
437
431
  }
438
432
 
439
- export type PurchaseState = 'deferred' | 'failed' | 'pending' | 'purchased' | 'restored' | 'unknown';
433
+ export type PurchaseState = 'pending' | 'purchased' | 'failed' | 'restored' | 'deferred' | 'unknown';
440
434
 
441
435
  export interface Query {
442
436
  /** Get current StoreKit 2 entitlements (iOS 15+) */
443
- currentEntitlementIOS: Promise<EntitlementIOS[]>;
437
+ currentEntitlementIOS?: Promise<(PurchaseIOS | null)>;
444
438
  /** Retrieve products or subscriptions from the store */
445
- fetchProducts: Promise<FetchProductsResult>;
439
+ fetchProducts: Promise<(Product[] | ProductSubscription[] | null)>;
446
440
  /** Get active subscriptions (filters by subscriptionIds when provided) */
447
441
  getActiveSubscriptions: Promise<ActiveSubscription[]>;
448
442
  /** Fetch the current app transaction (iOS 16+) */
@@ -454,14 +448,14 @@ export interface Query {
454
448
  /** Get the currently promoted product (iOS 11+) */
455
449
  getPromotedProductIOS?: Promise<(ProductIOS | null)>;
456
450
  /** Get base64-encoded receipt data for validation */
457
- getReceiptDataIOS: Promise<string>;
451
+ getReceiptDataIOS?: Promise<(string | null)>;
458
452
  /** Get the current App Store storefront country code */
459
453
  getStorefrontIOS: Promise<string>;
460
454
  /** Get the transaction JWS (StoreKit 2) */
461
- getTransactionJwsIOS: Promise<string>;
455
+ getTransactionJwsIOS?: Promise<(string | null)>;
462
456
  /** Check whether the user has active subscriptions */
463
457
  hasActiveSubscriptions: Promise<boolean>;
464
- /** Check introductory offer eligibility for specific products */
458
+ /** Check introductory offer eligibility for a subscription group */
465
459
  isEligibleForIntroOfferIOS: Promise<boolean>;
466
460
  /** Verify a StoreKit 2 transaction signature */
467
461
  isTransactionVerifiedIOS: Promise<boolean>;
@@ -469,57 +463,33 @@ export interface Query {
469
463
  latestTransactionIOS?: Promise<(PurchaseIOS | null)>;
470
464
  /** Get StoreKit 2 subscription status details (iOS 15+) */
471
465
  subscriptionStatusIOS: Promise<SubscriptionStatusIOS[]>;
466
+ /** Validate a receipt for a specific product */
467
+ validateReceiptIOS: Promise<ReceiptValidationResultIOS>;
472
468
  }
473
469
 
474
470
 
475
- export interface QueryCurrentEntitlementIosArgs {
476
- skus?: (string[] | null);
477
- }
478
471
 
472
+ export type QueryCurrentEntitlementIosArgs = string;
479
473
 
480
- export interface QueryFetchProductsArgs {
481
- params: ProductRequest;
482
- }
483
-
484
-
485
- export interface QueryGetActiveSubscriptionsArgs {
486
- subscriptionIds?: (string[] | null);
487
- }
488
-
489
-
490
- export interface QueryGetAvailablePurchasesArgs {
491
- options?: (PurchaseOptions | null);
492
- }
493
-
494
-
495
- export interface QueryGetTransactionJwsIosArgs {
496
- transactionId: string;
497
- }
474
+ export type QueryFetchProductsArgs = ProductRequest;
498
475
 
476
+ export type QueryGetActiveSubscriptionsArgs = (string[] | null) | undefined;
499
477
 
500
- export interface QueryHasActiveSubscriptionsArgs {
501
- subscriptionIds?: (string[] | null);
502
- }
478
+ export type QueryGetAvailablePurchasesArgs = (PurchaseOptions | null) | undefined;
503
479
 
480
+ export type QueryGetTransactionJwsIosArgs = string;
504
481
 
505
- export interface QueryIsEligibleForIntroOfferIosArgs {
506
- productIds: string[];
507
- }
482
+ export type QueryHasActiveSubscriptionsArgs = (string[] | null) | undefined;
508
483
 
484
+ export type QueryIsEligibleForIntroOfferIosArgs = string;
509
485
 
510
- export interface QueryIsTransactionVerifiedIosArgs {
511
- transactionId: string;
512
- }
486
+ export type QueryIsTransactionVerifiedIosArgs = string;
513
487
 
488
+ export type QueryLatestTransactionIosArgs = string;
514
489
 
515
- export interface QueryLatestTransactionIosArgs {
516
- sku: string;
517
- }
490
+ export type QuerySubscriptionStatusIosArgs = string;
518
491
 
519
-
520
- export interface QuerySubscriptionStatusIosArgs {
521
- skus?: (string[] | null);
522
- }
492
+ export type QueryValidateReceiptIosArgs = ReceiptValidationProps;
523
493
 
524
494
  export interface ReceiptValidationAndroidOptions {
525
495
  accessToken: string;
@@ -623,10 +593,7 @@ export interface RequestPurchasePropsByPlatforms {
623
593
  ios?: (RequestPurchaseIosProps | null);
624
594
  }
625
595
 
626
- export interface RequestPurchaseResult {
627
- purchase?: (Purchase | null);
628
- purchases?: (Purchase[] | null);
629
- }
596
+ export type RequestPurchaseResult = Purchase | Purchase[] | null;
630
597
 
631
598
  export interface RequestSubscriptionAndroidProps {
632
599
  /** Personalized offer flag */
@@ -669,6 +636,7 @@ export interface Subscription {
669
636
  purchaseUpdated: Purchase;
670
637
  }
671
638
 
639
+
672
640
  export interface SubscriptionInfoIOS {
673
641
  introductoryOffer?: (SubscriptionOfferIOS | null);
674
642
  promotionalOffers?: (SubscriptionOfferIOS[] | null);
@@ -688,7 +656,7 @@ export interface SubscriptionOfferIOS {
688
656
 
689
657
  export type SubscriptionOfferTypeIOS = 'introductory' | 'promotional';
690
658
 
691
- export type SubscriptionPeriodIOS = 'day' | 'empty' | 'month' | 'week' | 'year';
659
+ export type SubscriptionPeriodIOS = 'day' | 'week' | 'month' | 'year' | 'empty';
692
660
 
693
661
  export interface SubscriptionPeriodValueIOS {
694
662
  unit: SubscriptionPeriodIOS;
@@ -700,6 +668,86 @@ export interface SubscriptionStatusIOS {
700
668
  state: string;
701
669
  }
702
670
 
703
- export interface VoidResult {
704
- success: boolean;
705
- }
671
+ export type VoidResult = void;
672
+
673
+ // -- Query helper types (auto-generated)
674
+ export type QueryArgsMap = {
675
+ currentEntitlementIOS: QueryCurrentEntitlementIosArgs;
676
+ fetchProducts: QueryFetchProductsArgs;
677
+ getActiveSubscriptions: QueryGetActiveSubscriptionsArgs;
678
+ getAppTransactionIOS: never;
679
+ getAvailablePurchases: QueryGetAvailablePurchasesArgs;
680
+ getPendingTransactionsIOS: never;
681
+ getPromotedProductIOS: never;
682
+ getReceiptDataIOS: never;
683
+ getStorefrontIOS: never;
684
+ getTransactionJwsIOS: QueryGetTransactionJwsIosArgs;
685
+ hasActiveSubscriptions: QueryHasActiveSubscriptionsArgs;
686
+ isEligibleForIntroOfferIOS: QueryIsEligibleForIntroOfferIosArgs;
687
+ isTransactionVerifiedIOS: QueryIsTransactionVerifiedIosArgs;
688
+ latestTransactionIOS: QueryLatestTransactionIosArgs;
689
+ subscriptionStatusIOS: QuerySubscriptionStatusIosArgs;
690
+ validateReceiptIOS: QueryValidateReceiptIosArgs;
691
+ };
692
+
693
+ export type QueryField<K extends keyof Query> =
694
+ QueryArgsMap[K] extends never
695
+ ? () => NonNullable<Query[K]>
696
+ : undefined extends QueryArgsMap[K]
697
+ ? (args?: QueryArgsMap[K]) => NonNullable<Query[K]>
698
+ : (args: QueryArgsMap[K]) => NonNullable<Query[K]>;
699
+
700
+ export type QueryFieldMap = {
701
+ [K in keyof Query]?: QueryField<K>;
702
+ };
703
+ // -- End query helper types
704
+
705
+ // -- Mutation helper types (auto-generated)
706
+ export type MutationArgsMap = {
707
+ acknowledgePurchaseAndroid: MutationAcknowledgePurchaseAndroidArgs;
708
+ beginRefundRequestIOS: MutationBeginRefundRequestIosArgs;
709
+ clearTransactionIOS: never;
710
+ consumePurchaseAndroid: MutationConsumePurchaseAndroidArgs;
711
+ deepLinkToSubscriptions: MutationDeepLinkToSubscriptionsArgs;
712
+ endConnection: never;
713
+ finishTransaction: MutationFinishTransactionArgs;
714
+ initConnection: never;
715
+ presentCodeRedemptionSheetIOS: never;
716
+ requestPurchase: MutationRequestPurchaseArgs;
717
+ requestPurchaseOnPromotedProductIOS: never;
718
+ restorePurchases: never;
719
+ showManageSubscriptionsIOS: never;
720
+ syncIOS: never;
721
+ validateReceipt: MutationValidateReceiptArgs;
722
+ };
723
+
724
+ export type MutationField<K extends keyof Mutation> =
725
+ MutationArgsMap[K] extends never
726
+ ? () => NonNullable<Mutation[K]>
727
+ : undefined extends MutationArgsMap[K]
728
+ ? (args?: MutationArgsMap[K]) => NonNullable<Mutation[K]>
729
+ : (args: MutationArgsMap[K]) => NonNullable<Mutation[K]>;
730
+
731
+ export type MutationFieldMap = {
732
+ [K in keyof Mutation]?: MutationField<K>;
733
+ };
734
+ // -- End mutation helper types
735
+
736
+ // -- Subscription helper types (auto-generated)
737
+ export type SubscriptionArgsMap = {
738
+ promotedProductIOS: never;
739
+ purchaseError: never;
740
+ purchaseUpdated: never;
741
+ };
742
+
743
+ export type SubscriptionField<K extends keyof Subscription> =
744
+ SubscriptionArgsMap[K] extends never
745
+ ? () => NonNullable<Subscription[K]>
746
+ : undefined extends SubscriptionArgsMap[K]
747
+ ? (args?: SubscriptionArgsMap[K]) => NonNullable<Subscription[K]>
748
+ : (args: SubscriptionArgsMap[K]) => NonNullable<Subscription[K]>;
749
+
750
+ export type SubscriptionFieldMap = {
751
+ [K in keyof Subscription]?: SubscriptionField<K>;
752
+ };
753
+ // -- End subscription helper types
package/src/useIAP.ts CHANGED
@@ -19,7 +19,6 @@ import {
19
19
  hasActiveSubscriptions,
20
20
  type ActiveSubscription,
21
21
  type ProductTypeInput,
22
- type PurchaseRequestInput,
23
22
  restorePurchases,
24
23
  } from './index';
25
24
  import {
@@ -28,15 +27,19 @@ import {
28
27
  } from './modules/ios';
29
28
 
30
29
  // Types
31
- import {
30
+ import type {
32
31
  Product,
33
- Purchase,
34
32
  ProductSubscription,
35
- ErrorCode,
36
- VoidResult,
33
+ ProductQueryType,
34
+ ProductRequest,
35
+ Purchase,
36
+ MutationRequestPurchaseArgs,
37
+ PurchaseInput,
38
+ ReceiptValidationProps,
37
39
  ReceiptValidationResult,
38
40
  } from './types';
39
- import {PurchaseError} from './purchase-error';
41
+ import {ErrorCode} from './types';
42
+ import type {PurchaseError} from './utils/errorMapping';
40
43
  import {
41
44
  getUserFriendlyErrorMessage,
42
45
  isUserCancelledError,
@@ -58,7 +61,7 @@ type UseIap = {
58
61
  }: {
59
62
  purchase: Purchase;
60
63
  isConsumable?: boolean;
61
- }) => Promise<VoidResult | boolean>;
64
+ }) => Promise<void>;
62
65
  getAvailablePurchases: () => Promise<void>;
63
66
  fetchProducts: (params: {
64
67
  skus: string[];
@@ -66,20 +69,14 @@ type UseIap = {
66
69
  }) => Promise<void>;
67
70
 
68
71
  requestPurchase: (
69
- params: PurchaseRequestInput,
72
+ params: MutationRequestPurchaseArgs,
70
73
  ) => ReturnType<typeof requestPurchaseInternal>;
71
74
  validateReceipt: (
72
- sku: string,
73
- androidOptions?: {
74
- packageName: string;
75
- productToken: string;
76
- accessToken: string;
77
- isSub?: boolean;
78
- },
75
+ props: ReceiptValidationProps,
79
76
  ) => Promise<ReceiptValidationResult>;
80
77
  restorePurchases: () => Promise<void>;
81
78
  getPromotedProductIOS: () => Promise<Product | null>;
82
- requestPurchaseOnPromotedProductIOS: () => Promise<void>;
79
+ requestPurchaseOnPromotedProductIOS: () => Promise<boolean>;
83
80
  getActiveSubscriptions: (subscriptionIds?: string[]) => Promise<void>;
84
81
  hasActiveSubscriptions: (subscriptionIds?: string[]) => Promise<boolean>;
85
82
  };
@@ -154,22 +151,40 @@ export function useIAP(options?: UseIAPOptions): UseIap {
154
151
  subscriptionsRefState.current = subscriptions;
155
152
  }, [subscriptions]);
156
153
 
157
- const getSubscriptionsInternal = useCallback(
158
- async (skus: string[]): Promise<void> => {
159
- try {
160
- const result = await fetchProducts({skus, type: 'subs'});
161
- setSubscriptions((prevSubscriptions) =>
162
- mergeWithDuplicateCheck(
163
- prevSubscriptions,
164
- result as ProductSubscription[],
165
- (subscription) => subscription.id,
166
- ),
167
- );
168
- } catch (error) {
169
- console.error('Error fetching subscriptions:', error);
154
+ const normalizeProductQueryType = useCallback(
155
+ (type?: ProductTypeInput): ProductQueryType => {
156
+ if (!type || type === 'inapp' || type === 'in-app') {
157
+ return 'in-app';
170
158
  }
159
+ return type;
171
160
  },
172
- [mergeWithDuplicateCheck],
161
+ [],
162
+ );
163
+
164
+ const canonicalProductType = useCallback(
165
+ (value?: string): ProductQueryType => {
166
+ if (!value) {
167
+ return 'in-app';
168
+ }
169
+ const normalized = value.trim().toLowerCase().replace(/[_-]/g, '');
170
+ return normalized === 'subs' ? 'subs' : 'in-app';
171
+ },
172
+ [],
173
+ );
174
+
175
+ const toPurchaseInput = useCallback(
176
+ (purchase: Purchase): PurchaseInput => ({
177
+ id: purchase.id,
178
+ ids: purchase.ids ?? undefined,
179
+ isAutoRenewing: purchase.isAutoRenewing,
180
+ platform: purchase.platform,
181
+ productId: purchase.productId,
182
+ purchaseState: purchase.purchaseState,
183
+ purchaseToken: purchase.purchaseToken ?? null,
184
+ quantity: purchase.quantity,
185
+ transactionDate: purchase.transactionDate,
186
+ }),
187
+ [],
173
188
  );
174
189
 
175
190
  const fetchProductsInternal = useCallback(
@@ -178,30 +193,58 @@ export function useIAP(options?: UseIAPOptions): UseIap {
178
193
  type?: ProductTypeInput;
179
194
  }): Promise<void> => {
180
195
  try {
181
- const result = await fetchProducts(params);
196
+ const queryType = normalizeProductQueryType(params.type);
197
+ const request: ProductRequest = {skus: params.skus, type: queryType};
198
+ const result = await fetchProducts(request);
199
+ const items = (result ?? []) as (Product | ProductSubscription)[];
182
200
 
183
- if (params.type === 'subs') {
201
+ if (queryType === 'subs') {
202
+ const subscriptionsResult = items as ProductSubscription[];
184
203
  setSubscriptions((prevSubscriptions) =>
185
204
  mergeWithDuplicateCheck(
186
205
  prevSubscriptions,
187
- result as ProductSubscription[],
206
+ subscriptionsResult,
188
207
  (subscription) => subscription.id,
189
208
  ),
190
209
  );
210
+ } else if (queryType === 'in-app') {
211
+ const productsResult = items as Product[];
212
+ setProducts((prevProducts) =>
213
+ mergeWithDuplicateCheck(
214
+ prevProducts,
215
+ productsResult,
216
+ (product) => product.id,
217
+ ),
218
+ );
191
219
  } else {
220
+ const productItems = items.filter(
221
+ (item) => canonicalProductType(item.type as string) === 'in-app',
222
+ ) as Product[];
223
+ const subscriptionItems = items.filter(
224
+ (item) => canonicalProductType(item.type as string) === 'subs',
225
+ ) as ProductSubscription[];
226
+
192
227
  setProducts((prevProducts) =>
193
228
  mergeWithDuplicateCheck(
194
229
  prevProducts,
195
- result as Product[],
230
+ productItems,
196
231
  (product) => product.id,
197
232
  ),
198
233
  );
234
+
235
+ setSubscriptions((prevSubscriptions) =>
236
+ mergeWithDuplicateCheck(
237
+ prevSubscriptions,
238
+ subscriptionItems,
239
+ (subscription) => subscription.id,
240
+ ),
241
+ );
199
242
  }
200
243
  } catch (error) {
201
244
  console.error('Error fetching products:', error);
202
245
  }
203
246
  },
204
- [mergeWithDuplicateCheck],
247
+ [canonicalProductType, mergeWithDuplicateCheck, normalizeProductQueryType],
205
248
  );
206
249
 
207
250
  const getAvailablePurchasesInternal = useCallback(async (): Promise<void> => {
@@ -242,23 +285,23 @@ export function useIAP(options?: UseIAPOptions): UseIap {
242
285
  );
243
286
 
244
287
  const finishTransaction = useCallback(
245
- ({
288
+ async ({
246
289
  purchase,
247
290
  isConsumable,
248
291
  }: {
249
292
  purchase: Purchase;
250
293
  isConsumable?: boolean;
251
- }): Promise<VoidResult | boolean> => {
252
- return finishTransactionInternal({
253
- purchase,
294
+ }): Promise<void> => {
295
+ await finishTransactionInternal({
296
+ purchase: toPurchaseInput(purchase),
254
297
  isConsumable,
255
298
  });
256
299
  },
257
- [],
300
+ [toPurchaseInput],
258
301
  );
259
302
 
260
303
  const requestPurchaseWithReset = useCallback(
261
- (requestObj: PurchaseRequestInput) => {
304
+ (requestObj: MutationRequestPurchaseArgs) => {
262
305
  return requestPurchaseInternal(requestObj);
263
306
  },
264
307
  [],
@@ -268,14 +311,14 @@ export function useIAP(options?: UseIAPOptions): UseIap {
268
311
  async (productId: string) => {
269
312
  try {
270
313
  if (subscriptionsRefState.current.some((sub) => sub.id === productId)) {
271
- await getSubscriptionsInternal([productId]);
314
+ await fetchProductsInternal({skus: [productId], type: 'subs'});
272
315
  await getAvailablePurchasesInternal();
273
316
  }
274
317
  } catch (error) {
275
318
  console.warn('Failed to refresh subscription status:', error);
276
319
  }
277
320
  },
278
- [getAvailablePurchasesInternal, getSubscriptionsInternal],
321
+ [fetchProductsInternal, getAvailablePurchasesInternal],
279
322
  );
280
323
 
281
324
  // Restore completed transactions with cross-platform behavior.
@@ -283,7 +326,8 @@ export function useIAP(options?: UseIAPOptions): UseIap {
283
326
  // Android: fetch available purchases directly.
284
327
  const restorePurchasesInternal = useCallback(async (): Promise<void> => {
285
328
  try {
286
- const purchases = await restorePurchases({
329
+ await restorePurchases();
330
+ const purchases = await getAvailablePurchases({
287
331
  alsoPublishToEventListenerIOS: false,
288
332
  onlyIncludeActiveItemsIOS: true,
289
333
  });
@@ -293,20 +337,9 @@ export function useIAP(options?: UseIAPOptions): UseIap {
293
337
  }
294
338
  }, []);
295
339
 
296
- const validateReceipt = useCallback(
297
- async (
298
- sku: string,
299
- androidOptions?: {
300
- packageName: string;
301
- productToken: string;
302
- accessToken: string;
303
- isSub?: boolean;
304
- },
305
- ) => {
306
- return validateReceiptInternal(sku, androidOptions);
307
- },
308
- [],
309
- );
340
+ const validateReceipt = useCallback(async (props: ReceiptValidationProps) => {
341
+ return validateReceiptInternal(props);
342
+ }, []);
310
343
 
311
344
  const initIapWithSubscriptions = useCallback(async (): Promise<void> => {
312
345
  // CRITICAL: Register listeners BEFORE initConnection to avoid race condition