# react-native-iap > High-performance in-app purchase library for React Native using Nitro Modules, supporting iOS StoreKit 2 and Android Play Billing 8.x react-native-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/react-native-iap/) - [Detailed AI Reference](https://hyochan.github.io/react-native-iap/llms-full.txt) - [OpenIAP Specification](https://openiap.dev) ## Quick Start ### Installation ```bash npm install react-native-iap react-native-nitro-modules # or yarn add react-native-iap react-native-nitro-modules ``` ### Basic Usage ```tsx import {useIAP} from 'react-native-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]}, }, type: 'in-app', }); }; } ``` ## 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 restorePurchases, // () => Promise // ═══════════════════════════════════════════════════════════════ // METHODS THAT RETURN VALUES (exceptions to the pattern) // ═══════════════════════════════════════════════════════════════ getActiveSubscriptions, // (ids?) => Promise (also updates state) 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` - Purchase error callback - `onError?: (error: Error) => void` - Non-purchase error callback (fetchProducts, getAvailablePurchases, etc.) ### 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 'react-native-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 'react-native-iap'; const unsubscribe = purchaseUpdatedListener((purchase) => { // Handle purchase }); const unsubscribeError = purchaseErrorListener((error) => { // Handle error }); // Cleanup unsubscribe.remove(); unsubscribeError.remove(); ``` ## 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'; // Cross-platform standardized offers (v14.7.2+) subscriptionOffers?: SubscriptionOffer[]; // For subscriptions discountOffers?: DiscountOffer[]; // For one-time purchases // iOS specific (deprecated - use subscriptionOffers) displayNameIOS?: string; isFamilyShareableIOS?: boolean; subscriptionInfoIOS?: SubscriptionInfoIOS; // Android specific (deprecated - use subscriptionOffers) subscriptionOfferDetailsAndroid?: SubscriptionOfferDetails[]; } ``` ### SubscriptionOffer (v14.7.2+) Cross-platform subscription offer type: ```tsx interface SubscriptionOffer { id: string; // Unique offer identifier displayPrice: string; // Formatted price (e.g., "$4.99/mo" or "Free") price: number; // Numeric price type: 'introductory' | 'promotional'; paymentMode?: 'free-trial' | 'pay-as-you-go' | 'pay-up-front'; period?: { unit: 'day' | 'week' | 'month' | 'year'; value: number }; periodCount?: number; // Android specific basePlanIdAndroid?: string; offerTokenAndroid?: string; // Required for Android purchases // iOS specific keyIdentifierIOS?: string; numberOfPeriodsIOS?: number; } ``` ### DiscountOffer (v14.7.2+) Cross-platform discount offer for one-time purchases: ```tsx interface DiscountOffer { currency: string; displayPrice: string; price: number; id?: string; // Android specific offerTokenAndroid?: string; discountAmountMicrosAndroid?: string; formattedDiscountAmountAndroid?: string; purchaseOptionIdAndroid?: string; // v14.7.10+, Billing Library 7.0+ } ``` ### InstallmentPlanDetailsAndroid (v14.7.10+, Billing Library 7.0+) Subscription installment plan details: ```tsx interface InstallmentPlanDetailsAndroid { commitmentPaymentsCount: number; // Initial commitment (e.g., 12 months) subsequentCommitmentPaymentsCount: number; // Renewal commitment (0 if reverts to normal) } ``` Available on `ProductSubscriptionAndroidOfferDetails.installmentPlanDetails`. ### PendingPurchaseUpdateAndroid (v14.7.10+, Billing Library 5.0+) Pending subscription upgrade/downgrade details: ```tsx interface PendingPurchaseUpdateAndroid { products: string[]; // New products being switched to purchaseToken: string; // Pending transaction token } ``` Available on `PurchaseAndroid.pendingPurchaseUpdateAndroid`. ### 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; isSuspendedAndroid?: boolean; // v14.7.3+, Billing Library 8.1+ pendingPurchaseUpdateAndroid?: PendingPurchaseUpdateAndroid; // v14.7.10+, Billing Library 5.0+ } ``` ### ProductStatusAndroid (v14.7.3+, Billing Library 8.0+) ```tsx type ProductStatusAndroid = 'ok' | 'not-found' | 'no-offers-available' | 'unknown'; ``` Products now include `productStatusAndroid` field to explain why a product couldn't be fetched. ### 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', }); ``` ### Subscription Upgrade/Downgrade (Android) ```tsx await requestPurchase({ request: { apple: {sku: 'premium_yearly'}, google: { skus: ['premium_yearly'], subscriptionOffers: offers, purchaseToken: currentPurchaseToken, // Required for upgrades replacementMode: 1, // WITH_TIME_PRORATION }, }, type: 'subs', }); ``` ### One-Time Purchase Discount (Android 7.0+, v14.7.4+) ```tsx const product = products.find(p => p.id === 'premium_unlock'); const discountOffer = product?.discountOffers?.[0]; await requestPurchase({ request: { apple: {sku: 'premium_unlock'}, google: { skus: ['premium_unlock'], offerToken: discountOffer?.offerTokenAndroid, // Apply discount }, }, type: 'in-app', }); ``` ### iOS Subscription Offers (v14.7.3+) ```tsx // Win-back offers (iOS 18+) await requestPurchase({ request: { apple: { sku: 'monthly_sub', winBackOffer: { offerId: 'win_back_50_percent' }, }, }, type: 'subs', }); // JWS promotional offers (iOS 15+, WWDC 2025) await requestPurchase({ request: { apple: { sku: 'monthly_sub', promotionalOfferJWS: { jws: 'eyJ...server-signed-jws...', offerId: 'promo_offer_id', }, }, }, type: 'subs', }); // Override introductory offer eligibility (iOS 15+) await requestPurchase({ request: { apple: { sku: 'monthly_sub', introductoryOfferEligibility: true, // Force eligibility }, }, 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 } }; ``` ### Include Suspended Subscriptions (Android 8.1+) ```tsx // By default, suspended subscriptions are excluded // To include them: const purchases = await getAvailablePurchases({ android: { includeSuspended: true } }); // Check for suspended subscriptions purchases.filter(p => p.isSuspendedAndroid).forEach(p => { // DO NOT grant entitlements for suspended subscriptions // Direct user to resolve payment issues }); ``` ### iOS External Purchase Custom Link (iOS 18.1+, v14.7.5+) For apps with custom external purchase links entitlement: ```tsx import { isEligibleForExternalPurchaseCustomLinkIOS, getExternalPurchaseCustomLinkTokenIOS, showExternalPurchaseCustomLinkNoticeIOS, } from 'react-native-iap'; // Check eligibility const isEligible = await isEligibleForExternalPurchaseCustomLinkIOS(); if (isEligible) { // Show disclosure notice const notice = await showExternalPurchaseCustomLinkNoticeIOS('browser'); if (notice.continued) { // Get token for reporting to Apple const result = await getExternalPurchaseCustomLinkTokenIOS('acquisition'); if (result.token) { // Open external link and report to Apple's External Purchase Server API } } } ``` ### 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 'react-native-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 - **React Native**: 0.79+ (Nitro Modules requirement) - **iOS**: 15.0+ for StoreKit 2 features - **Android**: API 21+, Kotlin 2.0+ - **Expo**: Requires custom dev client (not Expo Go) ## Expo Configuration ```json { "expo": { "plugins": [ "react-native-iap", ["expo-build-properties", {"android": {"kotlinVersion": "2.2.0"}}] ] } } ``` ## Links - [GitHub](https://github.com/hyochan/react-native-iap) - [NPM](https://npmjs.com/package/react-native-iap) - [Full Documentation](https://hyochan.github.io/react-native-iap/) - [OpenIAP Specification](https://openiap.dev) - [Examples](https://github.com/hyochan/react-native-iap/tree/main/example)