expo-iap 3.0.3 → 3.0.5

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 (70) hide show
  1. package/.eslintignore +1 -1
  2. package/.eslintrc.js +1 -0
  3. package/.prettierignore +1 -0
  4. package/CHANGELOG.md +13 -0
  5. package/CLAUDE.md +2 -0
  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/helpers/subscription.d.ts +1 -12
  11. package/build/helpers/subscription.d.ts.map +1 -1
  12. package/build/helpers/subscription.js +12 -7
  13. package/build/helpers/subscription.js.map +1 -1
  14. package/build/index.d.ts +34 -18
  15. package/build/index.d.ts.map +1 -1
  16. package/build/index.js +40 -17
  17. package/build/index.js.map +1 -1
  18. package/build/modules/android.d.ts +5 -5
  19. package/build/modules/android.d.ts.map +1 -1
  20. package/build/modules/android.js +17 -4
  21. package/build/modules/android.js.map +1 -1
  22. package/build/modules/ios.d.ts +4 -8
  23. package/build/modules/ios.d.ts.map +1 -1
  24. package/build/modules/ios.js.map +1 -1
  25. package/build/purchase-error.d.ts +67 -0
  26. package/build/purchase-error.d.ts.map +1 -0
  27. package/build/purchase-error.js +166 -0
  28. package/build/purchase-error.js.map +1 -0
  29. package/build/types.d.ts +604 -0
  30. package/build/types.d.ts.map +1 -0
  31. package/build/types.js +42 -0
  32. package/build/types.js.map +1 -0
  33. package/build/useIAP.d.ts +8 -10
  34. package/build/useIAP.d.ts.map +1 -1
  35. package/build/useIAP.js +1 -1
  36. package/build/useIAP.js.map +1 -1
  37. package/build/utils/errorMapping.d.ts +1 -1
  38. package/build/utils/errorMapping.d.ts.map +1 -1
  39. package/build/utils/errorMapping.js +19 -3
  40. package/build/utils/errorMapping.js.map +1 -1
  41. package/ios/ExpoIap.podspec +1 -1
  42. package/ios/ExpoIapModule.swift +103 -89
  43. package/jest.config.js +1 -1
  44. package/package.json +2 -1
  45. package/plugin/build/withIAP.js +4 -5
  46. package/plugin/src/withIAP.ts +4 -5
  47. package/scripts/update-types.mjs +61 -0
  48. package/src/helpers/subscription.ts +12 -20
  49. package/src/index.ts +89 -41
  50. package/src/modules/android.ts +24 -8
  51. package/src/modules/ios.ts +7 -11
  52. package/src/purchase-error.ts +265 -0
  53. package/src/types.ts +705 -0
  54. package/src/useIAP.ts +16 -16
  55. package/src/utils/errorMapping.ts +24 -3
  56. package/build/ExpoIap.types.d.ts +0 -307
  57. package/build/ExpoIap.types.d.ts.map +0 -1
  58. package/build/ExpoIap.types.js +0 -235
  59. package/build/ExpoIap.types.js.map +0 -1
  60. package/build/types/ExpoIapAndroid.types.d.ts +0 -114
  61. package/build/types/ExpoIapAndroid.types.d.ts.map +0 -1
  62. package/build/types/ExpoIapAndroid.types.js +0 -29
  63. package/build/types/ExpoIapAndroid.types.js.map +0 -1
  64. package/build/types/ExpoIapIOS.types.d.ts +0 -149
  65. package/build/types/ExpoIapIOS.types.d.ts.map +0 -1
  66. package/build/types/ExpoIapIOS.types.js +0 -8
  67. package/build/types/ExpoIapIOS.types.js.map +0 -1
  68. package/src/ExpoIap.types.ts +0 -444
  69. package/src/types/ExpoIapAndroid.types.ts +0 -133
  70. package/src/types/ExpoIapIOS.types.ts +0 -172
@@ -1,18 +1,6 @@
1
1
  import {Platform} from 'react-native';
2
2
  import {getAvailablePurchases} from '../index';
3
-
4
- export interface ActiveSubscription {
5
- productId: string;
6
- isActive: boolean;
7
- transactionId: string; // Transaction identifier for backend validation
8
- purchaseToken?: string; // JWT token (iOS) or purchase token (Android) for backend validation
9
- transactionDate: number; // Transaction timestamp
10
- expirationDateIOS?: Date;
11
- autoRenewingAndroid?: boolean;
12
- environmentIOS?: string;
13
- willExpireSoon?: boolean;
14
- daysUntilExpirationIOS?: number;
15
- }
3
+ import type {ActiveSubscription} from '../types';
16
4
 
17
5
  /**
18
6
  * Get all active subscriptions with detailed information
@@ -91,8 +79,7 @@ export const getActiveSubscriptions = async (
91
79
  const subscription: ActiveSubscription = {
92
80
  productId: purchase.productId,
93
81
  isActive: true,
94
- // Use unified id as transaction identifier in v3
95
- transactionId: purchase.id,
82
+ transactionId: String(purchase.id),
96
83
  purchaseToken: purchase.purchaseToken,
97
84
  transactionDate: purchase.transactionDate,
98
85
  };
@@ -100,8 +87,7 @@ export const getActiveSubscriptions = async (
100
87
  // Add platform-specific details
101
88
  if (Platform.OS === 'ios') {
102
89
  if ('expirationDateIOS' in purchase && purchase.expirationDateIOS) {
103
- const expirationDate = new Date(purchase.expirationDateIOS);
104
- subscription.expirationDateIOS = expirationDate;
90
+ subscription.expirationDateIOS = purchase.expirationDateIOS;
105
91
 
106
92
  // Calculate days until expiration (round to nearest day)
107
93
  const daysUntilExpiration = Math.round(
@@ -112,13 +98,19 @@ export const getActiveSubscriptions = async (
112
98
  }
113
99
 
114
100
  if ('environmentIOS' in purchase) {
115
- subscription.environmentIOS = purchase.environmentIOS;
101
+ subscription.environmentIOS = purchase.environmentIOS ?? undefined;
116
102
  }
117
103
  } else if (Platform.OS === 'android') {
118
104
  if ('autoRenewingAndroid' in purchase) {
119
- subscription.autoRenewingAndroid = purchase.autoRenewingAndroid;
105
+ if (typeof purchase.autoRenewingAndroid !== 'undefined') {
106
+ subscription.autoRenewingAndroid = purchase.autoRenewingAndroid;
107
+ }
120
108
  // If auto-renewing is false, subscription will expire soon
121
- subscription.willExpireSoon = !purchase.autoRenewingAndroid;
109
+ if (purchase.autoRenewingAndroid === false) {
110
+ subscription.willExpireSoon = true;
111
+ } else if (purchase.autoRenewingAndroid === true) {
112
+ subscription.willExpireSoon = false;
113
+ }
122
114
  }
123
115
  }
124
116
 
package/src/index.ts CHANGED
@@ -20,19 +20,21 @@ import {
20
20
  import {
21
21
  Product,
22
22
  Purchase,
23
- PurchaseError,
24
23
  ErrorCode,
25
- PurchaseResult,
26
- RequestSubscriptionProps,
27
24
  RequestPurchaseProps,
25
+ RequestPurchasePropsByPlatforms,
26
+ RequestSubscriptionPropsByPlatforms,
28
27
  ProductSubscription,
29
- // Bring platform types from the barrel to avoid deep imports
30
28
  PurchaseAndroid,
31
- PaymentDiscount,
32
- } from './ExpoIap.types';
29
+ DiscountOfferInputIOS,
30
+ VoidResult,
31
+ ReceiptValidationResult,
32
+ } from './types';
33
+ import {PurchaseError} from './purchase-error';
33
34
 
34
35
  // Export all types
35
- export * from './ExpoIap.types';
36
+ export * from './types';
37
+ export {ErrorCodeUtils, ErrorCodeMapping} from './purchase-error';
36
38
  export * from './modules/android';
37
39
  export * from './modules/ios';
38
40
 
@@ -40,7 +42,6 @@ export * from './modules/ios';
40
42
  export {
41
43
  getActiveSubscriptions,
42
44
  hasActiveSubscriptions,
43
- type ActiveSubscription,
44
45
  } from './helpers/subscription';
45
46
 
46
47
  // Get the native constant value
@@ -68,6 +69,58 @@ export const emitter = (ExpoIapModule || NativeModulesProxy.ExpoIap) as {
68
69
  ) => void;
69
70
  };
70
71
 
72
+ /**
73
+ * TODO(v3.1.0): Remove legacy 'inapp' alias once downstream apps migrate to 'in-app'.
74
+ */
75
+ export type ProductTypeInput = 'inapp' | 'in-app' | 'subs';
76
+ export type InAppTypeInput = Exclude<ProductTypeInput, 'subs'>;
77
+
78
+ type PurchaseRequestInApp = {
79
+ request: RequestPurchasePropsByPlatforms;
80
+ type?: InAppTypeInput;
81
+ };
82
+
83
+ type PurchaseRequestSubscription = {
84
+ request: RequestSubscriptionPropsByPlatforms;
85
+ type: 'subs';
86
+ };
87
+
88
+ export type PurchaseRequestInput =
89
+ | PurchaseRequestInApp
90
+ | PurchaseRequestSubscription;
91
+
92
+ export type PurchaseRequest =
93
+ | {
94
+ request: RequestPurchaseProps;
95
+ type?: InAppTypeInput;
96
+ }
97
+ | {
98
+ request: RequestSubscriptionPropsByPlatforms;
99
+ type: 'subs';
100
+ };
101
+
102
+ const normalizeProductType = (type?: ProductTypeInput) => {
103
+ if (type === 'inapp') {
104
+ console.warn(
105
+ "expo-iap: 'inapp' product type is deprecated and will be removed in v3.1.0. Use 'in-app' instead.",
106
+ );
107
+ }
108
+
109
+ if (!type || type === 'inapp' || type === 'in-app') {
110
+ return {
111
+ canonical: 'in-app' as const,
112
+ native: 'inapp' as const,
113
+ };
114
+ }
115
+ if (type === 'subs') {
116
+ return {
117
+ canonical: 'subs' as const,
118
+ native: 'subs' as const,
119
+ };
120
+ }
121
+ throw new Error(`Unsupported product type: ${type}`);
122
+ };
123
+
71
124
  export const purchaseUpdatedListener = (
72
125
  listener: (event: Purchase) => void,
73
126
  ) => {
@@ -146,14 +199,14 @@ export async function endConnection(): Promise<boolean> {
146
199
  *
147
200
  * @param params - Product fetch configuration
148
201
  * @param params.skus - Array of product SKUs to fetch
149
- * @param params.type - Type of products: 'inapp' for regular products (default) or 'subs' for subscriptions
202
+ * @param params.type - Type of products: 'in-app' for regular products (default) or 'subs' for subscriptions
150
203
  *
151
204
  * @example
152
205
  * ```typescript
153
206
  * // Regular products
154
207
  * const products = await fetchProducts({
155
208
  * skus: ['product1', 'product2'],
156
- * type: 'inapp'
209
+ * type: 'in-app'
157
210
  * });
158
211
  *
159
212
  * // Subscriptions
@@ -165,10 +218,10 @@ export async function endConnection(): Promise<boolean> {
165
218
  */
166
219
  export const fetchProducts = async ({
167
220
  skus,
168
- type = 'inapp',
221
+ type,
169
222
  }: {
170
223
  skus: string[];
171
- type?: 'inapp' | 'subs';
224
+ type?: ProductTypeInput;
172
225
  }): Promise<Product[] | ProductSubscription[]> => {
173
226
  if (!skus?.length) {
174
227
  throw new PurchaseError({
@@ -177,8 +230,10 @@ export const fetchProducts = async ({
177
230
  });
178
231
  }
179
232
 
233
+ const {canonical, native} = normalizeProductType(type);
234
+
180
235
  if (Platform.OS === 'ios') {
181
- const rawItems = await ExpoIapModule.fetchProducts({skus, type});
236
+ const rawItems = await ExpoIapModule.fetchProducts({skus, type: native});
182
237
 
183
238
  const filteredItems = rawItems.filter((item: unknown) => {
184
239
  if (!isProductIOS(item)) {
@@ -193,13 +248,13 @@ export const fetchProducts = async ({
193
248
  return isValid;
194
249
  });
195
250
 
196
- return type === 'inapp'
251
+ return canonical === 'in-app'
197
252
  ? (filteredItems as Product[])
198
253
  : (filteredItems as ProductSubscription[]);
199
254
  }
200
255
 
201
256
  if (Platform.OS === 'android') {
202
- const items = await ExpoIapModule.fetchProducts(type, skus);
257
+ const items = await ExpoIapModule.fetchProducts(native, skus);
203
258
  const filteredItems = items.filter((item: unknown) => {
204
259
  if (!isProductAndroid(item)) return false;
205
260
  return (
@@ -211,7 +266,7 @@ export const fetchProducts = async ({
211
266
  );
212
267
  });
213
268
 
214
- return type === 'inapp'
269
+ return canonical === 'in-app'
215
270
  ? (filteredItems as Product[])
216
271
  : (filteredItems as ProductSubscription[]);
217
272
  }
@@ -272,8 +327,8 @@ export const restorePurchases = async (
272
327
  };
273
328
 
274
329
  const offerToRecordIOS = (
275
- offer: PaymentDiscount | undefined,
276
- ): Record<keyof PaymentDiscount, string> | undefined => {
330
+ offer: DiscountOfferInputIOS | undefined,
331
+ ): Record<keyof DiscountOfferInputIOS, string> | undefined => {
277
332
  if (!offer) return undefined;
278
333
  return {
279
334
  identifier: offer.identifier,
@@ -284,22 +339,13 @@ const offerToRecordIOS = (
284
339
  };
285
340
  };
286
341
 
287
- // Define discriminated union with explicit type parameter
288
- type PurchaseRequest =
289
- | {
290
- request: RequestPurchaseProps;
291
- type?: 'inapp';
292
- }
293
- | {
294
- request: RequestSubscriptionProps;
295
- type: 'subs';
296
- };
297
-
298
342
  /**
299
343
  * Helper to normalize request props to platform-specific format
300
344
  */
301
345
  const normalizeRequestProps = (
302
- request: RequestPurchaseProps | RequestSubscriptionProps,
346
+ request:
347
+ | RequestPurchasePropsByPlatforms
348
+ | RequestSubscriptionPropsByPlatforms,
303
349
  platform: 'ios' | 'android',
304
350
  ): any => {
305
351
  // Platform-specific format - directly return the appropriate platform data
@@ -311,7 +357,7 @@ const normalizeRequestProps = (
311
357
  *
312
358
  * @param requestObj - Purchase request configuration
313
359
  * @param requestObj.request - Platform-specific purchase parameters
314
- * @param requestObj.type - Type of purchase: 'inapp' for products (default) or 'subs' for subscriptions
360
+ * @param requestObj.type - Type of purchase: 'in-app' for products (default) or 'subs' for subscriptions
315
361
  *
316
362
  * @example
317
363
  * ```typescript
@@ -321,7 +367,7 @@ const normalizeRequestProps = (
321
367
  * ios: { sku: productId },
322
368
  * android: { skus: [productId] }
323
369
  * },
324
- * type: 'inapp'
370
+ * type: 'in-app'
325
371
  * });
326
372
  *
327
373
  * // Subscription purchase
@@ -338,9 +384,11 @@ const normalizeRequestProps = (
338
384
  * ```
339
385
  */
340
386
  export const requestPurchase = (
341
- requestObj: PurchaseRequest,
387
+ requestObj: PurchaseRequestInput,
342
388
  ): Promise<Purchase | Purchase[] | void> => {
343
- const {request, type = 'inapp'} = requestObj;
389
+ const {request, type} = requestObj;
390
+ const {canonical, native} = normalizeProductType(type);
391
+ const isInAppPurchase = canonical === 'in-app';
344
392
 
345
393
  if (Platform.OS === 'ios') {
346
394
  const normalizedRequest = normalizeRequestProps(request, 'ios');
@@ -369,7 +417,7 @@ export const requestPurchase = (
369
417
  withOffer: offer,
370
418
  });
371
419
 
372
- return type === 'inapp' ? (purchase as Purchase) : (purchase as Purchase);
420
+ return purchase as Purchase;
373
421
  })();
374
422
  }
375
423
 
@@ -382,7 +430,7 @@ export const requestPurchase = (
382
430
  );
383
431
  }
384
432
 
385
- if (type === 'inapp') {
433
+ if (isInAppPurchase) {
386
434
  const {
387
435
  skus,
388
436
  obfuscatedAccountIdAndroid,
@@ -392,7 +440,7 @@ export const requestPurchase = (
392
440
 
393
441
  return (async () => {
394
442
  return ExpoIapModule.requestPurchase({
395
- type: 'inapp',
443
+ type: native,
396
444
  skuArr: skus,
397
445
  purchaseToken: undefined,
398
446
  replacementMode: -1,
@@ -404,7 +452,7 @@ export const requestPurchase = (
404
452
  })();
405
453
  }
406
454
 
407
- if (type === 'subs') {
455
+ if (canonical === 'subs') {
408
456
  const {
409
457
  skus,
410
458
  obfuscatedAccountIdAndroid,
@@ -417,7 +465,7 @@ export const requestPurchase = (
417
465
 
418
466
  return (async () => {
419
467
  return ExpoIapModule.requestPurchase({
420
- type: 'subs',
468
+ type: native,
421
469
  skuArr: skus,
422
470
  purchaseToken,
423
471
  replacementMode: replacementModeAndroid,
@@ -443,7 +491,7 @@ export const finishTransaction = ({
443
491
  }: {
444
492
  purchase: Purchase;
445
493
  isConsumable?: boolean;
446
- }): Promise<PurchaseResult | boolean> => {
494
+ }): Promise<VoidResult | boolean> => {
447
495
  return (
448
496
  Platform.select({
449
497
  ios: async () => {
@@ -539,7 +587,7 @@ export const validateReceipt = async (
539
587
  accessToken: string;
540
588
  isSub?: boolean;
541
589
  },
542
- ): Promise<any> => {
590
+ ): Promise<ReceiptValidationResult> => {
543
591
  if (Platform.OS === 'ios') {
544
592
  return await validateReceiptIOS(sku);
545
593
  } else if (Platform.OS === 'android') {
@@ -5,7 +5,7 @@ import {Linking} from 'react-native';
5
5
  import ExpoIapModule from '../ExpoIapModule';
6
6
 
7
7
  // Types
8
- import type {PurchaseResult, ReceiptAndroid} from '../ExpoIap.types';
8
+ import type {ReceiptValidationResultAndroid, VoidResult} from '../types';
9
9
 
10
10
  // Type guards
11
11
  export function isProductAndroid<T extends {platform?: string}>(
@@ -15,7 +15,7 @@ export function isProductAndroid<T extends {platform?: string}>(
15
15
  item != null &&
16
16
  typeof item === 'object' &&
17
17
  'platform' in item &&
18
- item.platform === 'android'
18
+ (item as any).platform === 'android'
19
19
  );
20
20
  }
21
21
 
@@ -71,7 +71,7 @@ export const deepLinkToSubscriptionsAndroid = async ({
71
71
  * @param {string} params.productId - product id for your in app product.
72
72
  * @param {string} params.productToken - token for your purchase (called 'token' in the API documentation).
73
73
  * @param {string} params.accessToken - OAuth access token with androidpublisher scope. Required for authentication.
74
- * @param {boolean} params.isSub - whether this is subscription or inapp. `true` for subscription.
74
+ * @param {boolean} params.isSub - whether this is subscription or in-app. `true` for subscription.
75
75
  * @returns {Promise<ReceiptAndroid>}
76
76
  */
77
77
  export const validateReceiptAndroid = async ({
@@ -86,7 +86,7 @@ export const validateReceiptAndroid = async ({
86
86
  productToken: string;
87
87
  accessToken: string;
88
88
  isSub?: boolean;
89
- }): Promise<ReceiptAndroid> => {
89
+ }): Promise<ReceiptValidationResultAndroid> => {
90
90
  const type = isSub ? 'subscriptions' : 'products';
91
91
 
92
92
  const url =
@@ -115,14 +115,30 @@ export const validateReceiptAndroid = async ({
115
115
  * Acknowledge a product (on Android.) No-op on iOS.
116
116
  * @param {Object} params - The parameters object
117
117
  * @param {string} params.token - The product's token (on Android)
118
- * @returns {Promise<PurchaseResult | void>}
118
+ * @returns {Promise<VoidResult | void>}
119
119
  */
120
- export const acknowledgePurchaseAndroid = ({
120
+ export const acknowledgePurchaseAndroid = async ({
121
121
  token,
122
122
  }: {
123
123
  token: string;
124
- }): Promise<PurchaseResult | boolean | void> => {
125
- return ExpoIapModule.acknowledgePurchaseAndroid(token);
124
+ }): Promise<VoidResult | boolean | void> => {
125
+ const result = await ExpoIapModule.acknowledgePurchaseAndroid(token);
126
+
127
+ if (typeof result === 'boolean' || typeof result === 'undefined') {
128
+ return result;
129
+ }
130
+
131
+ if (result && typeof result === 'object') {
132
+ const record = result as Record<string, unknown>;
133
+ if (typeof record.success === 'boolean') {
134
+ return {success: record.success};
135
+ }
136
+ if (typeof record.responseCode === 'number') {
137
+ return {success: record.responseCode === 0};
138
+ }
139
+ }
140
+
141
+ return {success: true};
126
142
  };
127
143
 
128
144
  /**
@@ -8,10 +8,11 @@ import ExpoIapModule from '../ExpoIapModule';
8
8
  import type {
9
9
  Product,
10
10
  Purchase,
11
- PurchaseError,
12
11
  SubscriptionStatusIOS,
13
- AppTransactionIOS,
14
- } from '../ExpoIap.types';
12
+ AppTransaction,
13
+ ReceiptValidationResultIOS,
14
+ } from '../types';
15
+ import type {PurchaseError} from '../purchase-error';
15
16
  import {Linking} from 'react-native';
16
17
 
17
18
  export type TransactionEvent = {
@@ -29,7 +30,7 @@ export function isProductIOS<T extends {platform?: string}>(
29
30
  item != null &&
30
31
  typeof item === 'object' &&
31
32
  'platform' in item &&
32
- item.platform === 'ios'
33
+ (item as any).platform === 'ios'
33
34
  );
34
35
  }
35
36
 
@@ -191,12 +192,7 @@ export const getTransactionJwsIOS = (sku: string): Promise<string> => {
191
192
  */
192
193
  export const validateReceiptIOS = async (
193
194
  sku: string,
194
- ): Promise<{
195
- isValid: boolean;
196
- receiptData: string;
197
- jwsRepresentation: string;
198
- latestTransaction?: Purchase;
199
- }> => {
195
+ ): Promise<ReceiptValidationResultIOS> => {
200
196
  const result = await ExpoIapModule.validateReceiptIOS(sku);
201
197
  return result;
202
198
  };
@@ -230,7 +226,7 @@ export const presentCodeRedemptionSheetIOS = (): Promise<boolean> => {
230
226
  * @platform iOS
231
227
  * @since iOS 16.0
232
228
  */
233
- export const getAppTransactionIOS = (): Promise<AppTransactionIOS | null> => {
229
+ export const getAppTransactionIOS = (): Promise<AppTransaction | null> => {
234
230
  return ExpoIapModule.getAppTransactionIOS();
235
231
  };
236
232