expo-iap 3.0.8 → 3.1.1-rc.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 (99) hide show
  1. package/CLAUDE.md +2 -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/ExpoIapModule.d.ts +1 -0
  8. package/build/ExpoIapModule.d.ts.map +1 -1
  9. package/build/ExpoIapModule.js +29 -4
  10. package/build/ExpoIapModule.js.map +1 -1
  11. package/build/index.d.ts +20 -47
  12. package/build/index.d.ts.map +1 -1
  13. package/build/index.js +94 -137
  14. package/build/index.js.map +1 -1
  15. package/build/modules/android.d.ts.map +1 -1
  16. package/build/modules/android.js +2 -1
  17. package/build/modules/android.js.map +1 -1
  18. package/build/modules/ios.d.ts +16 -1
  19. package/build/modules/ios.d.ts.map +1 -1
  20. package/build/modules/ios.js +29 -16
  21. package/build/modules/ios.js.map +1 -1
  22. package/build/types.d.ts +8 -6
  23. package/build/types.d.ts.map +1 -1
  24. package/build/types.js.map +1 -1
  25. package/build/useIAP.d.ts +1 -1
  26. package/build/useIAP.d.ts.map +1 -1
  27. package/build/useIAP.js +12 -15
  28. package/build/useIAP.js.map +1 -1
  29. package/build/utils/constants.d.ts +2 -0
  30. package/build/utils/constants.d.ts.map +1 -1
  31. package/build/utils/constants.js +9 -2
  32. package/build/utils/constants.js.map +1 -1
  33. package/build/utils/errorMapping.d.ts +32 -23
  34. package/build/utils/errorMapping.d.ts.map +1 -1
  35. package/build/utils/errorMapping.js +117 -22
  36. package/build/utils/errorMapping.js.map +1 -1
  37. package/coverage/clover.xml +497 -0
  38. package/coverage/coverage-final.json +6 -0
  39. package/coverage/lcov-report/base.css +224 -0
  40. package/coverage/lcov-report/block-navigation.js +87 -0
  41. package/coverage/lcov-report/favicon.png +0 -0
  42. package/coverage/lcov-report/index.html +161 -0
  43. package/coverage/lcov-report/prettify.css +1 -0
  44. package/coverage/lcov-report/prettify.js +2 -0
  45. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  46. package/coverage/lcov-report/sorter.js +196 -0
  47. package/coverage/lcov-report/src/ExpoIap.types.ts.html +1243 -0
  48. package/coverage/lcov-report/src/PurchaseError.ts.html +787 -0
  49. package/coverage/lcov-report/src/helpers/index.html +116 -0
  50. package/coverage/lcov-report/src/helpers/subscription.ts.html +496 -0
  51. package/coverage/lcov-report/src/index.html +116 -0
  52. package/coverage/lcov-report/src/index.ts.html +1993 -0
  53. package/coverage/lcov-report/src/modules/android.ts.html +550 -0
  54. package/coverage/lcov-report/src/modules/index.html +131 -0
  55. package/coverage/lcov-report/src/modules/ios.ts.html +1222 -0
  56. package/coverage/lcov-report/src/purchase-error.ts.html +880 -0
  57. package/coverage/lcov-report/src/types/ExpoIapAndroid.types.ts.html +493 -0
  58. package/coverage/lcov-report/src/types/index.html +116 -0
  59. package/coverage/lcov-report/src/useIap.ts.html +1483 -0
  60. package/coverage/lcov-report/src/utils/errorMapping.ts.html +1069 -0
  61. package/coverage/lcov-report/src/utils/index.html +116 -0
  62. package/coverage/lcov-report/src/utils/purchase.ts.html +241 -0
  63. package/coverage/lcov.info +929 -0
  64. package/expo-module.config.json +10 -3
  65. package/ios/ExpoIap.podspec +3 -2
  66. package/ios/ExpoIapHelper.swift +96 -0
  67. package/ios/ExpoIapLog.swift +127 -0
  68. package/ios/ExpoIapModule.swift +218 -340
  69. package/ios/OneSideModule.swift +489 -0
  70. package/ios/expoiap.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  71. package/ios/expoiap.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  72. package/openiap-versions.json +5 -0
  73. package/package.json +4 -3
  74. package/plugin/build/withIAP.d.ts +22 -9
  75. package/plugin/build/withIAP.js +163 -13
  76. package/plugin/jest.config.js +13 -3
  77. package/plugin/src/expoConfig.augmentation.d.ts +38 -0
  78. package/plugin/src/withIAP.ts +272 -22
  79. package/plugin/tsconfig.json +2 -1
  80. package/plugin/tsconfig.tsbuildinfo +1 -1
  81. package/scripts/update-types.mjs +20 -1
  82. package/src/ExpoIapModule.ts +45 -4
  83. package/src/index.ts +122 -165
  84. package/src/modules/android.ts +2 -1
  85. package/src/modules/ios.ts +31 -19
  86. package/src/types.ts +8 -6
  87. package/src/useIAP.ts +17 -25
  88. package/src/utils/constants.ts +11 -2
  89. package/src/utils/errorMapping.ts +203 -23
  90. package/build/purchase-error.d.ts +0 -67
  91. package/build/purchase-error.d.ts.map +0 -1
  92. package/build/purchase-error.js +0 -166
  93. package/build/purchase-error.js.map +0 -1
  94. package/build/utils/purchase.d.ts +0 -9
  95. package/build/utils/purchase.d.ts.map +0 -1
  96. package/build/utils/purchase.js +0 -34
  97. package/build/utils/purchase.js.map +0 -1
  98. package/src/purchase-error.ts +0 -265
  99. package/src/utils/purchase.ts +0 -52
@@ -3,8 +3,27 @@ import {mkdtempSync, readFileSync, writeFileSync, rmSync} from 'node:fs';
3
3
  import {join} from 'node:path';
4
4
  import {tmpdir} from 'node:os';
5
5
  import {execFileSync} from 'node:child_process';
6
+ import {fileURLToPath, URL} from 'node:url';
7
+
8
+ const __dirname = fileURLToPath(new URL('.', import.meta.url));
9
+ let versions;
10
+ try {
11
+ versions = JSON.parse(
12
+ readFileSync(join(__dirname, '..', 'openiap-versions.json'), 'utf8'),
13
+ );
14
+ } catch {
15
+ throw new Error(
16
+ 'expo-iap: Unable to load openiap-versions.json. Ensure the file exists and is valid JSON.',
17
+ );
18
+ }
19
+
20
+ const DEFAULT_TAG = versions?.gql;
21
+ if (typeof DEFAULT_TAG !== 'string' || DEFAULT_TAG.length === 0) {
22
+ throw new Error(
23
+ 'expo-iap: "gql" version missing in openiap-versions.json. Specify --tag manually or update the file.',
24
+ );
25
+ }
6
26
 
7
- const DEFAULT_TAG = '1.0.1';
8
27
  const PROJECT_ROOT = process.cwd();
9
28
 
10
29
  function parseArgs() {
@@ -1,10 +1,51 @@
1
- import {requireNativeModule} from 'expo-modules-core';
1
+ import {requireNativeModule, UnavailabilityError} from 'expo-modules-core';
2
2
 
3
- // It loads the native module object from the JSI or falls back to
4
- // the bridge module (from NativeModulesProxy) if the remote debugger is on.
5
- const ExpoIapModule = requireNativeModule('ExpoIap');
3
+ type NativeIapModuleName = 'ExpoIapOnside' | 'ExpoIap';
4
+
5
+ const {module: ExpoIapModule, name: resolvedNativeModuleName} =
6
+ resolveNativeModule();
7
+
8
+ export const USING_ONSIDE_SDK = resolvedNativeModuleName === 'ExpoIapOnside';
6
9
 
7
10
  // Platform-specific error codes from native modules
8
11
  export const NATIVE_ERROR_CODES = ExpoIapModule.ERROR_CODES || {};
9
12
 
10
13
  export default ExpoIapModule;
14
+
15
+ function resolveNativeModule(): {
16
+ module: any;
17
+ name: NativeIapModuleName;
18
+ } {
19
+ const candidates: NativeIapModuleName[] = ['ExpoIapOnside', 'ExpoIap'];
20
+
21
+ for (const name of candidates) {
22
+ try {
23
+ const module = requireNativeModule(name);
24
+ return {module, name};
25
+ } catch (error) {
26
+ if (name === 'ExpoIapOnside' && isMissingModuleError(error, name)) {
27
+ // Onside module is optional. If unavailable, fall back to ExpoIap.
28
+ continue;
29
+ }
30
+
31
+ throw error;
32
+ }
33
+ }
34
+
35
+ throw new UnavailabilityError(
36
+ 'expo-iap',
37
+ 'ExpoIap native module is unavailable',
38
+ );
39
+ }
40
+
41
+ function isMissingModuleError(error: unknown, moduleName: string): boolean {
42
+ if (error instanceof UnavailabilityError) {
43
+ return true;
44
+ }
45
+
46
+ if (error instanceof Error) {
47
+ return error.message.includes(`Cannot find native module '${moduleName}'`);
48
+ }
49
+
50
+ return false;
51
+ }
package/src/index.ts CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  validateReceiptIOS,
10
10
  deepLinkToSubscriptionsIOS,
11
11
  syncIOS,
12
+ getStorefrontIOS,
12
13
  } from './modules/ios';
13
14
  import {
14
15
  isProductAndroid,
@@ -22,14 +23,12 @@ import type {
22
23
  DeepLinkOptions,
23
24
  FetchProductsResult,
24
25
  MutationField,
26
+ MutationRequestPurchaseArgs,
25
27
  MutationValidateReceiptArgs,
26
28
  Product,
27
- ProductAndroid,
28
- ProductIOS,
29
29
  ProductQueryType,
30
30
  ProductSubscription,
31
31
  Purchase,
32
- PurchaseInput,
33
32
  PurchaseOptions,
34
33
  QueryField,
35
34
  RequestPurchasePropsByPlatforms,
@@ -38,15 +37,12 @@ import type {
38
37
  RequestSubscriptionPropsByPlatforms,
39
38
  RequestSubscriptionAndroidProps,
40
39
  RequestSubscriptionIosProps,
41
- DiscountOfferInputIOS,
42
40
  } from './types';
43
41
  import {ErrorCode} from './types';
44
- import {PurchaseError} from './purchase-error';
45
- import {normalizePurchaseId, normalizePurchaseList} from './utils/purchase';
42
+ import {createPurchaseError, type PurchaseError} from './utils/errorMapping';
46
43
 
47
44
  // Export all types
48
45
  export * from './types';
49
- export {ErrorCodeUtils, ErrorCodeMapping} from './purchase-error';
50
46
  export * from './modules/android';
51
47
  export * from './modules/ios';
52
48
 
@@ -57,18 +53,12 @@ export {
57
53
  } from './helpers/subscription';
58
54
 
59
55
  // Get the native constant value
60
- export const PI = ExpoIapModule.PI;
61
-
62
56
  export enum OpenIapEvent {
63
57
  PurchaseUpdated = 'purchase-updated',
64
58
  PurchaseError = 'purchase-error',
65
59
  PromotedProductIOS = 'promoted-product-ios',
66
60
  }
67
61
 
68
- export function setValueAsync(value: string) {
69
- return ExpoIapModule.setValueAsync(value);
70
- }
71
-
72
62
  type ExpoIapEventPayloads = {
73
63
  [OpenIapEvent.PurchaseUpdated]: Purchase;
74
64
  [OpenIapEvent.PurchaseError]: PurchaseError;
@@ -109,7 +99,7 @@ const normalizeProductType = (type?: ProductTypeInput) => {
109
99
  if (!type || type === 'inapp' || type === 'in-app') {
110
100
  return {
111
101
  canonical: 'in-app' as ProductQueryType,
112
- native: 'inapp' as const,
102
+ native: 'in-app' as const,
113
103
  };
114
104
  }
115
105
  if (type === 'subs') {
@@ -127,36 +117,47 @@ const normalizeProductType = (type?: ProductTypeInput) => {
127
117
  throw new Error(`Unsupported product type: ${type}`);
128
118
  };
129
119
 
120
+ const normalizePurchasePlatform = (purchase: Purchase): Purchase => {
121
+ const platform = purchase.platform;
122
+ if (typeof platform !== 'string') {
123
+ return purchase;
124
+ }
125
+
126
+ const lowered = platform.toLowerCase();
127
+ if (lowered === platform || (lowered !== 'ios' && lowered !== 'android')) {
128
+ return purchase;
129
+ }
130
+
131
+ return {...purchase, platform: lowered};
132
+ };
133
+
134
+ const normalizePurchaseArray = (purchases: Purchase[]): Purchase[] =>
135
+ purchases.map((purchase) => normalizePurchasePlatform(purchase));
136
+
130
137
  export const purchaseUpdatedListener = (
131
138
  listener: (event: Purchase) => void,
132
139
  ) => {
133
- console.log('[JS] Registering purchaseUpdatedListener');
134
140
  const wrappedListener = (event: Purchase) => {
135
- const normalized = normalizePurchaseId(event);
136
- console.log('[JS] purchaseUpdatedListener fired:', normalized);
141
+ const normalized = normalizePurchasePlatform(event);
137
142
  listener(normalized);
138
143
  };
139
144
  const emitterSubscription = emitter.addListener(
140
145
  OpenIapEvent.PurchaseUpdated,
141
146
  wrappedListener,
142
147
  );
143
- console.log('[JS] purchaseUpdatedListener registered successfully');
144
148
  return emitterSubscription;
145
149
  };
146
150
 
147
151
  export const purchaseErrorListener = (
148
152
  listener: (error: PurchaseError) => void,
149
153
  ) => {
150
- console.log('[JS] Registering purchaseErrorListener');
151
154
  const wrappedListener = (error: PurchaseError) => {
152
- console.log('[JS] purchaseErrorListener fired:', error);
153
155
  listener(error);
154
156
  };
155
157
  const emitterSubscription = emitter.addListener(
156
158
  OpenIapEvent.PurchaseError,
157
159
  wrappedListener,
158
160
  );
159
- console.log('[JS] purchaseErrorListener registered successfully');
160
161
  return emitterSubscription;
161
162
  };
162
163
 
@@ -206,10 +207,11 @@ export const endConnection: MutationField<'endConnection'> = async () =>
206
207
  * @param request.type - Product query type: 'in-app', 'subs', or 'all'
207
208
  */
208
209
  export const fetchProducts: QueryField<'fetchProducts'> = async (request) => {
210
+ console.log('fetchProducts called with:', request);
209
211
  const {skus, type} = request ?? {};
210
212
 
211
213
  if (!Array.isArray(skus) || skus.length === 0) {
212
- throw new PurchaseError({
214
+ throw createPurchaseError({
213
215
  message: 'No SKUs provided',
214
216
  code: ErrorCode.EmptySkuList,
215
217
  });
@@ -223,22 +225,22 @@ export const fetchProducts: QueryField<'fetchProducts'> = async (request) => {
223
225
  const filterIosItems = (
224
226
  items: unknown[],
225
227
  ): Product[] | ProductSubscription[] =>
226
- items.filter((item): item is ProductIOS | ProductSubscription => {
228
+ items.filter((item): item is Product | ProductSubscription => {
227
229
  if (!isProductIOS(item)) {
228
230
  return false;
229
231
  }
230
- const candidate = item as ProductIOS | ProductSubscription;
232
+ const candidate = item as Product | ProductSubscription;
231
233
  return typeof candidate.id === 'string' && skuSet.has(candidate.id);
232
234
  });
233
235
 
234
236
  const filterAndroidItems = (
235
237
  items: unknown[],
236
238
  ): Product[] | ProductSubscription[] =>
237
- items.filter((item): item is ProductAndroid | ProductSubscription => {
239
+ items.filter((item): item is Product | ProductSubscription => {
238
240
  if (!isProductAndroid(item)) {
239
241
  return false;
240
242
  }
241
- const candidate = item as ProductAndroid | ProductSubscription;
243
+ const candidate = item as Product | ProductSubscription;
242
244
  return typeof candidate.id === 'string' && skuSet.has(candidate.id);
243
245
  });
244
246
 
@@ -287,44 +289,18 @@ export const getAvailablePurchases: QueryField<
287
289
  }) ?? (() => Promise.resolve([] as Purchase[]));
288
290
 
289
291
  const purchases = await resolvePurchases();
290
- return normalizePurchaseList(purchases);
292
+ return normalizePurchaseArray(purchases as Purchase[]);
291
293
  };
292
294
 
293
- /**
294
- * Restore completed transactions (cross-platform behavior)
295
- *
296
- * - iOS: perform a lightweight sync to refresh transactions and ignore sync errors,
297
- * then fetch available purchases to surface restored items to the app.
298
- * - Android: simply fetch available purchases (restoration happens via query).
299
- *
300
- * This helper returns the restored/available purchases so callers can update UI/state.
301
- *
302
- * @param options.alsoPublishToEventListenerIOS - iOS only: whether to also publish to the event listener
303
- * @param options.onlyIncludeActiveItemsIOS - iOS only: whether to only include active items
304
- * @returns Promise resolving to the list of available/restored purchases
305
- */
306
- export const restorePurchases: MutationField<'restorePurchases'> = async () => {
307
- if (Platform.OS === 'ios') {
308
- await syncIOS().catch(() => undefined);
295
+ export const getStorefront: QueryField<'getStorefrontIOS'> = async () => {
296
+ // Cross-platform storefront
297
+ if (Platform.OS === 'android') {
298
+ if (typeof ExpoIapModule.getStorefrontAndroid === 'function') {
299
+ return ExpoIapModule.getStorefrontAndroid();
300
+ }
301
+ return '';
309
302
  }
310
-
311
- await getAvailablePurchases({
312
- alsoPublishToEventListenerIOS: false,
313
- onlyIncludeActiveItemsIOS: true,
314
- });
315
- };
316
-
317
- const offerToRecordIOS = (
318
- offer: DiscountOfferInputIOS | undefined,
319
- ): Record<keyof DiscountOfferInputIOS, string> | undefined => {
320
- if (!offer) return undefined;
321
- return {
322
- identifier: offer.identifier,
323
- keyIdentifier: offer.keyIdentifier,
324
- nonce: offer.nonce,
325
- signature: offer.signature,
326
- timestamp: offer.timestamp.toString(),
327
- };
303
+ return getStorefrontIOS();
328
304
  };
329
305
 
330
306
  /**
@@ -403,24 +379,35 @@ export const requestPurchase: MutationField<'requestPurchase'> = async (
403
379
  );
404
380
  }
405
381
 
406
- const {
407
- sku,
408
- andDangerouslyFinishTransactionAutomatically = false,
409
- appAccountToken,
410
- quantity,
411
- withOffer,
412
- } = normalizedRequest;
413
-
414
- const offer = offerToRecordIOS(withOffer ?? undefined);
415
- const purchase = await ExpoIapModule.requestPurchase({
416
- sku,
417
- andDangerouslyFinishTransactionAutomatically,
418
- appAccountToken,
419
- quantity,
420
- withOffer: offer,
421
- });
382
+ let payload: MutationRequestPurchaseArgs;
383
+ if (canonical === 'in-app') {
384
+ payload = {
385
+ type: 'in-app',
386
+ request: request as RequestPurchasePropsByPlatforms,
387
+ };
388
+ } else if (canonical === 'subs') {
389
+ payload = {
390
+ type: 'subs',
391
+ request: request as RequestSubscriptionPropsByPlatforms,
392
+ };
393
+ } else {
394
+ throw new Error(`Unsupported product type: ${canonical}`);
395
+ }
396
+
397
+ const purchase = (await ExpoIapModule.requestPurchase(payload)) as
398
+ | Purchase
399
+ | Purchase[]
400
+ | null;
401
+
402
+ if (Array.isArray(purchase)) {
403
+ return normalizePurchaseArray(purchase);
404
+ }
422
405
 
423
- return normalizePurchaseId(purchase as Purchase);
406
+ if (purchase) {
407
+ return normalizePurchasePlatform(purchase);
408
+ }
409
+
410
+ return canonical === 'subs' ? [] : null;
424
411
  }
425
412
 
426
413
  if (Platform.OS === 'android') {
@@ -454,7 +441,7 @@ export const requestPurchase: MutationField<'requestPurchase'> = async (
454
441
  isOfferPersonalized: isOfferPersonalized ?? false,
455
442
  })) as Purchase[];
456
443
 
457
- return normalizePurchaseList(result);
444
+ return normalizePurchaseArray(result);
458
445
  }
459
446
 
460
447
  if (canonical === 'subs') {
@@ -497,7 +484,7 @@ export const requestPurchase: MutationField<'requestPurchase'> = async (
497
484
  isOfferPersonalized: isOfferPersonalized ?? false,
498
485
  })) as Purchase[];
499
486
 
500
- return normalizePurchaseList(result);
487
+ return normalizePurchaseArray(result);
501
488
  }
502
489
 
503
490
  throw new Error(
@@ -508,43 +495,23 @@ export const requestPurchase: MutationField<'requestPurchase'> = async (
508
495
  throw new Error('Platform not supported');
509
496
  };
510
497
 
511
- const toPurchaseInput = (
512
- purchase: Purchase | PurchaseInput,
513
- ): PurchaseInput => ({
514
- id: purchase.id,
515
- ids: purchase.ids ?? undefined,
516
- isAutoRenewing: purchase.isAutoRenewing,
517
- platform: purchase.platform,
518
- productId: purchase.productId,
519
- purchaseState: purchase.purchaseState,
520
- purchaseToken: purchase.purchaseToken ?? null,
521
- quantity: purchase.quantity,
522
- transactionDate: purchase.transactionDate,
523
- });
524
-
525
498
  export const finishTransaction: MutationField<'finishTransaction'> = async ({
526
499
  purchase,
527
500
  isConsumable = false,
528
501
  }) => {
529
- const normalizedPurchase = toPurchaseInput(purchase);
530
-
531
502
  if (Platform.OS === 'ios') {
532
- const transactionId = normalizedPurchase.id;
533
- if (!transactionId) {
534
- throw new Error('purchase.id required to finish iOS transaction');
535
- }
536
- await ExpoIapModule.finishTransaction(transactionId);
503
+ await ExpoIapModule.finishTransaction(purchase, isConsumable);
537
504
  return;
538
505
  }
539
506
 
540
507
  if (Platform.OS === 'android') {
541
- const token = normalizedPurchase.purchaseToken ?? undefined;
508
+ const token = purchase.purchaseToken ?? undefined;
542
509
 
543
510
  if (!token) {
544
- throw new PurchaseError({
511
+ throw createPurchaseError({
545
512
  message: 'Purchase token is required to finish transaction',
546
513
  code: ErrorCode.DeveloperError,
547
- productId: normalizedPurchase.productId,
514
+ productId: purchase.productId,
548
515
  platform: 'android',
549
516
  });
550
517
  }
@@ -562,43 +529,58 @@ export const finishTransaction: MutationField<'finishTransaction'> = async ({
562
529
  };
563
530
 
564
531
  /**
565
- * Retrieves the current storefront information from iOS App Store
566
- *
567
- * @returns Promise resolving to the storefront country code
568
- * @throws Error if called on non-iOS platform
532
+ * Restore completed transactions (cross-platform behavior)
569
533
  *
570
- * @example
571
- * ```typescript
572
- * const storefront = await getStorefrontIOS();
573
- * console.log(storefront); // 'US'
574
- * ```
534
+ * - iOS: perform a lightweight sync to refresh transactions and ignore sync errors,
535
+ * then fetch available purchases to surface restored items to the app.
536
+ * - Android: simply fetch available purchases (restoration happens via query).
575
537
  *
576
- * @platform iOS
538
+ * This helper triggers the refresh flows but does not return the purchases; consumers should
539
+ * call `getAvailablePurchases` or rely on hook state to inspect the latest items.
577
540
  */
578
- export const getStorefrontIOS = (): Promise<string> => {
579
- if (Platform.OS !== 'ios') {
580
- console.warn('getStorefrontIOS: This method is only available on iOS');
581
- return Promise.resolve('');
541
+ export const restorePurchases: MutationField<'restorePurchases'> = async () => {
542
+ if (Platform.OS === 'ios') {
543
+ await syncIOS().catch(() => undefined);
582
544
  }
583
- return ExpoIapModule.getStorefrontIOS();
545
+
546
+ await getAvailablePurchases({
547
+ alsoPublishToEventListenerIOS: false,
548
+ onlyIncludeActiveItemsIOS: true,
549
+ });
584
550
  };
585
551
 
586
552
  /**
587
- * Gets the storefront country code from the underlying native store.
588
- * Returns a two-letter country code such as 'US', 'KR', or empty string on failure.
553
+ * Deeplinks to native interface that allows users to manage their subscriptions
554
+ * @param options.skuAndroid - Required for Android to locate specific subscription (ignored on iOS)
555
+ * @param options.packageNameAndroid - Required for Android to identify your app (ignored on iOS)
556
+ *
557
+ * @returns Promise that resolves when the deep link is successfully opened
558
+ *
559
+ * @throws {Error} When called on unsupported platform or when required Android parameters are missing
560
+ *
561
+ * @example
562
+ * import { deepLinkToSubscriptions } from 'expo-iap';
589
563
  *
590
- * @platform ios
591
- * @platform android
564
+ * // Works on both iOS and Android
565
+ * await deepLinkToSubscriptions({
566
+ * skuAndroid: 'your_subscription_sku',
567
+ * packageNameAndroid: 'com.example.app'
568
+ * });
592
569
  */
593
- export const getStorefront = (): Promise<string> => {
594
- // Cross-platform storefront
570
+ export const deepLinkToSubscriptions: MutationField<
571
+ 'deepLinkToSubscriptions'
572
+ > = async (options) => {
573
+ if (Platform.OS === 'ios') {
574
+ await deepLinkToSubscriptionsIOS();
575
+ return;
576
+ }
577
+
595
578
  if (Platform.OS === 'android') {
596
- if (typeof ExpoIapModule.getStorefrontAndroid === 'function') {
597
- return ExpoIapModule.getStorefrontAndroid();
598
- }
599
- return Promise.resolve('');
579
+ await deepLinkToSubscriptionsAndroid((options as DeepLinkOptions) ?? null);
580
+ return;
600
581
  }
601
- return getStorefrontIOS();
582
+
583
+ throw new Error(`Unsupported platform: ${Platform.OS}`);
602
584
  };
603
585
 
604
586
  /**
@@ -641,39 +623,14 @@ export const validateReceipt: MutationField<'validateReceipt'> = async (
641
623
  throw new Error('Platform not supported');
642
624
  };
643
625
 
644
- /**
645
- * Deeplinks to native interface that allows users to manage their subscriptions
646
- * @param options.skuAndroid - Required for Android to locate specific subscription (ignored on iOS)
647
- * @param options.packageNameAndroid - Required for Android to identify your app (ignored on iOS)
648
- *
649
- * @returns Promise that resolves when the deep link is successfully opened
650
- *
651
- * @throws {Error} When called on unsupported platform or when required Android parameters are missing
652
- *
653
- * @example
654
- * import { deepLinkToSubscriptions } from 'expo-iap';
655
- *
656
- * // Works on both iOS and Android
657
- * await deepLinkToSubscriptions({
658
- * skuAndroid: 'your_subscription_sku',
659
- * packageNameAndroid: 'com.example.app'
660
- * });
661
- */
662
- export const deepLinkToSubscriptions: MutationField<
663
- 'deepLinkToSubscriptions'
664
- > = async (options) => {
665
- if (Platform.OS === 'ios') {
666
- await deepLinkToSubscriptionsIOS();
667
- return;
668
- }
669
-
670
- if (Platform.OS === 'android') {
671
- await deepLinkToSubscriptionsAndroid((options as DeepLinkOptions) ?? null);
672
- return;
673
- }
674
-
675
- throw new Error(`Unsupported platform: ${Platform.OS}`);
676
- };
677
-
678
626
  export * from './useIAP';
679
- export * from './utils/errorMapping';
627
+ export {
628
+ ErrorCodeUtils,
629
+ ErrorCodeMapping,
630
+ createPurchaseError,
631
+ createPurchaseErrorFromPlatform,
632
+ } from './utils/errorMapping';
633
+ export type {
634
+ PurchaseError as ExpoPurchaseError,
635
+ PurchaseErrorProps,
636
+ } from './utils/errorMapping';
@@ -19,7 +19,8 @@ export function isProductAndroid<T extends {platform?: string}>(
19
19
  item != null &&
20
20
  typeof item === 'object' &&
21
21
  'platform' in item &&
22
- (item as any).platform === 'android'
22
+ typeof (item as any).platform === 'string' &&
23
+ (item as any).platform.toLowerCase() === 'android'
23
24
  );
24
25
  }
25
26
 
@@ -15,9 +15,8 @@ import type {
15
15
  ReceiptValidationResultIOS,
16
16
  SubscriptionStatusIOS,
17
17
  } from '../types';
18
- import type {PurchaseError} from '../purchase-error';
19
- import {Linking} from 'react-native';
20
- import {normalizePurchaseId, normalizePurchaseList} from '../utils/purchase';
18
+ import type {PurchaseError} from '../utils/errorMapping';
19
+ import {Linking, Platform} from 'react-native';
21
20
 
22
21
  export type TransactionEvent = {
23
22
  transaction?: Purchase;
@@ -34,7 +33,8 @@ export function isProductIOS<T extends {platform?: string}>(
34
33
  item != null &&
35
34
  typeof item === 'object' &&
36
35
  'platform' in item &&
37
- (item as any).platform === 'ios'
36
+ typeof (item as any).platform === 'string' &&
37
+ (item as any).platform.toLowerCase() === 'ios'
38
38
  );
39
39
  }
40
40
 
@@ -105,7 +105,7 @@ export const currentEntitlementIOS: QueryField<
105
105
  throw new Error('currentEntitlementIOS requires a SKU');
106
106
  }
107
107
  const purchase = await ExpoIapModule.currentEntitlementIOS(sku);
108
- return normalizePurchaseId((purchase ?? null) as PurchaseIOS | null);
108
+ return (purchase ?? null) as PurchaseIOS | null;
109
109
  };
110
110
 
111
111
  /**
@@ -124,7 +124,7 @@ export const latestTransactionIOS: QueryField<'latestTransactionIOS'> = async (
124
124
  throw new Error('latestTransactionIOS requires a SKU');
125
125
  }
126
126
  const transaction = await ExpoIapModule.latestTransactionIOS(sku);
127
- return normalizePurchaseId((transaction ?? null) as PurchaseIOS | null);
127
+ return (transaction ?? null) as PurchaseIOS | null;
128
128
  };
129
129
 
130
130
  /**
@@ -159,7 +159,7 @@ export const showManageSubscriptionsIOS: MutationField<
159
159
  'showManageSubscriptionsIOS'
160
160
  > = async () => {
161
161
  const purchases = await ExpoIapModule.showManageSubscriptionsIOS();
162
- return normalizePurchaseList((purchases ?? []) as PurchaseIOS[]);
162
+ return (purchases ?? []) as PurchaseIOS[];
163
163
  };
164
164
 
165
165
  /**
@@ -178,6 +178,28 @@ export const getReceiptDataIOS: QueryField<'getReceiptDataIOS'> = async () => {
178
178
 
179
179
  export const getReceiptIOS = getReceiptDataIOS;
180
180
 
181
+ /**
182
+ * Retrieves the current storefront information from the iOS App Store.
183
+ *
184
+ * @returns Promise resolving to the storefront country code
185
+ * @throws Error if called on non-iOS platform
186
+ *
187
+ * @example
188
+ * ```typescript
189
+ * const storefront = await getStorefrontIOS();
190
+ * console.log(storefront); // 'US'
191
+ * ```
192
+ *
193
+ * @platform iOS
194
+ */
195
+ export const getStorefrontIOS: QueryField<'getStorefrontIOS'> = async () => {
196
+ if (Platform.OS !== 'ios') {
197
+ console.warn('getStorefrontIOS: This method is only available on iOS');
198
+ return '';
199
+ }
200
+ return ExpoIapModule.getStorefrontIOS();
201
+ };
202
+
181
203
  /**
182
204
  * Check if a transaction is verified through StoreKit 2.
183
205
  * StoreKit 2 performs local verification of transaction JWS signatures.
@@ -242,19 +264,9 @@ const validateReceiptIOSImpl = async (
242
264
  throw new Error('validateReceiptIOS requires a SKU');
243
265
  }
244
266
 
245
- const result = (await ExpoIapModule.validateReceiptIOS(
267
+ return (await ExpoIapModule.validateReceiptIOS(
246
268
  sku,
247
269
  )) as ReceiptValidationResultIOS;
248
- const normalizedLatest = normalizePurchaseId(
249
- result.latestTransaction ?? undefined,
250
- );
251
- if (normalizedLatest === result.latestTransaction) {
252
- return result;
253
- }
254
- return {
255
- ...result,
256
- latestTransaction: normalizedLatest ?? null,
257
- };
258
270
  };
259
271
 
260
272
  export const validateReceiptIOS =
@@ -340,7 +352,7 @@ export const getPendingTransactionsIOS: QueryField<
340
352
  'getPendingTransactionsIOS'
341
353
  > = async () => {
342
354
  const transactions = await ExpoIapModule.getPendingTransactionsIOS();
343
- return normalizePurchaseList((transactions ?? []) as PurchaseIOS[]);
355
+ return (transactions ?? []) as PurchaseIOS[];
344
356
  };
345
357
 
346
358
  /**