expo-iap 3.0.8 → 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 +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/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/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 +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/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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-iap",
|
|
3
|
-
"version": "3.0
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "In App Purchase module in Expo",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"docs:build": "cd docs && bun run build",
|
|
28
28
|
"docs:serve": "cd docs && bun run serve",
|
|
29
29
|
"docs:install": "cd docs && bun install",
|
|
30
|
-
"generate:types": "node scripts/update-types.mjs
|
|
30
|
+
"generate:types": "node scripts/update-types.mjs",
|
|
31
31
|
"generate:icon": "npx sharp-cli resize 32 32 -i docs/static/img/icon.png -o docs/static/img/favicon-32x32.png && npx sharp-cli resize 16 16 -i docs/static/img/icon.png -o docs/static/img/favicon-16x16.png && npx sharp-cli resize 180 180 -i docs/static/img/icon.png -o docs/static/img/apple-touch-icon.png && npx sharp-cli resize 192 192 -i docs/static/img/icon.png -o docs/static/img/android-chrome-192x192.png && npx sharp-cli resize 512 512 -i docs/static/img/icon.png -o docs/static/img/android-chrome-512x512.png && npx sharp-cli resize 150 150 -i docs/static/img/icon.png -o docs/static/img/mstile-150x150.png && npx sharp-cli resize 1200 630 -i docs/static/img/icon.png -o docs/static/img/og-image.png && npx sharp-cli resize 1200 600 -i docs/static/img/icon.png -o docs/static/img/twitter-card.png && npx sharp-cli resize 16 16 -i docs/static/img/icon.png -o docs/static/img/favicon.png && cp docs/static/img/favicon-16x16.png docs/static/img/favicon.ico"
|
|
32
32
|
},
|
|
33
33
|
"keywords": [
|
package/plugin/build/withIAP.js
CHANGED
|
@@ -41,6 +41,8 @@ const fs = __importStar(require("fs"));
|
|
|
41
41
|
const path = __importStar(require("path"));
|
|
42
42
|
const withLocalOpenIAP_1 = __importDefault(require("./withLocalOpenIAP"));
|
|
43
43
|
const pkg = require('../../package.json');
|
|
44
|
+
const openiapVersions = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../../openiap-versions.json'), 'utf8'));
|
|
45
|
+
const OPENIAP_ANDROID_VERSION = openiapVersions.google;
|
|
44
46
|
// Log a message only once per Node process
|
|
45
47
|
const logOnce = (() => {
|
|
46
48
|
const printed = new Set();
|
|
@@ -69,7 +71,7 @@ const modifyAppBuildGradle = (gradle, language) => {
|
|
|
69
71
|
const impl = (ga, v) => language === 'kotlin'
|
|
70
72
|
? ` implementation("${ga}:${v}")`
|
|
71
73
|
: ` implementation "${ga}:${v}"`;
|
|
72
|
-
const openiapDep = impl('io.github.hyochan.openiap:openiap-google',
|
|
74
|
+
const openiapDep = impl('io.github.hyochan.openiap:openiap-google', OPENIAP_ANDROID_VERSION);
|
|
73
75
|
// Remove any existing openiap-google lines (any version, groovy/kotlin, implementation/api)
|
|
74
76
|
const openiapAnyLine = /^\s*(?:implementation|api)\s*\(?\s*["']io\.github\.hyochan\.openiap:openiap-google:[^"']+["']\s*\)?\s*$/gm;
|
|
75
77
|
const hadExisting = openiapAnyLine.test(modified);
|
|
@@ -77,12 +79,12 @@ const modifyAppBuildGradle = (gradle, language) => {
|
|
|
77
79
|
modified = modified.replace(openiapAnyLine, '').replace(/\n{3,}/g, '\n\n');
|
|
78
80
|
}
|
|
79
81
|
// Ensure the desired dependency line is present
|
|
80
|
-
if (!new RegExp(String.raw `io\.github\.hyochan\.openiap:openiap-google
|
|
82
|
+
if (!new RegExp(String.raw `io\.github\.hyochan\.openiap:openiap-google:${OPENIAP_ANDROID_VERSION}`).test(modified)) {
|
|
81
83
|
// Insert just after the opening `dependencies {` line
|
|
82
84
|
modified = addLineToGradle(modified, /dependencies\s*{/, openiapDep, 1);
|
|
83
85
|
logOnce(hadExisting
|
|
84
|
-
?
|
|
85
|
-
:
|
|
86
|
+
? `🛠️ expo-iap: Replaced OpenIAP dependency with ${OPENIAP_ANDROID_VERSION}`
|
|
87
|
+
: `🛠️ expo-iap: Added OpenIAP dependency (${OPENIAP_ANDROID_VERSION}) to build.gradle`);
|
|
86
88
|
}
|
|
87
89
|
return modified;
|
|
88
90
|
};
|
package/plugin/src/withIAP.ts
CHANGED
|
@@ -11,6 +11,13 @@ import * as path from 'path';
|
|
|
11
11
|
import withLocalOpenIAP from './withLocalOpenIAP';
|
|
12
12
|
|
|
13
13
|
const pkg = require('../../package.json');
|
|
14
|
+
const openiapVersions = JSON.parse(
|
|
15
|
+
fs.readFileSync(
|
|
16
|
+
path.resolve(__dirname, '../../openiap-versions.json'),
|
|
17
|
+
'utf8',
|
|
18
|
+
),
|
|
19
|
+
);
|
|
20
|
+
const OPENIAP_ANDROID_VERSION = openiapVersions.google;
|
|
14
21
|
|
|
15
22
|
// Log a message only once per Node process
|
|
16
23
|
const logOnce = (() => {
|
|
@@ -54,7 +61,10 @@ const modifyAppBuildGradle = (
|
|
|
54
61
|
language === 'kotlin'
|
|
55
62
|
? ` implementation("${ga}:${v}")`
|
|
56
63
|
: ` implementation "${ga}:${v}"`;
|
|
57
|
-
const openiapDep = impl(
|
|
64
|
+
const openiapDep = impl(
|
|
65
|
+
'io.github.hyochan.openiap:openiap-google',
|
|
66
|
+
OPENIAP_ANDROID_VERSION,
|
|
67
|
+
);
|
|
58
68
|
|
|
59
69
|
// Remove any existing openiap-google lines (any version, groovy/kotlin, implementation/api)
|
|
60
70
|
const openiapAnyLine =
|
|
@@ -67,15 +77,15 @@ const modifyAppBuildGradle = (
|
|
|
67
77
|
// Ensure the desired dependency line is present
|
|
68
78
|
if (
|
|
69
79
|
!new RegExp(
|
|
70
|
-
String.raw`io\.github\.hyochan\.openiap:openiap-google
|
|
80
|
+
String.raw`io\.github\.hyochan\.openiap:openiap-google:${OPENIAP_ANDROID_VERSION}`,
|
|
71
81
|
).test(modified)
|
|
72
82
|
) {
|
|
73
83
|
// Insert just after the opening `dependencies {` line
|
|
74
84
|
modified = addLineToGradle(modified, /dependencies\s*{/, openiapDep, 1);
|
|
75
85
|
logOnce(
|
|
76
86
|
hadExisting
|
|
77
|
-
?
|
|
78
|
-
:
|
|
87
|
+
? `🛠️ expo-iap: Replaced OpenIAP dependency with ${OPENIAP_ANDROID_VERSION}`
|
|
88
|
+
: `🛠️ expo-iap: Added OpenIAP dependency (${OPENIAP_ANDROID_VERSION}) to build.gradle`,
|
|
79
89
|
);
|
|
80
90
|
}
|
|
81
91
|
|
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/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
|
|