# expo-iap > In-App Purchase solution for Expo and React Native supporting iOS StoreKit 2 and Android Play Billing 8.x expo-iap is a powerful cross-platform in-app purchase library that conforms to the OpenIAP specification. It provides a unified API for handling purchases across iOS and Android with TypeScript support. ## Documentation - [Full Documentation](https://hyochan.github.io/expo-iap/) - [Detailed AI Reference](https://hyochan.github.io/expo-iap/llms-full.txt) - [OpenIAP Specification](https://openiap.dev) ## Quick Start ### Installation ```bash npx expo install expo-iap ``` ### Basic Usage ```tsx import {useIAP} from 'expo-iap'; function Store() { const {connected, products, fetchProducts, requestPurchase, finishTransaction} = useIAP({ onPurchaseSuccess: async (purchase) => { // Verify on your backend first await finishTransaction({purchase, isConsumable: true}); }, onPurchaseError: (error) => { console.error('Purchase failed:', error); }, }); useEffect(() => { if (connected) { fetchProducts({skus: ['product.id'], type: 'in-app'}); } }, [connected]); const handlePurchase = (productId: string) => { requestPurchase({ request: { apple: {sku: productId}, google: {skus: [productId]}, }, }); }; } ``` ## Core API Reference ### useIAP Hook The main interface for in-app purchases. Auto-manages connection lifecycle. **Design Pattern:** The hook follows React's state-driven pattern. Methods return `Promise` and update internal state. You must read data from the returned state variables, not from method return values. ```tsx const { // ═══════════════════════════════════════════════════════════════ // STATE VARIABLES (read data from these) // ═══════════════════════════════════════════════════════════════ connected, // boolean - IAP service ready products, // Product[] - populated by fetchProducts() subscriptions, // ProductSubscription[] - populated by fetchProducts() availablePurchases, // Purchase[] - populated by getAvailablePurchases() activeSubscriptions, // ActiveSubscription[] - populated by getActiveSubscriptions() // ═══════════════════════════════════════════════════════════════ // METHODS (return Promise, update state above) // ═══════════════════════════════════════════════════════════════ fetchProducts, // ({skus, type}) => Promise requestPurchase, // (props) => Promise finishTransaction, // ({purchase, isConsumable}) => Promise getAvailablePurchases, // () => Promise getActiveSubscriptions, // (ids?) => Promise restorePurchases, // () => Promise // ═══════════════════════════════════════════════════════════════ // METHODS THAT RETURN VALUES (exceptions to the pattern) // ═══════════════════════════════════════════════════════════════ hasActiveSubscriptions, // (ids?) => Promise verifyPurchase, // (props) => Promise verifyPurchaseWithProvider, // (props) => Promise } = useIAP(options); ``` **Usage Pattern:** ```tsx // ✅ CORRECT: Call method, then read from state await fetchProducts({skus: ['product1'], type: 'in-app'}); console.log(products); // Read from state variable await getAvailablePurchases(); console.log(availablePurchases); // Read from state variable // ❌ WRONG: Don't expect return values from methods const result = await fetchProducts({...}); // result is void! const purchases = await getAvailablePurchases(); // purchases is void! ``` **Options:** - `onPurchaseSuccess?: (purchase: Purchase) => void` - Success callback - `onPurchaseError?: (error: PurchaseError) => void` - Error callback ### Direct API Functions For use outside React components. Unlike hook methods, these return data directly. ```tsx import { initConnection, // () => Promise endConnection, // () => Promise fetchProducts, // ({skus, type}) => Promise requestPurchase, // (props) => Promise finishTransaction, // ({purchase, isConsumable}) => Promise getAvailablePurchases, // (options?) => Promise - returns Purchase array directly getActiveSubscriptions, // (ids?) => Promise hasActiveSubscriptions, // (ids?) => Promise deepLinkToSubscriptions, // ({skuAndroid?}) => Promise getStorefront, // () => Promise } from 'expo-iap'; ``` **Note:** The root API `getAvailablePurchases` returns `Promise`, while the hook's `getAvailablePurchases` returns `Promise` and updates internal state. ### Event Listeners ```tsx import {purchaseUpdatedListener, purchaseErrorListener} from 'expo-iap'; const unsubscribe = purchaseUpdatedListener((purchase) => { // Handle purchase }); const unsubscribeError = purchaseErrorListener((error) => { // Handle error }); ``` ## Key Types ### Product ```tsx interface Product { id: string; title: string; description: string; displayPrice: string; price?: number; currency: string; type: 'in-app' | 'subs'; store: 'apple' | 'google' | 'horizon'; // iOS specific displayNameIOS?: string; isFamilyShareableIOS?: boolean; subscriptionInfoIOS?: SubscriptionInfoIOS; // Android specific subscriptionOfferDetailsAndroid?: SubscriptionOfferDetails[]; } ``` ### Purchase ```tsx interface Purchase { id: string; productId: string; purchaseState: 'pending' | 'purchased' | 'unknown'; transactionDate: number; purchaseToken?: string; // iOS specific expirationDateIOS?: number; environmentIOS?: 'Sandbox' | 'Production'; // Android specific autoRenewingAndroid?: boolean; packageNameAndroid?: string; } ``` ### ErrorCode ```tsx enum ErrorCode { UserCancelled = 'user-cancelled', NetworkError = 'network-error', ItemUnavailable = 'item-unavailable', AlreadyOwned = 'already-owned', NotPrepared = 'not-prepared', ServiceError = 'service-error', BillingUnavailable = 'billing-unavailable', // ... see full docs } ``` ## Common Patterns ### Fetch Products ```tsx // In-app products await fetchProducts({skus: ['coins_100', 'premium_unlock'], type: 'in-app'}); // Subscriptions await fetchProducts({skus: ['monthly_sub', 'yearly_sub'], type: 'subs'}); ``` ### Purchase with Platform-Specific Options ```tsx // For subscriptions with Android offers const subscription = subscriptions.find(s => s.id === 'monthly_sub'); const offers = subscription?.subscriptionOfferDetailsAndroid?.map(offer => ({ sku: subscription.id, offerToken: offer.offerToken, })) || []; await requestPurchase({ request: { apple: {sku: 'monthly_sub'}, google: { skus: ['monthly_sub'], subscriptionOffers: offers, }, }, type: 'subs', }); ``` ### Restore Purchases ```tsx const {getAvailablePurchases, availablePurchases} = useIAP(); const restore = async () => { await getAvailablePurchases(); // availablePurchases now contains restorable items for (const purchase of availablePurchases) { // Validate and restore each } }; ``` ### Check Active Subscriptions ```tsx const hasActive = await hasActiveSubscriptions(['premium_monthly', 'premium_yearly']); if (hasActive) { // User has premium access } ``` ### Verify Purchase with IAPKit ```tsx const result = await verifyPurchaseWithProvider({ provider: 'iapkit', iapkit: { apple: {jws: purchase.purchaseToken}, google: {purchaseToken: purchase.purchaseToken}, }, }); if (result.iapkit?.isValid) { // Grant entitlement } ``` ## Error Handling ```tsx import {ErrorCode, isUserCancelledError} from 'expo-iap'; const {requestPurchase} = useIAP({ onPurchaseError: (error) => { if (error.code === ErrorCode.UserCancelled) { return; // User cancelled, no action needed } if (error.code === ErrorCode.NetworkError) { showRetryDialog(); } console.error(error.message); }, }); ``` ## Platform Requirements - **iOS**: 15.0+ for StoreKit 2 features - **Android**: API 21+, Kotlin 2.0+ (Expo SDK 53+) - **Expo**: Requires custom dev client (not Expo Go) ## Config Plugin ```json { "expo": { "plugins": [ ["expo-iap", {"iapkitApiKey": "your_key"}], ["expo-build-properties", {"android": {"kotlinVersion": "2.2.0"}}] ] } } ``` ## Links - [GitHub](https://github.com/hyochan/expo-iap) - [NPM](https://npmjs.com/package/expo-iap) - [Full Documentation](https://hyochan.github.io/expo-iap/) - [OpenIAP Specification](https://openiap.dev) - [Examples](https://github.com/hyochan/expo-iap/tree/main/example)