expo-iap 2.4.4 → 2.4.5
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/README.md +52 -1
- package/android/src/main/java/expo/modules/iap/ExpoIapModule.kt +31 -6
- package/build/ExpoIap.types.d.ts +116 -2
- package/build/ExpoIap.types.d.ts.map +1 -1
- package/build/ExpoIap.types.js +142 -1
- package/build/ExpoIap.types.js.map +1 -1
- package/build/ExpoIapModule.d.ts +3 -2
- package/build/ExpoIapModule.d.ts.map +1 -1
- package/build/ExpoIapModule.js +4 -1
- package/build/ExpoIapModule.js.map +1 -1
- package/build/index.d.ts +1 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -0
- package/build/index.js.map +1 -1
- package/build/useIap.js.map +1 -1
- package/build/utils/errorMapping.d.ts +29 -0
- package/build/utils/errorMapping.d.ts.map +1 -0
- package/build/utils/errorMapping.js +79 -0
- package/build/utils/errorMapping.js.map +1 -0
- package/bun.lockb +0 -0
- package/docs/ERROR_CODES.md +172 -0
- package/{iap.md → docs/IAP.md} +191 -192
- package/docs/README.md +30 -0
- package/ios/ExpoIapModule.swift +68 -43
- package/ios/Types.swift +26 -18
- package/package.json +5 -2
- package/src/ExpoIap.types.ts +173 -0
- package/src/ExpoIapModule.ts +6 -1
- package/src/index.ts +1 -0
- package/src/useIap.ts +1 -1
- package/src/utils/errorMapping.ts +88 -0
package/{iap.md → docs/IAP.md}
RENAMED
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
7
|
-
`expo-iap` is an Expo module for handling in-app purchases (IAP) on iOS (StoreKit 2) and Android (Google Play Billing). It supports consumables, non-consumables, and subscriptions. Unlike [`react-native-iap`](https://github.com/hyochan/react-native-iap), which requires native setup, `expo-iap` integrates seamlessly into Expo's
|
|
7
|
+
`expo-iap` is an Expo module for handling in-app purchases (IAP) on iOS (StoreKit 2) and Android (Google Play Billing). It supports consumables, non-consumables, and subscriptions. Unlike [`react-native-iap`](https://github.com/hyochan/react-native-iap), which requires native setup, `expo-iap` integrates seamlessly into Expo's managed workflow! However, you’ll need a [development client](https://docs.expo.dev/development/introduction/) instead of Expo Go for full functionality. Starting from version 2.2.8, most features of `react-native-iap` have been ported.
|
|
8
8
|
|
|
9
9
|
## Installation
|
|
10
10
|
|
|
11
|
-
`expo-iap` is compatible with [Expo SDK](https://expo.dev) 51+ and supports both managed workflows and React Native CLI projects. Official documentation is in progress for SDK inclusion (see [Expo IAP Documentation](
|
|
11
|
+
`expo-iap` is compatible with [Expo SDK](https://expo.dev) 51+ and supports both managed workflows and React Native CLI projects. Official documentation is in progress for SDK inclusion (see [Expo IAP Documentation](https://github.com/hyochan/expo-iap/blob/main/docs/IAP.md#expo-iap-documentation)).
|
|
12
12
|
|
|
13
13
|
### Add the Package
|
|
14
14
|
|
|
@@ -262,43 +262,49 @@ export default function SimpleIAP() {
|
|
|
262
262
|
}
|
|
263
263
|
```
|
|
264
264
|
|
|
265
|
-
## Using useIAP Hook
|
|
265
|
+
## Using `useIAP` Hook
|
|
266
266
|
|
|
267
|
-
The `useIAP` hook
|
|
267
|
+
The `useIAP` hook from `expo-iap` lets you manage in-app purchases in functional React components. This example reflects a **real-world production use case** with:
|
|
268
|
+
|
|
269
|
+
- Consumable & subscription support
|
|
270
|
+
- Purchase lifecycle management
|
|
271
|
+
- Platform-specific `requestPurchase` formats
|
|
272
|
+
- UX alerts and loading states
|
|
273
|
+
- Optional receipt validation before finishing the transaction
|
|
274
|
+
|
|
275
|
+
### 🧭 Flow Overview
|
|
276
|
+
|
|
277
|
+
| Step | Description |
|
|
278
|
+
| --- | --- |
|
|
279
|
+
| 1️⃣ | Wait for `connected === true` before fetching products and subscriptions |
|
|
280
|
+
| 2️⃣ | Render UI with products/subscriptions dynamically from store |
|
|
281
|
+
| 3️⃣ | Trigger purchases via `requestPurchase()` (with Android/iOS handling) |
|
|
282
|
+
| 4️⃣ | When `currentPurchase` updates, validate & finish the transaction |
|
|
283
|
+
| 5️⃣ | Handle `currentPurchaseError` for graceful UX |
|
|
284
|
+
|
|
285
|
+
### ✅ Realistic Example with `useIAP`
|
|
268
286
|
|
|
269
287
|
```tsx
|
|
270
|
-
import {useEffect, useState} from 'react';
|
|
288
|
+
import {useEffect, useState, useCallback} from 'react';
|
|
271
289
|
import {
|
|
272
|
-
SafeAreaView,
|
|
273
|
-
ScrollView,
|
|
274
|
-
StyleSheet,
|
|
275
|
-
Text,
|
|
276
290
|
View,
|
|
277
|
-
|
|
291
|
+
Text,
|
|
292
|
+
ScrollView,
|
|
278
293
|
Button,
|
|
279
|
-
InteractionManager,
|
|
280
294
|
Alert,
|
|
295
|
+
Platform,
|
|
296
|
+
InteractionManager,
|
|
281
297
|
} from 'react-native';
|
|
282
298
|
import {useIAP} from 'expo-iap';
|
|
283
|
-
import type {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const subscriptionSkus = [
|
|
293
|
-
'cpk.membership.monthly.bronze',
|
|
294
|
-
'cpk.membership.monthly.silver',
|
|
295
|
-
];
|
|
296
|
-
|
|
297
|
-
// Define operations
|
|
298
|
-
const operations = ['getProducts', 'getSubscriptions'] as const;
|
|
299
|
-
type Operation = (typeof operations)[number];
|
|
300
|
-
|
|
301
|
-
export default function IAPWithHook() {
|
|
299
|
+
import type {
|
|
300
|
+
ProductAndroid,
|
|
301
|
+
ProductPurchaseAndroid,
|
|
302
|
+
} from 'expo-iap/build/types/ExpoIapAndroid.types';
|
|
303
|
+
|
|
304
|
+
const productSkus = ['dev.hyo.luent.10bulbs', 'dev.hyo.luent.30bulbs'];
|
|
305
|
+
const subscriptionSkus = ['dev.hyo.luent.premium'];
|
|
306
|
+
|
|
307
|
+
export default function PurchaseScreen() {
|
|
302
308
|
const {
|
|
303
309
|
connected,
|
|
304
310
|
products,
|
|
@@ -307,195 +313,188 @@ export default function IAPWithHook() {
|
|
|
307
313
|
currentPurchaseError,
|
|
308
314
|
getProducts,
|
|
309
315
|
getSubscriptions,
|
|
310
|
-
finishTransaction,
|
|
311
316
|
requestPurchase,
|
|
317
|
+
finishTransaction,
|
|
318
|
+
validateReceipt,
|
|
312
319
|
} = useIAP();
|
|
313
320
|
|
|
314
321
|
const [isReady, setIsReady] = useState(false);
|
|
322
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
315
323
|
|
|
316
|
-
//
|
|
324
|
+
// 1️⃣ Initialize products & subscriptions
|
|
317
325
|
useEffect(() => {
|
|
318
326
|
if (!connected) return;
|
|
319
327
|
|
|
320
|
-
const
|
|
328
|
+
const loadStoreItems = async () => {
|
|
321
329
|
try {
|
|
322
|
-
await
|
|
323
|
-
|
|
324
|
-
getSubscriptions(subscriptionSkus),
|
|
325
|
-
]);
|
|
330
|
+
await getProducts(productSkus);
|
|
331
|
+
await getSubscriptions(subscriptionSkus);
|
|
326
332
|
setIsReady(true);
|
|
327
|
-
} catch (
|
|
328
|
-
console.error('
|
|
333
|
+
} catch (e) {
|
|
334
|
+
console.error('IAP init error:', e);
|
|
329
335
|
}
|
|
330
336
|
};
|
|
331
|
-
initializeIAP();
|
|
332
|
-
}, [connected, getProducts, getSubscriptions]);
|
|
333
337
|
|
|
334
|
-
|
|
338
|
+
loadStoreItems();
|
|
339
|
+
}, [connected]);
|
|
340
|
+
|
|
341
|
+
// 2️⃣ Purchase handler when currentPurchase updates
|
|
335
342
|
useEffect(() => {
|
|
336
|
-
if (currentPurchase)
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
343
|
+
if (!currentPurchase) return;
|
|
344
|
+
|
|
345
|
+
const handlePurchase = async () => {
|
|
346
|
+
try {
|
|
347
|
+
setIsLoading(true);
|
|
348
|
+
|
|
349
|
+
const productId = currentPurchase.id;
|
|
350
|
+
const isConsumable = productSkus.includes(productId);
|
|
351
|
+
|
|
352
|
+
// ✅ Optionally validate receipt before finishing
|
|
353
|
+
let isValid = true;
|
|
354
|
+
if (Platform.OS === 'ios') {
|
|
355
|
+
const result = await validateReceipt(productId);
|
|
356
|
+
isValid = result?.isValid ?? true;
|
|
357
|
+
} else if (Platform.OS === 'android') {
|
|
358
|
+
const token = (currentPurchase as ProductPurchaseAndroid)
|
|
359
|
+
.purchaseTokenAndroid;
|
|
360
|
+
const packageName = 'your.android.package.name';
|
|
361
|
+
|
|
362
|
+
const result = await validateReceipt(productId, {
|
|
363
|
+
productToken: token,
|
|
364
|
+
packageName,
|
|
365
|
+
isSub: subscriptionSkus.includes(productId),
|
|
342
366
|
});
|
|
343
|
-
|
|
344
|
-
} catch (error) {
|
|
345
|
-
console.error('Error finishing transaction:', error);
|
|
346
|
-
Alert.alert('Transaction Error', String(error));
|
|
367
|
+
isValid = result?.isValid ?? true;
|
|
347
368
|
}
|
|
348
|
-
});
|
|
349
|
-
}
|
|
350
369
|
|
|
370
|
+
if (!isValid) {
|
|
371
|
+
Alert.alert('Invalid purchase', 'Receipt validation failed');
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// 🧾 Finish transaction (important!)
|
|
376
|
+
await finishTransaction({
|
|
377
|
+
purchase: currentPurchase,
|
|
378
|
+
isConsumable,
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
// ✅ Grant item or unlock feature
|
|
382
|
+
Alert.alert(
|
|
383
|
+
'Thank you!',
|
|
384
|
+
isConsumable
|
|
385
|
+
? 'Bulbs added to your account!'
|
|
386
|
+
: 'Premium subscription activated.',
|
|
387
|
+
);
|
|
388
|
+
} catch (err) {
|
|
389
|
+
console.error('Finish transaction error:', err);
|
|
390
|
+
} finally {
|
|
391
|
+
setIsLoading(false);
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
handlePurchase();
|
|
396
|
+
}, [currentPurchase]);
|
|
397
|
+
|
|
398
|
+
// 3️⃣ Error handling
|
|
399
|
+
useEffect(() => {
|
|
351
400
|
if (currentPurchaseError) {
|
|
352
401
|
InteractionManager.runAfterInteractions(() => {
|
|
353
|
-
Alert.alert('Purchase
|
|
402
|
+
Alert.alert('Purchase error', currentPurchaseError.message);
|
|
354
403
|
});
|
|
404
|
+
setIsLoading(false);
|
|
355
405
|
}
|
|
356
|
-
}, [
|
|
406
|
+
}, [currentPurchaseError]);
|
|
357
407
|
|
|
358
|
-
//
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
}
|
|
408
|
+
// 4️⃣ Purchase trigger
|
|
409
|
+
const handleBuy = useCallback(
|
|
410
|
+
async (productId: string, type?: 'subs') => {
|
|
411
|
+
try {
|
|
412
|
+
setIsLoading(true);
|
|
364
413
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
414
|
+
if (Platform.OS === 'ios') {
|
|
415
|
+
await requestPurchase({
|
|
416
|
+
request: {sku: productId},
|
|
417
|
+
type,
|
|
418
|
+
});
|
|
419
|
+
} else {
|
|
420
|
+
const request: any = {skus: [productId]};
|
|
421
|
+
|
|
422
|
+
if (type === 'subs') {
|
|
423
|
+
const sub = subscriptions.find(
|
|
424
|
+
(s) => s.id === productId,
|
|
425
|
+
) as ProductAndroid;
|
|
426
|
+
const offers =
|
|
427
|
+
sub?.subscriptionOfferDetails?.map((offer) => ({
|
|
428
|
+
sku: productId,
|
|
429
|
+
offerToken: offer.offerToken,
|
|
430
|
+
})) || [];
|
|
431
|
+
|
|
432
|
+
request.subscriptionOffers = offers;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
await requestPurchase({request, type});
|
|
436
|
+
}
|
|
437
|
+
} catch (err) {
|
|
438
|
+
console.error('Purchase request failed:', err);
|
|
439
|
+
Alert.alert(
|
|
440
|
+
'Error',
|
|
441
|
+
err instanceof Error ? err.message : 'Purchase failed',
|
|
442
|
+
);
|
|
443
|
+
setIsLoading(false);
|
|
373
444
|
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
};
|
|
445
|
+
},
|
|
446
|
+
[subscriptions],
|
|
447
|
+
);
|
|
378
448
|
|
|
379
|
-
if (!connected)
|
|
380
|
-
|
|
381
|
-
<SafeAreaView style={styles.container}>
|
|
382
|
-
<Text style={styles.title}>Connecting to IAP...</Text>
|
|
383
|
-
</SafeAreaView>
|
|
384
|
-
);
|
|
385
|
-
}
|
|
449
|
+
if (!connected) return <Text>Connecting to store...</Text>;
|
|
450
|
+
if (!isReady) return <Text>Loading products...</Text>;
|
|
386
451
|
|
|
387
452
|
return (
|
|
388
|
-
<
|
|
389
|
-
<Text style={
|
|
390
|
-
|
|
391
|
-
<
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
<
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
title="Buy"
|
|
420
|
-
onPress={() =>
|
|
421
|
-
requestPurchase({
|
|
422
|
-
request:
|
|
423
|
-
item.platform === 'android'
|
|
424
|
-
? {skus: [item.id]}
|
|
425
|
-
: {sku: item.id},
|
|
426
|
-
})
|
|
427
|
-
}
|
|
428
|
-
/>
|
|
429
|
-
</View>
|
|
430
|
-
))}
|
|
431
|
-
|
|
432
|
-
<Text style={{fontSize: 20}}>Subscriptions</Text>
|
|
433
|
-
{subscriptions.map((item) => (
|
|
434
|
-
<View key={item.id} style={{gap: 12}}>
|
|
435
|
-
<Text>
|
|
436
|
-
{item.title || item.displayName} -{' '}
|
|
437
|
-
{item.platform === 'android' && item.subscriptionOfferDetails
|
|
438
|
-
? item.subscriptionOfferDetails[0]?.pricingPhases
|
|
439
|
-
.pricingPhaseList[0].formattedPrice
|
|
440
|
-
: item.displayPrice}
|
|
441
|
-
</Text>
|
|
442
|
-
<Button
|
|
443
|
-
title="Subscribe"
|
|
444
|
-
onPress={() =>
|
|
445
|
-
requestPurchase({
|
|
446
|
-
request:
|
|
447
|
-
item.platform === 'android'
|
|
448
|
-
? {
|
|
449
|
-
skus: [item.id],
|
|
450
|
-
subscriptionOffers:
|
|
451
|
-
item.subscriptionOfferDetails?.map((offer) => ({
|
|
452
|
-
sku: item.id,
|
|
453
|
-
offerToken: offer.offerToken,
|
|
454
|
-
})) || [],
|
|
455
|
-
}
|
|
456
|
-
: {sku: item.id},
|
|
457
|
-
type: 'subs',
|
|
458
|
-
})
|
|
459
|
-
}
|
|
460
|
-
/>
|
|
461
|
-
</View>
|
|
462
|
-
))}
|
|
463
|
-
</View>
|
|
464
|
-
)}
|
|
465
|
-
</View>
|
|
466
|
-
</SafeAreaView>
|
|
453
|
+
<ScrollView contentContainerStyle={{padding: 20}}>
|
|
454
|
+
<Text style={{fontSize: 18, fontWeight: 'bold'}}>💡 Bulb Packs</Text>
|
|
455
|
+
{products.map((p) => (
|
|
456
|
+
<View key={p.id} style={{marginVertical: 10}}>
|
|
457
|
+
<Text>
|
|
458
|
+
{p.title} - {p.displayPrice}
|
|
459
|
+
</Text>
|
|
460
|
+
<Button
|
|
461
|
+
title={isLoading ? 'Processing...' : 'Buy'}
|
|
462
|
+
onPress={() => handleBuy(p.id)}
|
|
463
|
+
disabled={isLoading}
|
|
464
|
+
/>
|
|
465
|
+
</View>
|
|
466
|
+
))}
|
|
467
|
+
|
|
468
|
+
<Text style={{fontSize: 18, fontWeight: 'bold', marginTop: 30}}>
|
|
469
|
+
⭐ Subscription
|
|
470
|
+
</Text>
|
|
471
|
+
{subscriptions.map((s) => (
|
|
472
|
+
<View key={s.id} style={{marginVertical: 10}}>
|
|
473
|
+
<Text>
|
|
474
|
+
{s.title} - {s.displayPrice}
|
|
475
|
+
</Text>
|
|
476
|
+
<Button
|
|
477
|
+
title={isLoading ? 'Processing...' : 'Subscribe'}
|
|
478
|
+
onPress={() => handleBuy(s.id, 'subs')}
|
|
479
|
+
disabled={isLoading}
|
|
480
|
+
/>
|
|
481
|
+
</View>
|
|
482
|
+
))}
|
|
483
|
+
</ScrollView>
|
|
467
484
|
);
|
|
468
485
|
}
|
|
469
|
-
|
|
470
|
-
const styles = StyleSheet.create({
|
|
471
|
-
container: {
|
|
472
|
-
flex: 1,
|
|
473
|
-
backgroundColor: '#fff',
|
|
474
|
-
alignItems: 'center',
|
|
475
|
-
},
|
|
476
|
-
title: {
|
|
477
|
-
marginTop: 24,
|
|
478
|
-
fontSize: 20,
|
|
479
|
-
fontWeight: 'bold',
|
|
480
|
-
},
|
|
481
|
-
buttons: {
|
|
482
|
-
height: 90,
|
|
483
|
-
},
|
|
484
|
-
buttonsWrapper: {
|
|
485
|
-
padding: 24,
|
|
486
|
-
gap: 8,
|
|
487
|
-
},
|
|
488
|
-
buttonView: {
|
|
489
|
-
borderRadius: 8,
|
|
490
|
-
borderWidth: 1,
|
|
491
|
-
borderColor: '#000',
|
|
492
|
-
padding: 8,
|
|
493
|
-
},
|
|
494
|
-
content: {
|
|
495
|
-
flex: 1,
|
|
496
|
-
alignSelf: 'stretch',
|
|
497
|
-
padding: 24,
|
|
498
|
-
gap: 12,
|
|
499
|
-
},
|
|
500
|
-
});
|
|
501
486
|
```
|
|
487
|
+
|
|
488
|
+
---
|
|
489
|
+
|
|
490
|
+
물론입니다. 아래는 보다 자연스럽고 요약된 느낌으로 다듬은 영어 버전입니다:
|
|
491
|
+
|
|
492
|
+
---
|
|
493
|
+
|
|
494
|
+
### 🔎 Key Benefits of This Approach
|
|
495
|
+
|
|
496
|
+
- ✅ Supports **both Android and iOS**, with platform-aware purchase handling
|
|
497
|
+
- ✅ Covers **both consumable items and subscriptions**
|
|
498
|
+
- ✅ Includes a **receipt validation flow** using `validateReceipt` (server-ready)
|
|
499
|
+
- ✅ Handles iOS cases where **auto-finishing transactions is disabled**
|
|
500
|
+
- ✅ Provides **user-friendly error and loading state management**
|
package/docs/README.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Documentation
|
|
2
|
+
|
|
3
|
+
This directory contains detailed documentation for expo-iap.
|
|
4
|
+
|
|
5
|
+
## Available Documents
|
|
6
|
+
|
|
7
|
+
- **[API Documentation](./IAP.md)** - Complete API reference and usage examples
|
|
8
|
+
- **[Error Code Management](./ERROR_CODES.md)** - Centralized error handling system documentation
|
|
9
|
+
|
|
10
|
+
## Quick Links
|
|
11
|
+
|
|
12
|
+
- [Main README](../README.md) - Project overview and quick start
|
|
13
|
+
- [Example App](../example/) - Sample implementation
|
|
14
|
+
- [GitHub Issues](https://github.com/hyochan/expo-iap/issues) - Bug reports and feature requests
|
|
15
|
+
|
|
16
|
+
## Contributing
|
|
17
|
+
|
|
18
|
+
If you find any documentation errors or have suggestions for improvements, please:
|
|
19
|
+
|
|
20
|
+
1. Check existing [issues](https://github.com/hyochan/expo-iap/issues)
|
|
21
|
+
2. Create a new issue with the `documentation` label
|
|
22
|
+
3. Submit a pull request with your improvements
|
|
23
|
+
|
|
24
|
+
## Support
|
|
25
|
+
|
|
26
|
+
For questions and support:
|
|
27
|
+
|
|
28
|
+
- 📚 Read the documentation in this folder
|
|
29
|
+
- 🐛 Report bugs via [GitHub Issues](https://github.com/hyochan/expo-iap/issues)
|
|
30
|
+
- 💬 Join discussions in [GitHub Discussions](https://github.com/hyochan/expo-iap/discussions)
|