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.
- package/CLAUDE.md +14 -2
- package/CONTRIBUTING.md +19 -0
- package/README.md +18 -6
- package/android/build.gradle +24 -1
- package/android/src/main/java/expo/modules/iap/ExpoIapLog.kt +69 -0
- package/android/src/main/java/expo/modules/iap/ExpoIapModule.kt +190 -59
- package/build/index.d.ts +32 -111
- package/build/index.d.ts.map +1 -1
- package/build/index.js +198 -243
- package/build/index.js.map +1 -1
- package/build/modules/android.d.ts +7 -12
- package/build/modules/android.d.ts.map +1 -1
- package/build/modules/android.js +15 -12
- package/build/modules/android.js.map +1 -1
- package/build/modules/ios.d.ts +35 -36
- package/build/modules/ios.d.ts.map +1 -1
- package/build/modules/ios.js +101 -35
- package/build/modules/ios.js.map +1 -1
- package/build/types.d.ts +107 -82
- package/build/types.d.ts.map +1 -1
- package/build/types.js +1 -0
- package/build/types.js.map +1 -1
- package/build/useIAP.d.ts +7 -12
- package/build/useIAP.d.ts.map +1 -1
- package/build/useIAP.js +49 -23
- package/build/useIAP.js.map +1 -1
- package/build/utils/errorMapping.d.ts +32 -23
- package/build/utils/errorMapping.d.ts.map +1 -1
- package/build/utils/errorMapping.js +117 -22
- package/build/utils/errorMapping.js.map +1 -1
- package/ios/ExpoIap.podspec +3 -2
- package/ios/ExpoIapHelper.swift +96 -0
- package/ios/ExpoIapLog.swift +127 -0
- package/ios/ExpoIapModule.swift +218 -340
- package/openiap-versions.json +5 -0
- package/package.json +2 -2
- package/plugin/build/withIAP.js +6 -4
- package/plugin/src/withIAP.ts +14 -4
- package/scripts/update-types.mjs +20 -1
- package/src/index.ts +280 -356
- package/src/modules/android.ts +25 -23
- package/src/modules/ios.ts +138 -48
- package/src/types.ts +139 -91
- package/src/useIAP.ts +91 -58
- package/src/utils/errorMapping.ts +203 -23
- package/.copilot-instructions.md +0 -321
- package/.cursorrules +0 -321
- package/build/purchase-error.d.ts +0 -67
- package/build/purchase-error.d.ts.map +0 -1
- package/build/purchase-error.js +0 -166
- package/build/purchase-error.js.map +0 -1
- package/src/purchase-error.ts +0 -265
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,
|
|
@@ -17,29 +18,31 @@ import {
|
|
|
17
18
|
} from './modules/android';
|
|
18
19
|
|
|
19
20
|
// Types
|
|
20
|
-
import {
|
|
21
|
+
import type {
|
|
22
|
+
AndroidSubscriptionOfferInput,
|
|
23
|
+
DeepLinkOptions,
|
|
24
|
+
FetchProductsResult,
|
|
25
|
+
MutationField,
|
|
26
|
+
MutationRequestPurchaseArgs,
|
|
27
|
+
MutationValidateReceiptArgs,
|
|
21
28
|
Product,
|
|
29
|
+
ProductQueryType,
|
|
30
|
+
ProductSubscription,
|
|
22
31
|
Purchase,
|
|
23
|
-
|
|
24
|
-
|
|
32
|
+
PurchaseOptions,
|
|
33
|
+
QueryField,
|
|
25
34
|
RequestPurchasePropsByPlatforms,
|
|
26
35
|
RequestPurchaseAndroidProps,
|
|
27
36
|
RequestPurchaseIosProps,
|
|
28
37
|
RequestSubscriptionPropsByPlatforms,
|
|
29
38
|
RequestSubscriptionAndroidProps,
|
|
30
39
|
RequestSubscriptionIosProps,
|
|
31
|
-
ProductSubscription,
|
|
32
|
-
PurchaseAndroid,
|
|
33
|
-
DiscountOfferInputIOS,
|
|
34
|
-
VoidResult,
|
|
35
|
-
ReceiptValidationResult,
|
|
36
|
-
AndroidSubscriptionOfferInput,
|
|
37
40
|
} from './types';
|
|
38
|
-
import {
|
|
41
|
+
import {ErrorCode} from './types';
|
|
42
|
+
import {createPurchaseError, type PurchaseError} from './utils/errorMapping';
|
|
39
43
|
|
|
40
44
|
// Export all types
|
|
41
45
|
export * from './types';
|
|
42
|
-
export {ErrorCodeUtils, ErrorCodeMapping} from './purchase-error';
|
|
43
46
|
export * from './modules/android';
|
|
44
47
|
export * from './modules/ios';
|
|
45
48
|
|
|
@@ -50,18 +53,12 @@ export {
|
|
|
50
53
|
} from './helpers/subscription';
|
|
51
54
|
|
|
52
55
|
// Get the native constant value
|
|
53
|
-
export const PI = ExpoIapModule.PI;
|
|
54
|
-
|
|
55
56
|
export enum OpenIapEvent {
|
|
56
57
|
PurchaseUpdated = 'purchase-updated',
|
|
57
58
|
PurchaseError = 'purchase-error',
|
|
58
59
|
PromotedProductIOS = 'promoted-product-ios',
|
|
59
60
|
}
|
|
60
61
|
|
|
61
|
-
export function setValueAsync(value: string) {
|
|
62
|
-
return ExpoIapModule.setValueAsync(value);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
62
|
type ExpoIapEventPayloads = {
|
|
66
63
|
[OpenIapEvent.PurchaseUpdated]: Purchase;
|
|
67
64
|
[OpenIapEvent.PurchaseError]: PurchaseError;
|
|
@@ -90,32 +87,7 @@ export const emitter = (ExpoIapModule ||
|
|
|
90
87
|
/**
|
|
91
88
|
* TODO(v3.1.0): Remove legacy 'inapp' alias once downstream apps migrate to 'in-app'.
|
|
92
89
|
*/
|
|
93
|
-
export type ProductTypeInput =
|
|
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
|
-
};
|
|
90
|
+
export type ProductTypeInput = ProductQueryType | 'inapp';
|
|
119
91
|
|
|
120
92
|
const normalizeProductType = (type?: ProductTypeInput) => {
|
|
121
93
|
if (type === 'inapp') {
|
|
@@ -126,48 +98,66 @@ const normalizeProductType = (type?: ProductTypeInput) => {
|
|
|
126
98
|
|
|
127
99
|
if (!type || type === 'inapp' || type === 'in-app') {
|
|
128
100
|
return {
|
|
129
|
-
canonical: 'in-app' as
|
|
130
|
-
native: '
|
|
101
|
+
canonical: 'in-app' as ProductQueryType,
|
|
102
|
+
native: 'in-app' as const,
|
|
131
103
|
};
|
|
132
104
|
}
|
|
133
105
|
if (type === 'subs') {
|
|
134
106
|
return {
|
|
135
|
-
canonical: 'subs' as
|
|
107
|
+
canonical: 'subs' as ProductQueryType,
|
|
136
108
|
native: 'subs' as const,
|
|
137
109
|
};
|
|
138
110
|
}
|
|
111
|
+
if (type === 'all') {
|
|
112
|
+
return {
|
|
113
|
+
canonical: 'all' as ProductQueryType,
|
|
114
|
+
native: 'all' as const,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
139
117
|
throw new Error(`Unsupported product type: ${type}`);
|
|
140
118
|
};
|
|
141
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
|
+
|
|
142
137
|
export const purchaseUpdatedListener = (
|
|
143
138
|
listener: (event: Purchase) => void,
|
|
144
139
|
) => {
|
|
145
|
-
console.log('[JS] Registering purchaseUpdatedListener');
|
|
146
140
|
const wrappedListener = (event: Purchase) => {
|
|
147
|
-
|
|
148
|
-
listener(
|
|
141
|
+
const normalized = normalizePurchasePlatform(event);
|
|
142
|
+
listener(normalized);
|
|
149
143
|
};
|
|
150
144
|
const emitterSubscription = emitter.addListener(
|
|
151
145
|
OpenIapEvent.PurchaseUpdated,
|
|
152
146
|
wrappedListener,
|
|
153
147
|
);
|
|
154
|
-
console.log('[JS] purchaseUpdatedListener registered successfully');
|
|
155
148
|
return emitterSubscription;
|
|
156
149
|
};
|
|
157
150
|
|
|
158
151
|
export const purchaseErrorListener = (
|
|
159
152
|
listener: (error: PurchaseError) => void,
|
|
160
153
|
) => {
|
|
161
|
-
console.log('[JS] Registering purchaseErrorListener');
|
|
162
154
|
const wrappedListener = (error: PurchaseError) => {
|
|
163
|
-
console.log('[JS] purchaseErrorListener fired:', error);
|
|
164
155
|
listener(error);
|
|
165
156
|
};
|
|
166
157
|
const emitterSubscription = emitter.addListener(
|
|
167
158
|
OpenIapEvent.PurchaseError,
|
|
168
159
|
wrappedListener,
|
|
169
160
|
);
|
|
170
|
-
console.log('[JS] purchaseErrorListener registered successfully');
|
|
171
161
|
return emitterSubscription;
|
|
172
162
|
};
|
|
173
163
|
|
|
@@ -203,158 +193,114 @@ export const promotedProductListenerIOS = (
|
|
|
203
193
|
return emitter.addListener(OpenIapEvent.PromotedProductIOS, listener);
|
|
204
194
|
};
|
|
205
195
|
|
|
206
|
-
export
|
|
207
|
-
|
|
208
|
-
return Promise.resolve(result);
|
|
209
|
-
}
|
|
196
|
+
export const initConnection: MutationField<'initConnection'> = async () =>
|
|
197
|
+
ExpoIapModule.initConnection();
|
|
210
198
|
|
|
211
|
-
export
|
|
212
|
-
|
|
213
|
-
}
|
|
199
|
+
export const endConnection: MutationField<'endConnection'> = async () =>
|
|
200
|
+
ExpoIapModule.endConnection();
|
|
214
201
|
|
|
215
202
|
/**
|
|
216
203
|
* Fetch products with unified API (v2.7.0+)
|
|
217
204
|
*
|
|
218
|
-
* @param
|
|
219
|
-
* @param
|
|
220
|
-
* @param
|
|
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
|
-
* ```
|
|
205
|
+
* @param request - Product fetch configuration
|
|
206
|
+
* @param request.skus - Array of product SKUs to fetch
|
|
207
|
+
* @param request.type - Product query type: 'in-app', 'subs', or 'all'
|
|
236
208
|
*/
|
|
237
|
-
export const fetchProducts = async ({
|
|
238
|
-
|
|
239
|
-
type
|
|
240
|
-
|
|
241
|
-
skus
|
|
242
|
-
|
|
243
|
-
}): Promise<Product[] | ProductSubscription[]> => {
|
|
244
|
-
if (!skus?.length) {
|
|
245
|
-
throw new PurchaseError({
|
|
209
|
+
export const fetchProducts: QueryField<'fetchProducts'> = async (request) => {
|
|
210
|
+
console.log('fetchProducts called with:', request);
|
|
211
|
+
const {skus, type} = request ?? {};
|
|
212
|
+
|
|
213
|
+
if (!Array.isArray(skus) || skus.length === 0) {
|
|
214
|
+
throw createPurchaseError({
|
|
246
215
|
message: 'No SKUs provided',
|
|
247
216
|
code: ErrorCode.EmptySkuList,
|
|
248
217
|
});
|
|
249
218
|
}
|
|
250
219
|
|
|
251
|
-
const {canonical, native} = normalizeProductType(
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
220
|
+
const {canonical, native} = normalizeProductType(
|
|
221
|
+
type as ProductTypeInput | undefined,
|
|
222
|
+
);
|
|
223
|
+
const skuSet = new Set(skus);
|
|
255
224
|
|
|
256
|
-
|
|
225
|
+
const filterIosItems = (
|
|
226
|
+
items: unknown[],
|
|
227
|
+
): Product[] | ProductSubscription[] =>
|
|
228
|
+
items.filter((item): item is Product | ProductSubscription => {
|
|
257
229
|
if (!isProductIOS(item)) {
|
|
258
230
|
return false;
|
|
259
231
|
}
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
232
|
+
const candidate = item as Product | ProductSubscription;
|
|
233
|
+
return typeof candidate.id === 'string' && skuSet.has(candidate.id);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
const filterAndroidItems = (
|
|
237
|
+
items: unknown[],
|
|
238
|
+
): Product[] | ProductSubscription[] =>
|
|
239
|
+
items.filter((item): item is Product | ProductSubscription => {
|
|
240
|
+
if (!isProductAndroid(item)) {
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
const candidate = item as Product | ProductSubscription;
|
|
244
|
+
return typeof candidate.id === 'string' && skuSet.has(candidate.id);
|
|
267
245
|
});
|
|
268
246
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
247
|
+
const castResult = (
|
|
248
|
+
items: Product[] | ProductSubscription[],
|
|
249
|
+
): FetchProductsResult => {
|
|
250
|
+
if (canonical === 'in-app') {
|
|
251
|
+
return items as Product[];
|
|
252
|
+
}
|
|
253
|
+
if (canonical === 'subs') {
|
|
254
|
+
return items as ProductSubscription[];
|
|
255
|
+
}
|
|
256
|
+
return items;
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
if (Platform.OS === 'ios') {
|
|
260
|
+
const rawItems = await ExpoIapModule.fetchProducts({skus, type: native});
|
|
261
|
+
return castResult(filterIosItems(rawItems));
|
|
272
262
|
}
|
|
273
263
|
|
|
274
264
|
if (Platform.OS === 'android') {
|
|
275
|
-
const
|
|
276
|
-
|
|
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[]);
|
|
265
|
+
const rawItems = await ExpoIapModule.fetchProducts(native, skus);
|
|
266
|
+
return castResult(filterAndroidItems(rawItems));
|
|
290
267
|
}
|
|
291
268
|
|
|
292
269
|
throw new Error('Unsupported platform');
|
|
293
270
|
};
|
|
294
271
|
|
|
295
|
-
export const getAvailablePurchases
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
272
|
+
export const getAvailablePurchases: QueryField<
|
|
273
|
+
'getAvailablePurchases'
|
|
274
|
+
> = async (options) => {
|
|
275
|
+
const normalizedOptions: PurchaseOptions = {
|
|
276
|
+
alsoPublishToEventListenerIOS:
|
|
277
|
+
options?.alsoPublishToEventListenerIOS ?? false,
|
|
278
|
+
onlyIncludeActiveItemsIOS: options?.onlyIncludeActiveItemsIOS ?? true,
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const resolvePurchases: () => Promise<Purchase[]> =
|
|
303
282
|
Platform.select({
|
|
304
283
|
ios: () =>
|
|
305
284
|
ExpoIapModule.getAvailableItems(
|
|
306
|
-
alsoPublishToEventListenerIOS,
|
|
307
|
-
onlyIncludeActiveItemsIOS,
|
|
308
|
-
)
|
|
309
|
-
android: () => ExpoIapModule.getAvailableItems()
|
|
310
|
-
})
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
* Restore completed transactions (cross-platform behavior)
|
|
315
|
-
*
|
|
316
|
-
* - iOS: perform a lightweight sync to refresh transactions and ignore sync errors,
|
|
317
|
-
* then fetch available purchases to surface restored items to the app.
|
|
318
|
-
* - Android: simply fetch available purchases (restoration happens via query).
|
|
319
|
-
*
|
|
320
|
-
* This helper returns the restored/available purchases so callers can update UI/state.
|
|
321
|
-
*
|
|
322
|
-
* @param options.alsoPublishToEventListenerIOS - iOS only: whether to also publish to the event listener
|
|
323
|
-
* @param options.onlyIncludeActiveItemsIOS - iOS only: whether to only include active items
|
|
324
|
-
* @returns Promise resolving to the list of available/restored purchases
|
|
325
|
-
*/
|
|
326
|
-
export const restorePurchases = async (
|
|
327
|
-
options: {
|
|
328
|
-
alsoPublishToEventListenerIOS?: boolean;
|
|
329
|
-
onlyIncludeActiveItemsIOS?: boolean;
|
|
330
|
-
} = {},
|
|
331
|
-
): Promise<Purchase[]> => {
|
|
332
|
-
if (Platform.OS === 'ios') {
|
|
333
|
-
// Perform best-effort sync on iOS and ignore sync errors to avoid blocking restore flow
|
|
334
|
-
await syncIOS().catch(() => undefined);
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// Then, fetch available purchases for both platforms
|
|
338
|
-
const purchases = await getAvailablePurchases({
|
|
339
|
-
alsoPublishToEventListenerIOS:
|
|
340
|
-
options.alsoPublishToEventListenerIOS ?? false,
|
|
341
|
-
onlyIncludeActiveItemsIOS: options.onlyIncludeActiveItemsIOS ?? true,
|
|
342
|
-
});
|
|
343
|
-
|
|
344
|
-
return purchases;
|
|
285
|
+
normalizedOptions.alsoPublishToEventListenerIOS,
|
|
286
|
+
normalizedOptions.onlyIncludeActiveItemsIOS,
|
|
287
|
+
) as Promise<Purchase[]>,
|
|
288
|
+
android: () => ExpoIapModule.getAvailableItems() as Promise<Purchase[]>,
|
|
289
|
+
}) ?? (() => Promise.resolve([] as Purchase[]));
|
|
290
|
+
|
|
291
|
+
const purchases = await resolvePurchases();
|
|
292
|
+
return normalizePurchaseArray(purchases as Purchase[]);
|
|
345
293
|
};
|
|
346
294
|
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
timestamp: offer.timestamp.toString(),
|
|
357
|
-
};
|
|
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 '';
|
|
302
|
+
}
|
|
303
|
+
return getStorefrontIOS();
|
|
358
304
|
};
|
|
359
305
|
|
|
360
306
|
/**
|
|
@@ -417,11 +363,11 @@ function normalizeRequestProps(
|
|
|
417
363
|
* });
|
|
418
364
|
* ```
|
|
419
365
|
*/
|
|
420
|
-
export const requestPurchase = (
|
|
421
|
-
|
|
422
|
-
)
|
|
423
|
-
const {request, type} =
|
|
424
|
-
const {canonical, native} = normalizeProductType(type);
|
|
366
|
+
export const requestPurchase: MutationField<'requestPurchase'> = async (
|
|
367
|
+
args,
|
|
368
|
+
) => {
|
|
369
|
+
const {request, type} = args;
|
|
370
|
+
const {canonical, native} = normalizeProductType(type as ProductTypeInput);
|
|
425
371
|
const isInAppPurchase = canonical === 'in-app';
|
|
426
372
|
|
|
427
373
|
if (Platform.OS === 'ios') {
|
|
@@ -433,35 +379,43 @@ export const requestPurchase = (
|
|
|
433
379
|
);
|
|
434
380
|
}
|
|
435
381
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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
|
+
}
|
|
405
|
+
|
|
406
|
+
if (purchase) {
|
|
407
|
+
return normalizePurchasePlatform(purchase);
|
|
408
|
+
}
|
|
453
409
|
|
|
454
|
-
|
|
455
|
-
})();
|
|
410
|
+
return canonical === 'subs' ? [] : null;
|
|
456
411
|
}
|
|
457
412
|
|
|
458
413
|
if (Platform.OS === 'android') {
|
|
459
414
|
if (isInAppPurchase) {
|
|
460
|
-
const normalizedRequest
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
);
|
|
415
|
+
const normalizedRequest = normalizeRequestProps(
|
|
416
|
+
request as RequestPurchasePropsByPlatforms,
|
|
417
|
+
'android',
|
|
418
|
+
) as RequestPurchaseAndroidProps | null | undefined;
|
|
465
419
|
|
|
466
420
|
if (!normalizedRequest?.skus?.length) {
|
|
467
421
|
throw new Error(
|
|
@@ -476,28 +430,25 @@ export const requestPurchase = (
|
|
|
476
430
|
isOfferPersonalized,
|
|
477
431
|
} = normalizedRequest;
|
|
478
432
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
433
|
+
const result = (await ExpoIapModule.requestPurchase({
|
|
434
|
+
type: native,
|
|
435
|
+
skuArr: skus,
|
|
436
|
+
purchaseToken: undefined,
|
|
437
|
+
replacementMode: -1,
|
|
438
|
+
obfuscatedAccountId: obfuscatedAccountIdAndroid,
|
|
439
|
+
obfuscatedProfileId: obfuscatedProfileIdAndroid,
|
|
440
|
+
offerTokenArr: [],
|
|
441
|
+
isOfferPersonalized: isOfferPersonalized ?? false,
|
|
442
|
+
})) as Purchase[];
|
|
443
|
+
|
|
444
|
+
return normalizePurchaseArray(result);
|
|
491
445
|
}
|
|
492
446
|
|
|
493
447
|
if (canonical === 'subs') {
|
|
494
|
-
const normalizedRequest
|
|
495
|
-
| RequestSubscriptionAndroidProps
|
|
496
|
-
| null
|
|
497
|
-
| undefined = normalizeRequestProps(
|
|
448
|
+
const normalizedRequest = normalizeRequestProps(
|
|
498
449
|
request as RequestSubscriptionPropsByPlatforms,
|
|
499
450
|
'android',
|
|
500
|
-
);
|
|
451
|
+
) as RequestSubscriptionAndroidProps | null | undefined;
|
|
501
452
|
|
|
502
453
|
if (!normalizedRequest?.skus?.length) {
|
|
503
454
|
throw new Error(
|
|
@@ -519,21 +470,21 @@ export const requestPurchase = (
|
|
|
519
470
|
const replacementMode = replacementModeAndroid ?? -1;
|
|
520
471
|
const purchaseToken = purchaseTokenAndroid ?? undefined;
|
|
521
472
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
473
|
+
const result = (await ExpoIapModule.requestPurchase({
|
|
474
|
+
type: native,
|
|
475
|
+
skuArr: skus,
|
|
476
|
+
purchaseToken,
|
|
477
|
+
replacementMode,
|
|
478
|
+
obfuscatedAccountId: obfuscatedAccountIdAndroid,
|
|
479
|
+
obfuscatedProfileId: obfuscatedProfileIdAndroid,
|
|
480
|
+
offerTokenArr: normalizedOffers.map(
|
|
481
|
+
(offer: AndroidSubscriptionOfferInput) => offer.offerToken,
|
|
482
|
+
),
|
|
483
|
+
subscriptionOffers: normalizedOffers,
|
|
484
|
+
isOfferPersonalized: isOfferPersonalized ?? false,
|
|
485
|
+
})) as Purchase[];
|
|
486
|
+
|
|
487
|
+
return normalizePurchaseArray(result);
|
|
537
488
|
}
|
|
538
489
|
|
|
539
490
|
throw new Error(
|
|
@@ -541,93 +492,95 @@ export const requestPurchase = (
|
|
|
541
492
|
);
|
|
542
493
|
}
|
|
543
494
|
|
|
544
|
-
|
|
495
|
+
throw new Error('Platform not supported');
|
|
545
496
|
};
|
|
546
497
|
|
|
547
|
-
export const finishTransaction = ({
|
|
498
|
+
export const finishTransaction: MutationField<'finishTransaction'> = async ({
|
|
548
499
|
purchase,
|
|
549
500
|
isConsumable = false,
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
platform: 'android',
|
|
579
|
-
}),
|
|
580
|
-
);
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
if (isConsumable) {
|
|
584
|
-
return ExpoIapModule.consumePurchaseAndroid(token);
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
return ExpoIapModule.acknowledgePurchaseAndroid(token);
|
|
588
|
-
},
|
|
589
|
-
}) || (() => Promise.reject(new Error('Unsupported Platform')))
|
|
590
|
-
)();
|
|
501
|
+
}) => {
|
|
502
|
+
if (Platform.OS === 'ios') {
|
|
503
|
+
await ExpoIapModule.finishTransaction(purchase, isConsumable);
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (Platform.OS === 'android') {
|
|
508
|
+
const token = purchase.purchaseToken ?? undefined;
|
|
509
|
+
|
|
510
|
+
if (!token) {
|
|
511
|
+
throw createPurchaseError({
|
|
512
|
+
message: 'Purchase token is required to finish transaction',
|
|
513
|
+
code: ErrorCode.DeveloperError,
|
|
514
|
+
productId: purchase.productId,
|
|
515
|
+
platform: 'android',
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (isConsumable) {
|
|
520
|
+
await ExpoIapModule.consumePurchaseAndroid(token);
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
await ExpoIapModule.acknowledgePurchaseAndroid(token);
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
throw new Error('Unsupported Platform');
|
|
591
529
|
};
|
|
592
530
|
|
|
593
531
|
/**
|
|
594
|
-
*
|
|
595
|
-
*
|
|
596
|
-
* @returns Promise resolving to the storefront country code
|
|
597
|
-
* @throws Error if called on non-iOS platform
|
|
532
|
+
* Restore completed transactions (cross-platform behavior)
|
|
598
533
|
*
|
|
599
|
-
*
|
|
600
|
-
*
|
|
601
|
-
*
|
|
602
|
-
* console.log(storefront); // 'US'
|
|
603
|
-
* ```
|
|
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).
|
|
604
537
|
*
|
|
605
|
-
*
|
|
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.
|
|
606
540
|
*/
|
|
607
|
-
export const
|
|
608
|
-
if (Platform.OS
|
|
609
|
-
|
|
610
|
-
return Promise.resolve('');
|
|
541
|
+
export const restorePurchases: MutationField<'restorePurchases'> = async () => {
|
|
542
|
+
if (Platform.OS === 'ios') {
|
|
543
|
+
await syncIOS().catch(() => undefined);
|
|
611
544
|
}
|
|
612
|
-
|
|
545
|
+
|
|
546
|
+
await getAvailablePurchases({
|
|
547
|
+
alsoPublishToEventListenerIOS: false,
|
|
548
|
+
onlyIncludeActiveItemsIOS: true,
|
|
549
|
+
});
|
|
613
550
|
};
|
|
614
551
|
|
|
615
552
|
/**
|
|
616
|
-
*
|
|
617
|
-
*
|
|
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)
|
|
618
556
|
*
|
|
619
|
-
* @
|
|
620
|
-
*
|
|
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';
|
|
563
|
+
*
|
|
564
|
+
* // Works on both iOS and Android
|
|
565
|
+
* await deepLinkToSubscriptions({
|
|
566
|
+
* skuAndroid: 'your_subscription_sku',
|
|
567
|
+
* packageNameAndroid: 'com.example.app'
|
|
568
|
+
* });
|
|
621
569
|
*/
|
|
622
|
-
export const
|
|
623
|
-
|
|
570
|
+
export const deepLinkToSubscriptions: MutationField<
|
|
571
|
+
'deepLinkToSubscriptions'
|
|
572
|
+
> = async (options) => {
|
|
573
|
+
if (Platform.OS === 'ios') {
|
|
574
|
+
await deepLinkToSubscriptionsIOS();
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
|
|
624
578
|
if (Platform.OS === 'android') {
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
}
|
|
628
|
-
return Promise.resolve('');
|
|
579
|
+
await deepLinkToSubscriptionsAndroid((options as DeepLinkOptions) ?? null);
|
|
580
|
+
return;
|
|
629
581
|
}
|
|
630
|
-
|
|
582
|
+
|
|
583
|
+
throw new Error(`Unsupported platform: ${Platform.OS}`);
|
|
631
584
|
};
|
|
632
585
|
|
|
633
586
|
/**
|
|
@@ -638,18 +591,16 @@ export const getStorefront = (): Promise<string> => {
|
|
|
638
591
|
* - iOS: Send receipt data to Apple's verification endpoint from your server
|
|
639
592
|
* - Android: Use Google Play Developer API with service account credentials
|
|
640
593
|
*/
|
|
641
|
-
export const validateReceipt = async (
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
accessToken: string;
|
|
647
|
-
isSub?: boolean;
|
|
648
|
-
},
|
|
649
|
-
): Promise<ReceiptValidationResult> => {
|
|
594
|
+
export const validateReceipt: MutationField<'validateReceipt'> = async (
|
|
595
|
+
options,
|
|
596
|
+
) => {
|
|
597
|
+
const {sku, androidOptions} = options as MutationValidateReceiptArgs;
|
|
598
|
+
|
|
650
599
|
if (Platform.OS === 'ios') {
|
|
651
|
-
return
|
|
652
|
-
}
|
|
600
|
+
return validateReceiptIOS({sku});
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (Platform.OS === 'android') {
|
|
653
604
|
if (
|
|
654
605
|
!androidOptions ||
|
|
655
606
|
!androidOptions.packageName ||
|
|
@@ -660,53 +611,26 @@ export const validateReceipt = async (
|
|
|
660
611
|
'Android validation requires packageName, productToken, and accessToken',
|
|
661
612
|
);
|
|
662
613
|
}
|
|
663
|
-
return
|
|
614
|
+
return validateReceiptAndroid({
|
|
664
615
|
packageName: androidOptions.packageName,
|
|
665
616
|
productId: sku,
|
|
666
617
|
productToken: androidOptions.productToken,
|
|
667
618
|
accessToken: androidOptions.accessToken,
|
|
668
|
-
isSub: androidOptions.isSub,
|
|
619
|
+
isSub: androidOptions.isSub ?? undefined,
|
|
669
620
|
});
|
|
670
|
-
} else {
|
|
671
|
-
throw new Error('Platform not supported');
|
|
672
621
|
}
|
|
673
|
-
};
|
|
674
622
|
|
|
675
|
-
|
|
676
|
-
* Deeplinks to native interface that allows users to manage their subscriptions
|
|
677
|
-
* @param options.skuAndroid - Required for Android to locate specific subscription (ignored on iOS)
|
|
678
|
-
* @param options.packageNameAndroid - Required for Android to identify your app (ignored on iOS)
|
|
679
|
-
*
|
|
680
|
-
* @returns Promise that resolves when the deep link is successfully opened
|
|
681
|
-
*
|
|
682
|
-
* @throws {Error} When called on unsupported platform or when required Android parameters are missing
|
|
683
|
-
*
|
|
684
|
-
* @example
|
|
685
|
-
* import { deepLinkToSubscriptions } from 'expo-iap';
|
|
686
|
-
*
|
|
687
|
-
* // Works on both iOS and Android
|
|
688
|
-
* await deepLinkToSubscriptions({
|
|
689
|
-
* skuAndroid: 'your_subscription_sku',
|
|
690
|
-
* packageNameAndroid: 'com.example.app'
|
|
691
|
-
* });
|
|
692
|
-
*/
|
|
693
|
-
export const deepLinkToSubscriptions = (options: {
|
|
694
|
-
skuAndroid?: string;
|
|
695
|
-
packageNameAndroid?: string;
|
|
696
|
-
}): Promise<void> => {
|
|
697
|
-
if (Platform.OS === 'ios') {
|
|
698
|
-
return deepLinkToSubscriptionsIOS();
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
if (Platform.OS === 'android') {
|
|
702
|
-
return deepLinkToSubscriptionsAndroid({
|
|
703
|
-
sku: options?.skuAndroid,
|
|
704
|
-
packageName: options?.packageNameAndroid,
|
|
705
|
-
});
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
return Promise.reject(new Error(`Unsupported platform: ${Platform.OS}`));
|
|
623
|
+
throw new Error('Platform not supported');
|
|
709
624
|
};
|
|
710
625
|
|
|
711
626
|
export * from './useIAP';
|
|
712
|
-
export
|
|
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';
|