expo-iap 3.0.4 → 3.0.6

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 (45) hide show
  1. package/.eslintignore +1 -1
  2. package/.eslintrc.js +1 -0
  3. package/.prettierignore +1 -0
  4. package/CHANGELOG.md +10 -0
  5. package/CLAUDE.md +9 -1
  6. package/CONTRIBUTING.md +10 -0
  7. package/android/build.gradle +1 -1
  8. package/android/src/main/java/expo/modules/iap/ExpoIapModule.kt +12 -12
  9. package/android/src/main/java/expo/modules/iap/PromiseUtils.kt +2 -2
  10. package/build/index.d.ts +28 -14
  11. package/build/index.d.ts.map +1 -1
  12. package/build/index.js +36 -15
  13. package/build/index.js.map +1 -1
  14. package/build/modules/android.d.ts +3 -4
  15. package/build/modules/android.d.ts.map +1 -1
  16. package/build/modules/android.js +2 -4
  17. package/build/modules/android.js.map +1 -1
  18. package/build/modules/ios.d.ts +2 -3
  19. package/build/modules/ios.d.ts.map +1 -1
  20. package/build/modules/ios.js +1 -3
  21. package/build/modules/ios.js.map +1 -1
  22. package/build/purchase-error.d.ts +8 -10
  23. package/build/purchase-error.d.ts.map +1 -1
  24. package/build/purchase-error.js +4 -2
  25. package/build/purchase-error.js.map +1 -1
  26. package/build/types.d.ts +159 -204
  27. package/build/types.d.ts.map +1 -1
  28. package/build/types.js +1 -59
  29. package/build/types.js.map +1 -1
  30. package/build/useIAP.d.ts +5 -12
  31. package/build/useIAP.d.ts.map +1 -1
  32. package/build/useIAP.js +10 -75
  33. package/build/useIAP.js.map +1 -1
  34. package/ios/ExpoIap.podspec +1 -1
  35. package/ios/ExpoIapModule.swift +103 -89
  36. package/package.json +2 -1
  37. package/plugin/build/withIAP.js +4 -5
  38. package/plugin/src/withIAP.ts +4 -5
  39. package/scripts/update-types.mjs +61 -0
  40. package/src/index.ts +77 -29
  41. package/src/modules/android.ts +5 -7
  42. package/src/modules/ios.ts +3 -5
  43. package/src/purchase-error.ts +13 -16
  44. package/src/types.ts +183 -216
  45. package/src/useIAP.ts +19 -94
package/src/useIAP.ts CHANGED
@@ -18,6 +18,8 @@ import {
18
18
  getActiveSubscriptions,
19
19
  hasActiveSubscriptions,
20
20
  type ActiveSubscription,
21
+ type ProductTypeInput,
22
+ type PurchaseRequestInput,
21
23
  restorePurchases,
22
24
  } from './index';
23
25
  import {
@@ -30,10 +32,9 @@ import {
30
32
  Product,
31
33
  Purchase,
32
34
  ProductSubscription,
33
- RequestPurchaseProps,
34
- RequestSubscriptionPropsByPlatforms,
35
35
  ErrorCode,
36
36
  VoidResult,
37
+ ReceiptValidationResult,
37
38
  } from './types';
38
39
  import {PurchaseError} from './purchase-error';
39
40
  import {
@@ -42,10 +43,6 @@ import {
42
43
  isRecoverableError,
43
44
  } from './utils/errorMapping';
44
45
 
45
- // Deduplicate purchase success events across re-mounts (dev StrictMode, nav returns)
46
- // Keep minimal in-memory state; safe for subscriptions since renewals use new ids
47
- const handledPurchaseIds = new Set<string>();
48
-
49
46
  type UseIap = {
50
47
  connected: boolean;
51
48
  products: Product[];
@@ -53,12 +50,8 @@ type UseIap = {
53
50
  promotedProductIdIOS?: string;
54
51
  subscriptions: ProductSubscription[];
55
52
  availablePurchases: Purchase[];
56
- currentPurchase?: Purchase;
57
- currentPurchaseError?: PurchaseError;
58
53
  promotedProductIOS?: Product;
59
54
  activeSubscriptions: ActiveSubscription[];
60
- clearCurrentPurchase: () => void;
61
- clearCurrentPurchaseError: () => void;
62
55
  finishTransaction: ({
63
56
  purchase,
64
57
  isConsumable,
@@ -69,13 +62,12 @@ type UseIap = {
69
62
  getAvailablePurchases: () => Promise<void>;
70
63
  fetchProducts: (params: {
71
64
  skus: string[];
72
- type?: 'inapp' | 'subs';
65
+ type?: ProductTypeInput;
73
66
  }) => Promise<void>;
74
67
 
75
- requestPurchase: (params: {
76
- request: RequestPurchaseProps | RequestSubscriptionPropsByPlatforms;
77
- type?: 'inapp' | 'subs';
78
- }) => Promise<any>;
68
+ requestPurchase: (
69
+ params: PurchaseRequestInput,
70
+ ) => ReturnType<typeof requestPurchaseInternal>;
79
71
  validateReceipt: (
80
72
  sku: string,
81
73
  androidOptions?: {
@@ -84,7 +76,7 @@ type UseIap = {
84
76
  accessToken: string;
85
77
  isSub?: boolean;
86
78
  },
87
- ) => Promise<any>;
79
+ ) => Promise<ReceiptValidationResult>;
88
80
  restorePurchases: () => Promise<void>;
89
81
  getPromotedProductIOS: () => Promise<Product | null>;
90
82
  requestPurchaseOnPromotedProductIOS: () => Promise<void>;
@@ -111,10 +103,7 @@ export function useIAP(options?: UseIAPOptions): UseIap {
111
103
  const [subscriptions, setSubscriptions] = useState<ProductSubscription[]>([]);
112
104
 
113
105
  const [availablePurchases, setAvailablePurchases] = useState<Purchase[]>([]);
114
- const [currentPurchase, setCurrentPurchase] = useState<Purchase>();
115
106
  const [promotedProductIOS, setPromotedProductIOS] = useState<Product>();
116
- const [currentPurchaseError, setCurrentPurchaseError] =
117
- useState<PurchaseError>();
118
107
  const [promotedProductIdIOS] = useState<string>();
119
108
  const [activeSubscriptions, setActiveSubscriptions] = useState<
120
109
  ActiveSubscription[]
@@ -165,14 +154,6 @@ export function useIAP(options?: UseIAPOptions): UseIap {
165
154
  subscriptionsRefState.current = subscriptions;
166
155
  }, [subscriptions]);
167
156
 
168
- const clearCurrentPurchase = useCallback(() => {
169
- setCurrentPurchase(undefined);
170
- }, []);
171
-
172
- const clearCurrentPurchaseError = useCallback(() => {
173
- setCurrentPurchaseError(undefined);
174
- }, []);
175
-
176
157
  const getSubscriptionsInternal = useCallback(
177
158
  async (skus: string[]): Promise<void> => {
178
159
  try {
@@ -194,7 +175,7 @@ export function useIAP(options?: UseIAPOptions): UseIap {
194
175
  const fetchProductsInternal = useCallback(
195
176
  async (params: {
196
177
  skus: string[];
197
- type?: 'inapp' | 'subs';
178
+ type?: ProductTypeInput;
198
179
  }): Promise<void> => {
199
180
  try {
200
181
  const result = await fetchProducts(params);
@@ -261,49 +242,26 @@ export function useIAP(options?: UseIAPOptions): UseIap {
261
242
  );
262
243
 
263
244
  const finishTransaction = useCallback(
264
- async ({
245
+ ({
265
246
  purchase,
266
247
  isConsumable,
267
248
  }: {
268
249
  purchase: Purchase;
269
250
  isConsumable?: boolean;
270
251
  }): Promise<VoidResult | boolean> => {
271
- try {
272
- return await finishTransactionInternal({
273
- purchase,
274
- isConsumable,
275
- });
276
- } catch (err) {
277
- throw err;
278
- } finally {
279
- if (purchase.id === currentPurchase?.id) {
280
- clearCurrentPurchase();
281
- }
282
- if (purchase.id === currentPurchaseError?.productId) {
283
- clearCurrentPurchaseError();
284
- }
285
- }
252
+ return finishTransactionInternal({
253
+ purchase,
254
+ isConsumable,
255
+ });
286
256
  },
287
- [
288
- currentPurchase?.id,
289
- currentPurchaseError?.productId,
290
- clearCurrentPurchase,
291
- clearCurrentPurchaseError,
292
- ],
257
+ [],
293
258
  );
294
259
 
295
260
  const requestPurchaseWithReset = useCallback(
296
- async (requestObj: {request: any; type?: 'inapp' | 'subs'}) => {
297
- clearCurrentPurchase();
298
- clearCurrentPurchaseError();
299
-
300
- try {
301
- return await requestPurchaseInternal(requestObj);
302
- } catch (error) {
303
- throw error;
304
- }
261
+ (requestObj: PurchaseRequestInput) => {
262
+ return requestPurchaseInternal(requestObj);
305
263
  },
306
- [clearCurrentPurchase, clearCurrentPurchaseError],
264
+ [],
307
265
  );
308
266
 
309
267
  const refreshSubscriptionStatus = useCallback(
@@ -353,24 +311,9 @@ export function useIAP(options?: UseIAPOptions): UseIap {
353
311
  const initIapWithSubscriptions = useCallback(async (): Promise<void> => {
354
312
  // CRITICAL: Register listeners BEFORE initConnection to avoid race condition
355
313
  // Events might fire immediately after initConnection, so listeners must be ready
356
- console.log('[useIAP] Setting up event listeners BEFORE initConnection...');
357
-
358
314
  // Register purchase update listener BEFORE initConnection to avoid race conditions.
359
315
  subscriptionsRef.current.purchaseUpdate = purchaseUpdatedListener(
360
316
  async (purchase: Purchase) => {
361
- console.log('[useIAP] Purchase success callback triggered:', purchase);
362
-
363
- // Guard against duplicate emissions for the same transaction
364
- const dedupeKey = purchase.id;
365
- if (dedupeKey && handledPurchaseIds.has(dedupeKey)) {
366
- console.log('[useIAP] Duplicate purchase event ignored:', dedupeKey);
367
- return;
368
- }
369
- if (dedupeKey) handledPurchaseIds.add(dedupeKey);
370
-
371
- setCurrentPurchaseError(undefined);
372
- setCurrentPurchase(purchase);
373
-
374
317
  if ('expirationDateIOS' in purchase) {
375
318
  await refreshSubscriptionStatus(purchase.id);
376
319
  }
@@ -388,16 +331,9 @@ export function useIAP(options?: UseIAPOptions): UseIap {
388
331
  return; // Ignore initialization error before connected
389
332
  }
390
333
  const friendly = getUserFriendlyErrorMessage(error);
391
- console.log('[useIAP] Purchase error callback triggered:', error);
392
- if (isUserCancelledError(error)) {
393
- console.log('[useIAP] User cancelled purchase');
394
- } else if (isRecoverableError(error)) {
395
- console.log('[useIAP] Recoverable purchase error:', friendly);
396
- } else {
334
+ if (!isUserCancelledError(error) && !isRecoverableError(error)) {
397
335
  console.warn('[useIAP] Purchase error:', friendly);
398
336
  }
399
- setCurrentPurchase(undefined);
400
- setCurrentPurchaseError(error);
401
337
 
402
338
  if (optionsRef.current?.onPurchaseError) {
403
339
  optionsRef.current.onPurchaseError(error);
@@ -409,7 +345,6 @@ export function useIAP(options?: UseIAPOptions): UseIap {
409
345
  // iOS promoted products listener
410
346
  subscriptionsRef.current.promotedProductsIOS = promotedProductListenerIOS(
411
347
  (product: Product) => {
412
- console.log('[useIAP] Promoted product callback triggered:', product);
413
348
  setPromotedProductIOS(product);
414
349
 
415
350
  if (optionsRef.current?.onPromotedProductIOS) {
@@ -419,14 +354,9 @@ export function useIAP(options?: UseIAPOptions): UseIap {
419
354
  );
420
355
  }
421
356
 
422
- console.log(
423
- '[useIAP] Event listeners registered, now calling initConnection...',
424
- );
425
-
426
357
  // NOW call initConnection after listeners are ready
427
358
  const result = await initConnection();
428
359
  setConnected(result);
429
- console.log('[useIAP] initConnection result:', result);
430
360
  if (!result) {
431
361
  // If connection failed, clean up listeners
432
362
  console.warn('[useIAP] Connection failed, cleaning up listeners...');
@@ -450,7 +380,6 @@ export function useIAP(options?: UseIAPOptions): UseIap {
450
380
  currentSubscriptions.promotedProductIOS?.remove();
451
381
  endConnection();
452
382
  setConnected(false);
453
- handledPurchaseIds.clear();
454
383
  };
455
384
  }, [initIapWithSubscriptions]);
456
385
 
@@ -462,12 +391,8 @@ export function useIAP(options?: UseIAPOptions): UseIap {
462
391
  subscriptions,
463
392
  finishTransaction,
464
393
  availablePurchases,
465
- currentPurchase,
466
- currentPurchaseError,
467
394
  promotedProductIOS,
468
395
  activeSubscriptions,
469
- clearCurrentPurchase,
470
- clearCurrentPurchaseError,
471
396
  getAvailablePurchases: getAvailablePurchasesInternal,
472
397
  fetchProducts: fetchProductsInternal,
473
398
  requestPurchase: requestPurchaseWithReset,