expo-iap 2.9.0 → 2.9.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.
Files changed (39) hide show
  1. package/CHANGELOG.md +27 -1
  2. package/CONTRIBUTING.md +2 -2
  3. package/README.md +3 -3
  4. package/build/ExpoIap.types.d.ts +33 -15
  5. package/build/ExpoIap.types.d.ts.map +1 -1
  6. package/build/ExpoIap.types.js +64 -17
  7. package/build/ExpoIap.types.js.map +1 -1
  8. package/build/index.d.ts +15 -11
  9. package/build/index.d.ts.map +1 -1
  10. package/build/index.js +48 -23
  11. package/build/index.js.map +1 -1
  12. package/build/modules/ios.d.ts +3 -7
  13. package/build/modules/ios.d.ts.map +1 -1
  14. package/build/modules/ios.js +1 -6
  15. package/build/modules/ios.js.map +1 -1
  16. package/build/types/ExpoIapAndroid.types.d.ts +0 -32
  17. package/build/types/ExpoIapAndroid.types.d.ts.map +1 -1
  18. package/build/types/ExpoIapAndroid.types.js +1 -5
  19. package/build/types/ExpoIapAndroid.types.js.map +1 -1
  20. package/build/types/ExpoIapIOS.types.d.ts +3 -27
  21. package/build/types/ExpoIapIOS.types.d.ts.map +1 -1
  22. package/build/types/ExpoIapIOS.types.js.map +1 -1
  23. package/build/useIAP.d.ts +1 -5
  24. package/build/useIAP.d.ts.map +1 -1
  25. package/build/useIAP.js +47 -41
  26. package/build/useIAP.js.map +1 -1
  27. package/build/utils/errorMapping.d.ts.map +1 -1
  28. package/build/utils/errorMapping.js +24 -0
  29. package/build/utils/errorMapping.js.map +1 -1
  30. package/ios/ExpoIap.podspec +1 -1
  31. package/ios/ExpoIapModule.swift +73 -52
  32. package/package.json +1 -1
  33. package/src/ExpoIap.types.ts +84 -37
  34. package/src/index.ts +60 -49
  35. package/src/modules/ios.ts +4 -9
  36. package/src/types/ExpoIapAndroid.types.ts +2 -36
  37. package/src/types/ExpoIapIOS.types.ts +3 -27
  38. package/src/useIAP.ts +53 -48
  39. package/src/utils/errorMapping.ts +24 -0
@@ -32,22 +32,6 @@ export type ProductIOS = ProductCommon & {
32
32
  jsonRepresentationIOS: string;
33
33
  platform: 'ios';
34
34
  subscriptionInfoIOS?: SubscriptionInfo;
35
- /**
36
- * @deprecated Use `displayNameIOS` instead. This field will be removed in v2.9.0.
37
- */
38
- displayName?: string;
39
- /**
40
- * @deprecated Use `isFamilyShareableIOS` instead. This field will be removed in v2.9.0.
41
- */
42
- isFamilyShareable?: boolean;
43
- /**
44
- * @deprecated Use `jsonRepresentationIOS` instead. This field will be removed in v2.9.0.
45
- */
46
- jsonRepresentation?: string;
47
- /**
48
- * @deprecated Use `subscriptionInfoIOS` instead. This field will be removed in v2.9.0.
49
- */
50
- subscription?: SubscriptionInfo;
51
35
  introductoryPriceNumberOfPeriodsIOS?: string;
52
36
  introductoryPriceSubscriptionPeriodIOS?: SubscriptionIosPeriod;
53
37
  };
@@ -72,14 +56,6 @@ export type ProductSubscriptionIOS = ProductIOS & {
72
56
  platform: 'ios';
73
57
  subscriptionPeriodNumberIOS?: string;
74
58
  subscriptionPeriodUnitIOS?: SubscriptionIosPeriod;
75
- /**
76
- * @deprecated Use `discountsIOS` instead. This field will be removed in v2.9.0.
77
- */
78
- discounts?: Discount[];
79
- /**
80
- * @deprecated Use `introductoryPriceIOS` instead. This field will be removed in v2.9.0.
81
- */
82
- introductoryPrice?: string;
83
59
  };
84
60
 
85
61
  // Legacy naming for backward compatibility
@@ -119,7 +95,7 @@ export type RequestPurchaseIosProps = {
119
95
  withOffer?: PaymentDiscount;
120
96
  };
121
97
 
122
- type SubscriptionStatus =
98
+ type SubscriptionState =
123
99
  | 'expired'
124
100
  | 'inBillingRetryPeriod'
125
101
  | 'inGracePeriod'
@@ -132,8 +108,8 @@ type RenewalInfo = {
132
108
  autoRenewPreference?: string;
133
109
  };
134
110
 
135
- export type ProductStatusIOS = {
136
- state: SubscriptionStatus;
111
+ export type SubscriptionStatusIOS = {
112
+ state: SubscriptionState;
137
113
  renewalInfo?: RenewalInfo;
138
114
  };
139
115
 
package/src/useIAP.ts CHANGED
@@ -11,7 +11,6 @@ import {
11
11
  purchaseUpdatedListener,
12
12
  promotedProductListenerIOS,
13
13
  getAvailablePurchases,
14
- getPurchaseHistories,
15
14
  finishTransaction as finishTransactionInternal,
16
15
  requestPurchase as requestPurchaseInternal,
17
16
  fetchProducts,
@@ -19,9 +18,9 @@ import {
19
18
  getActiveSubscriptions,
20
19
  hasActiveSubscriptions,
21
20
  type ActiveSubscription,
21
+ restorePurchases,
22
22
  } from '.';
23
23
  import {
24
- syncIOS,
25
24
  getPromotedProductIOS,
26
25
  requestPurchaseOnPromotedProductIOS,
27
26
  } from './modules/ios';
@@ -35,7 +34,13 @@ import {
35
34
  SubscriptionProduct,
36
35
  RequestPurchaseProps,
37
36
  RequestSubscriptionProps,
37
+ ErrorCode,
38
38
  } from './ExpoIap.types';
39
+ import {
40
+ getUserFriendlyErrorMessage,
41
+ isUserCancelledError,
42
+ isRecoverableError,
43
+ } from './utils/errorMapping';
39
44
 
40
45
  type UseIap = {
41
46
  connected: boolean;
@@ -43,7 +48,6 @@ type UseIap = {
43
48
  promotedProductsIOS: Purchase[];
44
49
  promotedProductIdIOS?: string;
45
50
  subscriptions: SubscriptionProduct[];
46
- purchaseHistories: Purchase[];
47
51
  availablePurchases: Purchase[];
48
52
  currentPurchase?: Purchase;
49
53
  currentPurchaseError?: PurchaseError;
@@ -59,7 +63,6 @@ type UseIap = {
59
63
  isConsumable?: boolean;
60
64
  }) => Promise<PurchaseResult | boolean>;
61
65
  getAvailablePurchases: (skus: string[]) => Promise<void>;
62
- getPurchaseHistories: (skus: string[]) => Promise<void>;
63
66
  fetchProducts: (params: {
64
67
  skus: string[];
65
68
  type?: 'inapp' | 'subs';
@@ -98,8 +101,6 @@ type UseIap = {
98
101
  restorePurchases: () => Promise<void>;
99
102
  getPromotedProductIOS: () => Promise<Product | null>;
100
103
  requestPurchaseOnPromotedProductIOS: () => Promise<void>;
101
- /** @deprecated Use requestPurchaseOnPromotedProductIOS instead */
102
- buyPromotedProductIOS: () => Promise<void>;
103
104
  getActiveSubscriptions: (subscriptionIds?: string[]) => Promise<void>;
104
105
  hasActiveSubscriptions: (subscriptionIds?: string[]) => Promise<boolean>;
105
106
  };
@@ -114,14 +115,14 @@ export interface UseIAPOptions {
114
115
 
115
116
  /**
116
117
  * React Hook for managing In-App Purchases.
117
- * See documentation at https://expo-iap.hyo.dev/docs/hooks/useIAP
118
+ * See documentation at https://hyochan.github.io/expo-iap/docs/hooks/useIAP
118
119
  */
119
120
  export function useIAP(options?: UseIAPOptions): UseIap {
120
121
  const [connected, setConnected] = useState<boolean>(false);
121
122
  const [products, setProducts] = useState<Product[]>([]);
122
123
  const [promotedProductsIOS] = useState<Purchase[]>([]);
123
124
  const [subscriptions, setSubscriptions] = useState<SubscriptionProduct[]>([]);
124
- const [purchaseHistories, setPurchaseHistories] = useState<Purchase[]>([]);
125
+ // Removed in v2.9.0: purchaseHistories state and related API
125
126
  const [availablePurchases, setAvailablePurchases] = useState<Purchase[]>([]);
126
127
  const [currentPurchase, setCurrentPurchase] = useState<Purchase>();
127
128
  const [promotedProductIOS, setPromotedProductIOS] = useState<Product>();
@@ -133,6 +134,7 @@ export function useIAP(options?: UseIAPOptions): UseIap {
133
134
  >([]);
134
135
 
135
136
  const optionsRef = useRef<UseIAPOptions | undefined>(options);
137
+ const connectedRef = useRef<boolean>(false);
136
138
 
137
139
  // Helper function to merge arrays with duplicate checking
138
140
  const mergeWithDuplicateCheck = useCallback(
@@ -159,6 +161,10 @@ export function useIAP(options?: UseIAPOptions): UseIap {
159
161
  optionsRef.current = options;
160
162
  }, [options]);
161
163
 
164
+ useEffect(() => {
165
+ connectedRef.current = connected;
166
+ }, [connected]);
167
+
162
168
  const subscriptionsRef = useRef<{
163
169
  purchaseUpdate?: EventSubscription;
164
170
  purchaseError?: EventSubscription;
@@ -298,13 +304,7 @@ export function useIAP(options?: UseIAPOptions): UseIap {
298
304
  [],
299
305
  );
300
306
 
301
- /**
302
- * @deprecated Use getAvailablePurchases instead. This function is just calling getAvailablePurchases internally.
303
- * Will be removed in v2.9.0
304
- */
305
- const getPurchaseHistoriesInternal = useCallback(async (): Promise<void> => {
306
- setPurchaseHistories(await getPurchaseHistories());
307
- }, []);
307
+ // NOTE: getPurchaseHistories removed in v2.9.0. Use getAvailablePurchases instead.
308
308
 
309
309
  const finishTransaction = useCallback(
310
310
  async ({
@@ -366,22 +366,20 @@ export function useIAP(options?: UseIAPOptions): UseIap {
366
366
  [getAvailablePurchasesInternal, getSubscriptionsInternal],
367
367
  );
368
368
 
369
- const restorePurchases = useCallback(async (): Promise<void> => {
369
+ // Restore completed transactions with cross-platform behavior.
370
+ // iOS: best-effort sync (ignore sync errors) then fetch available purchases.
371
+ // Android: fetch available purchases directly.
372
+ const restorePurchasesInternal = useCallback(async (): Promise<void> => {
370
373
  try {
371
- if (Platform.OS === 'ios') {
372
- await syncIOS().catch((error) => {
373
- if (optionsRef.current?.onSyncError) {
374
- optionsRef.current.onSyncError(error);
375
- } else {
376
- console.warn('Error restoring purchases:', error);
377
- }
378
- });
379
- }
380
- await getAvailablePurchasesInternal();
374
+ const purchases = await restorePurchases({
375
+ alsoPublishToEventListenerIOS: false,
376
+ onlyIncludeActiveItemsIOS: true,
377
+ });
378
+ setAvailablePurchases(purchases);
381
379
  } catch (error) {
382
380
  console.warn('Failed to restore purchases:', error);
383
381
  }
384
- }, [getAvailablePurchasesInternal]);
382
+ }, []);
385
383
 
386
384
  const validateReceipt = useCallback(
387
385
  async (
@@ -420,9 +418,32 @@ export function useIAP(options?: UseIAPOptions): UseIap {
420
418
  },
421
419
  );
422
420
 
423
- // IMPORTANT: Do NOT register the purchase error listener until after initConnection succeeds.
424
- // Some platforms may emit an initialization error event (E_INIT_CONNECTION) during startup.
425
- // Delaying registration prevents noisy, misleading errors before the connection is ready.
421
+ // Register purchase error listener EARLY. Ignore init-related errors until connected.
422
+ subscriptionsRef.current.purchaseError = purchaseErrorListener(
423
+ (error: PurchaseError) => {
424
+ if (
425
+ !connectedRef.current &&
426
+ error.code === ErrorCode.E_INIT_CONNECTION
427
+ ) {
428
+ return; // Ignore initialization error before connected
429
+ }
430
+ const friendly = getUserFriendlyErrorMessage(error);
431
+ console.log('[useIAP] Purchase error callback triggered:', error);
432
+ if (isUserCancelledError(error)) {
433
+ console.log('[useIAP] User cancelled purchase');
434
+ } else if (isRecoverableError(error)) {
435
+ console.log('[useIAP] Recoverable purchase error:', friendly);
436
+ } else {
437
+ console.warn('[useIAP] Purchase error:', friendly);
438
+ }
439
+ setCurrentPurchase(undefined);
440
+ setCurrentPurchaseError(error);
441
+
442
+ if (optionsRef.current?.onPurchaseError) {
443
+ optionsRef.current.onPurchaseError(error);
444
+ }
445
+ },
446
+ );
426
447
 
427
448
  if (Platform.OS === 'ios') {
428
449
  // iOS promoted products listener
@@ -453,22 +474,9 @@ export function useIAP(options?: UseIAPOptions): UseIap {
453
474
  subscriptionsRef.current.promotedProductsIOS?.remove();
454
475
  subscriptionsRef.current.purchaseUpdate = undefined;
455
476
  subscriptionsRef.current.promotedProductsIOS = undefined;
456
- // Do not register error listener when connection fails
477
+ // Keep purchaseError listener registered to capture subsequent retries
457
478
  return;
458
479
  }
459
-
460
- // Now that the connection is established, register the purchase error listener.
461
- subscriptionsRef.current.purchaseError = purchaseErrorListener(
462
- (error: PurchaseError) => {
463
- console.log('[useIAP] Purchase error callback triggered:', error);
464
- setCurrentPurchase(undefined);
465
- setCurrentPurchaseError(error);
466
-
467
- if (optionsRef.current?.onPurchaseError) {
468
- optionsRef.current.onPurchaseError(error);
469
- }
470
- },
471
- );
472
480
  }, [refreshSubscriptionStatus]);
473
481
 
474
482
  useEffect(() => {
@@ -491,7 +499,6 @@ export function useIAP(options?: UseIAPOptions): UseIap {
491
499
  promotedProductsIOS,
492
500
  promotedProductIdIOS,
493
501
  subscriptions,
494
- purchaseHistories,
495
502
  finishTransaction,
496
503
  availablePurchases,
497
504
  currentPurchase,
@@ -501,17 +508,15 @@ export function useIAP(options?: UseIAPOptions): UseIap {
501
508
  clearCurrentPurchase,
502
509
  clearCurrentPurchaseError,
503
510
  getAvailablePurchases: getAvailablePurchasesInternal,
504
- getPurchaseHistories: getPurchaseHistoriesInternal,
505
511
  fetchProducts: fetchProductsInternal,
506
512
  requestProducts: requestProductsInternal,
507
513
  requestPurchase: requestPurchaseWithReset,
508
514
  validateReceipt,
509
- restorePurchases,
515
+ restorePurchases: restorePurchasesInternal,
510
516
  getProducts: getProductsInternal,
511
517
  getSubscriptions: getSubscriptionsInternal,
512
518
  getPromotedProductIOS,
513
519
  requestPurchaseOnPromotedProductIOS,
514
- buyPromotedProductIOS: requestPurchaseOnPromotedProductIOS, // deprecated alias
515
520
  getActiveSubscriptions: getActiveSubscriptionsInternal,
516
521
  hasActiveSubscriptions: hasActiveSubscriptionsInternal,
517
522
  };
@@ -32,6 +32,8 @@ export function isNetworkError(error: any): boolean {
32
32
  ErrorCode.E_NETWORK_ERROR,
33
33
  ErrorCode.E_REMOTE_ERROR,
34
34
  ErrorCode.E_SERVICE_ERROR,
35
+ ErrorCode.E_SERVICE_DISCONNECTED,
36
+ ErrorCode.E_BILLING_UNAVAILABLE,
35
37
  ];
36
38
 
37
39
  const errorCode = typeof error === 'string' ? error : error?.code;
@@ -49,6 +51,10 @@ export function isRecoverableError(error: any): boolean {
49
51
  ErrorCode.E_REMOTE_ERROR,
50
52
  ErrorCode.E_SERVICE_ERROR,
51
53
  ErrorCode.E_INTERRUPTED,
54
+ ErrorCode.E_SERVICE_DISCONNECTED,
55
+ ErrorCode.E_BILLING_UNAVAILABLE,
56
+ ErrorCode.E_QUERY_PRODUCT,
57
+ ErrorCode.E_INIT_CONNECTION,
52
58
  ];
53
59
 
54
60
  const errorCode = typeof error === 'string' ? error : error?.code;
@@ -68,20 +74,38 @@ export function getUserFriendlyErrorMessage(error: any): string {
68
74
  return 'Purchase was cancelled by user';
69
75
  case ErrorCode.E_NETWORK_ERROR:
70
76
  return 'Network connection error. Please check your internet connection and try again.';
77
+ case ErrorCode.E_SERVICE_DISCONNECTED:
78
+ return 'Billing service disconnected. Please try again.';
79
+ case ErrorCode.E_BILLING_UNAVAILABLE:
80
+ return 'Billing is unavailable on this device or account.';
71
81
  case ErrorCode.E_ITEM_UNAVAILABLE:
72
82
  return 'This item is not available for purchase';
83
+ case ErrorCode.E_ITEM_NOT_OWNED:
84
+ return "You don't own this item";
73
85
  case ErrorCode.E_ALREADY_OWNED:
74
86
  return 'You already own this item';
87
+ case ErrorCode.E_SKU_NOT_FOUND:
88
+ return 'Requested product could not be found';
89
+ case ErrorCode.E_SKU_OFFER_MISMATCH:
90
+ return 'Selected offer does not match the SKU';
75
91
  case ErrorCode.E_DEFERRED_PAYMENT:
76
92
  return 'Payment is pending approval';
77
93
  case ErrorCode.E_NOT_PREPARED:
78
94
  return 'In-app purchase is not ready. Please try again later.';
79
95
  case ErrorCode.E_SERVICE_ERROR:
80
96
  return 'Store service error. Please try again later.';
97
+ case ErrorCode.E_FEATURE_NOT_SUPPORTED:
98
+ return 'This feature is not supported on this device.';
81
99
  case ErrorCode.E_TRANSACTION_VALIDATION_FAILED:
82
100
  return 'Transaction could not be verified';
83
101
  case ErrorCode.E_RECEIPT_FAILED:
84
102
  return 'Receipt processing failed';
103
+ case ErrorCode.E_EMPTY_SKU_LIST:
104
+ return 'No product IDs provided';
105
+ case ErrorCode.E_INIT_CONNECTION:
106
+ return 'Failed to initialize billing connection';
107
+ case ErrorCode.E_QUERY_PRODUCT:
108
+ return 'Failed to query products. Please try again later.';
85
109
  default:
86
110
  return error?.message || 'An unexpected error occurred';
87
111
  }