expo-iap 3.0.7-rc.1 → 3.0.8

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 (67) hide show
  1. package/CHANGELOG.md +1 -310
  2. package/CLAUDE.md +12 -0
  3. package/android/build.gradle +1 -1
  4. package/build/index.d.ts +14 -66
  5. package/build/index.d.ts.map +1 -1
  6. package/build/index.js +149 -154
  7. package/build/index.js.map +1 -1
  8. package/build/modules/android.d.ts +7 -12
  9. package/build/modules/android.d.ts.map +1 -1
  10. package/build/modules/android.js +13 -11
  11. package/build/modules/android.js.map +1 -1
  12. package/build/modules/ios.d.ts +19 -35
  13. package/build/modules/ios.d.ts.map +1 -1
  14. package/build/modules/ios.js +86 -33
  15. package/build/modules/ios.js.map +1 -1
  16. package/build/types.d.ts +99 -76
  17. package/build/types.d.ts.map +1 -1
  18. package/build/types.js +1 -0
  19. package/build/types.js.map +1 -1
  20. package/build/useIAP.d.ts +6 -11
  21. package/build/useIAP.d.ts.map +1 -1
  22. package/build/useIAP.js +44 -15
  23. package/build/useIAP.js.map +1 -1
  24. package/build/utils/purchase.d.ts +9 -0
  25. package/build/utils/purchase.d.ts.map +1 -0
  26. package/build/utils/purchase.js +34 -0
  27. package/build/utils/purchase.js.map +1 -0
  28. package/package.json +2 -2
  29. package/plugin/build/withIAP.js +3 -3
  30. package/plugin/src/withIAP.ts +3 -3
  31. package/plugin/tsconfig.tsbuildinfo +1 -1
  32. package/src/index.ts +217 -255
  33. package/src/modules/android.ts +23 -22
  34. package/src/modules/ios.ts +123 -45
  35. package/src/types.ts +131 -85
  36. package/src/useIAP.ts +83 -42
  37. package/src/utils/purchase.ts +52 -0
  38. package/.copilot-instructions.md +0 -321
  39. package/.cursorrules +0 -321
  40. package/coverage/clover.xml +0 -440
  41. package/coverage/coverage-final.json +0 -7
  42. package/coverage/lcov-report/base.css +0 -224
  43. package/coverage/lcov-report/block-navigation.js +0 -87
  44. package/coverage/lcov-report/favicon.png +0 -0
  45. package/coverage/lcov-report/index.html +0 -161
  46. package/coverage/lcov-report/prettify.css +0 -1
  47. package/coverage/lcov-report/prettify.js +0 -2
  48. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  49. package/coverage/lcov-report/sorter.js +0 -196
  50. package/coverage/lcov-report/src/ExpoIap.types.ts.html +0 -1243
  51. package/coverage/lcov-report/src/PurchaseError.ts.html +0 -787
  52. package/coverage/lcov-report/src/helpers/index.html +0 -116
  53. package/coverage/lcov-report/src/helpers/subscription.ts.html +0 -496
  54. package/coverage/lcov-report/src/index.html +0 -131
  55. package/coverage/lcov-report/src/index.ts.html +0 -2236
  56. package/coverage/lcov-report/src/modules/android.ts.html +0 -544
  57. package/coverage/lcov-report/src/modules/index.html +0 -131
  58. package/coverage/lcov-report/src/modules/ios.ts.html +0 -952
  59. package/coverage/lcov-report/src/purchase-error.ts.html +0 -880
  60. package/coverage/lcov-report/src/types/ExpoIapAndroid.types.ts.html +0 -493
  61. package/coverage/lcov-report/src/types/index.html +0 -116
  62. package/coverage/lcov-report/src/useIap.ts.html +0 -1483
  63. package/coverage/lcov-report/src/utils/errorMapping.ts.html +0 -529
  64. package/coverage/lcov-report/src/utils/index.html +0 -116
  65. package/coverage/lcov.info +0 -852
  66. package/ios/expoiap.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -7
  67. package/ios/expoiap.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -8
package/src/index.ts CHANGED
@@ -17,25 +17,32 @@ import {
17
17
  } from './modules/android';
18
18
 
19
19
  // Types
20
- import {
20
+ import type {
21
+ AndroidSubscriptionOfferInput,
22
+ DeepLinkOptions,
23
+ FetchProductsResult,
24
+ MutationField,
25
+ MutationValidateReceiptArgs,
21
26
  Product,
27
+ ProductAndroid,
28
+ ProductIOS,
29
+ ProductQueryType,
30
+ ProductSubscription,
22
31
  Purchase,
23
- ErrorCode,
24
- RequestPurchaseProps,
32
+ PurchaseInput,
33
+ PurchaseOptions,
34
+ QueryField,
25
35
  RequestPurchasePropsByPlatforms,
26
36
  RequestPurchaseAndroidProps,
27
37
  RequestPurchaseIosProps,
28
38
  RequestSubscriptionPropsByPlatforms,
29
39
  RequestSubscriptionAndroidProps,
30
40
  RequestSubscriptionIosProps,
31
- ProductSubscription,
32
- PurchaseAndroid,
33
41
  DiscountOfferInputIOS,
34
- VoidResult,
35
- ReceiptValidationResult,
36
- AndroidSubscriptionOfferInput,
37
42
  } from './types';
43
+ import {ErrorCode} from './types';
38
44
  import {PurchaseError} from './purchase-error';
45
+ import {normalizePurchaseId, normalizePurchaseList} from './utils/purchase';
39
46
 
40
47
  // Export all types
41
48
  export * from './types';
@@ -90,32 +97,7 @@ export const emitter = (ExpoIapModule ||
90
97
  /**
91
98
  * TODO(v3.1.0): Remove legacy 'inapp' alias once downstream apps migrate to 'in-app'.
92
99
  */
93
- export type ProductTypeInput = 'inapp' | 'in-app' | 'subs';
94
- export type InAppTypeInput = Exclude<ProductTypeInput, 'subs'>;
95
-
96
- type PurchaseRequestInApp = {
97
- request: RequestPurchasePropsByPlatforms;
98
- type?: InAppTypeInput;
99
- };
100
-
101
- type PurchaseRequestSubscription = {
102
- request: RequestSubscriptionPropsByPlatforms;
103
- type: 'subs';
104
- };
105
-
106
- export type PurchaseRequestInput =
107
- | PurchaseRequestInApp
108
- | PurchaseRequestSubscription;
109
-
110
- export type PurchaseRequest =
111
- | {
112
- request: RequestPurchaseProps;
113
- type?: InAppTypeInput;
114
- }
115
- | {
116
- request: RequestSubscriptionPropsByPlatforms;
117
- type: 'subs';
118
- };
100
+ export type ProductTypeInput = ProductQueryType | 'inapp';
119
101
 
120
102
  const normalizeProductType = (type?: ProductTypeInput) => {
121
103
  if (type === 'inapp') {
@@ -126,16 +108,22 @@ const normalizeProductType = (type?: ProductTypeInput) => {
126
108
 
127
109
  if (!type || type === 'inapp' || type === 'in-app') {
128
110
  return {
129
- canonical: 'in-app' as const,
111
+ canonical: 'in-app' as ProductQueryType,
130
112
  native: 'inapp' as const,
131
113
  };
132
114
  }
133
115
  if (type === 'subs') {
134
116
  return {
135
- canonical: 'subs' as const,
117
+ canonical: 'subs' as ProductQueryType,
136
118
  native: 'subs' as const,
137
119
  };
138
120
  }
121
+ if (type === 'all') {
122
+ return {
123
+ canonical: 'all' as ProductQueryType,
124
+ native: 'all' as const,
125
+ };
126
+ }
139
127
  throw new Error(`Unsupported product type: ${type}`);
140
128
  };
141
129
 
@@ -144,8 +132,9 @@ export const purchaseUpdatedListener = (
144
132
  ) => {
145
133
  console.log('[JS] Registering purchaseUpdatedListener');
146
134
  const wrappedListener = (event: Purchase) => {
147
- console.log('[JS] purchaseUpdatedListener fired:', event);
148
- listener(event);
135
+ const normalized = normalizePurchaseId(event);
136
+ console.log('[JS] purchaseUpdatedListener fired:', normalized);
137
+ listener(normalized);
149
138
  };
150
139
  const emitterSubscription = emitter.addListener(
151
140
  OpenIapEvent.PurchaseUpdated,
@@ -203,112 +192,103 @@ export const promotedProductListenerIOS = (
203
192
  return emitter.addListener(OpenIapEvent.PromotedProductIOS, listener);
204
193
  };
205
194
 
206
- export function initConnection(): Promise<boolean> {
207
- const result = ExpoIapModule.initConnection();
208
- return Promise.resolve(result);
209
- }
195
+ export const initConnection: MutationField<'initConnection'> = async () =>
196
+ ExpoIapModule.initConnection();
210
197
 
211
- export async function endConnection(): Promise<boolean> {
212
- return ExpoIapModule.endConnection();
213
- }
198
+ export const endConnection: MutationField<'endConnection'> = async () =>
199
+ ExpoIapModule.endConnection();
214
200
 
215
201
  /**
216
202
  * Fetch products with unified API (v2.7.0+)
217
203
  *
218
- * @param params - Product fetch configuration
219
- * @param params.skus - Array of product SKUs to fetch
220
- * @param params.type - Type of products: 'in-app' for regular products (default) or 'subs' for subscriptions
221
- *
222
- * @example
223
- * ```typescript
224
- * // Regular products
225
- * const products = await fetchProducts({
226
- * skus: ['product1', 'product2'],
227
- * type: 'in-app'
228
- * });
229
- *
230
- * // Subscriptions
231
- * const subscriptions = await fetchProducts({
232
- * skus: ['sub1', 'sub2'],
233
- * type: 'subs'
234
- * });
235
- * ```
204
+ * @param request - Product fetch configuration
205
+ * @param request.skus - Array of product SKUs to fetch
206
+ * @param request.type - Product query type: 'in-app', 'subs', or 'all'
236
207
  */
237
- export const fetchProducts = async ({
238
- skus,
239
- type,
240
- }: {
241
- skus: string[];
242
- type?: ProductTypeInput;
243
- }): Promise<Product[] | ProductSubscription[]> => {
244
- if (!skus?.length) {
208
+ export const fetchProducts: QueryField<'fetchProducts'> = async (request) => {
209
+ const {skus, type} = request ?? {};
210
+
211
+ if (!Array.isArray(skus) || skus.length === 0) {
245
212
  throw new PurchaseError({
246
213
  message: 'No SKUs provided',
247
214
  code: ErrorCode.EmptySkuList,
248
215
  });
249
216
  }
250
217
 
251
- const {canonical, native} = normalizeProductType(type);
252
-
253
- if (Platform.OS === 'ios') {
254
- const rawItems = await ExpoIapModule.fetchProducts({skus, type: native});
218
+ const {canonical, native} = normalizeProductType(
219
+ type as ProductTypeInput | undefined,
220
+ );
221
+ const skuSet = new Set(skus);
255
222
 
256
- const filteredItems = rawItems.filter((item: unknown) => {
223
+ const filterIosItems = (
224
+ items: unknown[],
225
+ ): Product[] | ProductSubscription[] =>
226
+ items.filter((item): item is ProductIOS | ProductSubscription => {
257
227
  if (!isProductIOS(item)) {
258
228
  return false;
259
229
  }
260
- const isValid =
261
- typeof item === 'object' &&
262
- item !== null &&
263
- 'id' in item &&
264
- typeof item.id === 'string' &&
265
- skus.includes(item.id);
266
- return isValid;
230
+ const candidate = item as ProductIOS | ProductSubscription;
231
+ return typeof candidate.id === 'string' && skuSet.has(candidate.id);
267
232
  });
268
233
 
269
- return canonical === 'in-app'
270
- ? (filteredItems as Product[])
271
- : (filteredItems as ProductSubscription[]);
234
+ const filterAndroidItems = (
235
+ items: unknown[],
236
+ ): Product[] | ProductSubscription[] =>
237
+ items.filter((item): item is ProductAndroid | ProductSubscription => {
238
+ if (!isProductAndroid(item)) {
239
+ return false;
240
+ }
241
+ const candidate = item as ProductAndroid | ProductSubscription;
242
+ return typeof candidate.id === 'string' && skuSet.has(candidate.id);
243
+ });
244
+
245
+ const castResult = (
246
+ items: Product[] | ProductSubscription[],
247
+ ): FetchProductsResult => {
248
+ if (canonical === 'in-app') {
249
+ return items as Product[];
250
+ }
251
+ if (canonical === 'subs') {
252
+ return items as ProductSubscription[];
253
+ }
254
+ return items;
255
+ };
256
+
257
+ if (Platform.OS === 'ios') {
258
+ const rawItems = await ExpoIapModule.fetchProducts({skus, type: native});
259
+ return castResult(filterIosItems(rawItems));
272
260
  }
273
261
 
274
262
  if (Platform.OS === 'android') {
275
- const items = await ExpoIapModule.fetchProducts(native, skus);
276
- const filteredItems = items.filter((item: unknown) => {
277
- if (!isProductAndroid(item)) return false;
278
- return (
279
- typeof item === 'object' &&
280
- item !== null &&
281
- 'id' in item &&
282
- typeof item.id === 'string' &&
283
- skus.includes(item.id)
284
- );
285
- });
286
-
287
- return canonical === 'in-app'
288
- ? (filteredItems as Product[])
289
- : (filteredItems as ProductSubscription[]);
263
+ const rawItems = await ExpoIapModule.fetchProducts(native, skus);
264
+ return castResult(filterAndroidItems(rawItems));
290
265
  }
291
266
 
292
267
  throw new Error('Unsupported platform');
293
268
  };
294
269
 
295
- export const getAvailablePurchases = ({
296
- alsoPublishToEventListenerIOS = false,
297
- onlyIncludeActiveItemsIOS = true,
298
- }: {
299
- alsoPublishToEventListenerIOS?: boolean;
300
- onlyIncludeActiveItemsIOS?: boolean;
301
- } = {}): Promise<Purchase[]> =>
302
- (
270
+ export const getAvailablePurchases: QueryField<
271
+ 'getAvailablePurchases'
272
+ > = async (options) => {
273
+ const normalizedOptions: PurchaseOptions = {
274
+ alsoPublishToEventListenerIOS:
275
+ options?.alsoPublishToEventListenerIOS ?? false,
276
+ onlyIncludeActiveItemsIOS: options?.onlyIncludeActiveItemsIOS ?? true,
277
+ };
278
+
279
+ const resolvePurchases: () => Promise<Purchase[]> =
303
280
  Platform.select({
304
281
  ios: () =>
305
282
  ExpoIapModule.getAvailableItems(
306
- alsoPublishToEventListenerIOS,
307
- onlyIncludeActiveItemsIOS,
308
- ),
309
- android: () => ExpoIapModule.getAvailableItems(),
310
- }) || (() => Promise.resolve([]))
311
- )();
283
+ normalizedOptions.alsoPublishToEventListenerIOS,
284
+ normalizedOptions.onlyIncludeActiveItemsIOS,
285
+ ) as Promise<Purchase[]>,
286
+ android: () => ExpoIapModule.getAvailableItems() as Promise<Purchase[]>,
287
+ }) ?? (() => Promise.resolve([] as Purchase[]));
288
+
289
+ const purchases = await resolvePurchases();
290
+ return normalizePurchaseList(purchases);
291
+ };
312
292
 
313
293
  /**
314
294
  * Restore completed transactions (cross-platform behavior)
@@ -323,25 +303,15 @@ export const getAvailablePurchases = ({
323
303
  * @param options.onlyIncludeActiveItemsIOS - iOS only: whether to only include active items
324
304
  * @returns Promise resolving to the list of available/restored purchases
325
305
  */
326
- export const restorePurchases = async (
327
- options: {
328
- alsoPublishToEventListenerIOS?: boolean;
329
- onlyIncludeActiveItemsIOS?: boolean;
330
- } = {},
331
- ): Promise<Purchase[]> => {
306
+ export const restorePurchases: MutationField<'restorePurchases'> = async () => {
332
307
  if (Platform.OS === 'ios') {
333
- // Perform best-effort sync on iOS and ignore sync errors to avoid blocking restore flow
334
308
  await syncIOS().catch(() => undefined);
335
309
  }
336
310
 
337
- // Then, fetch available purchases for both platforms
338
- const purchases = await getAvailablePurchases({
339
- alsoPublishToEventListenerIOS:
340
- options.alsoPublishToEventListenerIOS ?? false,
341
- onlyIncludeActiveItemsIOS: options.onlyIncludeActiveItemsIOS ?? true,
311
+ await getAvailablePurchases({
312
+ alsoPublishToEventListenerIOS: false,
313
+ onlyIncludeActiveItemsIOS: true,
342
314
  });
343
-
344
- return purchases;
345
315
  };
346
316
 
347
317
  const offerToRecordIOS = (
@@ -417,11 +387,11 @@ function normalizeRequestProps(
417
387
  * });
418
388
  * ```
419
389
  */
420
- export const requestPurchase = (
421
- requestObj: PurchaseRequestInput,
422
- ): Promise<Purchase | Purchase[] | void> => {
423
- const {request, type} = requestObj;
424
- const {canonical, native} = normalizeProductType(type);
390
+ export const requestPurchase: MutationField<'requestPurchase'> = async (
391
+ args,
392
+ ) => {
393
+ const {request, type} = args;
394
+ const {canonical, native} = normalizeProductType(type as ProductTypeInput);
425
395
  const isInAppPurchase = canonical === 'in-app';
426
396
 
427
397
  if (Platform.OS === 'ios') {
@@ -441,27 +411,24 @@ export const requestPurchase = (
441
411
  withOffer,
442
412
  } = normalizedRequest;
443
413
 
444
- return (async () => {
445
- const offer = offerToRecordIOS(withOffer ?? undefined);
446
- const purchase = await ExpoIapModule.requestPurchase({
447
- sku,
448
- andDangerouslyFinishTransactionAutomatically,
449
- appAccountToken,
450
- quantity,
451
- withOffer: offer,
452
- });
414
+ const offer = offerToRecordIOS(withOffer ?? undefined);
415
+ const purchase = await ExpoIapModule.requestPurchase({
416
+ sku,
417
+ andDangerouslyFinishTransactionAutomatically,
418
+ appAccountToken,
419
+ quantity,
420
+ withOffer: offer,
421
+ });
453
422
 
454
- return purchase as Purchase;
455
- })();
423
+ return normalizePurchaseId(purchase as Purchase);
456
424
  }
457
425
 
458
426
  if (Platform.OS === 'android') {
459
427
  if (isInAppPurchase) {
460
- const normalizedRequest: RequestPurchaseAndroidProps | null | undefined =
461
- normalizeRequestProps(
462
- request as RequestPurchasePropsByPlatforms,
463
- 'android',
464
- );
428
+ const normalizedRequest = normalizeRequestProps(
429
+ request as RequestPurchasePropsByPlatforms,
430
+ 'android',
431
+ ) as RequestPurchaseAndroidProps | null | undefined;
465
432
 
466
433
  if (!normalizedRequest?.skus?.length) {
467
434
  throw new Error(
@@ -476,28 +443,25 @@ export const requestPurchase = (
476
443
  isOfferPersonalized,
477
444
  } = normalizedRequest;
478
445
 
479
- return (async () => {
480
- return ExpoIapModule.requestPurchase({
481
- type: native,
482
- skuArr: skus,
483
- purchaseToken: undefined,
484
- replacementMode: -1,
485
- obfuscatedAccountId: obfuscatedAccountIdAndroid,
486
- obfuscatedProfileId: obfuscatedProfileIdAndroid,
487
- offerTokenArr: [],
488
- isOfferPersonalized: isOfferPersonalized ?? false,
489
- }) as Promise<Purchase[]>;
490
- })();
446
+ const result = (await ExpoIapModule.requestPurchase({
447
+ type: native,
448
+ skuArr: skus,
449
+ purchaseToken: undefined,
450
+ replacementMode: -1,
451
+ obfuscatedAccountId: obfuscatedAccountIdAndroid,
452
+ obfuscatedProfileId: obfuscatedProfileIdAndroid,
453
+ offerTokenArr: [],
454
+ isOfferPersonalized: isOfferPersonalized ?? false,
455
+ })) as Purchase[];
456
+
457
+ return normalizePurchaseList(result);
491
458
  }
492
459
 
493
460
  if (canonical === 'subs') {
494
- const normalizedRequest:
495
- | RequestSubscriptionAndroidProps
496
- | null
497
- | undefined = normalizeRequestProps(
461
+ const normalizedRequest = normalizeRequestProps(
498
462
  request as RequestSubscriptionPropsByPlatforms,
499
463
  'android',
500
- );
464
+ ) as RequestSubscriptionAndroidProps | null | undefined;
501
465
 
502
466
  if (!normalizedRequest?.skus?.length) {
503
467
  throw new Error(
@@ -519,26 +483,21 @@ export const requestPurchase = (
519
483
  const replacementMode = replacementModeAndroid ?? -1;
520
484
  const purchaseToken = purchaseTokenAndroid ?? undefined;
521
485
 
522
- return (async () => {
523
- return ExpoIapModule.requestPurchase({
524
- type: native,
525
- skuArr: skus,
526
- purchaseToken,
527
- replacementMode,
528
- obfuscatedAccountId: obfuscatedAccountIdAndroid,
529
- obfuscatedProfileId: obfuscatedProfileIdAndroid,
530
- offerTokenArr: normalizedOffers.map(
531
- (so: AndroidSubscriptionOfferInput) => so.offerToken,
532
- ),
533
- subscriptionOffers: normalizedOffers.map(
534
- (so: AndroidSubscriptionOfferInput) => ({
535
- sku: so.sku,
536
- offerToken: so.offerToken,
537
- }),
538
- ),
539
- isOfferPersonalized: isOfferPersonalized ?? false,
540
- }) as Promise<Purchase[]>;
541
- })();
486
+ const result = (await ExpoIapModule.requestPurchase({
487
+ type: native,
488
+ skuArr: skus,
489
+ purchaseToken,
490
+ replacementMode,
491
+ obfuscatedAccountId: obfuscatedAccountIdAndroid,
492
+ obfuscatedProfileId: obfuscatedProfileIdAndroid,
493
+ offerTokenArr: normalizedOffers.map(
494
+ (offer: AndroidSubscriptionOfferInput) => offer.offerToken,
495
+ ),
496
+ subscriptionOffers: normalizedOffers,
497
+ isOfferPersonalized: isOfferPersonalized ?? false,
498
+ })) as Purchase[];
499
+
500
+ return normalizePurchaseList(result);
542
501
  }
543
502
 
544
503
  throw new Error(
@@ -546,53 +505,60 @@ export const requestPurchase = (
546
505
  );
547
506
  }
548
507
 
549
- return Promise.resolve(); // Fallback for unsupported platforms
508
+ throw new Error('Platform not supported');
550
509
  };
551
510
 
552
- export const finishTransaction = ({
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
+ export const finishTransaction: MutationField<'finishTransaction'> = async ({
553
526
  purchase,
554
527
  isConsumable = false,
555
- }: {
556
- purchase: Purchase;
557
- isConsumable?: boolean;
558
- }): Promise<VoidResult | boolean> => {
559
- return (
560
- Platform.select({
561
- ios: async () => {
562
- const transactionId = purchase.id;
563
- if (!transactionId) {
564
- return Promise.reject(
565
- new Error('purchase.id required to finish iOS transaction'),
566
- );
567
- }
568
- await ExpoIapModule.finishTransaction(transactionId);
569
- return Promise.resolve(true);
570
- },
571
- android: async () => {
572
- const androidPurchase = purchase as PurchaseAndroid;
573
-
574
- // Use purchaseToken if available, fallback to purchaseTokenAndroid for backward compatibility
575
- const token = androidPurchase.purchaseToken;
576
-
577
- if (!token) {
578
- return Promise.reject(
579
- new PurchaseError({
580
- message: 'Purchase token is required to finish transaction',
581
- code: ErrorCode.DeveloperError,
582
- productId: androidPurchase.productId,
583
- platform: 'android',
584
- }),
585
- );
586
- }
587
-
588
- if (isConsumable) {
589
- return ExpoIapModule.consumePurchaseAndroid(token);
590
- }
591
-
592
- return ExpoIapModule.acknowledgePurchaseAndroid(token);
593
- },
594
- }) || (() => Promise.reject(new Error('Unsupported Platform')))
595
- )();
528
+ }) => {
529
+ const normalizedPurchase = toPurchaseInput(purchase);
530
+
531
+ 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);
537
+ return;
538
+ }
539
+
540
+ if (Platform.OS === 'android') {
541
+ const token = normalizedPurchase.purchaseToken ?? undefined;
542
+
543
+ if (!token) {
544
+ throw new PurchaseError({
545
+ message: 'Purchase token is required to finish transaction',
546
+ code: ErrorCode.DeveloperError,
547
+ productId: normalizedPurchase.productId,
548
+ platform: 'android',
549
+ });
550
+ }
551
+
552
+ if (isConsumable) {
553
+ await ExpoIapModule.consumePurchaseAndroid(token);
554
+ return;
555
+ }
556
+
557
+ await ExpoIapModule.acknowledgePurchaseAndroid(token);
558
+ return;
559
+ }
560
+
561
+ throw new Error('Unsupported Platform');
596
562
  };
597
563
 
598
564
  /**
@@ -643,18 +609,16 @@ export const getStorefront = (): Promise<string> => {
643
609
  * - iOS: Send receipt data to Apple's verification endpoint from your server
644
610
  * - Android: Use Google Play Developer API with service account credentials
645
611
  */
646
- export const validateReceipt = async (
647
- sku: string,
648
- androidOptions?: {
649
- packageName: string;
650
- productToken: string;
651
- accessToken: string;
652
- isSub?: boolean;
653
- },
654
- ): Promise<ReceiptValidationResult> => {
612
+ export const validateReceipt: MutationField<'validateReceipt'> = async (
613
+ options,
614
+ ) => {
615
+ const {sku, androidOptions} = options as MutationValidateReceiptArgs;
616
+
655
617
  if (Platform.OS === 'ios') {
656
- return await validateReceiptIOS(sku);
657
- } else if (Platform.OS === 'android') {
618
+ return validateReceiptIOS({sku});
619
+ }
620
+
621
+ if (Platform.OS === 'android') {
658
622
  if (
659
623
  !androidOptions ||
660
624
  !androidOptions.packageName ||
@@ -665,16 +629,16 @@ export const validateReceipt = async (
665
629
  'Android validation requires packageName, productToken, and accessToken',
666
630
  );
667
631
  }
668
- return await validateReceiptAndroid({
632
+ return validateReceiptAndroid({
669
633
  packageName: androidOptions.packageName,
670
634
  productId: sku,
671
635
  productToken: androidOptions.productToken,
672
636
  accessToken: androidOptions.accessToken,
673
- isSub: androidOptions.isSub,
637
+ isSub: androidOptions.isSub ?? undefined,
674
638
  });
675
- } else {
676
- throw new Error('Platform not supported');
677
639
  }
640
+
641
+ throw new Error('Platform not supported');
678
642
  };
679
643
 
680
644
  /**
@@ -695,22 +659,20 @@ export const validateReceipt = async (
695
659
  * packageNameAndroid: 'com.example.app'
696
660
  * });
697
661
  */
698
- export const deepLinkToSubscriptions = (options: {
699
- skuAndroid?: string;
700
- packageNameAndroid?: string;
701
- }): Promise<void> => {
662
+ export const deepLinkToSubscriptions: MutationField<
663
+ 'deepLinkToSubscriptions'
664
+ > = async (options) => {
702
665
  if (Platform.OS === 'ios') {
703
- return deepLinkToSubscriptionsIOS();
666
+ await deepLinkToSubscriptionsIOS();
667
+ return;
704
668
  }
705
669
 
706
670
  if (Platform.OS === 'android') {
707
- return deepLinkToSubscriptionsAndroid({
708
- sku: options?.skuAndroid,
709
- packageName: options?.packageNameAndroid,
710
- });
671
+ await deepLinkToSubscriptionsAndroid((options as DeepLinkOptions) ?? null);
672
+ return;
711
673
  }
712
674
 
713
- return Promise.reject(new Error(`Unsupported platform: ${Platform.OS}`));
675
+ throw new Error(`Unsupported platform: ${Platform.OS}`);
714
676
  };
715
677
 
716
678
  export * from './useIAP';