expo-iap 2.3.0 → 2.3.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/iap.md +20 -38
- package/package.json +1 -1
- package/plugin/build/withIAP.js +34 -38
- package/plugin/src/withIAP.ts +39 -53
package/iap.md
CHANGED
|
@@ -209,8 +209,8 @@ Transactions map to `Purchase` or `SubscriptionPurchase` with platform-specific
|
|
|
209
209
|
Below is a simple example of fetching products and making a purchase with `expo-iap` in a managed workflow, updated to use the new `requestPurchase` signature:
|
|
210
210
|
|
|
211
211
|
```tsx
|
|
212
|
-
import {useEffect, useState} from 'react';
|
|
213
|
-
import {Button, Text, View} from 'react-native';
|
|
212
|
+
import { useEffect, useState } from 'react';
|
|
213
|
+
import { Button, Text, View } from 'react-native';
|
|
214
214
|
import {
|
|
215
215
|
initConnection,
|
|
216
216
|
endConnection,
|
|
@@ -237,7 +237,7 @@ export default function SimpleIAP() {
|
|
|
237
237
|
|
|
238
238
|
const purchaseListener = purchaseUpdatedListener(async (purchase) => {
|
|
239
239
|
if (purchase) {
|
|
240
|
-
await finishTransaction({purchase, isConsumable: true});
|
|
240
|
+
await finishTransaction({ purchase, isConsumable: true });
|
|
241
241
|
alert('Purchase completed!');
|
|
242
242
|
}
|
|
243
243
|
});
|
|
@@ -252,12 +252,12 @@ export default function SimpleIAP() {
|
|
|
252
252
|
const buyItem = async () => {
|
|
253
253
|
if (!product) return;
|
|
254
254
|
await requestPurchase({
|
|
255
|
-
request: {skus: [product.id]}, // Android expects 'skus'; iOS would use 'sku'
|
|
255
|
+
request: { skus: [product.id] }, // Android expects 'skus'; iOS would use 'sku'
|
|
256
256
|
});
|
|
257
257
|
};
|
|
258
258
|
|
|
259
259
|
return (
|
|
260
|
-
<View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}>
|
|
260
|
+
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
|
|
261
261
|
<Text>{isConnected ? 'Connected' : 'Connecting...'}</Text>
|
|
262
262
|
{product ? (
|
|
263
263
|
<>
|
|
@@ -277,7 +277,7 @@ export default function SimpleIAP() {
|
|
|
277
277
|
The `useIAP` hook simplifies managing in-app purchases. Below is an example updated to use the new `requestPurchase` signature:
|
|
278
278
|
|
|
279
279
|
```tsx
|
|
280
|
-
import {useEffect, useState} from 'react';
|
|
280
|
+
import { useEffect, useState } from 'react';
|
|
281
281
|
import {
|
|
282
282
|
SafeAreaView,
|
|
283
283
|
ScrollView,
|
|
@@ -289,20 +289,12 @@ import {
|
|
|
289
289
|
InteractionManager,
|
|
290
290
|
Alert,
|
|
291
291
|
} from 'react-native';
|
|
292
|
-
import {useIAP} from 'expo-iap';
|
|
293
|
-
import type {ProductPurchase, SubscriptionProduct} from 'expo-iap';
|
|
292
|
+
import { useIAP } from 'expo-iap';
|
|
293
|
+
import type { ProductPurchase, SubscriptionProduct } from 'expo-iap';
|
|
294
294
|
|
|
295
295
|
// Define SKUs
|
|
296
|
-
const productSkus = [
|
|
297
|
-
|
|
298
|
-
'cpk.points.5000',
|
|
299
|
-
'cpk.points.10000',
|
|
300
|
-
'cpk.points.30000',
|
|
301
|
-
];
|
|
302
|
-
const subscriptionSkus = [
|
|
303
|
-
'cpk.membership.monthly.bronze',
|
|
304
|
-
'cpk.membership.monthly.silver',
|
|
305
|
-
];
|
|
296
|
+
const productSkus = ['cpk.points.1000', 'cpk.points.5000', 'cpk.points.10000', 'cpk.points.30000'];
|
|
297
|
+
const subscriptionSkus = ['cpk.membership.monthly.bronze', 'cpk.membership.monthly.silver'];
|
|
306
298
|
|
|
307
299
|
// Define operations
|
|
308
300
|
const operations = ['getProducts', 'getSubscriptions'] as const;
|
|
@@ -329,10 +321,7 @@ export default function IAPWithHook() {
|
|
|
329
321
|
|
|
330
322
|
const initializeIAP = async () => {
|
|
331
323
|
try {
|
|
332
|
-
await Promise.all([
|
|
333
|
-
getProducts(productSkus),
|
|
334
|
-
getSubscriptions(subscriptionSkus),
|
|
335
|
-
]);
|
|
324
|
+
await Promise.all([getProducts(productSkus), getSubscriptions(subscriptionSkus)]);
|
|
336
325
|
setIsReady(true);
|
|
337
326
|
} catch (error) {
|
|
338
327
|
console.error('Error initializing IAP:', error);
|
|
@@ -400,10 +389,7 @@ export default function IAPWithHook() {
|
|
|
400
389
|
<View style={styles.buttons}>
|
|
401
390
|
<ScrollView contentContainerStyle={styles.buttonsWrapper} horizontal>
|
|
402
391
|
{operations.map((operation) => (
|
|
403
|
-
<Pressable
|
|
404
|
-
key={operation}
|
|
405
|
-
onPress={() => handleOperation(operation)}
|
|
406
|
-
>
|
|
392
|
+
<Pressable key={operation} onPress={() => handleOperation(operation)}>
|
|
407
393
|
<View style={styles.buttonView}>
|
|
408
394
|
<Text>{operation}</Text>
|
|
409
395
|
</View>
|
|
@@ -415,10 +401,10 @@ export default function IAPWithHook() {
|
|
|
415
401
|
{!isReady ? (
|
|
416
402
|
<Text>Loading...</Text>
|
|
417
403
|
) : (
|
|
418
|
-
<View style={{gap: 12}}>
|
|
419
|
-
<Text style={{fontSize: 20}}>Products</Text>
|
|
404
|
+
<View style={{ gap: 12 }}>
|
|
405
|
+
<Text style={{ fontSize: 20 }}>Products</Text>
|
|
420
406
|
{products.map((item) => (
|
|
421
|
-
<View key={item.id} style={{gap: 12}}>
|
|
407
|
+
<View key={item.id} style={{ gap: 12 }}>
|
|
422
408
|
<Text>
|
|
423
409
|
{item.title} -{' '}
|
|
424
410
|
{item.platform === 'android'
|
|
@@ -429,24 +415,20 @@ export default function IAPWithHook() {
|
|
|
429
415
|
title="Buy"
|
|
430
416
|
onPress={() =>
|
|
431
417
|
requestPurchase({
|
|
432
|
-
request:
|
|
433
|
-
item.platform === 'android'
|
|
434
|
-
? {skus: [item.id]}
|
|
435
|
-
: {sku: item.id},
|
|
418
|
+
request: item.platform === 'android' ? { skus: [item.id] } : { sku: item.id },
|
|
436
419
|
})
|
|
437
420
|
}
|
|
438
421
|
/>
|
|
439
422
|
</View>
|
|
440
423
|
))}
|
|
441
424
|
|
|
442
|
-
<Text style={{fontSize: 20}}>Subscriptions</Text>
|
|
425
|
+
<Text style={{ fontSize: 20 }}>Subscriptions</Text>
|
|
443
426
|
{subscriptions.map((item) => (
|
|
444
|
-
<View key={item.id} style={{gap: 12}}>
|
|
427
|
+
<View key={item.id} style={{ gap: 12 }}>
|
|
445
428
|
<Text>
|
|
446
429
|
{item.title || item.displayName} -{' '}
|
|
447
430
|
{item.platform === 'android' && item.subscriptionOfferDetails
|
|
448
|
-
? item.subscriptionOfferDetails[0]?.pricingPhases
|
|
449
|
-
.pricingPhaseList[0].formattedPrice
|
|
431
|
+
? item.subscriptionOfferDetails[0]?.pricingPhases.pricingPhaseList[0].formattedPrice
|
|
450
432
|
: item.displayPrice}
|
|
451
433
|
</Text>
|
|
452
434
|
<Button
|
|
@@ -463,7 +445,7 @@ export default function IAPWithHook() {
|
|
|
463
445
|
offerToken: offer.offerToken,
|
|
464
446
|
})) || [],
|
|
465
447
|
}
|
|
466
|
-
: {sku: item.id},
|
|
448
|
+
: { sku: item.id },
|
|
467
449
|
type: 'subs',
|
|
468
450
|
})
|
|
469
451
|
}
|
package/package.json
CHANGED
package/plugin/build/withIAP.js
CHANGED
|
@@ -2,63 +2,59 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const config_plugins_1 = require("expo/config-plugins");
|
|
4
4
|
const pkg = require('../../package.json');
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
const BILLING_PERMISSION = 'com.android.vending.BILLING';
|
|
6
|
+
const BILLING_DEP = ` implementation "com.android.billingclient:billing-ktx:7.0.0"`;
|
|
7
|
+
const GMS_DEP = ` implementation "com.google.android.gms:play-services-base:18.1.0"`;
|
|
8
|
+
/**
|
|
9
|
+
* Injects implementation lines into app/build.gradle
|
|
10
|
+
*/
|
|
11
|
+
function modifyAppBuildGradle(buildGradle) {
|
|
12
|
+
const lines = buildGradle.split('\n');
|
|
13
|
+
const anchor = /dependencies\s*{/;
|
|
14
|
+
const index = lines.findIndex((line) => anchor.test(line));
|
|
15
|
+
if (index !== -1) {
|
|
16
|
+
if (!buildGradle.includes(BILLING_DEP)) {
|
|
17
|
+
lines.splice(index + 1, 0, BILLING_DEP);
|
|
18
|
+
}
|
|
19
|
+
if (!buildGradle.includes(GMS_DEP)) {
|
|
20
|
+
lines.splice(index + 2, 0, GMS_DEP);
|
|
21
|
+
}
|
|
11
22
|
}
|
|
12
23
|
else {
|
|
13
|
-
|
|
24
|
+
console.warn('Could not find dependencies block in app/build.gradle');
|
|
14
25
|
}
|
|
15
26
|
return lines.join('\n');
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const supportLib = `supportLibVersion = "28.0.0"`;
|
|
21
|
-
if (!modified.includes(supportLib)) {
|
|
22
|
-
modified = addLineToGradle(modified, /ext\s*{/, supportLib);
|
|
23
|
-
}
|
|
24
|
-
// 2. billing library
|
|
25
|
-
const billingDep = ` implementation "com.android.billingclient:billing-ktx:7.0.0"`;
|
|
26
|
-
const gmsDep = ` implementation "com.google.android.gms:play-services-base:18.1.0"`;
|
|
27
|
-
if (!modified.includes(billingDep)) {
|
|
28
|
-
modified = addLineToGradle(modified, /dependencies\s*{/, billingDep);
|
|
29
|
-
}
|
|
30
|
-
if (!modified.includes(gmsDep)) {
|
|
31
|
-
modified = addLineToGradle(modified, /dependencies\s*{/, gmsDep, 1);
|
|
32
|
-
}
|
|
33
|
-
return modified;
|
|
34
|
-
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Adds BILLING permission to AndroidManifest.xml
|
|
30
|
+
*/
|
|
35
31
|
const withIAPAndroid = (config) => {
|
|
36
|
-
config = (0, config_plugins_1.
|
|
37
|
-
config.modResults.contents =
|
|
32
|
+
config = (0, config_plugins_1.withAppBuildGradle)(config, (config) => {
|
|
33
|
+
config.modResults.contents = modifyAppBuildGradle(config.modResults.contents);
|
|
38
34
|
return config;
|
|
39
35
|
});
|
|
40
36
|
config = (0, config_plugins_1.withAndroidManifest)(config, (config) => {
|
|
41
37
|
const manifest = config.modResults;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
const permissions = manifest.manifest['uses-permission'];
|
|
46
|
-
const billingPerm = { $: { 'android:name': 'com.android.vending.BILLING' } };
|
|
47
|
-
const alreadyExists = permissions.some((p) => p.$['android:name'] === 'com.android.vending.BILLING');
|
|
38
|
+
const permissions = manifest.manifest['uses-permission'] ?? [];
|
|
39
|
+
const alreadyExists = permissions.some((p) => p.$['android:name'] === BILLING_PERMISSION);
|
|
48
40
|
if (!alreadyExists) {
|
|
49
|
-
permissions.push(
|
|
50
|
-
|
|
41
|
+
permissions.push({ $: { 'android:name': BILLING_PERMISSION } });
|
|
42
|
+
manifest.manifest['uses-permission'] = permissions;
|
|
43
|
+
console.log(`✅ Added ${BILLING_PERMISSION} to AndroidManifest.xml`);
|
|
51
44
|
}
|
|
52
45
|
else {
|
|
53
|
-
console.log(
|
|
46
|
+
console.log(`ℹ️ ${BILLING_PERMISSION} already exists in AndroidManifest.xml`);
|
|
54
47
|
}
|
|
55
48
|
return config;
|
|
56
49
|
});
|
|
57
50
|
return config;
|
|
58
51
|
};
|
|
52
|
+
/**
|
|
53
|
+
* Main plugin entry
|
|
54
|
+
*/
|
|
59
55
|
const withIAP = (config, _props) => {
|
|
60
56
|
try {
|
|
61
|
-
console.log('🛠️ Applying expo-iap
|
|
57
|
+
console.log('🛠️ Applying expo-iap plugin...');
|
|
62
58
|
return withIAPAndroid(config);
|
|
63
59
|
}
|
|
64
60
|
catch (error) {
|
package/plugin/src/withIAP.ts
CHANGED
|
@@ -1,57 +1,45 @@
|
|
|
1
1
|
import {
|
|
2
|
+
ConfigPlugin,
|
|
2
3
|
WarningAggregator,
|
|
3
4
|
withAndroidManifest,
|
|
4
|
-
|
|
5
|
-
ConfigPlugin,
|
|
5
|
+
withAppBuildGradle,
|
|
6
6
|
createRunOncePlugin,
|
|
7
7
|
} from 'expo/config-plugins';
|
|
8
8
|
|
|
9
9
|
const pkg = require('../../package.json');
|
|
10
10
|
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
lineToAdd: string,
|
|
15
|
-
offset: number = 1,
|
|
16
|
-
): string => {
|
|
17
|
-
const lines = content.split('\n');
|
|
18
|
-
const index = lines.findIndex((line) => line.match(anchor));
|
|
19
|
-
if (index === -1) {
|
|
20
|
-
console.warn(
|
|
21
|
-
`Anchor "${anchor}" not found in build.gradle. Appending to end.`,
|
|
22
|
-
);
|
|
23
|
-
lines.push(lineToAdd);
|
|
24
|
-
} else {
|
|
25
|
-
lines.splice(index + offset, 0, lineToAdd);
|
|
26
|
-
}
|
|
27
|
-
return lines.join('\n');
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
const modifyProjectBuildGradle = (gradle: string): string => {
|
|
31
|
-
let modified = gradle;
|
|
11
|
+
const BILLING_PERMISSION = 'com.android.vending.BILLING';
|
|
12
|
+
const BILLING_DEP = ` implementation "com.android.billingclient:billing-ktx:7.0.0"`;
|
|
13
|
+
const GMS_DEP = ` implementation "com.google.android.gms:play-services-base:18.1.0"`;
|
|
32
14
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Injects implementation lines into app/build.gradle
|
|
17
|
+
*/
|
|
18
|
+
function modifyAppBuildGradle(buildGradle: string): string {
|
|
19
|
+
const lines = buildGradle.split('\n');
|
|
20
|
+
const anchor = /dependencies\s*{/;
|
|
21
|
+
const index = lines.findIndex((line) => anchor.test(line));
|
|
38
22
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
23
|
+
if (index !== -1) {
|
|
24
|
+
if (!buildGradle.includes(BILLING_DEP)) {
|
|
25
|
+
lines.splice(index + 1, 0, BILLING_DEP);
|
|
26
|
+
}
|
|
27
|
+
if (!buildGradle.includes(GMS_DEP)) {
|
|
28
|
+
lines.splice(index + 2, 0, GMS_DEP);
|
|
29
|
+
}
|
|
30
|
+
} else {
|
|
31
|
+
console.warn('Could not find dependencies block in app/build.gradle');
|
|
47
32
|
}
|
|
48
33
|
|
|
49
|
-
return
|
|
50
|
-
}
|
|
34
|
+
return lines.join('\n');
|
|
35
|
+
}
|
|
51
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Adds BILLING permission to AndroidManifest.xml
|
|
39
|
+
*/
|
|
52
40
|
const withIAPAndroid: ConfigPlugin = (config) => {
|
|
53
|
-
config =
|
|
54
|
-
config.modResults.contents =
|
|
41
|
+
config = withAppBuildGradle(config, (config) => {
|
|
42
|
+
config.modResults.contents = modifyAppBuildGradle(
|
|
55
43
|
config.modResults.contents,
|
|
56
44
|
);
|
|
57
45
|
return config;
|
|
@@ -59,24 +47,19 @@ const withIAPAndroid: ConfigPlugin = (config) => {
|
|
|
59
47
|
|
|
60
48
|
config = withAndroidManifest(config, (config) => {
|
|
61
49
|
const manifest = config.modResults;
|
|
62
|
-
|
|
63
|
-
manifest.manifest['uses-permission'] = [];
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const permissions = manifest.manifest['uses-permission'];
|
|
67
|
-
const billingPerm = {$: {'android:name': 'com.android.vending.BILLING'}};
|
|
50
|
+
const permissions = manifest.manifest['uses-permission'] ?? [];
|
|
68
51
|
|
|
69
52
|
const alreadyExists = permissions.some(
|
|
70
|
-
(p) => p.$['android:name'] ===
|
|
53
|
+
(p) => p.$['android:name'] === BILLING_PERMISSION,
|
|
71
54
|
);
|
|
55
|
+
|
|
72
56
|
if (!alreadyExists) {
|
|
73
|
-
permissions.push(
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
);
|
|
57
|
+
permissions.push({$: {'android:name': BILLING_PERMISSION}});
|
|
58
|
+
manifest.manifest['uses-permission'] = permissions;
|
|
59
|
+
console.log(`✅ Added ${BILLING_PERMISSION} to AndroidManifest.xml`);
|
|
77
60
|
} else {
|
|
78
61
|
console.log(
|
|
79
|
-
|
|
62
|
+
`ℹ️ ${BILLING_PERMISSION} already exists in AndroidManifest.xml`,
|
|
80
63
|
);
|
|
81
64
|
}
|
|
82
65
|
|
|
@@ -86,9 +69,12 @@ const withIAPAndroid: ConfigPlugin = (config) => {
|
|
|
86
69
|
return config;
|
|
87
70
|
};
|
|
88
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Main plugin entry
|
|
74
|
+
*/
|
|
89
75
|
const withIAP: ConfigPlugin = (config, _props) => {
|
|
90
76
|
try {
|
|
91
|
-
console.log('🛠️ Applying expo-iap
|
|
77
|
+
console.log('🛠️ Applying expo-iap plugin...');
|
|
92
78
|
return withIAPAndroid(config);
|
|
93
79
|
} catch (error) {
|
|
94
80
|
WarningAggregator.addWarningAndroid(
|