expo-iap 3.0.7 → 3.1.0

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 (52) hide show
  1. package/CLAUDE.md +14 -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/index.d.ts +32 -111
  8. package/build/index.d.ts.map +1 -1
  9. package/build/index.js +198 -243
  10. package/build/index.js.map +1 -1
  11. package/build/modules/android.d.ts +7 -12
  12. package/build/modules/android.d.ts.map +1 -1
  13. package/build/modules/android.js +15 -12
  14. package/build/modules/android.js.map +1 -1
  15. package/build/modules/ios.d.ts +35 -36
  16. package/build/modules/ios.d.ts.map +1 -1
  17. package/build/modules/ios.js +101 -35
  18. package/build/modules/ios.js.map +1 -1
  19. package/build/types.d.ts +107 -82
  20. package/build/types.d.ts.map +1 -1
  21. package/build/types.js +1 -0
  22. package/build/types.js.map +1 -1
  23. package/build/useIAP.d.ts +7 -12
  24. package/build/useIAP.d.ts.map +1 -1
  25. package/build/useIAP.js +49 -23
  26. package/build/useIAP.js.map +1 -1
  27. package/build/utils/errorMapping.d.ts +32 -23
  28. package/build/utils/errorMapping.d.ts.map +1 -1
  29. package/build/utils/errorMapping.js +117 -22
  30. package/build/utils/errorMapping.js.map +1 -1
  31. package/ios/ExpoIap.podspec +3 -2
  32. package/ios/ExpoIapHelper.swift +96 -0
  33. package/ios/ExpoIapLog.swift +127 -0
  34. package/ios/ExpoIapModule.swift +218 -340
  35. package/openiap-versions.json +5 -0
  36. package/package.json +2 -2
  37. package/plugin/build/withIAP.js +6 -4
  38. package/plugin/src/withIAP.ts +14 -4
  39. package/scripts/update-types.mjs +20 -1
  40. package/src/index.ts +280 -356
  41. package/src/modules/android.ts +25 -23
  42. package/src/modules/ios.ts +138 -48
  43. package/src/types.ts +139 -91
  44. package/src/useIAP.ts +91 -58
  45. package/src/utils/errorMapping.ts +203 -23
  46. package/.copilot-instructions.md +0 -321
  47. package/.cursorrules +0 -321
  48. package/build/purchase-error.d.ts +0 -67
  49. package/build/purchase-error.d.ts.map +0 -1
  50. package/build/purchase-error.js +0 -166
  51. package/build/purchase-error.js.map +0 -1
  52. package/src/purchase-error.ts +0 -265
@@ -5,7 +5,11 @@ import {Linking} from 'react-native';
5
5
  import ExpoIapModule from '../ExpoIapModule';
6
6
 
7
7
  // Types
8
- import type {ReceiptValidationResultAndroid, VoidResult} from '../types';
8
+ import type {
9
+ DeepLinkOptions,
10
+ MutationField,
11
+ ReceiptValidationResultAndroid,
12
+ } from '../types';
9
13
 
10
14
  // Type guards
11
15
  export function isProductAndroid<T extends {platform?: string}>(
@@ -15,32 +19,32 @@ export function isProductAndroid<T extends {platform?: string}>(
15
19
  item != null &&
16
20
  typeof item === 'object' &&
17
21
  'platform' in item &&
18
- (item as any).platform === 'android'
22
+ typeof (item as any).platform === 'string' &&
23
+ (item as any).platform.toLowerCase() === 'android'
19
24
  );
20
25
  }
21
26
 
22
27
  /**
23
28
  * Deep link to subscriptions screen on Android.
24
29
  * @param {Object} params - The parameters object
25
- * @param {string} params.sku - The product's SKU (on Android)
26
- * @param {string} params.packageName - The package name of your Android app (e.g., 'com.example.app')
30
+ * @param {string} params.skuAndroid - The product's SKU (on Android)
31
+ * @param {string} params.packageNameAndroid - The package name of your Android app (e.g., 'com.example.app')
27
32
  * @returns {Promise<void>}
28
33
  *
29
34
  * @example
30
35
  * ```typescript
31
36
  * await deepLinkToSubscriptionsAndroid({
32
- * sku: 'subscription_id',
33
- * packageName: 'com.example.app'
37
+ * skuAndroid: 'subscription_id',
38
+ * packageNameAndroid: 'com.example.app'
34
39
  * });
35
40
  * ```
36
41
  */
37
- export const deepLinkToSubscriptionsAndroid = async ({
38
- sku,
39
- packageName,
40
- }: {
41
- sku?: string;
42
- packageName?: string;
43
- }): Promise<void> => {
42
+ export const deepLinkToSubscriptionsAndroid = async (
43
+ options?: DeepLinkOptions | null,
44
+ ): Promise<void> => {
45
+ const sku = options?.skuAndroid ?? undefined;
46
+ const packageName = options?.packageNameAndroid ?? undefined;
47
+
44
48
  // Prefer native deep link implementation via OpenIAP module
45
49
  if (ExpoIapModule?.deepLinkToSubscriptionsAndroid) {
46
50
  return (ExpoIapModule as any).deepLinkToSubscriptionsAndroid({
@@ -117,28 +121,26 @@ export const validateReceiptAndroid = async ({
117
121
  * @param {string} params.token - The product's token (on Android)
118
122
  * @returns {Promise<VoidResult | void>}
119
123
  */
120
- export const acknowledgePurchaseAndroid = async ({
121
- token,
122
- }: {
123
- token: string;
124
- }): Promise<VoidResult | boolean | void> => {
125
- const result = await ExpoIapModule.acknowledgePurchaseAndroid(token);
124
+ export const acknowledgePurchaseAndroid: MutationField<
125
+ 'acknowledgePurchaseAndroid'
126
+ > = async (purchaseToken) => {
127
+ const result = await ExpoIapModule.acknowledgePurchaseAndroid(purchaseToken);
126
128
 
127
- if (typeof result === 'boolean' || typeof result === 'undefined') {
129
+ if (typeof result === 'boolean') {
128
130
  return result;
129
131
  }
130
132
 
131
133
  if (result && typeof result === 'object') {
132
134
  const record = result as Record<string, unknown>;
133
135
  if (typeof record.success === 'boolean') {
134
- return {success: record.success};
136
+ return record.success;
135
137
  }
136
138
  if (typeof record.responseCode === 'number') {
137
- return {success: record.responseCode === 0};
139
+ return record.responseCode === 0;
138
140
  }
139
141
  }
140
142
 
141
- return {success: true};
143
+ return true;
142
144
  };
143
145
 
144
146
  /**
@@ -6,14 +6,17 @@ import ExpoIapModule from '../ExpoIapModule';
6
6
 
7
7
  // Types
8
8
  import type {
9
- Product,
9
+ MutationField,
10
+ ProductIOS,
10
11
  Purchase,
11
- SubscriptionStatusIOS,
12
- AppTransaction,
12
+ PurchaseIOS,
13
+ QueryField,
14
+ ReceiptValidationProps,
13
15
  ReceiptValidationResultIOS,
16
+ SubscriptionStatusIOS,
14
17
  } from '../types';
15
- import type {PurchaseError} from '../purchase-error';
16
- import {Linking} from 'react-native';
18
+ import type {PurchaseError} from '../utils/errorMapping';
19
+ import {Linking, Platform} from 'react-native';
17
20
 
18
21
  export type TransactionEvent = {
19
22
  transaction?: Purchase;
@@ -30,7 +33,8 @@ export function isProductIOS<T extends {platform?: string}>(
30
33
  item != null &&
31
34
  typeof item === 'object' &&
32
35
  'platform' in item &&
33
- (item as any).platform === 'ios'
36
+ typeof (item as any).platform === 'string' &&
37
+ (item as any).platform.toLowerCase() === 'ios'
34
38
  );
35
39
  }
36
40
 
@@ -44,8 +48,8 @@ export function isProductIOS<T extends {platform?: string}>(
44
48
  *
45
49
  * @platform iOS
46
50
  */
47
- export const syncIOS = (): Promise<null> => {
48
- return ExpoIapModule.syncIOS();
51
+ export const syncIOS: MutationField<'syncIOS'> = async () => {
52
+ return Boolean(await ExpoIapModule.syncIOS());
49
53
  };
50
54
 
51
55
  /**
@@ -57,10 +61,13 @@ export const syncIOS = (): Promise<null> => {
57
61
  *
58
62
  * @platform iOS
59
63
  */
60
- export const isEligibleForIntroOfferIOS = (
61
- groupId: string,
62
- ): Promise<boolean> => {
63
- return ExpoIapModule.isEligibleForIntroOfferIOS(groupId);
64
+ export const isEligibleForIntroOfferIOS: QueryField<
65
+ 'isEligibleForIntroOfferIOS'
66
+ > = async (groupID) => {
67
+ if (!groupID) {
68
+ throw new Error('isEligibleForIntroOfferIOS requires a groupID');
69
+ }
70
+ return ExpoIapModule.isEligibleForIntroOfferIOS(groupID);
64
71
  };
65
72
 
66
73
  /**
@@ -72,10 +79,14 @@ export const isEligibleForIntroOfferIOS = (
72
79
  *
73
80
  * @platform iOS
74
81
  */
75
- export const subscriptionStatusIOS = (
76
- sku: string,
77
- ): Promise<SubscriptionStatusIOS[]> => {
78
- return ExpoIapModule.subscriptionStatusIOS(sku);
82
+ export const subscriptionStatusIOS: QueryField<
83
+ 'subscriptionStatusIOS'
84
+ > = async (sku) => {
85
+ if (!sku) {
86
+ throw new Error('subscriptionStatusIOS requires a SKU');
87
+ }
88
+ const status = await ExpoIapModule.subscriptionStatusIOS(sku);
89
+ return (status ?? []) as SubscriptionStatusIOS[];
79
90
  };
80
91
 
81
92
  /**
@@ -87,8 +98,14 @@ export const subscriptionStatusIOS = (
87
98
  *
88
99
  * @platform iOS
89
100
  */
90
- export const currentEntitlementIOS = (sku: string): Promise<Purchase> => {
91
- return ExpoIapModule.currentEntitlementIOS(sku);
101
+ export const currentEntitlementIOS: QueryField<
102
+ 'currentEntitlementIOS'
103
+ > = async (sku) => {
104
+ if (!sku) {
105
+ throw new Error('currentEntitlementIOS requires a SKU');
106
+ }
107
+ const purchase = await ExpoIapModule.currentEntitlementIOS(sku);
108
+ return (purchase ?? null) as PurchaseIOS | null;
92
109
  };
93
110
 
94
111
  /**
@@ -100,8 +117,14 @@ export const currentEntitlementIOS = (sku: string): Promise<Purchase> => {
100
117
  *
101
118
  * @platform iOS
102
119
  */
103
- export const latestTransactionIOS = (sku: string): Promise<Purchase> => {
104
- return ExpoIapModule.latestTransactionIOS(sku);
120
+ export const latestTransactionIOS: QueryField<'latestTransactionIOS'> = async (
121
+ sku,
122
+ ) => {
123
+ if (!sku) {
124
+ throw new Error('latestTransactionIOS requires a SKU');
125
+ }
126
+ const transaction = await ExpoIapModule.latestTransactionIOS(sku);
127
+ return (transaction ?? null) as PurchaseIOS | null;
105
128
  };
106
129
 
107
130
  /**
@@ -113,11 +136,14 @@ export const latestTransactionIOS = (sku: string): Promise<Purchase> => {
113
136
  *
114
137
  * @platform iOS
115
138
  */
116
- type RefundRequestStatus = 'success' | 'userCancelled';
117
- export const beginRefundRequestIOS = (
118
- sku: string,
119
- ): Promise<RefundRequestStatus> => {
120
- return ExpoIapModule.beginRefundRequestIOS(sku);
139
+ export const beginRefundRequestIOS: MutationField<
140
+ 'beginRefundRequestIOS'
141
+ > = async (sku) => {
142
+ if (!sku) {
143
+ throw new Error('beginRefundRequestIOS requires a SKU');
144
+ }
145
+ const status = await ExpoIapModule.beginRefundRequestIOS(sku);
146
+ return status ?? null;
121
147
  };
122
148
 
123
149
  /**
@@ -129,8 +155,11 @@ export const beginRefundRequestIOS = (
129
155
  *
130
156
  * @platform iOS
131
157
  */
132
- export const showManageSubscriptionsIOS = (): Promise<Purchase[]> => {
133
- return ExpoIapModule.showManageSubscriptionsIOS();
158
+ export const showManageSubscriptionsIOS: MutationField<
159
+ 'showManageSubscriptionsIOS'
160
+ > = async () => {
161
+ const purchases = await ExpoIapModule.showManageSubscriptionsIOS();
162
+ return (purchases ?? []) as PurchaseIOS[];
134
163
  };
135
164
 
136
165
  /**
@@ -143,10 +172,34 @@ export const showManageSubscriptionsIOS = (): Promise<Purchase[]> => {
143
172
  *
144
173
  * @returns {Promise<string>} Base64 encoded receipt data
145
174
  */
146
- export const getReceiptIOS = (): Promise<string> => {
175
+ export const getReceiptDataIOS: QueryField<'getReceiptDataIOS'> = async () => {
147
176
  return ExpoIapModule.getReceiptDataIOS();
148
177
  };
149
178
 
179
+ export const getReceiptIOS = getReceiptDataIOS;
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
+
150
203
  /**
151
204
  * Check if a transaction is verified through StoreKit 2.
152
205
  * StoreKit 2 performs local verification of transaction JWS signatures.
@@ -157,7 +210,12 @@ export const getReceiptIOS = (): Promise<string> => {
157
210
  *
158
211
  * @platform iOS
159
212
  */
160
- export const isTransactionVerifiedIOS = (sku: string): Promise<boolean> => {
213
+ export const isTransactionVerifiedIOS: QueryField<
214
+ 'isTransactionVerifiedIOS'
215
+ > = async (sku) => {
216
+ if (!sku) {
217
+ throw new Error('isTransactionVerifiedIOS requires a SKU');
218
+ }
161
219
  return ExpoIapModule.isTransactionVerifiedIOS(sku);
162
220
  };
163
221
 
@@ -171,8 +229,14 @@ export const isTransactionVerifiedIOS = (sku: string): Promise<boolean> => {
171
229
  *
172
230
  * @platform iOS
173
231
  */
174
- export const getTransactionJwsIOS = (sku: string): Promise<string> => {
175
- return ExpoIapModule.getTransactionJwsIOS(sku);
232
+ export const getTransactionJwsIOS: QueryField<'getTransactionJwsIOS'> = async (
233
+ sku,
234
+ ) => {
235
+ if (!sku) {
236
+ throw new Error('getTransactionJwsIOS requires a SKU');
237
+ }
238
+ const jws = await ExpoIapModule.getTransactionJwsIOS(sku);
239
+ return jws ?? '';
176
240
  };
177
241
 
178
242
  /**
@@ -190,13 +254,24 @@ export const getTransactionJwsIOS = (sku: string): Promise<string> => {
190
254
  * latestTransaction?: Purchase;
191
255
  * }>}
192
256
  */
193
- export const validateReceiptIOS = async (
194
- sku: string,
195
- ): Promise<ReceiptValidationResultIOS> => {
196
- const result = await ExpoIapModule.validateReceiptIOS(sku);
197
- return result;
257
+ const validateReceiptIOSImpl = async (
258
+ props: ReceiptValidationProps | string,
259
+ ) => {
260
+ const sku =
261
+ typeof props === 'string' ? props : (props as ReceiptValidationProps)?.sku;
262
+
263
+ if (!sku) {
264
+ throw new Error('validateReceiptIOS requires a SKU');
265
+ }
266
+
267
+ return (await ExpoIapModule.validateReceiptIOS(
268
+ sku,
269
+ )) as ReceiptValidationResultIOS;
198
270
  };
199
271
 
272
+ export const validateReceiptIOS =
273
+ validateReceiptIOSImpl as QueryField<'validateReceiptIOS'>;
274
+
200
275
  /**
201
276
  * Present the code redemption sheet for offer codes (iOS only).
202
277
  * This allows users to redeem promotional codes for in-app purchases and subscriptions.
@@ -208,8 +283,10 @@ export const validateReceiptIOS = async (
208
283
  *
209
284
  * @platform iOS
210
285
  */
211
- export const presentCodeRedemptionSheetIOS = (): Promise<boolean> => {
212
- return ExpoIapModule.presentCodeRedemptionSheetIOS();
286
+ export const presentCodeRedemptionSheetIOS: MutationField<
287
+ 'presentCodeRedemptionSheetIOS'
288
+ > = async () => {
289
+ return Boolean(await ExpoIapModule.presentCodeRedemptionSheetIOS());
213
290
  };
214
291
 
215
292
  /**
@@ -226,8 +303,10 @@ export const presentCodeRedemptionSheetIOS = (): Promise<boolean> => {
226
303
  * @platform iOS
227
304
  * @since iOS 16.0
228
305
  */
229
- export const getAppTransactionIOS = (): Promise<AppTransaction | null> => {
230
- return ExpoIapModule.getAppTransactionIOS();
306
+ export const getAppTransactionIOS: QueryField<
307
+ 'getAppTransactionIOS'
308
+ > = async () => {
309
+ return (await ExpoIapModule.getAppTransactionIOS()) ?? null;
231
310
  };
232
311
 
233
312
  /**
@@ -240,8 +319,11 @@ export const getAppTransactionIOS = (): Promise<AppTransaction | null> => {
240
319
  *
241
320
  * @platform iOS
242
321
  */
243
- export const getPromotedProductIOS = (): Promise<Product | null> => {
244
- return ExpoIapModule.getPromotedProductIOS();
322
+ export const getPromotedProductIOS: QueryField<
323
+ 'getPromotedProductIOS'
324
+ > = async () => {
325
+ const product = await ExpoIapModule.getPromotedProductIOS();
326
+ return (product ?? null) as ProductIOS | null;
245
327
  };
246
328
 
247
329
  /**
@@ -253,8 +335,11 @@ export const getPromotedProductIOS = (): Promise<Product | null> => {
253
335
  *
254
336
  * @platform iOS
255
337
  */
256
- export const requestPurchaseOnPromotedProductIOS = (): Promise<void> => {
257
- return ExpoIapModule.requestPurchaseOnPromotedProductIOS();
338
+ export const requestPurchaseOnPromotedProductIOS: MutationField<
339
+ 'requestPurchaseOnPromotedProductIOS'
340
+ > = async () => {
341
+ await ExpoIapModule.requestPurchaseOnPromotedProductIOS();
342
+ return true;
258
343
  };
259
344
 
260
345
  /**
@@ -263,8 +348,11 @@ export const requestPurchaseOnPromotedProductIOS = (): Promise<void> => {
263
348
  * @returns Promise resolving to array of pending transactions
264
349
  * @platform iOS
265
350
  */
266
- export const getPendingTransactionsIOS = (): Promise<any[]> => {
267
- return ExpoIapModule.getPendingTransactionsIOS();
351
+ export const getPendingTransactionsIOS: QueryField<
352
+ 'getPendingTransactionsIOS'
353
+ > = async () => {
354
+ const transactions = await ExpoIapModule.getPendingTransactionsIOS();
355
+ return (transactions ?? []) as PurchaseIOS[];
268
356
  };
269
357
 
270
358
  /**
@@ -273,8 +361,10 @@ export const getPendingTransactionsIOS = (): Promise<any[]> => {
273
361
  * @returns Promise resolving when transaction is cleared
274
362
  * @platform iOS
275
363
  */
276
- export const clearTransactionIOS = (): Promise<void> => {
277
- return ExpoIapModule.clearTransactionIOS();
364
+ export const clearTransactionIOS: MutationField<
365
+ 'clearTransactionIOS'
366
+ > = async () => {
367
+ return Boolean(await ExpoIapModule.clearTransactionIOS());
278
368
  };
279
369
 
280
370
  /**