expo-iap 2.4.3 → 2.4.5-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.
@@ -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 [managed workflow](https://docs.expo.dev/archive/managed-vs-bare)—no ejecting needed! 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.
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](link-to-docs)).
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
 
@@ -91,16 +91,6 @@ Manually apply the following changes:
91
91
 
92
92
  If there’s no `ext` block, append it at the end.
93
93
 
94
- 2. **Update `android/app/src/main/AndroidManifest.xml`**
95
- Add the BILLING permission:
96
-
97
- ```xml
98
- <manifest ...>
99
- <uses-permission android:name="com.android.vending.BILLING" />
100
- <!-- Other manifest content... -->
101
- </manifest>
102
- ```
103
-
104
94
  ## Current State & Feedback
105
95
 
106
96
  Updates are in progress to improve reliability and address remaining edge cases. For production apps, test thoroughly. Contributions (docs, code, or bug reports) are welcome—especially detailed error logs or use cases!
@@ -272,43 +262,49 @@ export default function SimpleIAP() {
272
262
  }
273
263
  ```
274
264
 
275
- ## Using useIAP Hook
265
+ ## Using `useIAP` Hook
266
+
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
276
 
277
- The `useIAP` hook simplifies managing in-app purchases. Below is an example updated to use the new `requestPurchase` signature:
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`
278
286
 
279
287
  ```tsx
280
- import {useEffect, useState} from 'react';
288
+ import {useEffect, useState, useCallback} from 'react';
281
289
  import {
282
- SafeAreaView,
283
- ScrollView,
284
- StyleSheet,
285
- Text,
286
290
  View,
287
- Pressable,
291
+ Text,
292
+ ScrollView,
288
293
  Button,
289
- InteractionManager,
290
294
  Alert,
295
+ Platform,
296
+ InteractionManager,
291
297
  } from 'react-native';
292
298
  import {useIAP} from 'expo-iap';
293
- import type {ProductPurchase, SubscriptionProduct} from 'expo-iap';
294
-
295
- // Define SKUs
296
- const productSkus = [
297
- 'cpk.points.1000',
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
- ];
306
-
307
- // Define operations
308
- const operations = ['getProducts', 'getSubscriptions'] as const;
309
- type Operation = (typeof operations)[number];
310
-
311
- 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() {
312
308
  const {
313
309
  connected,
314
310
  products,
@@ -317,195 +313,188 @@ export default function IAPWithHook() {
317
313
  currentPurchaseError,
318
314
  getProducts,
319
315
  getSubscriptions,
320
- finishTransaction,
321
316
  requestPurchase,
317
+ finishTransaction,
318
+ validateReceipt,
322
319
  } = useIAP();
323
320
 
324
321
  const [isReady, setIsReady] = useState(false);
322
+ const [isLoading, setIsLoading] = useState(false);
325
323
 
326
- // Fetch products and subscriptions only when connected
324
+ // 1️⃣ Initialize products & subscriptions
327
325
  useEffect(() => {
328
326
  if (!connected) return;
329
327
 
330
- const initializeIAP = async () => {
328
+ const loadStoreItems = async () => {
331
329
  try {
332
- await Promise.all([
333
- getProducts(productSkus),
334
- getSubscriptions(subscriptionSkus),
335
- ]);
330
+ await getProducts(productSkus);
331
+ await getSubscriptions(subscriptionSkus);
336
332
  setIsReady(true);
337
- } catch (error) {
338
- console.error('Error initializing IAP:', error);
333
+ } catch (e) {
334
+ console.error('IAP init error:', e);
339
335
  }
340
336
  };
341
- initializeIAP();
342
- }, [connected, getProducts, getSubscriptions]);
343
337
 
344
- // Handle purchase updates and errors
338
+ loadStoreItems();
339
+ }, [connected]);
340
+
341
+ // 2️⃣ Purchase handler when currentPurchase updates
345
342
  useEffect(() => {
346
- if (currentPurchase) {
347
- InteractionManager.runAfterInteractions(async () => {
348
- try {
349
- await finishTransaction({
350
- purchase: currentPurchase,
351
- isConsumable: currentPurchase.productType === 'inapp',
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),
352
366
  });
353
- Alert.alert('Purchase Successful', JSON.stringify(currentPurchase));
354
- } catch (error) {
355
- console.error('Error finishing transaction:', error);
356
- Alert.alert('Transaction Error', String(error));
367
+ isValid = result?.isValid ?? true;
357
368
  }
358
- });
359
- }
360
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(() => {
361
400
  if (currentPurchaseError) {
362
401
  InteractionManager.runAfterInteractions(() => {
363
- Alert.alert('Purchase Error', JSON.stringify(currentPurchaseError));
402
+ Alert.alert('Purchase error', currentPurchaseError.message);
364
403
  });
404
+ setIsLoading(false);
365
405
  }
366
- }, [currentPurchase, currentPurchaseError, finishTransaction]);
406
+ }, [currentPurchaseError]);
367
407
 
368
- // Handle operation buttons
369
- const handleOperation = async (operation: Operation) => {
370
- if (!connected) {
371
- Alert.alert('Not Connected', 'Please wait for IAP to connect.');
372
- return;
373
- }
408
+ // 4️⃣ Purchase trigger
409
+ const handleBuy = useCallback(
410
+ async (productId: string, type?: 'subs') => {
411
+ try {
412
+ setIsLoading(true);
374
413
 
375
- try {
376
- switch (operation) {
377
- case 'getProducts':
378
- await getProducts(productSkus);
379
- break;
380
- case 'getSubscriptions':
381
- await getSubscriptions(subscriptionSkus);
382
- break;
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);
383
444
  }
384
- } catch (error) {
385
- console.error(`Error in ${operation}:`, error);
386
- }
387
- };
445
+ },
446
+ [subscriptions],
447
+ );
388
448
 
389
- if (!connected) {
390
- return (
391
- <SafeAreaView style={styles.container}>
392
- <Text style={styles.title}>Connecting to IAP...</Text>
393
- </SafeAreaView>
394
- );
395
- }
449
+ if (!connected) return <Text>Connecting to store...</Text>;
450
+ if (!isReady) return <Text>Loading products...</Text>;
396
451
 
397
452
  return (
398
- <SafeAreaView style={styles.container}>
399
- <Text style={styles.title}>Expo IAP with useIAP Hook</Text>
400
- <View style={styles.buttons}>
401
- <ScrollView contentContainerStyle={styles.buttonsWrapper} horizontal>
402
- {operations.map((operation) => (
403
- <Pressable
404
- key={operation}
405
- onPress={() => handleOperation(operation)}
406
- >
407
- <View style={styles.buttonView}>
408
- <Text>{operation}</Text>
409
- </View>
410
- </Pressable>
411
- ))}
412
- </ScrollView>
413
- </View>
414
- <View style={styles.content}>
415
- {!isReady ? (
416
- <Text>Loading...</Text>
417
- ) : (
418
- <View style={{gap: 12}}>
419
- <Text style={{fontSize: 20}}>Products</Text>
420
- {products.map((item) => (
421
- <View key={item.id} style={{gap: 12}}>
422
- <Text>
423
- {item.title} -{' '}
424
- {item.platform === 'android'
425
- ? item.oneTimePurchaseOfferDetails?.formattedPrice
426
- : item.displayPrice}
427
- </Text>
428
- <Button
429
- title="Buy"
430
- onPress={() =>
431
- requestPurchase({
432
- request:
433
- item.platform === 'android'
434
- ? {skus: [item.id]}
435
- : {sku: item.id},
436
- })
437
- }
438
- />
439
- </View>
440
- ))}
441
-
442
- <Text style={{fontSize: 20}}>Subscriptions</Text>
443
- {subscriptions.map((item) => (
444
- <View key={item.id} style={{gap: 12}}>
445
- <Text>
446
- {item.title || item.displayName} -{' '}
447
- {item.platform === 'android' && item.subscriptionOfferDetails
448
- ? item.subscriptionOfferDetails[0]?.pricingPhases
449
- .pricingPhaseList[0].formattedPrice
450
- : item.displayPrice}
451
- </Text>
452
- <Button
453
- title="Subscribe"
454
- onPress={() =>
455
- requestPurchase({
456
- request:
457
- item.platform === 'android'
458
- ? {
459
- skus: [item.id],
460
- subscriptionOffers:
461
- item.subscriptionOfferDetails?.map((offer) => ({
462
- sku: item.id,
463
- offerToken: offer.offerToken,
464
- })) || [],
465
- }
466
- : {sku: item.id},
467
- type: 'subs',
468
- })
469
- }
470
- />
471
- </View>
472
- ))}
473
- </View>
474
- )}
475
- </View>
476
- </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>
477
484
  );
478
485
  }
479
-
480
- const styles = StyleSheet.create({
481
- container: {
482
- flex: 1,
483
- backgroundColor: '#fff',
484
- alignItems: 'center',
485
- },
486
- title: {
487
- marginTop: 24,
488
- fontSize: 20,
489
- fontWeight: 'bold',
490
- },
491
- buttons: {
492
- height: 90,
493
- },
494
- buttonsWrapper: {
495
- padding: 24,
496
- gap: 8,
497
- },
498
- buttonView: {
499
- borderRadius: 8,
500
- borderWidth: 1,
501
- borderColor: '#000',
502
- padding: 8,
503
- },
504
- content: {
505
- flex: 1,
506
- alignSelf: 'stretch',
507
- padding: 24,
508
- gap: 12,
509
- },
510
- });
511
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)