expo-iap 3.0.8 → 3.1.1-rc.1
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 +2 -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/ExpoIapModule.d.ts +1 -0
- package/build/ExpoIapModule.d.ts.map +1 -1
- package/build/ExpoIapModule.js +29 -4
- package/build/ExpoIapModule.js.map +1 -1
- package/build/index.d.ts +20 -47
- package/build/index.d.ts.map +1 -1
- package/build/index.js +94 -137
- package/build/index.js.map +1 -1
- package/build/modules/android.d.ts.map +1 -1
- package/build/modules/android.js +2 -1
- package/build/modules/android.js.map +1 -1
- package/build/modules/ios.d.ts +16 -1
- package/build/modules/ios.d.ts.map +1 -1
- package/build/modules/ios.js +29 -16
- package/build/modules/ios.js.map +1 -1
- package/build/types.d.ts +8 -6
- package/build/types.d.ts.map +1 -1
- package/build/types.js.map +1 -1
- package/build/useIAP.d.ts +1 -1
- package/build/useIAP.d.ts.map +1 -1
- package/build/useIAP.js +12 -15
- package/build/useIAP.js.map +1 -1
- package/build/utils/constants.d.ts +2 -0
- package/build/utils/constants.d.ts.map +1 -1
- package/build/utils/constants.js +9 -2
- package/build/utils/constants.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/coverage/clover.xml +497 -0
- package/coverage/coverage-final.json +6 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +161 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +196 -0
- package/coverage/lcov-report/src/ExpoIap.types.ts.html +1243 -0
- package/coverage/lcov-report/src/PurchaseError.ts.html +787 -0
- package/coverage/lcov-report/src/helpers/index.html +116 -0
- package/coverage/lcov-report/src/helpers/subscription.ts.html +496 -0
- package/coverage/lcov-report/src/index.html +116 -0
- package/coverage/lcov-report/src/index.ts.html +1993 -0
- package/coverage/lcov-report/src/modules/android.ts.html +550 -0
- package/coverage/lcov-report/src/modules/index.html +131 -0
- package/coverage/lcov-report/src/modules/ios.ts.html +1222 -0
- package/coverage/lcov-report/src/purchase-error.ts.html +880 -0
- package/coverage/lcov-report/src/types/ExpoIapAndroid.types.ts.html +493 -0
- package/coverage/lcov-report/src/types/index.html +116 -0
- package/coverage/lcov-report/src/useIap.ts.html +1483 -0
- package/coverage/lcov-report/src/utils/errorMapping.ts.html +1069 -0
- package/coverage/lcov-report/src/utils/index.html +116 -0
- package/coverage/lcov-report/src/utils/purchase.ts.html +241 -0
- package/coverage/lcov.info +929 -0
- package/expo-module.config.json +10 -3
- 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/ios/OneSideModule.swift +489 -0
- package/ios/expoiap.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- package/ios/expoiap.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- package/openiap-versions.json +5 -0
- package/package.json +4 -3
- package/plugin/build/withIAP.d.ts +22 -9
- package/plugin/build/withIAP.js +163 -13
- package/plugin/jest.config.js +13 -3
- package/plugin/src/expoConfig.augmentation.d.ts +38 -0
- package/plugin/src/withIAP.ts +272 -22
- package/plugin/tsconfig.json +2 -1
- package/plugin/tsconfig.tsbuildinfo +1 -1
- package/scripts/update-types.mjs +20 -1
- package/src/ExpoIapModule.ts +45 -4
- package/src/index.ts +122 -165
- package/src/modules/android.ts +2 -1
- package/src/modules/ios.ts +31 -19
- package/src/types.ts +8 -6
- package/src/useIAP.ts +17 -25
- package/src/utils/constants.ts +11 -2
- package/src/utils/errorMapping.ts +203 -23
- 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/build/utils/purchase.d.ts +0 -9
- package/build/utils/purchase.d.ts.map +0 -1
- package/build/utils/purchase.js +0 -34
- package/build/utils/purchase.js.map +0 -1
- package/src/purchase-error.ts +0 -265
- package/src/utils/purchase.ts +0 -52
package/scripts/update-types.mjs
CHANGED
|
@@ -3,8 +3,27 @@ import {mkdtempSync, readFileSync, writeFileSync, rmSync} from 'node:fs';
|
|
|
3
3
|
import {join} from 'node:path';
|
|
4
4
|
import {tmpdir} from 'node:os';
|
|
5
5
|
import {execFileSync} from 'node:child_process';
|
|
6
|
+
import {fileURLToPath, URL} from 'node:url';
|
|
7
|
+
|
|
8
|
+
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
|
9
|
+
let versions;
|
|
10
|
+
try {
|
|
11
|
+
versions = JSON.parse(
|
|
12
|
+
readFileSync(join(__dirname, '..', 'openiap-versions.json'), 'utf8'),
|
|
13
|
+
);
|
|
14
|
+
} catch {
|
|
15
|
+
throw new Error(
|
|
16
|
+
'expo-iap: Unable to load openiap-versions.json. Ensure the file exists and is valid JSON.',
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const DEFAULT_TAG = versions?.gql;
|
|
21
|
+
if (typeof DEFAULT_TAG !== 'string' || DEFAULT_TAG.length === 0) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
'expo-iap: "gql" version missing in openiap-versions.json. Specify --tag manually or update the file.',
|
|
24
|
+
);
|
|
25
|
+
}
|
|
6
26
|
|
|
7
|
-
const DEFAULT_TAG = '1.0.1';
|
|
8
27
|
const PROJECT_ROOT = process.cwd();
|
|
9
28
|
|
|
10
29
|
function parseArgs() {
|
package/src/ExpoIapModule.ts
CHANGED
|
@@ -1,10 +1,51 @@
|
|
|
1
|
-
import {requireNativeModule} from 'expo-modules-core';
|
|
1
|
+
import {requireNativeModule, UnavailabilityError} from 'expo-modules-core';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const ExpoIapModule =
|
|
3
|
+
type NativeIapModuleName = 'ExpoIapOnside' | 'ExpoIap';
|
|
4
|
+
|
|
5
|
+
const {module: ExpoIapModule, name: resolvedNativeModuleName} =
|
|
6
|
+
resolveNativeModule();
|
|
7
|
+
|
|
8
|
+
export const USING_ONSIDE_SDK = resolvedNativeModuleName === 'ExpoIapOnside';
|
|
6
9
|
|
|
7
10
|
// Platform-specific error codes from native modules
|
|
8
11
|
export const NATIVE_ERROR_CODES = ExpoIapModule.ERROR_CODES || {};
|
|
9
12
|
|
|
10
13
|
export default ExpoIapModule;
|
|
14
|
+
|
|
15
|
+
function resolveNativeModule(): {
|
|
16
|
+
module: any;
|
|
17
|
+
name: NativeIapModuleName;
|
|
18
|
+
} {
|
|
19
|
+
const candidates: NativeIapModuleName[] = ['ExpoIapOnside', 'ExpoIap'];
|
|
20
|
+
|
|
21
|
+
for (const name of candidates) {
|
|
22
|
+
try {
|
|
23
|
+
const module = requireNativeModule(name);
|
|
24
|
+
return {module, name};
|
|
25
|
+
} catch (error) {
|
|
26
|
+
if (name === 'ExpoIapOnside' && isMissingModuleError(error, name)) {
|
|
27
|
+
// Onside module is optional. If unavailable, fall back to ExpoIap.
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
throw new UnavailabilityError(
|
|
36
|
+
'expo-iap',
|
|
37
|
+
'ExpoIap native module is unavailable',
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isMissingModuleError(error: unknown, moduleName: string): boolean {
|
|
42
|
+
if (error instanceof UnavailabilityError) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (error instanceof Error) {
|
|
47
|
+
return error.message.includes(`Cannot find native module '${moduleName}'`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return false;
|
|
51
|
+
}
|
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,
|
|
@@ -22,14 +23,12 @@ import type {
|
|
|
22
23
|
DeepLinkOptions,
|
|
23
24
|
FetchProductsResult,
|
|
24
25
|
MutationField,
|
|
26
|
+
MutationRequestPurchaseArgs,
|
|
25
27
|
MutationValidateReceiptArgs,
|
|
26
28
|
Product,
|
|
27
|
-
ProductAndroid,
|
|
28
|
-
ProductIOS,
|
|
29
29
|
ProductQueryType,
|
|
30
30
|
ProductSubscription,
|
|
31
31
|
Purchase,
|
|
32
|
-
PurchaseInput,
|
|
33
32
|
PurchaseOptions,
|
|
34
33
|
QueryField,
|
|
35
34
|
RequestPurchasePropsByPlatforms,
|
|
@@ -38,15 +37,12 @@ import type {
|
|
|
38
37
|
RequestSubscriptionPropsByPlatforms,
|
|
39
38
|
RequestSubscriptionAndroidProps,
|
|
40
39
|
RequestSubscriptionIosProps,
|
|
41
|
-
DiscountOfferInputIOS,
|
|
42
40
|
} from './types';
|
|
43
41
|
import {ErrorCode} from './types';
|
|
44
|
-
import {PurchaseError} from './
|
|
45
|
-
import {normalizePurchaseId, normalizePurchaseList} from './utils/purchase';
|
|
42
|
+
import {createPurchaseError, type PurchaseError} from './utils/errorMapping';
|
|
46
43
|
|
|
47
44
|
// Export all types
|
|
48
45
|
export * from './types';
|
|
49
|
-
export {ErrorCodeUtils, ErrorCodeMapping} from './purchase-error';
|
|
50
46
|
export * from './modules/android';
|
|
51
47
|
export * from './modules/ios';
|
|
52
48
|
|
|
@@ -57,18 +53,12 @@ export {
|
|
|
57
53
|
} from './helpers/subscription';
|
|
58
54
|
|
|
59
55
|
// Get the native constant value
|
|
60
|
-
export const PI = ExpoIapModule.PI;
|
|
61
|
-
|
|
62
56
|
export enum OpenIapEvent {
|
|
63
57
|
PurchaseUpdated = 'purchase-updated',
|
|
64
58
|
PurchaseError = 'purchase-error',
|
|
65
59
|
PromotedProductIOS = 'promoted-product-ios',
|
|
66
60
|
}
|
|
67
61
|
|
|
68
|
-
export function setValueAsync(value: string) {
|
|
69
|
-
return ExpoIapModule.setValueAsync(value);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
62
|
type ExpoIapEventPayloads = {
|
|
73
63
|
[OpenIapEvent.PurchaseUpdated]: Purchase;
|
|
74
64
|
[OpenIapEvent.PurchaseError]: PurchaseError;
|
|
@@ -109,7 +99,7 @@ const normalizeProductType = (type?: ProductTypeInput) => {
|
|
|
109
99
|
if (!type || type === 'inapp' || type === 'in-app') {
|
|
110
100
|
return {
|
|
111
101
|
canonical: 'in-app' as ProductQueryType,
|
|
112
|
-
native: '
|
|
102
|
+
native: 'in-app' as const,
|
|
113
103
|
};
|
|
114
104
|
}
|
|
115
105
|
if (type === 'subs') {
|
|
@@ -127,36 +117,47 @@ const normalizeProductType = (type?: ProductTypeInput) => {
|
|
|
127
117
|
throw new Error(`Unsupported product type: ${type}`);
|
|
128
118
|
};
|
|
129
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
|
+
|
|
130
137
|
export const purchaseUpdatedListener = (
|
|
131
138
|
listener: (event: Purchase) => void,
|
|
132
139
|
) => {
|
|
133
|
-
console.log('[JS] Registering purchaseUpdatedListener');
|
|
134
140
|
const wrappedListener = (event: Purchase) => {
|
|
135
|
-
const normalized =
|
|
136
|
-
console.log('[JS] purchaseUpdatedListener fired:', normalized);
|
|
141
|
+
const normalized = normalizePurchasePlatform(event);
|
|
137
142
|
listener(normalized);
|
|
138
143
|
};
|
|
139
144
|
const emitterSubscription = emitter.addListener(
|
|
140
145
|
OpenIapEvent.PurchaseUpdated,
|
|
141
146
|
wrappedListener,
|
|
142
147
|
);
|
|
143
|
-
console.log('[JS] purchaseUpdatedListener registered successfully');
|
|
144
148
|
return emitterSubscription;
|
|
145
149
|
};
|
|
146
150
|
|
|
147
151
|
export const purchaseErrorListener = (
|
|
148
152
|
listener: (error: PurchaseError) => void,
|
|
149
153
|
) => {
|
|
150
|
-
console.log('[JS] Registering purchaseErrorListener');
|
|
151
154
|
const wrappedListener = (error: PurchaseError) => {
|
|
152
|
-
console.log('[JS] purchaseErrorListener fired:', error);
|
|
153
155
|
listener(error);
|
|
154
156
|
};
|
|
155
157
|
const emitterSubscription = emitter.addListener(
|
|
156
158
|
OpenIapEvent.PurchaseError,
|
|
157
159
|
wrappedListener,
|
|
158
160
|
);
|
|
159
|
-
console.log('[JS] purchaseErrorListener registered successfully');
|
|
160
161
|
return emitterSubscription;
|
|
161
162
|
};
|
|
162
163
|
|
|
@@ -206,10 +207,11 @@ export const endConnection: MutationField<'endConnection'> = async () =>
|
|
|
206
207
|
* @param request.type - Product query type: 'in-app', 'subs', or 'all'
|
|
207
208
|
*/
|
|
208
209
|
export const fetchProducts: QueryField<'fetchProducts'> = async (request) => {
|
|
210
|
+
console.log('fetchProducts called with:', request);
|
|
209
211
|
const {skus, type} = request ?? {};
|
|
210
212
|
|
|
211
213
|
if (!Array.isArray(skus) || skus.length === 0) {
|
|
212
|
-
throw
|
|
214
|
+
throw createPurchaseError({
|
|
213
215
|
message: 'No SKUs provided',
|
|
214
216
|
code: ErrorCode.EmptySkuList,
|
|
215
217
|
});
|
|
@@ -223,22 +225,22 @@ export const fetchProducts: QueryField<'fetchProducts'> = async (request) => {
|
|
|
223
225
|
const filterIosItems = (
|
|
224
226
|
items: unknown[],
|
|
225
227
|
): Product[] | ProductSubscription[] =>
|
|
226
|
-
items.filter((item): item is
|
|
228
|
+
items.filter((item): item is Product | ProductSubscription => {
|
|
227
229
|
if (!isProductIOS(item)) {
|
|
228
230
|
return false;
|
|
229
231
|
}
|
|
230
|
-
const candidate = item as
|
|
232
|
+
const candidate = item as Product | ProductSubscription;
|
|
231
233
|
return typeof candidate.id === 'string' && skuSet.has(candidate.id);
|
|
232
234
|
});
|
|
233
235
|
|
|
234
236
|
const filterAndroidItems = (
|
|
235
237
|
items: unknown[],
|
|
236
238
|
): Product[] | ProductSubscription[] =>
|
|
237
|
-
items.filter((item): item is
|
|
239
|
+
items.filter((item): item is Product | ProductSubscription => {
|
|
238
240
|
if (!isProductAndroid(item)) {
|
|
239
241
|
return false;
|
|
240
242
|
}
|
|
241
|
-
const candidate = item as
|
|
243
|
+
const candidate = item as Product | ProductSubscription;
|
|
242
244
|
return typeof candidate.id === 'string' && skuSet.has(candidate.id);
|
|
243
245
|
});
|
|
244
246
|
|
|
@@ -287,44 +289,18 @@ export const getAvailablePurchases: QueryField<
|
|
|
287
289
|
}) ?? (() => Promise.resolve([] as Purchase[]));
|
|
288
290
|
|
|
289
291
|
const purchases = await resolvePurchases();
|
|
290
|
-
return
|
|
292
|
+
return normalizePurchaseArray(purchases as Purchase[]);
|
|
291
293
|
};
|
|
292
294
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
* This helper returns the restored/available purchases so callers can update UI/state.
|
|
301
|
-
*
|
|
302
|
-
* @param options.alsoPublishToEventListenerIOS - iOS only: whether to also publish to the event listener
|
|
303
|
-
* @param options.onlyIncludeActiveItemsIOS - iOS only: whether to only include active items
|
|
304
|
-
* @returns Promise resolving to the list of available/restored purchases
|
|
305
|
-
*/
|
|
306
|
-
export const restorePurchases: MutationField<'restorePurchases'> = async () => {
|
|
307
|
-
if (Platform.OS === 'ios') {
|
|
308
|
-
await syncIOS().catch(() => undefined);
|
|
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 '';
|
|
309
302
|
}
|
|
310
|
-
|
|
311
|
-
await getAvailablePurchases({
|
|
312
|
-
alsoPublishToEventListenerIOS: false,
|
|
313
|
-
onlyIncludeActiveItemsIOS: true,
|
|
314
|
-
});
|
|
315
|
-
};
|
|
316
|
-
|
|
317
|
-
const offerToRecordIOS = (
|
|
318
|
-
offer: DiscountOfferInputIOS | undefined,
|
|
319
|
-
): Record<keyof DiscountOfferInputIOS, string> | undefined => {
|
|
320
|
-
if (!offer) return undefined;
|
|
321
|
-
return {
|
|
322
|
-
identifier: offer.identifier,
|
|
323
|
-
keyIdentifier: offer.keyIdentifier,
|
|
324
|
-
nonce: offer.nonce,
|
|
325
|
-
signature: offer.signature,
|
|
326
|
-
timestamp: offer.timestamp.toString(),
|
|
327
|
-
};
|
|
303
|
+
return getStorefrontIOS();
|
|
328
304
|
};
|
|
329
305
|
|
|
330
306
|
/**
|
|
@@ -403,24 +379,35 @@ export const requestPurchase: MutationField<'requestPurchase'> = async (
|
|
|
403
379
|
);
|
|
404
380
|
}
|
|
405
381
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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
|
+
}
|
|
422
405
|
|
|
423
|
-
|
|
406
|
+
if (purchase) {
|
|
407
|
+
return normalizePurchasePlatform(purchase);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return canonical === 'subs' ? [] : null;
|
|
424
411
|
}
|
|
425
412
|
|
|
426
413
|
if (Platform.OS === 'android') {
|
|
@@ -454,7 +441,7 @@ export const requestPurchase: MutationField<'requestPurchase'> = async (
|
|
|
454
441
|
isOfferPersonalized: isOfferPersonalized ?? false,
|
|
455
442
|
})) as Purchase[];
|
|
456
443
|
|
|
457
|
-
return
|
|
444
|
+
return normalizePurchaseArray(result);
|
|
458
445
|
}
|
|
459
446
|
|
|
460
447
|
if (canonical === 'subs') {
|
|
@@ -497,7 +484,7 @@ export const requestPurchase: MutationField<'requestPurchase'> = async (
|
|
|
497
484
|
isOfferPersonalized: isOfferPersonalized ?? false,
|
|
498
485
|
})) as Purchase[];
|
|
499
486
|
|
|
500
|
-
return
|
|
487
|
+
return normalizePurchaseArray(result);
|
|
501
488
|
}
|
|
502
489
|
|
|
503
490
|
throw new Error(
|
|
@@ -508,43 +495,23 @@ export const requestPurchase: MutationField<'requestPurchase'> = async (
|
|
|
508
495
|
throw new Error('Platform not supported');
|
|
509
496
|
};
|
|
510
497
|
|
|
511
|
-
const toPurchaseInput = (
|
|
512
|
-
purchase: Purchase | PurchaseInput,
|
|
513
|
-
): PurchaseInput => ({
|
|
514
|
-
id: purchase.id,
|
|
515
|
-
ids: purchase.ids ?? undefined,
|
|
516
|
-
isAutoRenewing: purchase.isAutoRenewing,
|
|
517
|
-
platform: purchase.platform,
|
|
518
|
-
productId: purchase.productId,
|
|
519
|
-
purchaseState: purchase.purchaseState,
|
|
520
|
-
purchaseToken: purchase.purchaseToken ?? null,
|
|
521
|
-
quantity: purchase.quantity,
|
|
522
|
-
transactionDate: purchase.transactionDate,
|
|
523
|
-
});
|
|
524
|
-
|
|
525
498
|
export const finishTransaction: MutationField<'finishTransaction'> = async ({
|
|
526
499
|
purchase,
|
|
527
500
|
isConsumable = false,
|
|
528
501
|
}) => {
|
|
529
|
-
const normalizedPurchase = toPurchaseInput(purchase);
|
|
530
|
-
|
|
531
502
|
if (Platform.OS === 'ios') {
|
|
532
|
-
|
|
533
|
-
if (!transactionId) {
|
|
534
|
-
throw new Error('purchase.id required to finish iOS transaction');
|
|
535
|
-
}
|
|
536
|
-
await ExpoIapModule.finishTransaction(transactionId);
|
|
503
|
+
await ExpoIapModule.finishTransaction(purchase, isConsumable);
|
|
537
504
|
return;
|
|
538
505
|
}
|
|
539
506
|
|
|
540
507
|
if (Platform.OS === 'android') {
|
|
541
|
-
const token =
|
|
508
|
+
const token = purchase.purchaseToken ?? undefined;
|
|
542
509
|
|
|
543
510
|
if (!token) {
|
|
544
|
-
throw
|
|
511
|
+
throw createPurchaseError({
|
|
545
512
|
message: 'Purchase token is required to finish transaction',
|
|
546
513
|
code: ErrorCode.DeveloperError,
|
|
547
|
-
productId:
|
|
514
|
+
productId: purchase.productId,
|
|
548
515
|
platform: 'android',
|
|
549
516
|
});
|
|
550
517
|
}
|
|
@@ -562,43 +529,58 @@ export const finishTransaction: MutationField<'finishTransaction'> = async ({
|
|
|
562
529
|
};
|
|
563
530
|
|
|
564
531
|
/**
|
|
565
|
-
*
|
|
566
|
-
*
|
|
567
|
-
* @returns Promise resolving to the storefront country code
|
|
568
|
-
* @throws Error if called on non-iOS platform
|
|
532
|
+
* Restore completed transactions (cross-platform behavior)
|
|
569
533
|
*
|
|
570
|
-
*
|
|
571
|
-
*
|
|
572
|
-
*
|
|
573
|
-
* console.log(storefront); // 'US'
|
|
574
|
-
* ```
|
|
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).
|
|
575
537
|
*
|
|
576
|
-
*
|
|
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.
|
|
577
540
|
*/
|
|
578
|
-
export const
|
|
579
|
-
if (Platform.OS
|
|
580
|
-
|
|
581
|
-
return Promise.resolve('');
|
|
541
|
+
export const restorePurchases: MutationField<'restorePurchases'> = async () => {
|
|
542
|
+
if (Platform.OS === 'ios') {
|
|
543
|
+
await syncIOS().catch(() => undefined);
|
|
582
544
|
}
|
|
583
|
-
|
|
545
|
+
|
|
546
|
+
await getAvailablePurchases({
|
|
547
|
+
alsoPublishToEventListenerIOS: false,
|
|
548
|
+
onlyIncludeActiveItemsIOS: true,
|
|
549
|
+
});
|
|
584
550
|
};
|
|
585
551
|
|
|
586
552
|
/**
|
|
587
|
-
*
|
|
588
|
-
*
|
|
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)
|
|
556
|
+
*
|
|
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';
|
|
589
563
|
*
|
|
590
|
-
*
|
|
591
|
-
*
|
|
564
|
+
* // Works on both iOS and Android
|
|
565
|
+
* await deepLinkToSubscriptions({
|
|
566
|
+
* skuAndroid: 'your_subscription_sku',
|
|
567
|
+
* packageNameAndroid: 'com.example.app'
|
|
568
|
+
* });
|
|
592
569
|
*/
|
|
593
|
-
export const
|
|
594
|
-
|
|
570
|
+
export const deepLinkToSubscriptions: MutationField<
|
|
571
|
+
'deepLinkToSubscriptions'
|
|
572
|
+
> = async (options) => {
|
|
573
|
+
if (Platform.OS === 'ios') {
|
|
574
|
+
await deepLinkToSubscriptionsIOS();
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
|
|
595
578
|
if (Platform.OS === 'android') {
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
}
|
|
599
|
-
return Promise.resolve('');
|
|
579
|
+
await deepLinkToSubscriptionsAndroid((options as DeepLinkOptions) ?? null);
|
|
580
|
+
return;
|
|
600
581
|
}
|
|
601
|
-
|
|
582
|
+
|
|
583
|
+
throw new Error(`Unsupported platform: ${Platform.OS}`);
|
|
602
584
|
};
|
|
603
585
|
|
|
604
586
|
/**
|
|
@@ -641,39 +623,14 @@ export const validateReceipt: MutationField<'validateReceipt'> = async (
|
|
|
641
623
|
throw new Error('Platform not supported');
|
|
642
624
|
};
|
|
643
625
|
|
|
644
|
-
/**
|
|
645
|
-
* Deeplinks to native interface that allows users to manage their subscriptions
|
|
646
|
-
* @param options.skuAndroid - Required for Android to locate specific subscription (ignored on iOS)
|
|
647
|
-
* @param options.packageNameAndroid - Required for Android to identify your app (ignored on iOS)
|
|
648
|
-
*
|
|
649
|
-
* @returns Promise that resolves when the deep link is successfully opened
|
|
650
|
-
*
|
|
651
|
-
* @throws {Error} When called on unsupported platform or when required Android parameters are missing
|
|
652
|
-
*
|
|
653
|
-
* @example
|
|
654
|
-
* import { deepLinkToSubscriptions } from 'expo-iap';
|
|
655
|
-
*
|
|
656
|
-
* // Works on both iOS and Android
|
|
657
|
-
* await deepLinkToSubscriptions({
|
|
658
|
-
* skuAndroid: 'your_subscription_sku',
|
|
659
|
-
* packageNameAndroid: 'com.example.app'
|
|
660
|
-
* });
|
|
661
|
-
*/
|
|
662
|
-
export const deepLinkToSubscriptions: MutationField<
|
|
663
|
-
'deepLinkToSubscriptions'
|
|
664
|
-
> = async (options) => {
|
|
665
|
-
if (Platform.OS === 'ios') {
|
|
666
|
-
await deepLinkToSubscriptionsIOS();
|
|
667
|
-
return;
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
if (Platform.OS === 'android') {
|
|
671
|
-
await deepLinkToSubscriptionsAndroid((options as DeepLinkOptions) ?? null);
|
|
672
|
-
return;
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
throw new Error(`Unsupported platform: ${Platform.OS}`);
|
|
676
|
-
};
|
|
677
|
-
|
|
678
626
|
export * from './useIAP';
|
|
679
|
-
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';
|
package/src/modules/android.ts
CHANGED
|
@@ -19,7 +19,8 @@ export function isProductAndroid<T extends {platform?: string}>(
|
|
|
19
19
|
item != null &&
|
|
20
20
|
typeof item === 'object' &&
|
|
21
21
|
'platform' in item &&
|
|
22
|
-
(item as any).platform === '
|
|
22
|
+
typeof (item as any).platform === 'string' &&
|
|
23
|
+
(item as any).platform.toLowerCase() === 'android'
|
|
23
24
|
);
|
|
24
25
|
}
|
|
25
26
|
|
package/src/modules/ios.ts
CHANGED
|
@@ -15,9 +15,8 @@ import type {
|
|
|
15
15
|
ReceiptValidationResultIOS,
|
|
16
16
|
SubscriptionStatusIOS,
|
|
17
17
|
} from '../types';
|
|
18
|
-
import type {PurchaseError} from '../
|
|
19
|
-
import {Linking} from 'react-native';
|
|
20
|
-
import {normalizePurchaseId, normalizePurchaseList} from '../utils/purchase';
|
|
18
|
+
import type {PurchaseError} from '../utils/errorMapping';
|
|
19
|
+
import {Linking, Platform} from 'react-native';
|
|
21
20
|
|
|
22
21
|
export type TransactionEvent = {
|
|
23
22
|
transaction?: Purchase;
|
|
@@ -34,7 +33,8 @@ export function isProductIOS<T extends {platform?: string}>(
|
|
|
34
33
|
item != null &&
|
|
35
34
|
typeof item === 'object' &&
|
|
36
35
|
'platform' in item &&
|
|
37
|
-
(item as any).platform === '
|
|
36
|
+
typeof (item as any).platform === 'string' &&
|
|
37
|
+
(item as any).platform.toLowerCase() === 'ios'
|
|
38
38
|
);
|
|
39
39
|
}
|
|
40
40
|
|
|
@@ -105,7 +105,7 @@ export const currentEntitlementIOS: QueryField<
|
|
|
105
105
|
throw new Error('currentEntitlementIOS requires a SKU');
|
|
106
106
|
}
|
|
107
107
|
const purchase = await ExpoIapModule.currentEntitlementIOS(sku);
|
|
108
|
-
return
|
|
108
|
+
return (purchase ?? null) as PurchaseIOS | null;
|
|
109
109
|
};
|
|
110
110
|
|
|
111
111
|
/**
|
|
@@ -124,7 +124,7 @@ export const latestTransactionIOS: QueryField<'latestTransactionIOS'> = async (
|
|
|
124
124
|
throw new Error('latestTransactionIOS requires a SKU');
|
|
125
125
|
}
|
|
126
126
|
const transaction = await ExpoIapModule.latestTransactionIOS(sku);
|
|
127
|
-
return
|
|
127
|
+
return (transaction ?? null) as PurchaseIOS | null;
|
|
128
128
|
};
|
|
129
129
|
|
|
130
130
|
/**
|
|
@@ -159,7 +159,7 @@ export const showManageSubscriptionsIOS: MutationField<
|
|
|
159
159
|
'showManageSubscriptionsIOS'
|
|
160
160
|
> = async () => {
|
|
161
161
|
const purchases = await ExpoIapModule.showManageSubscriptionsIOS();
|
|
162
|
-
return
|
|
162
|
+
return (purchases ?? []) as PurchaseIOS[];
|
|
163
163
|
};
|
|
164
164
|
|
|
165
165
|
/**
|
|
@@ -178,6 +178,28 @@ export const getReceiptDataIOS: QueryField<'getReceiptDataIOS'> = async () => {
|
|
|
178
178
|
|
|
179
179
|
export const getReceiptIOS = getReceiptDataIOS;
|
|
180
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
|
+
|
|
181
203
|
/**
|
|
182
204
|
* Check if a transaction is verified through StoreKit 2.
|
|
183
205
|
* StoreKit 2 performs local verification of transaction JWS signatures.
|
|
@@ -242,19 +264,9 @@ const validateReceiptIOSImpl = async (
|
|
|
242
264
|
throw new Error('validateReceiptIOS requires a SKU');
|
|
243
265
|
}
|
|
244
266
|
|
|
245
|
-
|
|
267
|
+
return (await ExpoIapModule.validateReceiptIOS(
|
|
246
268
|
sku,
|
|
247
269
|
)) as ReceiptValidationResultIOS;
|
|
248
|
-
const normalizedLatest = normalizePurchaseId(
|
|
249
|
-
result.latestTransaction ?? undefined,
|
|
250
|
-
);
|
|
251
|
-
if (normalizedLatest === result.latestTransaction) {
|
|
252
|
-
return result;
|
|
253
|
-
}
|
|
254
|
-
return {
|
|
255
|
-
...result,
|
|
256
|
-
latestTransaction: normalizedLatest ?? null,
|
|
257
|
-
};
|
|
258
270
|
};
|
|
259
271
|
|
|
260
272
|
export const validateReceiptIOS =
|
|
@@ -340,7 +352,7 @@ export const getPendingTransactionsIOS: QueryField<
|
|
|
340
352
|
'getPendingTransactionsIOS'
|
|
341
353
|
> = async () => {
|
|
342
354
|
const transactions = await ExpoIapModule.getPendingTransactionsIOS();
|
|
343
|
-
return
|
|
355
|
+
return (transactions ?? []) as PurchaseIOS[];
|
|
344
356
|
};
|
|
345
357
|
|
|
346
358
|
/**
|