expo-iap 3.4.11 → 3.4.13

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.
@@ -116,7 +116,7 @@
116
116
  <div class='footer quiet pad2 space-top1 center small'>
117
117
  Code coverage generated by
118
118
  <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
119
- at 2026-03-24T17:30:02.000Z
119
+ at 2026-03-26T18:03:32.860Z
120
120
  </div>
121
121
  <script src="../../prettify.js"></script>
122
122
  <script>
@@ -1513,7 +1513,7 @@ export const showExternalPurchaseCustomLinkNoticeIOS = async (
1513
1513
  <div class='footer quiet pad2 space-top1 center small'>
1514
1514
  Code coverage generated by
1515
1515
  <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
1516
- at 2026-03-24T17:30:02.000Z
1516
+ at 2026-03-26T18:03:32.860Z
1517
1517
  </div>
1518
1518
  <script src="../../prettify.js"></script>
1519
1519
  <script>
@@ -130,7 +130,7 @@ export const ExpoOnsideMarketplaceAvailabilityModule: NativeModuleType =
130
130
  <div class='footer quiet pad2 space-top1 center small'>
131
131
  Code coverage generated by
132
132
  <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
133
- at 2026-03-24T17:30:02.000Z
133
+ at 2026-03-26T18:03:32.860Z
134
134
  </div>
135
135
  <script src="../../prettify.js"></script>
136
136
  <script>
@@ -116,7 +116,7 @@
116
116
  <div class='footer quiet pad2 space-top1 center small'>
117
117
  Code coverage generated by
118
118
  <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
119
- at 2026-03-24T17:30:02.000Z
119
+ at 2026-03-26T18:03:32.860Z
120
120
  </div>
121
121
  <script src="../../prettify.js"></script>
122
122
  <script>
@@ -241,7 +241,7 @@ export {checkInstallationFromOnside, installedFromOnside, useOnside};
241
241
  <div class='footer quiet pad2 space-top1 center small'>
242
242
  Code coverage generated by
243
243
  <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
244
- at 2026-03-24T17:30:02.000Z
244
+ at 2026-03-26T18:03:32.860Z
245
245
  </div>
246
246
  <script src="../../prettify.js"></script>
247
247
  <script>
@@ -268,7 +268,7 @@ export const ExpoIapConsole = createConsole();
268
268
  <div class='footer quiet pad2 space-top1 center small'>
269
269
  Code coverage generated by
270
270
  <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
271
- at 2026-03-24T17:30:02.000Z
271
+ at 2026-03-26T18:03:32.860Z
272
272
  </div>
273
273
  <script src="../../prettify.js"></script>
274
274
  <script>
@@ -1126,7 +1126,7 @@ export function getUserFriendlyErrorMessage(error: ErrorLike): string {
1126
1126
  <div class='footer quiet pad2 space-top1 center small'>
1127
1127
  Code coverage generated by
1128
1128
  <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
1129
- at 2026-03-24T17:30:02.000Z
1129
+ at 2026-03-26T18:03:32.860Z
1130
1130
  </div>
1131
1131
  <script src="../../prettify.js"></script>
1132
1132
  <script>
@@ -116,7 +116,7 @@
116
116
  <div class='footer quiet pad2 space-top1 center small'>
117
117
  Code coverage generated by
118
118
  <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
119
- at 2026-03-24T17:30:02.000Z
119
+ at 2026-03-26T18:03:32.860Z
120
120
  </div>
121
121
  <script src="../../prettify.js"></script>
122
122
  <script>
@@ -363,7 +363,7 @@ BRDA:377,24,1,3
363
363
  BRDA:378,25,0,8
364
364
  BRDA:378,25,1,3
365
365
  BRDA:379,26,0,8
366
- BRDA:379,26,1,7
366
+ BRDA:379,26,1,4
367
367
  BRDA:383,27,0,8
368
368
  BRDA:383,27,1,1
369
369
  BRDA:431,28,0,7
@@ -26,14 +26,13 @@ Pod::Spec.new do |s|
26
26
  s.dependency 'ExpoModulesCore'
27
27
  s.dependency 'openiap', "#{versions['apple']}"
28
28
 
29
- # OnsideKit is optional; only included when modules.onside is enabled via the Expo plugin.
30
- # The plugin adds `pod 'ExpoIap/Onside'` to the Podfile when onside is enabled.
31
- s.subspec 'Onside' do |ss|
32
- ss.dependency 'OnsideKit'
29
+ # OnsideKit dependency is conditionally included via ENV var set by the Expo plugin.
30
+ # When modules.onside is enabled, the plugin prepends ENV['EXPO_IAP_ONSIDE']='1' to the
31
+ # Podfile, which makes this dependency active and enables #if canImport(OnsideKit) in Swift.
32
+ if ENV['EXPO_IAP_ONSIDE'] == '1'
33
+ s.dependency 'OnsideKit'
33
34
  end
34
35
 
35
- s.default_subspecs = []
36
-
37
36
  # Swift/Objective-C compatibility
38
37
  s.pod_target_xcconfig = {
39
38
  'DEFINES_MODULE' => 'YES',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-iap",
3
- "version": "3.4.11",
3
+ "version": "3.4.13",
4
4
  "description": "In App Purchase module in Expo",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -197,20 +197,15 @@ const withIapAndroid = (config, props) => {
197
197
  });
198
198
  return config;
199
199
  };
200
- const EXPO_IAP_IOS_PATH = '../node_modules/expo-iap/ios';
201
200
  const ensureOnsidePodIOS = (content) => {
202
- if (/^\s*pod\s+['"]ExpoIap\/Onside['"].*$/m.test(content)) {
201
+ // Set EXPO_IAP_ONSIDE env var at the top of the Podfile so that the ExpoIap podspec
202
+ // conditionally adds OnsideKit as a dependency. This makes #if canImport(OnsideKit)
203
+ // work inside ExpoIap's Swift source files.
204
+ if (content.includes("ENV['EXPO_IAP_ONSIDE'] = '1'")) {
203
205
  return content;
204
206
  }
205
- const targetMatch = content.match(/target\s+'[^']+'\s+do\s*\n/);
206
- if (!targetMatch) {
207
- config_plugins_1.WarningAggregator.addWarningIOS('expo-iap', 'Could not find a target block in Podfile when adding ExpoIap/Onside; skipping installation.');
208
- return content;
209
- }
210
- const podLine = ` pod 'ExpoIap/Onside', :path => '${EXPO_IAP_IOS_PATH}'\n`;
211
- const insertIndex = targetMatch.index + targetMatch[0].length;
212
- logOnce('📦 expo-iap: Added ExpoIap/Onside subspec to Podfile');
213
- return content.slice(0, insertIndex) + podLine + content.slice(insertIndex);
207
+ logOnce('📦 expo-iap: Enabled OnsideKit (EXPO_IAP_ONSIDE=1)');
208
+ return `ENV['EXPO_IAP_ONSIDE'] = '1'\n` + content;
214
209
  };
215
210
  exports.ensureOnsidePodIOS = ensureOnsidePodIOS;
216
211
  function computeAutolinkModules(existing, desired) {
@@ -262,28 +262,17 @@ const withIapAndroid: ConfigPlugin<
262
262
  return config;
263
263
  };
264
264
 
265
- const EXPO_IAP_IOS_PATH = '../node_modules/expo-iap/ios';
266
-
267
265
  export const ensureOnsidePodIOS = (content: string): string => {
268
- if (/^\s*pod\s+['"]ExpoIap\/Onside['"].*$/m.test(content)) {
269
- return content;
270
- }
271
-
272
- const targetMatch = content.match(/target\s+'[^']+'\s+do\s*\n/);
273
- if (!targetMatch) {
274
- WarningAggregator.addWarningIOS(
275
- 'expo-iap',
276
- 'Could not find a target block in Podfile when adding ExpoIap/Onside; skipping installation.',
277
- );
266
+ // Set EXPO_IAP_ONSIDE env var at the top of the Podfile so that the ExpoIap podspec
267
+ // conditionally adds OnsideKit as a dependency. This makes #if canImport(OnsideKit)
268
+ // work inside ExpoIap's Swift source files.
269
+ if (content.includes("ENV['EXPO_IAP_ONSIDE'] = '1'")) {
278
270
  return content;
279
271
  }
280
272
 
281
- const podLine = ` pod 'ExpoIap/Onside', :path => '${EXPO_IAP_IOS_PATH}'\n`;
282
- const insertIndex = targetMatch.index! + targetMatch[0].length;
283
-
284
- logOnce('📦 expo-iap: Added ExpoIap/Onside subspec to Podfile');
273
+ logOnce('📦 expo-iap: Enabled OnsideKit (EXPO_IAP_ONSIDE=1)');
285
274
 
286
- return content.slice(0, insertIndex) + podLine + content.slice(insertIndex);
275
+ return `ENV['EXPO_IAP_ONSIDE'] = '1'\n` + content;
287
276
  };
288
277
 
289
278
  export type AutolinkState = {expoIap: boolean; onside: boolean};
package/src/useIAP.ts CHANGED
@@ -49,6 +49,7 @@ import type {
49
49
  VerifyPurchaseWithProviderResult,
50
50
  ProductAndroid,
51
51
  ProductSubscriptionIOS,
52
+ PurchaseOptions,
52
53
  } from './types';
53
54
  import {ErrorCode} from './types';
54
55
  import type {PurchaseError} from './utils/errorMapping';
@@ -72,7 +73,7 @@ type UseIap = {
72
73
  purchase: Purchase;
73
74
  isConsumable?: boolean;
74
75
  }) => Promise<void>;
75
- getAvailablePurchases: () => Promise<void>;
76
+ getAvailablePurchases: (options?: PurchaseOptions) => Promise<void>;
76
77
  fetchProducts: (params: {
77
78
  skus: string[];
78
79
  type?: ProductTypeInput;
@@ -89,7 +90,7 @@ type UseIap = {
89
90
  verifyPurchaseWithProvider: (
90
91
  props: VerifyPurchaseWithProviderProps,
91
92
  ) => Promise<VerifyPurchaseWithProviderResult>;
92
- restorePurchases: () => Promise<void>;
93
+ restorePurchases: (options?: PurchaseOptions) => Promise<void>;
93
94
  getPromotedProductIOS: () => Promise<Product | null>;
94
95
  /**
95
96
  * @deprecated Use promotedProductListenerIOS to receive the productId,
@@ -98,6 +99,12 @@ type UseIap = {
98
99
  requestPurchaseOnPromotedProductIOS: () => Promise<boolean>;
99
100
  getActiveSubscriptions: (subscriptionIds?: string[]) => Promise<void>;
100
101
  hasActiveSubscriptions: (subscriptionIds?: string[]) => Promise<boolean>;
102
+ /**
103
+ * Manually retry the store connection.
104
+ * Useful when the initial auto-connect fails (e.g., Play Store not ready at mount time).
105
+ * Updates the `connected` state on success.
106
+ */
107
+ reconnect: () => Promise<boolean>;
101
108
  checkAlternativeBillingAvailabilityAndroid: () => Promise<boolean>;
102
109
  showAlternativeBillingDialogAndroid: () => Promise<boolean>;
103
110
  createAlternativeBillingTokenAndroid: (
@@ -342,19 +349,24 @@ export function useIAP(options?: UseIAPOptions): UseIap {
342
349
  ],
343
350
  );
344
351
 
345
- const getAvailablePurchasesInternal = useCallback(async (): Promise<void> => {
346
- try {
347
- const result = await getAvailablePurchases({
348
- alsoPublishToEventListenerIOS: false,
349
- onlyIncludeActiveItemsIOS: true,
350
- });
351
- setAvailablePurchases(result);
352
- } catch (error) {
353
- ExpoIapConsole.error('Error fetching available purchases:', error);
354
- invokeOnError(error);
355
- throw error;
356
- }
357
- }, [invokeOnError]);
352
+ const getAvailablePurchasesInternal = useCallback(
353
+ async (options?: PurchaseOptions): Promise<void> => {
354
+ try {
355
+ const result = await getAvailablePurchases({
356
+ alsoPublishToEventListenerIOS:
357
+ options?.alsoPublishToEventListenerIOS ?? false,
358
+ onlyIncludeActiveItemsIOS: options?.onlyIncludeActiveItemsIOS ?? true,
359
+ includeSuspendedAndroid: options?.includeSuspendedAndroid ?? false,
360
+ });
361
+ setAvailablePurchases(result);
362
+ } catch (error) {
363
+ ExpoIapConsole.error('Error fetching available purchases:', error);
364
+ invokeOnError(error);
365
+ throw error;
366
+ }
367
+ },
368
+ [invokeOnError],
369
+ );
358
370
 
359
371
  const getActiveSubscriptionsInternal = useCallback(
360
372
  async (subscriptionIds?: string[]): Promise<void> => {
@@ -411,35 +423,45 @@ export function useIAP(options?: UseIAPOptions): UseIap {
411
423
  if (subscriptionsRefState.current.some((sub) => sub.id === productId)) {
412
424
  await fetchProductsInternal({skus: [productId], type: 'subs'});
413
425
  await getAvailablePurchasesInternal();
426
+ await getActiveSubscriptionsInternal();
414
427
  }
415
428
  } catch (error) {
416
429
  ExpoIapConsole.warn('Failed to refresh subscription status:', error);
417
430
  }
418
431
  },
419
- [fetchProductsInternal, getAvailablePurchasesInternal],
432
+ [
433
+ fetchProductsInternal,
434
+ getAvailablePurchasesInternal,
435
+ getActiveSubscriptionsInternal,
436
+ ],
420
437
  );
421
438
 
422
439
  // Restore completed transactions with cross-platform behavior.
423
440
  // iOS: best-effort sync (ignore sync errors) then fetch available purchases.
424
441
  // Android: fetch available purchases directly.
425
- const restorePurchasesInternal = useCallback(async (): Promise<void> => {
426
- try {
427
- // iOS: Try to sync first, but don't fail if sync errors occur
428
- if (Platform.OS === 'ios') {
429
- await syncIOS().catch(() => undefined); // syncIOS returns Promise<boolean>, we don't need the result
430
- }
442
+ const restorePurchasesInternal = useCallback(
443
+ async (options?: PurchaseOptions): Promise<void> => {
444
+ try {
445
+ // iOS: Try to sync first, but don't fail if sync errors occur
446
+ if (Platform.OS === 'ios') {
447
+ await syncIOS().catch(() => undefined); // syncIOS returns Promise<boolean>, we don't need the result
448
+ }
431
449
 
432
- const purchases = await getAvailablePurchases({
433
- alsoPublishToEventListenerIOS: false,
434
- onlyIncludeActiveItemsIOS: true,
435
- });
436
- setAvailablePurchases(purchases);
437
- } catch (error) {
438
- ExpoIapConsole.warn('Failed to restore purchases:', error);
439
- invokeOnError(error);
440
- throw error;
441
- }
442
- }, [invokeOnError]);
450
+ const purchases = await getAvailablePurchases({
451
+ alsoPublishToEventListenerIOS:
452
+ options?.alsoPublishToEventListenerIOS ?? false,
453
+ onlyIncludeActiveItemsIOS: options?.onlyIncludeActiveItemsIOS ?? true,
454
+ includeSuspendedAndroid: options?.includeSuspendedAndroid ?? false,
455
+ });
456
+ setAvailablePurchases(purchases);
457
+ } catch (error) {
458
+ ExpoIapConsole.warn('Failed to restore purchases:', error);
459
+ invokeOnError(error);
460
+ throw error;
461
+ }
462
+ },
463
+ [invokeOnError],
464
+ );
443
465
 
444
466
  const validateReceipt = useCallback(async (props: VerifyPurchaseProps) => {
445
467
  return validateReceiptInternal(props);
@@ -456,15 +478,29 @@ export function useIAP(options?: UseIAPOptions): UseIap {
456
478
  [],
457
479
  );
458
480
 
481
+ // Build config from options (prefer new enableBillingProgramAndroid over deprecated alternativeBillingModeAndroid)
482
+ const buildConnectionConfig = useCallback(() => {
483
+ return optionsRef.current?.enableBillingProgramAndroid ||
484
+ optionsRef.current?.alternativeBillingModeAndroid
485
+ ? {
486
+ enableBillingProgramAndroid:
487
+ optionsRef.current.enableBillingProgramAndroid,
488
+ alternativeBillingModeAndroid:
489
+ optionsRef.current.alternativeBillingModeAndroid,
490
+ }
491
+ : undefined;
492
+ }, []);
493
+
459
494
  const initIapWithSubscriptions = useCallback(async (): Promise<void> => {
460
495
  // CRITICAL: Register listeners BEFORE initConnection to avoid race condition
461
496
  // Events might fire immediately after initConnection, so listeners must be ready
462
497
  // Register purchase update listener BEFORE initConnection to avoid race conditions.
463
498
  subscriptionsRef.current.purchaseUpdate = purchaseUpdatedListener(
464
499
  async (purchase: Purchase) => {
465
- if ('expirationDateIOS' in purchase) {
466
- await refreshSubscriptionStatus(purchase.id);
467
- }
500
+ // Refresh subscription status for both iOS and Android subscription purchases.
501
+ // refreshSubscriptionStatus internally checks whether the product is a known
502
+ // subscription, so it is safe to call unconditionally for any purchase event.
503
+ await refreshSubscriptionStatus(purchase.productId);
468
504
 
469
505
  if (optionsRef.current?.onPurchaseSuccess) {
470
506
  optionsRef.current.onPurchaseSuccess(purchase);
@@ -503,17 +539,7 @@ export function useIAP(options?: UseIAPOptions): UseIap {
503
539
  }
504
540
 
505
541
  // NOW call initConnection after listeners are ready
506
- // Build config from options (prefer new enableBillingProgramAndroid over deprecated alternativeBillingModeAndroid)
507
- const config =
508
- optionsRef.current?.enableBillingProgramAndroid ||
509
- optionsRef.current?.alternativeBillingModeAndroid
510
- ? {
511
- enableBillingProgramAndroid:
512
- optionsRef.current.enableBillingProgramAndroid,
513
- alternativeBillingModeAndroid:
514
- optionsRef.current.alternativeBillingModeAndroid,
515
- }
516
- : undefined;
542
+ const config = buildConnectionConfig();
517
543
 
518
544
  try {
519
545
  const result = await initConnection(config);
@@ -538,7 +564,54 @@ export function useIAP(options?: UseIAPOptions): UseIap {
538
564
  subscriptionsRef.current.purchaseUpdate = undefined;
539
565
  subscriptionsRef.current.promotedProductIOS = undefined;
540
566
  }
541
- }, [refreshSubscriptionStatus, invokeOnError]);
567
+ }, [buildConnectionConfig, refreshSubscriptionStatus, invokeOnError]);
568
+
569
+ // Manual reconnect method for when the initial auto-connect fails.
570
+ // Re-runs initConnection and updates the connected state.
571
+ // Re-registers event listeners if they were cleaned up during a previous failure.
572
+ const reconnect = useCallback(async (): Promise<boolean> => {
573
+ const config = buildConnectionConfig();
574
+
575
+ try {
576
+ const result = await initConnection(config);
577
+ setConnected(result);
578
+
579
+ if (result) {
580
+ // Re-register listeners if they were cleaned up during a previous failure
581
+ if (!subscriptionsRef.current.purchaseUpdate) {
582
+ subscriptionsRef.current.purchaseUpdate = purchaseUpdatedListener(
583
+ async (purchase: Purchase) => {
584
+ await refreshSubscriptionStatus(purchase.productId);
585
+
586
+ if (optionsRef.current?.onPurchaseSuccess) {
587
+ optionsRef.current.onPurchaseSuccess(purchase);
588
+ }
589
+ },
590
+ );
591
+ }
592
+
593
+ if (
594
+ Platform.OS === 'ios' &&
595
+ !subscriptionsRef.current.promotedProductIOS
596
+ ) {
597
+ subscriptionsRef.current.promotedProductIOS =
598
+ promotedProductListenerIOS((product: Product) => {
599
+ setPromotedProductIOS(product);
600
+
601
+ if (optionsRef.current?.onPromotedProductIOS) {
602
+ optionsRef.current.onPromotedProductIOS(product);
603
+ }
604
+ });
605
+ }
606
+ }
607
+
608
+ return result;
609
+ } catch (error) {
610
+ ExpoIapConsole.error('[useIAP] reconnect failed:', error);
611
+ invokeOnError(error);
612
+ return false;
613
+ }
614
+ }, [buildConnectionConfig, refreshSubscriptionStatus, invokeOnError]);
542
615
 
543
616
  useEffect(() => {
544
617
  initIapWithSubscriptions();
@@ -573,6 +646,8 @@ export function useIAP(options?: UseIAPOptions): UseIap {
573
646
  requestPurchaseOnPromotedProductIOS,
574
647
  getActiveSubscriptions: getActiveSubscriptionsInternal,
575
648
  hasActiveSubscriptions: hasActiveSubscriptionsInternal,
649
+ // Reconnect method for manual retry
650
+ reconnect,
576
651
  // Alternative billing methods (Android only)
577
652
  checkAlternativeBillingAvailabilityAndroid,
578
653
  showAlternativeBillingDialogAndroid,