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/build/index.js
CHANGED
|
@@ -3,29 +3,23 @@ import { NativeModulesProxy } from 'expo-modules-core';
|
|
|
3
3
|
import { Platform } from 'react-native';
|
|
4
4
|
// Internal modules
|
|
5
5
|
import ExpoIapModule from './ExpoIapModule';
|
|
6
|
-
import { isProductIOS, validateReceiptIOS, deepLinkToSubscriptionsIOS, syncIOS, } from './modules/ios';
|
|
6
|
+
import { isProductIOS, validateReceiptIOS, deepLinkToSubscriptionsIOS, syncIOS, getStorefrontIOS, } from './modules/ios';
|
|
7
7
|
import { isProductAndroid, validateReceiptAndroid, deepLinkToSubscriptionsAndroid, } from './modules/android';
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
import { PurchaseError } from './purchase-error';
|
|
8
|
+
import { ErrorCode } from './types';
|
|
9
|
+
import { createPurchaseError } from './utils/errorMapping';
|
|
11
10
|
// Export all types
|
|
12
11
|
export * from './types';
|
|
13
|
-
export { ErrorCodeUtils, ErrorCodeMapping } from './purchase-error';
|
|
14
12
|
export * from './modules/android';
|
|
15
13
|
export * from './modules/ios';
|
|
16
14
|
// Export subscription helpers
|
|
17
15
|
export { getActiveSubscriptions, hasActiveSubscriptions, } from './helpers/subscription';
|
|
18
16
|
// Get the native constant value
|
|
19
|
-
export const PI = ExpoIapModule.PI;
|
|
20
17
|
export var OpenIapEvent;
|
|
21
18
|
(function (OpenIapEvent) {
|
|
22
19
|
OpenIapEvent["PurchaseUpdated"] = "purchase-updated";
|
|
23
20
|
OpenIapEvent["PurchaseError"] = "purchase-error";
|
|
24
21
|
OpenIapEvent["PromotedProductIOS"] = "promoted-product-ios";
|
|
25
22
|
})(OpenIapEvent || (OpenIapEvent = {}));
|
|
26
|
-
export function setValueAsync(value) {
|
|
27
|
-
return ExpoIapModule.setValueAsync(value);
|
|
28
|
-
}
|
|
29
23
|
// Ensure the emitter has proper EventEmitter interface
|
|
30
24
|
export const emitter = (ExpoIapModule ||
|
|
31
25
|
NativeModulesProxy.ExpoIap);
|
|
@@ -36,7 +30,7 @@ const normalizeProductType = (type) => {
|
|
|
36
30
|
if (!type || type === 'inapp' || type === 'in-app') {
|
|
37
31
|
return {
|
|
38
32
|
canonical: 'in-app',
|
|
39
|
-
native: '
|
|
33
|
+
native: 'in-app',
|
|
40
34
|
};
|
|
41
35
|
}
|
|
42
36
|
if (type === 'subs') {
|
|
@@ -45,26 +39,39 @@ const normalizeProductType = (type) => {
|
|
|
45
39
|
native: 'subs',
|
|
46
40
|
};
|
|
47
41
|
}
|
|
42
|
+
if (type === 'all') {
|
|
43
|
+
return {
|
|
44
|
+
canonical: 'all',
|
|
45
|
+
native: 'all',
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
48
|
throw new Error(`Unsupported product type: ${type}`);
|
|
49
49
|
};
|
|
50
|
+
const normalizePurchasePlatform = (purchase) => {
|
|
51
|
+
const platform = purchase.platform;
|
|
52
|
+
if (typeof platform !== 'string') {
|
|
53
|
+
return purchase;
|
|
54
|
+
}
|
|
55
|
+
const lowered = platform.toLowerCase();
|
|
56
|
+
if (lowered === platform || (lowered !== 'ios' && lowered !== 'android')) {
|
|
57
|
+
return purchase;
|
|
58
|
+
}
|
|
59
|
+
return { ...purchase, platform: lowered };
|
|
60
|
+
};
|
|
61
|
+
const normalizePurchaseArray = (purchases) => purchases.map((purchase) => normalizePurchasePlatform(purchase));
|
|
50
62
|
export const purchaseUpdatedListener = (listener) => {
|
|
51
|
-
console.log('[JS] Registering purchaseUpdatedListener');
|
|
52
63
|
const wrappedListener = (event) => {
|
|
53
|
-
|
|
54
|
-
listener(
|
|
64
|
+
const normalized = normalizePurchasePlatform(event);
|
|
65
|
+
listener(normalized);
|
|
55
66
|
};
|
|
56
67
|
const emitterSubscription = emitter.addListener(OpenIapEvent.PurchaseUpdated, wrappedListener);
|
|
57
|
-
console.log('[JS] purchaseUpdatedListener registered successfully');
|
|
58
68
|
return emitterSubscription;
|
|
59
69
|
};
|
|
60
70
|
export const purchaseErrorListener = (listener) => {
|
|
61
|
-
console.log('[JS] Registering purchaseErrorListener');
|
|
62
71
|
const wrappedListener = (error) => {
|
|
63
|
-
console.log('[JS] purchaseErrorListener fired:', error);
|
|
64
72
|
listener(error);
|
|
65
73
|
};
|
|
66
74
|
const emitterSubscription = emitter.addListener(OpenIapEvent.PurchaseError, wrappedListener);
|
|
67
|
-
console.log('[JS] purchaseErrorListener registered successfully');
|
|
68
75
|
return emitterSubscription;
|
|
69
76
|
};
|
|
70
77
|
/**
|
|
@@ -94,116 +101,80 @@ export const promotedProductListenerIOS = (listener) => {
|
|
|
94
101
|
}
|
|
95
102
|
return emitter.addListener(OpenIapEvent.PromotedProductIOS, listener);
|
|
96
103
|
};
|
|
97
|
-
export
|
|
98
|
-
|
|
99
|
-
return Promise.resolve(result);
|
|
100
|
-
}
|
|
101
|
-
export async function endConnection() {
|
|
102
|
-
return ExpoIapModule.endConnection();
|
|
103
|
-
}
|
|
104
|
+
export const initConnection = async () => ExpoIapModule.initConnection();
|
|
105
|
+
export const endConnection = async () => ExpoIapModule.endConnection();
|
|
104
106
|
/**
|
|
105
107
|
* Fetch products with unified API (v2.7.0+)
|
|
106
108
|
*
|
|
107
|
-
* @param
|
|
108
|
-
* @param
|
|
109
|
-
* @param
|
|
110
|
-
*
|
|
111
|
-
* @example
|
|
112
|
-
* ```typescript
|
|
113
|
-
* // Regular products
|
|
114
|
-
* const products = await fetchProducts({
|
|
115
|
-
* skus: ['product1', 'product2'],
|
|
116
|
-
* type: 'in-app'
|
|
117
|
-
* });
|
|
118
|
-
*
|
|
119
|
-
* // Subscriptions
|
|
120
|
-
* const subscriptions = await fetchProducts({
|
|
121
|
-
* skus: ['sub1', 'sub2'],
|
|
122
|
-
* type: 'subs'
|
|
123
|
-
* });
|
|
124
|
-
* ```
|
|
109
|
+
* @param request - Product fetch configuration
|
|
110
|
+
* @param request.skus - Array of product SKUs to fetch
|
|
111
|
+
* @param request.type - Product query type: 'in-app', 'subs', or 'all'
|
|
125
112
|
*/
|
|
126
|
-
export const fetchProducts = async (
|
|
127
|
-
|
|
128
|
-
|
|
113
|
+
export const fetchProducts = async (request) => {
|
|
114
|
+
console.log('fetchProducts called with:', request);
|
|
115
|
+
const { skus, type } = request ?? {};
|
|
116
|
+
if (!Array.isArray(skus) || skus.length === 0) {
|
|
117
|
+
throw createPurchaseError({
|
|
129
118
|
message: 'No SKUs provided',
|
|
130
119
|
code: ErrorCode.EmptySkuList,
|
|
131
120
|
});
|
|
132
121
|
}
|
|
133
122
|
const { canonical, native } = normalizeProductType(type);
|
|
123
|
+
const skuSet = new Set(skus);
|
|
124
|
+
const filterIosItems = (items) => items.filter((item) => {
|
|
125
|
+
if (!isProductIOS(item)) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
const candidate = item;
|
|
129
|
+
return typeof candidate.id === 'string' && skuSet.has(candidate.id);
|
|
130
|
+
});
|
|
131
|
+
const filterAndroidItems = (items) => items.filter((item) => {
|
|
132
|
+
if (!isProductAndroid(item)) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
const candidate = item;
|
|
136
|
+
return typeof candidate.id === 'string' && skuSet.has(candidate.id);
|
|
137
|
+
});
|
|
138
|
+
const castResult = (items) => {
|
|
139
|
+
if (canonical === 'in-app') {
|
|
140
|
+
return items;
|
|
141
|
+
}
|
|
142
|
+
if (canonical === 'subs') {
|
|
143
|
+
return items;
|
|
144
|
+
}
|
|
145
|
+
return items;
|
|
146
|
+
};
|
|
134
147
|
if (Platform.OS === 'ios') {
|
|
135
148
|
const rawItems = await ExpoIapModule.fetchProducts({ skus, type: native });
|
|
136
|
-
|
|
137
|
-
if (!isProductIOS(item)) {
|
|
138
|
-
return false;
|
|
139
|
-
}
|
|
140
|
-
const isValid = typeof item === 'object' &&
|
|
141
|
-
item !== null &&
|
|
142
|
-
'id' in item &&
|
|
143
|
-
typeof item.id === 'string' &&
|
|
144
|
-
skus.includes(item.id);
|
|
145
|
-
return isValid;
|
|
146
|
-
});
|
|
147
|
-
return canonical === 'in-app'
|
|
148
|
-
? filteredItems
|
|
149
|
-
: filteredItems;
|
|
149
|
+
return castResult(filterIosItems(rawItems));
|
|
150
150
|
}
|
|
151
151
|
if (Platform.OS === 'android') {
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
if (!isProductAndroid(item))
|
|
155
|
-
return false;
|
|
156
|
-
return (typeof item === 'object' &&
|
|
157
|
-
item !== null &&
|
|
158
|
-
'id' in item &&
|
|
159
|
-
typeof item.id === 'string' &&
|
|
160
|
-
skus.includes(item.id));
|
|
161
|
-
});
|
|
162
|
-
return canonical === 'in-app'
|
|
163
|
-
? filteredItems
|
|
164
|
-
: filteredItems;
|
|
152
|
+
const rawItems = await ExpoIapModule.fetchProducts(native, skus);
|
|
153
|
+
return castResult(filterAndroidItems(rawItems));
|
|
165
154
|
}
|
|
166
155
|
throw new Error('Unsupported platform');
|
|
167
156
|
};
|
|
168
|
-
export const getAvailablePurchases = (
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Restore completed transactions (cross-platform behavior)
|
|
174
|
-
*
|
|
175
|
-
* - iOS: perform a lightweight sync to refresh transactions and ignore sync errors,
|
|
176
|
-
* then fetch available purchases to surface restored items to the app.
|
|
177
|
-
* - Android: simply fetch available purchases (restoration happens via query).
|
|
178
|
-
*
|
|
179
|
-
* This helper returns the restored/available purchases so callers can update UI/state.
|
|
180
|
-
*
|
|
181
|
-
* @param options.alsoPublishToEventListenerIOS - iOS only: whether to also publish to the event listener
|
|
182
|
-
* @param options.onlyIncludeActiveItemsIOS - iOS only: whether to only include active items
|
|
183
|
-
* @returns Promise resolving to the list of available/restored purchases
|
|
184
|
-
*/
|
|
185
|
-
export const restorePurchases = async (options = {}) => {
|
|
186
|
-
if (Platform.OS === 'ios') {
|
|
187
|
-
// Perform best-effort sync on iOS and ignore sync errors to avoid blocking restore flow
|
|
188
|
-
await syncIOS().catch(() => undefined);
|
|
189
|
-
}
|
|
190
|
-
// Then, fetch available purchases for both platforms
|
|
191
|
-
const purchases = await getAvailablePurchases({
|
|
192
|
-
alsoPublishToEventListenerIOS: options.alsoPublishToEventListenerIOS ?? false,
|
|
193
|
-
onlyIncludeActiveItemsIOS: options.onlyIncludeActiveItemsIOS ?? true,
|
|
194
|
-
});
|
|
195
|
-
return purchases;
|
|
196
|
-
};
|
|
197
|
-
const offerToRecordIOS = (offer) => {
|
|
198
|
-
if (!offer)
|
|
199
|
-
return undefined;
|
|
200
|
-
return {
|
|
201
|
-
identifier: offer.identifier,
|
|
202
|
-
keyIdentifier: offer.keyIdentifier,
|
|
203
|
-
nonce: offer.nonce,
|
|
204
|
-
signature: offer.signature,
|
|
205
|
-
timestamp: offer.timestamp.toString(),
|
|
157
|
+
export const getAvailablePurchases = async (options) => {
|
|
158
|
+
const normalizedOptions = {
|
|
159
|
+
alsoPublishToEventListenerIOS: options?.alsoPublishToEventListenerIOS ?? false,
|
|
160
|
+
onlyIncludeActiveItemsIOS: options?.onlyIncludeActiveItemsIOS ?? true,
|
|
206
161
|
};
|
|
162
|
+
const resolvePurchases = Platform.select({
|
|
163
|
+
ios: () => ExpoIapModule.getAvailableItems(normalizedOptions.alsoPublishToEventListenerIOS, normalizedOptions.onlyIncludeActiveItemsIOS),
|
|
164
|
+
android: () => ExpoIapModule.getAvailableItems(),
|
|
165
|
+
}) ?? (() => Promise.resolve([]));
|
|
166
|
+
const purchases = await resolvePurchases();
|
|
167
|
+
return normalizePurchaseArray(purchases);
|
|
168
|
+
};
|
|
169
|
+
export const getStorefront = async () => {
|
|
170
|
+
// Cross-platform storefront
|
|
171
|
+
if (Platform.OS === 'android') {
|
|
172
|
+
if (typeof ExpoIapModule.getStorefrontAndroid === 'function') {
|
|
173
|
+
return ExpoIapModule.getStorefrontAndroid();
|
|
174
|
+
}
|
|
175
|
+
return '';
|
|
176
|
+
}
|
|
177
|
+
return getStorefrontIOS();
|
|
207
178
|
};
|
|
208
179
|
function normalizeRequestProps(request, platform) {
|
|
209
180
|
// Platform-specific format - directly return the appropriate platform data
|
|
@@ -240,8 +211,8 @@ function normalizeRequestProps(request, platform) {
|
|
|
240
211
|
* });
|
|
241
212
|
* ```
|
|
242
213
|
*/
|
|
243
|
-
export const requestPurchase = (
|
|
244
|
-
const { request, type } =
|
|
214
|
+
export const requestPurchase = async (args) => {
|
|
215
|
+
const { request, type } = args;
|
|
245
216
|
const { canonical, native } = normalizeProductType(type);
|
|
246
217
|
const isInAppPurchase = canonical === 'in-app';
|
|
247
218
|
if (Platform.OS === 'ios') {
|
|
@@ -249,18 +220,30 @@ export const requestPurchase = (requestObj) => {
|
|
|
249
220
|
if (!normalizedRequest?.sku) {
|
|
250
221
|
throw new Error('Invalid request for iOS. The `sku` property is required and must be a string.');
|
|
251
222
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
223
|
+
let payload;
|
|
224
|
+
if (canonical === 'in-app') {
|
|
225
|
+
payload = {
|
|
226
|
+
type: 'in-app',
|
|
227
|
+
request: request,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
else if (canonical === 'subs') {
|
|
231
|
+
payload = {
|
|
232
|
+
type: 'subs',
|
|
233
|
+
request: request,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
throw new Error(`Unsupported product type: ${canonical}`);
|
|
238
|
+
}
|
|
239
|
+
const purchase = (await ExpoIapModule.requestPurchase(payload));
|
|
240
|
+
if (Array.isArray(purchase)) {
|
|
241
|
+
return normalizePurchaseArray(purchase);
|
|
242
|
+
}
|
|
243
|
+
if (purchase) {
|
|
244
|
+
return normalizePurchasePlatform(purchase);
|
|
245
|
+
}
|
|
246
|
+
return canonical === 'subs' ? [] : null;
|
|
264
247
|
}
|
|
265
248
|
if (Platform.OS === 'android') {
|
|
266
249
|
if (isInAppPurchase) {
|
|
@@ -269,18 +252,17 @@ export const requestPurchase = (requestObj) => {
|
|
|
269
252
|
throw new Error('Invalid request for Android. The `skus` property is required and must be a non-empty array.');
|
|
270
253
|
}
|
|
271
254
|
const { skus, obfuscatedAccountIdAndroid, obfuscatedProfileIdAndroid, isOfferPersonalized, } = normalizedRequest;
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
})();
|
|
255
|
+
const result = (await ExpoIapModule.requestPurchase({
|
|
256
|
+
type: native,
|
|
257
|
+
skuArr: skus,
|
|
258
|
+
purchaseToken: undefined,
|
|
259
|
+
replacementMode: -1,
|
|
260
|
+
obfuscatedAccountId: obfuscatedAccountIdAndroid,
|
|
261
|
+
obfuscatedProfileId: obfuscatedProfileIdAndroid,
|
|
262
|
+
offerTokenArr: [],
|
|
263
|
+
isOfferPersonalized: isOfferPersonalized ?? false,
|
|
264
|
+
}));
|
|
265
|
+
return normalizePurchaseArray(result);
|
|
284
266
|
}
|
|
285
267
|
if (canonical === 'subs') {
|
|
286
268
|
const normalizedRequest = normalizeRequestProps(request, 'android');
|
|
@@ -291,90 +273,94 @@ export const requestPurchase = (requestObj) => {
|
|
|
291
273
|
const normalizedOffers = subscriptionOffers ?? [];
|
|
292
274
|
const replacementMode = replacementModeAndroid ?? -1;
|
|
293
275
|
const purchaseToken = purchaseTokenAndroid ?? undefined;
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
})();
|
|
276
|
+
const result = (await ExpoIapModule.requestPurchase({
|
|
277
|
+
type: native,
|
|
278
|
+
skuArr: skus,
|
|
279
|
+
purchaseToken,
|
|
280
|
+
replacementMode,
|
|
281
|
+
obfuscatedAccountId: obfuscatedAccountIdAndroid,
|
|
282
|
+
obfuscatedProfileId: obfuscatedProfileIdAndroid,
|
|
283
|
+
offerTokenArr: normalizedOffers.map((offer) => offer.offerToken),
|
|
284
|
+
subscriptionOffers: normalizedOffers,
|
|
285
|
+
isOfferPersonalized: isOfferPersonalized ?? false,
|
|
286
|
+
}));
|
|
287
|
+
return normalizePurchaseArray(result);
|
|
307
288
|
}
|
|
308
289
|
throw new Error("Invalid request for Android: Expected a valid request object with 'skus' array.");
|
|
309
290
|
}
|
|
310
|
-
|
|
291
|
+
throw new Error('Platform not supported');
|
|
311
292
|
};
|
|
312
|
-
export const finishTransaction = ({ purchase, isConsumable = false, }) => {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
return ExpoIapModule.consumePurchaseAndroid(token);
|
|
336
|
-
}
|
|
337
|
-
return ExpoIapModule.acknowledgePurchaseAndroid(token);
|
|
338
|
-
},
|
|
339
|
-
}) || (() => Promise.reject(new Error('Unsupported Platform'))))();
|
|
293
|
+
export const finishTransaction = async ({ purchase, isConsumable = false, }) => {
|
|
294
|
+
if (Platform.OS === 'ios') {
|
|
295
|
+
await ExpoIapModule.finishTransaction(purchase, isConsumable);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (Platform.OS === 'android') {
|
|
299
|
+
const token = purchase.purchaseToken ?? undefined;
|
|
300
|
+
if (!token) {
|
|
301
|
+
throw createPurchaseError({
|
|
302
|
+
message: 'Purchase token is required to finish transaction',
|
|
303
|
+
code: ErrorCode.DeveloperError,
|
|
304
|
+
productId: purchase.productId,
|
|
305
|
+
platform: 'android',
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
if (isConsumable) {
|
|
309
|
+
await ExpoIapModule.consumePurchaseAndroid(token);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
await ExpoIapModule.acknowledgePurchaseAndroid(token);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
throw new Error('Unsupported Platform');
|
|
340
316
|
};
|
|
341
317
|
/**
|
|
342
|
-
*
|
|
343
|
-
*
|
|
344
|
-
* @returns Promise resolving to the storefront country code
|
|
345
|
-
* @throws Error if called on non-iOS platform
|
|
318
|
+
* Restore completed transactions (cross-platform behavior)
|
|
346
319
|
*
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
-
*
|
|
350
|
-
* console.log(storefront); // 'US'
|
|
351
|
-
* ```
|
|
320
|
+
* - iOS: perform a lightweight sync to refresh transactions and ignore sync errors,
|
|
321
|
+
* then fetch available purchases to surface restored items to the app.
|
|
322
|
+
* - Android: simply fetch available purchases (restoration happens via query).
|
|
352
323
|
*
|
|
353
|
-
*
|
|
324
|
+
* This helper triggers the refresh flows but does not return the purchases; consumers should
|
|
325
|
+
* call `getAvailablePurchases` or rely on hook state to inspect the latest items.
|
|
354
326
|
*/
|
|
355
|
-
export const
|
|
356
|
-
if (Platform.OS
|
|
357
|
-
|
|
358
|
-
return Promise.resolve('');
|
|
327
|
+
export const restorePurchases = async () => {
|
|
328
|
+
if (Platform.OS === 'ios') {
|
|
329
|
+
await syncIOS().catch(() => undefined);
|
|
359
330
|
}
|
|
360
|
-
|
|
331
|
+
await getAvailablePurchases({
|
|
332
|
+
alsoPublishToEventListenerIOS: false,
|
|
333
|
+
onlyIncludeActiveItemsIOS: true,
|
|
334
|
+
});
|
|
361
335
|
};
|
|
362
336
|
/**
|
|
363
|
-
*
|
|
364
|
-
*
|
|
337
|
+
* Deeplinks to native interface that allows users to manage their subscriptions
|
|
338
|
+
* @param options.skuAndroid - Required for Android to locate specific subscription (ignored on iOS)
|
|
339
|
+
* @param options.packageNameAndroid - Required for Android to identify your app (ignored on iOS)
|
|
340
|
+
*
|
|
341
|
+
* @returns Promise that resolves when the deep link is successfully opened
|
|
342
|
+
*
|
|
343
|
+
* @throws {Error} When called on unsupported platform or when required Android parameters are missing
|
|
344
|
+
*
|
|
345
|
+
* @example
|
|
346
|
+
* import { deepLinkToSubscriptions } from 'expo-iap';
|
|
365
347
|
*
|
|
366
|
-
*
|
|
367
|
-
*
|
|
348
|
+
* // Works on both iOS and Android
|
|
349
|
+
* await deepLinkToSubscriptions({
|
|
350
|
+
* skuAndroid: 'your_subscription_sku',
|
|
351
|
+
* packageNameAndroid: 'com.example.app'
|
|
352
|
+
* });
|
|
368
353
|
*/
|
|
369
|
-
export const
|
|
370
|
-
|
|
354
|
+
export const deepLinkToSubscriptions = async (options) => {
|
|
355
|
+
if (Platform.OS === 'ios') {
|
|
356
|
+
await deepLinkToSubscriptionsIOS();
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
371
359
|
if (Platform.OS === 'android') {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
}
|
|
375
|
-
return Promise.resolve('');
|
|
360
|
+
await deepLinkToSubscriptionsAndroid(options ?? null);
|
|
361
|
+
return;
|
|
376
362
|
}
|
|
377
|
-
|
|
363
|
+
throw new Error(`Unsupported platform: ${Platform.OS}`);
|
|
378
364
|
};
|
|
379
365
|
/**
|
|
380
366
|
* Internal receipt validation function (NOT RECOMMENDED for production use)
|
|
@@ -384,59 +370,28 @@ export const getStorefront = () => {
|
|
|
384
370
|
* - iOS: Send receipt data to Apple's verification endpoint from your server
|
|
385
371
|
* - Android: Use Google Play Developer API with service account credentials
|
|
386
372
|
*/
|
|
387
|
-
export const validateReceipt = async (
|
|
373
|
+
export const validateReceipt = async (options) => {
|
|
374
|
+
const { sku, androidOptions } = options;
|
|
388
375
|
if (Platform.OS === 'ios') {
|
|
389
|
-
return
|
|
376
|
+
return validateReceiptIOS({ sku });
|
|
390
377
|
}
|
|
391
|
-
|
|
378
|
+
if (Platform.OS === 'android') {
|
|
392
379
|
if (!androidOptions ||
|
|
393
380
|
!androidOptions.packageName ||
|
|
394
381
|
!androidOptions.productToken ||
|
|
395
382
|
!androidOptions.accessToken) {
|
|
396
383
|
throw new Error('Android validation requires packageName, productToken, and accessToken');
|
|
397
384
|
}
|
|
398
|
-
return
|
|
385
|
+
return validateReceiptAndroid({
|
|
399
386
|
packageName: androidOptions.packageName,
|
|
400
387
|
productId: sku,
|
|
401
388
|
productToken: androidOptions.productToken,
|
|
402
389
|
accessToken: androidOptions.accessToken,
|
|
403
|
-
isSub: androidOptions.isSub,
|
|
404
|
-
});
|
|
405
|
-
}
|
|
406
|
-
else {
|
|
407
|
-
throw new Error('Platform not supported');
|
|
408
|
-
}
|
|
409
|
-
};
|
|
410
|
-
/**
|
|
411
|
-
* Deeplinks to native interface that allows users to manage their subscriptions
|
|
412
|
-
* @param options.skuAndroid - Required for Android to locate specific subscription (ignored on iOS)
|
|
413
|
-
* @param options.packageNameAndroid - Required for Android to identify your app (ignored on iOS)
|
|
414
|
-
*
|
|
415
|
-
* @returns Promise that resolves when the deep link is successfully opened
|
|
416
|
-
*
|
|
417
|
-
* @throws {Error} When called on unsupported platform or when required Android parameters are missing
|
|
418
|
-
*
|
|
419
|
-
* @example
|
|
420
|
-
* import { deepLinkToSubscriptions } from 'expo-iap';
|
|
421
|
-
*
|
|
422
|
-
* // Works on both iOS and Android
|
|
423
|
-
* await deepLinkToSubscriptions({
|
|
424
|
-
* skuAndroid: 'your_subscription_sku',
|
|
425
|
-
* packageNameAndroid: 'com.example.app'
|
|
426
|
-
* });
|
|
427
|
-
*/
|
|
428
|
-
export const deepLinkToSubscriptions = (options) => {
|
|
429
|
-
if (Platform.OS === 'ios') {
|
|
430
|
-
return deepLinkToSubscriptionsIOS();
|
|
431
|
-
}
|
|
432
|
-
if (Platform.OS === 'android') {
|
|
433
|
-
return deepLinkToSubscriptionsAndroid({
|
|
434
|
-
sku: options?.skuAndroid,
|
|
435
|
-
packageName: options?.packageNameAndroid,
|
|
390
|
+
isSub: androidOptions.isSub ?? undefined,
|
|
436
391
|
});
|
|
437
392
|
}
|
|
438
|
-
|
|
393
|
+
throw new Error('Platform not supported');
|
|
439
394
|
};
|
|
440
395
|
export * from './useIAP';
|
|
441
|
-
export
|
|
396
|
+
export { ErrorCodeUtils, ErrorCodeMapping, createPurchaseError, createPurchaseErrorFromPlatform, } from './utils/errorMapping';
|
|
442
397
|
//# sourceMappingURL=index.js.map
|