expo-iap 2.9.0 → 2.9.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.
Files changed (39) hide show
  1. package/CHANGELOG.md +16 -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 +0 -13
  9. package/build/index.d.ts.map +1 -1
  10. package/build/index.js +22 -22
  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 +35 -26
  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 +43 -30
  32. package/package.json +1 -1
  33. package/src/ExpoIap.types.ts +84 -37
  34. package/src/index.ts +25 -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 +41 -34
  39. package/src/utils/errorMapping.ts +24 -0
@@ -70,15 +70,7 @@ export type Purchase =
70
70
  | (PurchaseAndroid & AndroidPlatform)
71
71
  | (PurchaseIOS & IosPlatform);
72
72
 
73
- // Legacy type aliases - deprecated, use Purchase instead
74
- /**
75
- * @deprecated Use `Purchase` instead. This type alias will be removed in v2.9.0.
76
- */
77
- export type ProductPurchase = Purchase;
78
- /**
79
- * @deprecated Use `Purchase` instead. This type alias will be removed in v2.9.0.
80
- */
81
- export type SubscriptionPurchase = Purchase;
73
+ // Removed legacy type aliases `ProductPurchase` and `SubscriptionPurchase` in v2.9.0
82
74
 
83
75
  export type PurchaseResult = {
84
76
  responseCode?: number;
@@ -120,6 +112,16 @@ export enum ErrorCode {
120
112
  E_ALREADY_PREPARED = 'E_ALREADY_PREPARED',
121
113
  E_PENDING = 'E_PENDING',
122
114
  E_CONNECTION_CLOSED = 'E_CONNECTION_CLOSED',
115
+ // Additional detailed errors (Android-focused, kept cross-platform)
116
+ E_INIT_CONNECTION = 'E_INIT_CONNECTION',
117
+ E_SERVICE_DISCONNECTED = 'E_SERVICE_DISCONNECTED',
118
+ E_QUERY_PRODUCT = 'E_QUERY_PRODUCT',
119
+ E_SKU_NOT_FOUND = 'E_SKU_NOT_FOUND',
120
+ E_SKU_OFFER_MISMATCH = 'E_SKU_OFFER_MISMATCH',
121
+ E_ITEM_NOT_OWNED = 'E_ITEM_NOT_OWNED',
122
+ E_BILLING_UNAVAILABLE = 'E_BILLING_UNAVAILABLE',
123
+ E_FEATURE_NOT_SUPPORTED = 'E_FEATURE_NOT_SUPPORTED',
124
+ E_EMPTY_SKU_LIST = 'E_EMPTY_SKU_LIST',
123
125
  }
124
126
 
125
127
  /**
@@ -180,26 +182,59 @@ export const ErrorCodeMapping = {
180
182
  [ErrorCode.E_ALREADY_PREPARED]: 'E_ALREADY_PREPARED',
181
183
  [ErrorCode.E_PENDING]: 'E_PENDING',
182
184
  [ErrorCode.E_CONNECTION_CLOSED]: 'E_CONNECTION_CLOSED',
185
+ [ErrorCode.E_INIT_CONNECTION]: 'E_INIT_CONNECTION',
186
+ [ErrorCode.E_SERVICE_DISCONNECTED]: 'E_SERVICE_DISCONNECTED',
187
+ [ErrorCode.E_QUERY_PRODUCT]: 'E_QUERY_PRODUCT',
188
+ [ErrorCode.E_SKU_NOT_FOUND]: 'E_SKU_NOT_FOUND',
189
+ [ErrorCode.E_SKU_OFFER_MISMATCH]: 'E_SKU_OFFER_MISMATCH',
190
+ [ErrorCode.E_ITEM_NOT_OWNED]: 'E_ITEM_NOT_OWNED',
191
+ [ErrorCode.E_BILLING_UNAVAILABLE]: 'E_BILLING_UNAVAILABLE',
192
+ [ErrorCode.E_FEATURE_NOT_SUPPORTED]: 'E_FEATURE_NOT_SUPPORTED',
193
+ [ErrorCode.E_EMPTY_SKU_LIST]: 'E_EMPTY_SKU_LIST',
183
194
  },
184
195
  } as const;
185
196
 
197
+ export type PurchaseErrorProps = {
198
+ message: string;
199
+ responseCode?: number;
200
+ debugMessage?: string;
201
+ code?: ErrorCode;
202
+ productId?: string;
203
+ platform?: 'ios' | 'android';
204
+ };
205
+
186
206
  export class PurchaseError implements Error {
187
- constructor(
188
- public name: string,
189
- public message: string,
190
- public responseCode?: number,
191
- public debugMessage?: string,
192
- public code?: ErrorCode,
193
- public productId?: string,
194
- public platform?: 'ios' | 'android',
195
- ) {
207
+ public name: string;
208
+ public message: string;
209
+ public responseCode?: number;
210
+ public debugMessage?: string;
211
+ public code?: ErrorCode;
212
+ public productId?: string;
213
+ public platform?: 'ios' | 'android';
214
+
215
+ // Backwards-compatible constructor: accepts either props object or legacy positional args
216
+ constructor(messageOrProps: string | PurchaseErrorProps, ...rest: any[]) {
196
217
  this.name = '[expo-iap]: PurchaseError';
197
- this.message = message;
198
- this.responseCode = responseCode;
199
- this.debugMessage = debugMessage;
200
- this.code = code;
201
- this.productId = productId;
202
- this.platform = platform;
218
+
219
+ if (typeof messageOrProps === 'string') {
220
+ // Legacy signature: (name, message, responseCode?, debugMessage?, code?, productId?, platform?)
221
+ // The first legacy argument was a name which we always override, so treat it as message here
222
+ const message = messageOrProps;
223
+ this.message = message;
224
+ this.responseCode = rest[0];
225
+ this.debugMessage = rest[1];
226
+ this.code = rest[2];
227
+ this.productId = rest[3];
228
+ this.platform = rest[4];
229
+ } else {
230
+ const props = messageOrProps;
231
+ this.message = props.message;
232
+ this.responseCode = props.responseCode;
233
+ this.debugMessage = props.debugMessage;
234
+ this.code = props.code;
235
+ this.productId = props.productId;
236
+ this.platform = props.platform;
237
+ }
203
238
  }
204
239
 
205
240
  /**
@@ -216,15 +251,14 @@ export class PurchaseError implements Error {
216
251
  ? ErrorCodeUtils.fromPlatformCode(errorData.code, platform)
217
252
  : ErrorCode.E_UNKNOWN;
218
253
 
219
- return new PurchaseError(
220
- '[expo-iap]: PurchaseError',
221
- errorData.message || 'Unknown error occurred',
222
- errorData.responseCode,
223
- errorData.debugMessage,
224
- errorCode,
225
- errorData.productId,
254
+ return new PurchaseError({
255
+ message: errorData.message || 'Unknown error occurred',
256
+ responseCode: errorData.responseCode,
257
+ debugMessage: errorData.debugMessage,
258
+ code: errorCode,
259
+ productId: errorData.productId,
226
260
  platform,
227
- );
261
+ });
228
262
  }
229
263
 
230
264
  /**
@@ -260,8 +294,16 @@ export const ErrorCodeUtils = {
260
294
  platformCode: string | number,
261
295
  platform: 'ios' | 'android',
262
296
  ): ErrorCode => {
263
- const mapping = ErrorCodeMapping[platform];
297
+ // Prefer dynamic native mapping for iOS to avoid drift
298
+ if (platform === 'ios') {
299
+ for (const [key, value] of Object.entries(NATIVE_ERROR_CODES || {})) {
300
+ if (value === platformCode) {
301
+ return key as ErrorCode;
302
+ }
303
+ }
304
+ }
264
305
 
306
+ const mapping = ErrorCodeMapping[platform];
265
307
  for (const [errorCode, mappedCode] of Object.entries(mapping)) {
266
308
  if (mappedCode === platformCode) {
267
309
  return errorCode as ErrorCode;
@@ -281,10 +323,15 @@ export const ErrorCodeUtils = {
281
323
  errorCode: ErrorCode,
282
324
  platform: 'ios' | 'android',
283
325
  ): string | number => {
284
- return (
285
- ErrorCodeMapping[platform][errorCode] ??
286
- (platform === 'ios' ? 0 : 'E_UNKNOWN')
287
- );
326
+ if (platform === 'ios') {
327
+ const native = NATIVE_ERROR_CODES?.[errorCode];
328
+ if (native !== undefined) return native;
329
+ }
330
+ const mapping = ErrorCodeMapping[platform] as Record<
331
+ ErrorCode,
332
+ string | number
333
+ >;
334
+ return mapping[errorCode] ?? (platform === 'ios' ? 0 : 'E_UNKNOWN');
288
335
  },
289
336
 
290
337
  /**
package/src/index.ts CHANGED
@@ -143,7 +143,12 @@ export const getProducts = async (skus: string[]): Promise<Product[]> => {
143
143
  "`getProducts` is deprecated. Use `fetchProducts({ skus, type: 'inapp' })` instead. This function will be removed in version 3.0.0.",
144
144
  );
145
145
  if (!skus?.length) {
146
- return Promise.reject(new Error('"skus" is required'));
146
+ return Promise.reject(
147
+ new PurchaseError({
148
+ message: 'No SKUs provided',
149
+ code: ErrorCode.E_EMPTY_SKU_LIST,
150
+ }),
151
+ );
147
152
  }
148
153
 
149
154
  return Platform.select({
@@ -177,7 +182,12 @@ export const getSubscriptions = async (
177
182
  "`getSubscriptions` is deprecated. Use `fetchProducts({ skus, type: 'subs' })` instead. This function will be removed in version 3.0.0.",
178
183
  );
179
184
  if (!skus?.length) {
180
- return Promise.reject(new Error('"skus" is required'));
185
+ return Promise.reject(
186
+ new PurchaseError({
187
+ message: 'No SKUs provided',
188
+ code: ErrorCode.E_EMPTY_SKU_LIST,
189
+ }),
190
+ );
181
191
  }
182
192
 
183
193
  return Platform.select({
@@ -245,7 +255,10 @@ export const fetchProducts = async ({
245
255
  type?: 'inapp' | 'subs';
246
256
  }): Promise<Product[] | SubscriptionProduct[]> => {
247
257
  if (!skus?.length) {
248
- throw new Error('No SKUs provided');
258
+ throw new PurchaseError({
259
+ message: 'No SKUs provided',
260
+ code: ErrorCode.E_EMPTY_SKU_LIST,
261
+ });
249
262
  }
250
263
 
251
264
  if (Platform.OS === 'ios') {
@@ -344,7 +357,8 @@ export const getPurchaseHistory = ({
344
357
  console.warn(
345
358
  '`getPurchaseHistory` is deprecated. Use `getPurchaseHistories` instead. This function will be removed in version 3.0.0.',
346
359
  );
347
- return getPurchaseHistories({
360
+ // Use available purchases as a best-effort replacement
361
+ return getAvailablePurchases({
348
362
  alsoPublishToEventListenerIOS:
349
363
  alsoPublishToEventListenerIOS ?? alsoPublishToEventListener,
350
364
  onlyIncludeActiveItemsIOS:
@@ -352,42 +366,7 @@ export const getPurchaseHistory = ({
352
366
  });
353
367
  };
354
368
 
355
- /**
356
- * @deprecated Use getAvailablePurchases instead. This function is just calling getAvailablePurchases internally on iOS
357
- * and returns an empty array on Android (Google Play Billing v8 removed purchase history API).
358
- * Will be removed in v2.9.0
359
- */
360
- export const getPurchaseHistories = ({
361
- alsoPublishToEventListener = false,
362
- onlyIncludeActiveItems = false,
363
- alsoPublishToEventListenerIOS,
364
- onlyIncludeActiveItemsIOS,
365
- }: {
366
- /** @deprecated Use alsoPublishToEventListenerIOS instead */
367
- alsoPublishToEventListener?: boolean;
368
- /** @deprecated Use onlyIncludeActiveItemsIOS instead */
369
- onlyIncludeActiveItems?: boolean;
370
- alsoPublishToEventListenerIOS?: boolean;
371
- onlyIncludeActiveItemsIOS?: boolean;
372
- } = {}): Promise<Purchase[]> =>
373
- (
374
- Platform.select({
375
- ios: async () => {
376
- return ExpoIapModule.getAvailableItems(
377
- alsoPublishToEventListenerIOS ?? alsoPublishToEventListener,
378
- onlyIncludeActiveItemsIOS ?? onlyIncludeActiveItems,
379
- );
380
- },
381
- android: async () => {
382
- // getPurchaseHistoryByType was removed in Google Play Billing Library v8
383
- // Android doesn't provide purchase history anymore, only active purchases
384
- console.warn(
385
- 'getPurchaseHistories is not supported on Android with Google Play Billing Library v8. Use getAvailablePurchases instead to get active purchases.',
386
- );
387
- return [];
388
- },
389
- }) || (() => Promise.resolve([]))
390
- )();
369
+ // NOTE: `getPurchaseHistories` removed in v2.9.0. Use `getAvailablePurchases` instead.
391
370
 
392
371
  export const getAvailablePurchases = ({
393
372
  alsoPublishToEventListener = false,
@@ -652,15 +631,12 @@ export const finishTransaction = ({
652
631
 
653
632
  if (!token) {
654
633
  return Promise.reject(
655
- new PurchaseError(
656
- '[expo-iap]: PurchaseError',
657
- 'Purchase token is required to finish transaction',
658
- undefined,
659
- undefined,
660
- 'E_DEVELOPER_ERROR' as ErrorCode,
661
- androidPurchase.productId,
662
- 'android',
663
- ),
634
+ new PurchaseError({
635
+ message: 'Purchase token is required to finish transaction',
636
+ code: 'E_DEVELOPER_ERROR' as ErrorCode,
637
+ productId: androidPurchase.productId,
638
+ platform: 'android',
639
+ }),
664
640
  );
665
641
  }
666
642
 
@@ -9,7 +9,7 @@ import type {
9
9
  Product,
10
10
  Purchase,
11
11
  PurchaseError,
12
- ProductStatusIOS,
12
+ SubscriptionStatusIOS,
13
13
  AppTransactionIOS,
14
14
  } from '../ExpoIap.types';
15
15
  import {Linking} from 'react-native';
@@ -122,7 +122,7 @@ export const isEligibleForIntroOfferIOS = (
122
122
  */
123
123
  export const subscriptionStatusIOS = (
124
124
  sku: string,
125
- ): Promise<ProductStatusIOS[]> => {
125
+ ): Promise<SubscriptionStatusIOS[]> => {
126
126
  return ExpoIapModule.subscriptionStatusIOS(sku);
127
127
  };
128
128
 
@@ -310,12 +310,7 @@ export const requestPurchaseOnPromotedProductIOS = (): Promise<void> => {
310
310
  return ExpoIapModule.requestPurchaseOnPromotedProductIOS();
311
311
  };
312
312
 
313
- /**
314
- * @deprecated Use requestPurchaseOnPromotedProductIOS instead. Will be removed in v2.9.0
315
- */
316
- export const buyPromotedProductIOS = (): Promise<void> => {
317
- return requestPurchaseOnPromotedProductIOS();
318
- };
313
+ // NOTE: buyPromotedProductIOS removed in v2.9.0. Use requestPurchaseOnPromotedProductIOS.
319
314
 
320
315
  /**
321
316
  * Get pending transactions that haven't been finished yet (iOS only).
@@ -374,7 +369,7 @@ export const isEligibleForIntroOffer = (groupId: string): Promise<boolean> => {
374
369
  */
375
370
  export const subscriptionStatus = (
376
371
  sku: string,
377
- ): Promise<ProductStatusIOS[]> => {
372
+ ): Promise<SubscriptionStatusIOS[]> => {
378
373
  console.warn(
379
374
  '`subscriptionStatus` is deprecated. Use `subscriptionStatusIOS` instead. This function will be removed in version 3.0.0.',
380
375
  );
@@ -33,18 +33,6 @@ export type ProductAndroid = ProductCommon & {
33
33
  oneTimePurchaseOfferDetailsAndroid?: ProductAndroidOneTimePurchaseOfferDetail;
34
34
  platform: 'android';
35
35
  subscriptionOfferDetailsAndroid?: ProductSubscriptionAndroidOfferDetail[];
36
- /**
37
- * @deprecated Use `nameAndroid` instead. This field will be removed in v2.9.0.
38
- */
39
- name?: string;
40
- /**
41
- * @deprecated Use `oneTimePurchaseOfferDetailsAndroid` instead. This field will be removed in v2.9.0.
42
- */
43
- oneTimePurchaseOfferDetails?: ProductAndroidOneTimePurchaseOfferDetail;
44
- /**
45
- * @deprecated Use `subscriptionOfferDetailsAndroid` instead. This field will be removed in v2.9.0.
46
- */
47
- subscriptionOfferDetails?: ProductSubscriptionAndroidOfferDetail[];
48
36
  };
49
37
 
50
38
  type ProductSubscriptionAndroidOfferDetails = {
@@ -57,10 +45,6 @@ type ProductSubscriptionAndroidOfferDetails = {
57
45
 
58
46
  export type ProductSubscriptionAndroid = ProductAndroid & {
59
47
  subscriptionOfferDetailsAndroid: ProductSubscriptionAndroidOfferDetails[];
60
- /**
61
- * @deprecated Use `subscriptionOfferDetailsAndroid` instead. This field will be removed in v2.9.0.
62
- */
63
- subscriptionOfferDetails?: ProductSubscriptionAndroidOfferDetails[];
64
48
  };
65
49
 
66
50
  // Legacy naming for backward compatibility
@@ -137,10 +121,7 @@ export enum PurchaseAndroidState {
137
121
  }
138
122
 
139
123
  // Legacy naming for backward compatibility
140
- /**
141
- * @deprecated Use `PurchaseAndroidState` instead. This enum will be removed in v2.9.0.
142
- */
143
- export const PurchaseStateAndroid = PurchaseAndroidState;
124
+ // Removed legacy alias `PurchaseStateAndroid` in v2.9.0
144
125
 
145
126
  // Legacy naming for backward compatibility
146
127
  export type ProductPurchaseAndroid = PurchaseCommon & {
@@ -163,19 +144,4 @@ export type ProductPurchaseAndroid = PurchaseCommon & {
163
144
  // Preferred naming
164
145
  export type PurchaseAndroid = ProductPurchaseAndroid;
165
146
 
166
- // Legacy type aliases for backward compatibility
167
- /**
168
- * @deprecated Use `ProductAndroidOneTimePurchaseOfferDetail` instead. This type will be removed in v2.9.0.
169
- */
170
- export type OneTimePurchaseOfferDetails =
171
- ProductAndroidOneTimePurchaseOfferDetail;
172
-
173
- /**
174
- * @deprecated Use `ProductSubscriptionAndroidOfferDetail` instead. This type will be removed in v2.9.0.
175
- */
176
- export type SubscriptionOfferDetail = ProductSubscriptionAndroidOfferDetail;
177
-
178
- /**
179
- * @deprecated Use `ProductSubscriptionAndroidOfferDetails` instead. This type will be removed in v2.9.0.
180
- */
181
- export type SubscriptionOfferAndroid = ProductSubscriptionAndroidOfferDetails;
147
+ // Removed legacy Android alias types in v2.9.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,
@@ -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 ({
@@ -420,9 +420,32 @@ export function useIAP(options?: UseIAPOptions): UseIap {
420
420
  },
421
421
  );
422
422
 
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.
423
+ // Register purchase error listener EARLY. Ignore init-related errors until connected.
424
+ subscriptionsRef.current.purchaseError = purchaseErrorListener(
425
+ (error: PurchaseError) => {
426
+ if (
427
+ !connectedRef.current &&
428
+ error.code === ErrorCode.E_INIT_CONNECTION
429
+ ) {
430
+ return; // Ignore initialization error before connected
431
+ }
432
+ const friendly = getUserFriendlyErrorMessage(error);
433
+ console.log('[useIAP] Purchase error callback triggered:', error);
434
+ if (isUserCancelledError(error)) {
435
+ console.log('[useIAP] User cancelled purchase');
436
+ } else if (isRecoverableError(error)) {
437
+ console.log('[useIAP] Recoverable purchase error:', friendly);
438
+ } else {
439
+ console.warn('[useIAP] Purchase error:', friendly);
440
+ }
441
+ setCurrentPurchase(undefined);
442
+ setCurrentPurchaseError(error);
443
+
444
+ if (optionsRef.current?.onPurchaseError) {
445
+ optionsRef.current.onPurchaseError(error);
446
+ }
447
+ },
448
+ );
426
449
 
427
450
  if (Platform.OS === 'ios') {
428
451
  // iOS promoted products listener
@@ -453,22 +476,9 @@ export function useIAP(options?: UseIAPOptions): UseIap {
453
476
  subscriptionsRef.current.promotedProductsIOS?.remove();
454
477
  subscriptionsRef.current.purchaseUpdate = undefined;
455
478
  subscriptionsRef.current.promotedProductsIOS = undefined;
456
- // Do not register error listener when connection fails
479
+ // Keep purchaseError listener registered to capture subsequent retries
457
480
  return;
458
481
  }
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
482
  }, [refreshSubscriptionStatus]);
473
483
 
474
484
  useEffect(() => {
@@ -491,7 +501,6 @@ export function useIAP(options?: UseIAPOptions): UseIap {
491
501
  promotedProductsIOS,
492
502
  promotedProductIdIOS,
493
503
  subscriptions,
494
- purchaseHistories,
495
504
  finishTransaction,
496
505
  availablePurchases,
497
506
  currentPurchase,
@@ -501,7 +510,6 @@ export function useIAP(options?: UseIAPOptions): UseIap {
501
510
  clearCurrentPurchase,
502
511
  clearCurrentPurchaseError,
503
512
  getAvailablePurchases: getAvailablePurchasesInternal,
504
- getPurchaseHistories: getPurchaseHistoriesInternal,
505
513
  fetchProducts: fetchProductsInternal,
506
514
  requestProducts: requestProductsInternal,
507
515
  requestPurchase: requestPurchaseWithReset,
@@ -511,7 +519,6 @@ export function useIAP(options?: UseIAPOptions): UseIap {
511
519
  getSubscriptions: getSubscriptionsInternal,
512
520
  getPromotedProductIOS,
513
521
  requestPurchaseOnPromotedProductIOS,
514
- buyPromotedProductIOS: requestPurchaseOnPromotedProductIOS, // deprecated alias
515
522
  getActiveSubscriptions: getActiveSubscriptionsInternal,
516
523
  hasActiveSubscriptions: hasActiveSubscriptionsInternal,
517
524
  };
@@ -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
  }