# OpenIAP Complete Reference > OpenIAP: Unified in-app purchase specification for iOS & Android > Documentation: https://openiap.dev > Quick Reference: https://openiap.dev/llms.txt > Generated: 2026-01-20T21:43:18.697Z ## Table of Contents 1. Installation 2. Core APIs (Connection, Products, Purchase, Subscription) 3. Platform-Specific APIs (iOS, Android) 4. Types Reference 5. Error Codes & Handling 6. Implementation Patterns --- ## 1. Installation ### React Native / Expo ```bash # expo-iap (Expo projects - recommended) npx expo install expo-iap # react-native-iap (React Native CLI) npm install react-native-iap cd ios && pod install ``` ### Swift (iOS/macOS) ```swift // Swift Package Manager .package(url: "https://github.com/hyodotdev/openiap.git", from: "1.0.0") // CocoaPods pod 'openiap', '~> 1.0.0' ``` ### Kotlin (Android) ```kotlin // Gradle (build.gradle.kts) implementation("io.github.hyochan.openiap:openiap-google:1.0.0") // For Meta Horizon OS implementation("io.github.hyochan.openiap:openiap-google-horizon:1.0.0") ``` ### Flutter ```yaml # pubspec.yaml dependencies: flutter_inapp_purchase: ^5.0.0 ``` --- # expo-iap API Reference > Reference documentation for expo-iap (Expo In-App Purchase module) > Adapt all patterns to match OpenIAP internal conventions. ## Overview expo-iap is the Expo-compatible version of react-native-iap, providing in-app purchase functionality for both iOS and Android in Expo projects. ## Installation ```bash npx expo install expo-iap ``` ## Connection Management ### initConnection Initialize connection to the app store. ```typescript import { initConnection } from 'expo-iap'; await initConnection(); ``` ### endConnection Close connection to the app store. ```typescript import { endConnection } from 'expo-iap'; await endConnection(); ``` ## Product Operations ### fetchProducts Fetch product information from the store. ```typescript import { fetchProducts } from 'expo-iap'; const products = await fetchProducts(['com.app.product1', 'com.app.sub_monthly']); ``` **Returns:** `Promise` ### Product Type ```typescript interface Product { productId: string; title: string; description: string; price: string; currency: string; localizedPrice: string; type: ProductType; // 'iap' | 'sub' // iOS only subscriptionPeriodNumberIOS?: string; subscriptionPeriodUnitIOS?: string; introductoryPrice?: string; introductoryPricePaymentModeIOS?: string; introductoryPriceNumberOfPeriodsIOS?: string; introductoryPriceSubscriptionPeriodIOS?: string; // Android only subscriptionOfferDetailsAndroid?: SubscriptionOffer[]; oneTimePurchaseOfferDetailsAndroid?: OneTimePurchaseOffer; } ``` ## Purchase Operations ### requestPurchase Initiate a purchase. ```typescript import { requestPurchase } from 'expo-iap'; // For consumables/non-consumables await requestPurchase({ sku: 'com.app.product1' }); // For subscriptions (Android) await requestPurchase({ sku: 'com.app.sub_monthly', subscriptionOffers: [{ sku: 'com.app.sub_monthly', offerToken: 'token' }] }); ``` ### finishTransaction Complete a transaction after processing. ```typescript import { finishTransaction } from 'expo-iap'; await finishTransaction({ purchase, isConsumable: true }); ``` ### getAvailablePurchases Get user's existing purchases (restore purchases). ```typescript import { getAvailablePurchases } from 'expo-iap'; const purchases = await getAvailablePurchases(); ``` ## Purchase Type ```typescript interface Purchase { productId: string; transactionId?: string; transactionDate: number; transactionReceipt: string; purchaseToken?: string; // Android // iOS only originalTransactionDateIOS?: number; originalTransactionIdentifierIOS?: string; // Android only purchaseStateAndroid?: number; isAcknowledgedAndroid?: boolean; packageNameAndroid?: string; obfuscatedAccountIdAndroid?: string; obfuscatedProfileIdAndroid?: string; } ``` ## iOS-Specific Functions ### clearTransactionIOS Clear finished transactions from the queue. ```typescript import { clearTransactionIOS } from 'expo-iap'; await clearTransactionIOS(); ``` ### getReceiptDataIOS Get the receipt data for validation. ```typescript import { getReceiptDataIOS } from 'expo-iap'; const receipt = await getReceiptDataIOS(); ``` ### syncIOS Sync transactions with the App Store. ```typescript import { syncIOS } from 'expo-iap'; await syncIOS(); ``` ### presentCodeRedemptionSheetIOS Show the offer code redemption sheet. ```typescript import { presentCodeRedemptionSheetIOS } from 'expo-iap'; await presentCodeRedemptionSheetIOS(); ``` ### showManageSubscriptionsIOS Open subscription management in App Store. ```typescript import { showManageSubscriptionsIOS } from 'expo-iap'; await showManageSubscriptionsIOS(); ``` ### isEligibleForIntroOfferIOS Check intro offer eligibility. ```typescript import { isEligibleForIntroOfferIOS } from 'expo-iap'; const eligible = await isEligibleForIntroOfferIOS('com.app.sub_monthly'); ``` ### beginRefundRequestIOS Start a refund request. ```typescript import { beginRefundRequestIOS } from 'expo-iap'; const result = await beginRefundRequestIOS('transaction_id'); ``` ## Android-Specific Functions ### acknowledgePurchaseAndroid Acknowledge a purchase (required within 3 days). ```typescript import { acknowledgePurchaseAndroid } from 'expo-iap'; await acknowledgePurchaseAndroid({ token: purchase.purchaseToken }); ``` ### consumePurchaseAndroid Consume a consumable purchase. ```typescript import { consumePurchaseAndroid } from 'expo-iap'; await consumePurchaseAndroid({ token: purchase.purchaseToken }); ``` ### getPackageNameAndroid Get the app's package name. ```typescript import { getPackageNameAndroid } from 'expo-iap'; const packageName = await getPackageNameAndroid(); ``` ## Cross-Platform Functions ### getActiveSubscriptions Get active subscriptions. ```typescript import { getActiveSubscriptions } from 'expo-iap'; const subscriptions = await getActiveSubscriptions(['com.app.sub_monthly']); ``` ### hasActiveSubscriptions Check if user has active subscriptions. ```typescript import { hasActiveSubscriptions } from 'expo-iap'; const hasActive = await hasActiveSubscriptions(['com.app.sub_monthly']); ``` ### deepLinkToSubscriptions Open subscription management on both platforms. ```typescript import { deepLinkToSubscriptions } from 'expo-iap'; await deepLinkToSubscriptions({ sku: 'com.app.sub_monthly' }); ``` ### getStorefront Get storefront information. ```typescript import { getStorefront } from 'expo-iap'; const storefront = await getStorefront(); // { countryCode: 'US', ... } ``` ## Event Listeners ### purchaseUpdatedListener Listen for purchase updates. ```typescript import { purchaseUpdatedListener } from 'expo-iap'; const subscription = purchaseUpdatedListener((purchase) => { console.log('Purchase updated:', purchase); // Process and finish transaction }); // Cleanup subscription.remove(); ``` ### purchaseErrorListener Listen for purchase errors. ```typescript import { purchaseErrorListener } from 'expo-iap'; const subscription = purchaseErrorListener((error) => { console.error('Purchase error:', error); }); // Cleanup subscription.remove(); ``` ## Error Codes | Code | Description | |------|-------------| | `E_UNKNOWN` | Unknown error | | `E_USER_CANCELLED` | User cancelled | | `E_ITEM_UNAVAILABLE` | Item not available | | `E_NETWORK_ERROR` | Network error | | `E_SERVICE_ERROR` | Store service error | | `E_ALREADY_OWNED` | Item already owned | | `E_NOT_PREPARED` | Not initialized | | `E_NOT_ENDED` | Connection not ended | | `E_DEVELOPER_ERROR` | Developer error | ## Usage Pattern ```typescript import { initConnection, endConnection, fetchProducts, requestPurchase, finishTransaction, purchaseUpdatedListener, purchaseErrorListener, } from 'expo-iap'; // Setup await initConnection(); const purchaseListener = purchaseUpdatedListener(async (purchase) => { // Verify purchase server-side // Then finish transaction await finishTransaction({ purchase, isConsumable: false }); }); const errorListener = purchaseErrorListener((error) => { console.error(error); }); // Fetch products const products = await fetchProducts(['com.app.premium']); // Make purchase await requestPurchase({ sku: 'com.app.premium' }); // Cleanup purchaseListener.remove(); errorListener.remove(); await endConnection(); ``` --- # Google Play Billing Library API Reference > Reference documentation for Google Play Billing Library 8.x > Adapt all patterns to match OpenIAP internal conventions. ## Overview Google Play Billing Library enables in-app purchases and subscriptions on Android devices. ## Version History | Version | Release Date | Key Features | |---------|--------------|--------------| | 8.0 | 2025-06-30 | Auto-reconnect, product-level status codes, one-time products with multiple offers, sub-response codes | | 8.1 | 2025-11-06 | Suspended subscriptions (`isSuspended`), `includeSuspended` parameter, pre-order details, product-level subscription replacement, `KEEP_EXISTING` mode | | 8.2 | 2025-12-09 | Billing Programs API (external content links, external offers), deprecates old External Offers API | | 8.2.1 | 2025-12-15 | Bug fix for `isBillingProgramAvailableAsync()` and `createBillingProgramReportingDetailsAsync()` | | 8.3 | 2025-12-23 | External Payments program (Japan only), developer billing options | **Current Version**: 8.3.0 (as of January 2026) ## Core Classes ### BillingClient The main interface for communicating with Google Play Billing. ```kotlin val billingClient = BillingClient.newBuilder(context) .setListener(purchasesUpdatedListener) .enablePendingPurchases() // New in 8.0: Auto-reconnect on service disconnect .enableAutoServiceReconnection() .build() ``` ### Auto Service Reconnection (8.0+) ```kotlin // Enables automatic reconnection when service disconnects BillingClient.newBuilder(context) .enableAutoServiceReconnection() .build() ``` When enabled, the library automatically re-establishes the connection if an API call is made while disconnected. This reduces `SERVICE_DISCONNECTED` errors. > **OpenIAP Note**: Auto-reconnection is **always enabled** internally since OpenIAP uses Billing Library 8.3.0+. No configuration needed. ### Connection Management ```kotlin billingClient.startConnection(object : BillingClientStateListener { override fun onBillingSetupFinished(billingResult: BillingResult) { if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) { // Ready to query purchases } } override fun onBillingServiceDisconnected() { // Reconnect on next request } }) ``` ## Product Details ### QueryProductDetailsParams ```kotlin val productList = listOf( QueryProductDetailsParams.Product.newBuilder() .setProductId("product_id") .setProductType(BillingClient.ProductType.SUBS) // or INAPP .build() ) val params = QueryProductDetailsParams.newBuilder() .setProductList(productList) .build() billingClient.queryProductDetailsAsync(params) { billingResult, productDetailsList -> // Handle product details } ``` ### ProductDetails Properties | Property | Type | Description | |----------|------|-------------| | `productId` | String | Unique product identifier | | `productType` | String | "subs" or "inapp" | | `title` | String | Localized product title | | `name` | String | Product name | | `description` | String | Localized description | | `oneTimePurchaseOfferDetails` | Object | For INAPP products | | `subscriptionOfferDetails` | List | For subscription products | ### Subscription Offer Details ```kotlin data class SubscriptionOfferDetails( val basePlanId: String, val offerId: String?, val offerToken: String, val pricingPhases: PricingPhases, val offerTags: List ) ``` ### Pricing Phases ```kotlin data class PricingPhase( val formattedPrice: String, val priceAmountMicros: Long, val priceCurrencyCode: String, val billingPeriod: String, // ISO 8601 (P1W, P1M, P1Y) val billingCycleCount: Int, val recurrenceMode: Int // FINITE or INFINITE ) ``` ## Purchase Flow ### Launch Purchase Flow ```kotlin val productDetailsParams = BillingFlowParams.ProductDetailsParams.newBuilder() .setProductDetails(productDetails) .setOfferToken(offerToken) // For subscriptions .build() val billingFlowParams = BillingFlowParams.newBuilder() .setProductDetailsParamsList(listOf(productDetailsParams)) .build() val billingResult = billingClient.launchBillingFlow(activity, billingFlowParams) ``` ### PurchasesUpdatedListener ```kotlin val purchasesUpdatedListener = PurchasesUpdatedListener { billingResult, purchases -> when (billingResult.responseCode) { BillingClient.BillingResponseCode.OK -> { purchases?.forEach { purchase -> handlePurchase(purchase) } } BillingClient.BillingResponseCode.USER_CANCELED -> { // User cancelled } else -> { // Handle error } } } ``` ## Purchase Verification & Acknowledgement ### Verify Purchase ```kotlin val purchase: Purchase // Check purchase state if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) { // Verify signature server-side // Then acknowledge or consume } ``` ### Acknowledge Purchase (Subscriptions/Non-consumables) ```kotlin if (!purchase.isAcknowledged) { val acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder() .setPurchaseToken(purchase.purchaseToken) .build() billingClient.acknowledgePurchase(acknowledgePurchaseParams) { billingResult -> // Handle result } } ``` ### Consume Purchase (Consumables) ```kotlin val consumeParams = ConsumeParams.newBuilder() .setPurchaseToken(purchase.purchaseToken) .build() billingClient.consumeAsync(consumeParams) { billingResult, purchaseToken -> // Handle result } ``` ## Query Existing Purchases ```kotlin // Query subscriptions billingClient.queryPurchasesAsync( QueryPurchasesParams.newBuilder() .setProductType(BillingClient.ProductType.SUBS) .build() ) { billingResult, purchasesList -> // Handle existing subscriptions } // Query in-app products billingClient.queryPurchasesAsync( QueryPurchasesParams.newBuilder() .setProductType(BillingClient.ProductType.INAPP) .build() ) { billingResult, purchasesList -> // Handle existing purchases } ``` ## Purchase Properties | Property | Type | Description | |----------|------|-------------| | `orderId` | String | Unique order identifier | | `purchaseToken` | String | Token for verification | | `purchaseState` | Int | PENDING, PURCHASED, UNSPECIFIED | | `purchaseTime` | Long | Timestamp in milliseconds | | `products` | List | Product IDs in purchase | | `isAcknowledged` | Boolean | Whether acknowledged | | `isAutoRenewing` | Boolean | Auto-renewal status | | `quantity` | Int | Quantity purchased | ## Response Codes | Code | Constant | Description | |------|----------|-------------| | 0 | OK | Success | | 1 | USER_CANCELED | User cancelled | | 2 | SERVICE_UNAVAILABLE | Network error | | 3 | BILLING_UNAVAILABLE | Billing not available | | 4 | ITEM_UNAVAILABLE | Item not available | | 5 | DEVELOPER_ERROR | Invalid arguments | | 6 | ERROR | Fatal error | | 7 | ITEM_ALREADY_OWNED | Already owned | | 8 | ITEM_NOT_OWNED | Not owned | ## Feature Support ```kotlin // Check if feature is supported val result = billingClient.isFeatureSupported(BillingClient.FeatureType.SUBSCRIPTIONS) if (result.responseCode == BillingClient.BillingResponseCode.OK) { // Subscriptions are supported } ``` ### Feature Types - `SUBSCRIPTIONS` - Subscription support - `SUBSCRIPTIONS_UPDATE` - Subscription upgrades/downgrades - `PRICE_CHANGE_CONFIRMATION` - Price change confirmation - `PRODUCT_DETAILS` - Product details API ## Product-Level Status Codes (8.0+) In Billing Library 8.0+, `queryProductDetailsAsync()` returns products that couldn't be fetched with a status code explaining why. ```kotlin billingClient.queryProductDetailsAsync(params) { billingResult, productDetailsList -> productDetailsList.forEach { productDetails -> when (productDetails.productStatus) { ProductDetails.ProductStatus.OK -> { // Product fetched successfully } ProductDetails.ProductStatus.NOT_FOUND -> { // SKU doesn't exist in Play Console } ProductDetails.ProductStatus.NO_OFFERS_AVAILABLE -> { // User not eligible for any offers } } } } ``` | Status | Description | |--------|-------------| | `OK` | Product fetched successfully | | `NOT_FOUND` | SKU doesn't exist in Play Console | | `NO_OFFERS_AVAILABLE` | User not eligible for any offers | ## Suspended Subscriptions (8.1+) ```kotlin val purchase: Purchase // Check if subscription is suspended due to billing issue if (purchase.isSuspended) { // User's payment method failed // Do NOT grant entitlements // Direct user to subscription center to fix payment } ``` ### Query Suspended Subscriptions (8.1+) ```kotlin // Include suspended subscriptions in query results val params = QueryPurchasesParams.newBuilder() .setProductType(BillingClient.ProductType.SUBS) .setIncludeSuspended(true) // New in 8.1 .build() billingClient.queryPurchasesAsync(params) { billingResult, purchases -> purchases.forEach { purchase -> if (purchase.isSuspended) { // Handle suspended subscription } } } ``` > **OpenIAP Note**: Use `includeSuspendedAndroid: true` in `PurchaseOptions` when calling `getAvailablePurchases()`. The `isSuspendedAndroid` field on purchases indicates suspension status. ## Sub-Response Codes (8.0+) `BillingResult` includes a sub-response code for more granular error information: ```kotlin val result = billingClient.launchBillingFlow(activity, params) when (result.subResponseCode) { BillingResult.SUB_RESPONSE_CODE_INSUFFICIENT_FUNDS -> { // User's payment method has insufficient funds } BillingResult.SUB_RESPONSE_CODE_USER_INELIGIBLE -> { // User doesn't meet offer eligibility requirements } } ``` | Sub-Response Code | Description | |-------------------|-------------| | `PAYMENT_DECLINED_DUE_TO_INSUFFICIENT_FUNDS` | User's payment method has insufficient funds | | `USER_INELIGIBLE` | User doesn't meet subscription offer eligibility | | `NO_APPLICABLE_SUB_RESPONSE_CODE` | No specific sub-code applies | ## Subscription Product Replacement (8.1+) Product-level replacement parameters for subscription upgrades/downgrades: ```kotlin val replacementParams = SubscriptionProductReplacementParams.newBuilder() .setOldProductId("old_subscription_id") .setReplacementMode(ReplacementMode.WITH_TIME_PRORATION) .build() val productDetailsParams = BillingFlowParams.ProductDetailsParams.newBuilder() .setProductDetails(newProductDetails) .setOfferToken(offerToken) .setSubscriptionProductReplacementParams(replacementParams) // New in 8.1 .build() ``` ### Replacement Modes | Mode | Description | |------|-------------| | `WITH_TIME_PRORATION` | Immediate, expiration time prorated | | `CHARGE_PRORATED_PRICE` | Immediate, same billing cycle | | `CHARGE_FULL_PRICE` | Immediate, full price charged | | `WITHOUT_PRORATION` | Takes effect on old plan expiration | | `DEFERRED` | Deferred, no charge | | `KEEP_EXISTING` | Keep existing payment schedule (8.1+) | ## Best Practices 1. **Always acknowledge purchases** within 3 days or they will be refunded 2. **Verify purchases server-side** using Google Play Developer API 3. **Handle pending purchases** for payment methods that require additional steps 4. **Auto-reconnect is enabled by default** in OpenIAP (8.0+) 5. **Check product status codes** (8.0+) to understand why products weren't fetched 6. **Check isSuspended** (8.1+) before granting entitlements 7. **Cache product details** to avoid repeated queries --- # Meta Horizon IAP API Reference > External reference for Meta Horizon Store in-app purchase APIs. > Source: [Meta Horizon Documentation](https://developers.meta.com/horizon/documentation/) ## Overview Meta Horizon provides IAP functionality for Quest VR applications. There are two main integration paths: 1. **Platform SDK IAP** - Native Horizon IAP APIs 2. **Billing Compatibility SDK** - Google Play Billing Library compatible wrapper ## Version Compatibility Matrix | Library | Version | Compatible With | |---------|---------|-----------------| | horizon-billing-compatibility | **1.1.1** (latest) | Google Play Billing **7.0** API | | Google Play Billing (Play flavor) | **8.3.0** (latest) | N/A | | react-native-iap | v14+ | Billing 7.0+, RN 0.79+, Kotlin 2.0+ | | expo-iap | latest | Billing 7.0+, Kotlin 2.0+ | **CRITICAL**: Horizon Billing Compatibility SDK implements Google Play Billing **7.0** API surface, NOT 8.x. When writing shared code for both Play and Horizon flavors: - Use only APIs that exist in **both** Billing 7.0 and 8.x - Horizon SDK does NOT support Billing 8.x features like auto-reconnect, product status codes, or `includeSuspended` - OpenIAP handles this automatically with flavor-specific implementations ### APIs Available in Both (Safe to use in shared code) - `BillingClient.Builder`, `BillingClient.newBuilder()` - `queryProductDetailsAsync()` - Core product query - `launchBillingFlow()` - Purchase flow - `acknowledgePurchase()` - Acknowledge (no-op in Horizon) - `consumeAsync()` - Consume purchase - `queryPurchasesAsync()` - Query purchases ### APIs Only in Billing 8.x (DO NOT use in shared code) - `enableAutoServiceReconnection()` - Auto reconnect feature (8.0+) - Product-level status codes in `queryProductDetailsAsync()` response (8.0+) - One-time products with multiple offers (8.0+) - Sub-response codes in `BillingResult` (8.0+) - `isSuspended` on Purchase (8.1+) - `includeSuspended` parameter in `QueryPurchasesParams` (8.1+) - `SubscriptionProductReplacementParams` (8.1+) - Billing Programs API (`isBillingProgramAvailableAsync`, etc.) (8.2+) - External Payments / Developer Billing Options (8.3+) ## Billing Compatibility SDK For apps already using Google Play Billing Library, the Horizon Billing Compatibility SDK provides a minimal migration path. ### Compatibility - Compatible with **Google Play Billing Library 7.0** API - Supports: consumable, durable, and subscription IAP - Kotlin 2+ required ### Migration Steps Replace imports from: ```kotlin import com.android.billingclient.api.* ``` To: ```kotlin import com.meta.horizon.billingclient.api.* ``` ### Key Differences from Google Play Billing | Feature | Google Play | Horizon | |---------|-------------|---------| | `acknowledgePurchase()` | Required within 3 days | No-op (not required) | | Non-acknowledgement | Auto-refund after 3 days | No auto-refund | | `enablePendingPurchases()` | Enables pending purchases | No-op (for compatibility) | | `onBillingServiceDisconnected()` | Called on disconnect | Never invoked | ### Important Notes - Keep SKUs on Meta Horizon Developer Center same as Google Play Console product IDs - Only call `consumeAsync()` on consumable items - `acknowledgePurchase()` is no-op - no acknowledgement requirements ## Server-to-Server (S2S) APIs ### Authentication Access token format: `OC|App_ID|App_Secret` ### Verify Entitlement Verify that a user owns an item (app or add-on). **Endpoint:** ```http POST https://graph.oculus.com/$APP_ID/verify_entitlement ``` **Parameters:** | Parameter | Type | Description | |-----------|------|-------------| | `access_token` | string | `OC\|App_ID\|App_Secret` format | | `user_id` | string | The user ID to verify | | `sku` | string | (Optional) SKU for add-on verification | **Example - Verify App Ownership:** ```bash curl -d "access_token=OC|$APP_ID|$APP_SECRET" \ -d "user_id=$USER_ID" \ https://graph.oculus.com/$APP_ID/verify_entitlement ``` **Example - Verify Add-on/IAP:** ```bash curl -d "access_token=OC|$APP_ID|$APP_SECRET" \ -d "user_id=$USER_ID" \ -d "sku=$SKU" \ https://graph.oculus.com/$APP_ID/verify_entitlement ``` **Response:** ```json { "success": true } ``` ### Refund IAP Entitlement Refund a DURABLE or CONSUMABLE entitlement (not yet consumed). **Endpoint:** ```http POST https://graph.oculus.com/$APP_ID/refund_iap_entitlement ``` **Parameters:** | Parameter | Type | Description | |-----------|------|-------------| | `access_token` | string | `OC\|App_ID\|App_Secret` format | | `user_id` | string | The user ID | | `sku` | string | SKU of item to refund | **Note:** Can only refund items not yet consumed via `consumeAsync()`. ## Platform SDK IAP (Native) ### Product Types | Type | Description | |------|-------------| | `CONSUMABLE` | Can be purchased multiple times (e.g., coins) | | `DURABLE` | One-time purchase, permanent ownership | | `SUBSCRIPTION` | Recurring billing | ### Key APIs #### Get Products Retrieve product information and pricing. #### Launch Purchase Flow Initiate purchase for an item. #### Query Purchase History Get user's purchase history. #### Consume Purchase Mark consumable item as used (required for re-purchase). ## OpenIAP Type Mapping | OpenIAP Type | Description | |--------------|-------------| | `IapStore.Horizon` | Store identifier for Horizon | | `VerifyPurchaseHorizonOptions` | Horizon verification parameters | | `VerifyPurchaseResultHorizon` | Horizon verification result | ### VerifyPurchaseHorizonOptions ```typescript interface VerifyPurchaseHorizonOptions { userId: string; // Horizon user ID sku: string; // Product SKU appId: string; // Horizon App ID appSecret: string; // Horizon App Secret } ``` ### VerifyPurchaseResultHorizon ```typescript interface VerifyPurchaseResultHorizon { success: boolean; // Verification result } ``` ## Entitlement Check Apps must perform entitlement check within 10 seconds of launch for VRC.Quest.Security.1 compliance. ## React Native / Expo Support Meta Quest supports React Native and Expo applications. ### Requirements | Library | Minimum Version | Notes | |---------|-----------------|-------| | react-native-iap | v14+ | Billing 7.0+, Kotlin 2.0+, RN 0.79+ | | expo-iap | latest | Uses expo-horizon-core plugin | | React Native | 0.79+ | Required for Nitro modules | | Kotlin | 2.0+ | Required for both billing SDKs | ### Expo Integration Use `expo-horizon-core` plugin for Quest support: ```bash npx expo install expo-horizon-core ``` The plugin: - Removes unsupported dependencies/permissions - Configures Android product flavors - Specifies Meta Horizon App ID - Provides Quest-specific JS utilities ### Known Limitations on Quest - No GPS sensor (limited location accuracy) - No geocoding support - No device heading - No background location - Some Expo libraries need forks (expo-location, expo-notifications) ## Documentation Links - [Platform SDK IAP Package](https://developers.meta.com/horizon/documentation/android-apps/ps-platform-sdk-iap) - [S2S APIs](https://developers.meta.com/horizon/documentation/unity/ps-iap-s2s/) - [Billing Compatibility SDK](https://developers.meta.com/horizon/documentation/spatial-sdk/horizon-billing-compatibility-sdk/) - [Entitlement Check](https://developers.meta.com/horizon/documentation/android-apps/ps-entitlement-check/) - [React Native on Quest](https://developers.meta.com/horizon/documentation/android-apps/react-native-apps) - [Expo Quest Setup](https://blog.swmansion.com/how-to-add-meta-quest-support-to-your-expo-app-68c52778b1fe) - [Subscriptions](https://developers.meta.com/horizon/resources/subscriptions/) - [Setting up Add-ons](https://developers.meta.com/horizon/resources/add-ons-setup/) --- # react-native-iap API Reference > Reference documentation for react-native-iap > Adapt all patterns to match OpenIAP internal conventions. ## Overview react-native-iap is a React Native library for in-app purchases on iOS and Android. expo-iap is built on top of this library. ## Installation ```bash npm install react-native-iap # or yarn add react-native-iap ``` ## Hook-Based API (Recommended) ### useIAP Hook ```typescript import { useIAP } from 'react-native-iap'; function PurchaseScreen() { const { connected, products, subscriptions, purchaseHistory, availablePurchases, currentPurchase, currentPurchaseError, initConnectionError, finishTransaction, getProducts, getSubscriptions, getAvailablePurchases, getPurchaseHistory, requestPurchase, requestSubscription, } = useIAP(); useEffect(() => { if (currentPurchase) { // Process purchase finishTransaction({ purchase: currentPurchase }); } }, [currentPurchase]); return (/* ... */); } ``` ### withIAPContext HOC Wrap your app with IAP context provider. ```typescript import { withIAPContext } from 'react-native-iap'; function App() { return ; } export default withIAPContext(App); ``` ## Imperative API ### Connection Management ```typescript import { initConnection, endConnection, getProducts, getSubscriptions, } from 'react-native-iap'; // Initialize const connected = await initConnection(); // Fetch products const products = await getProducts({ skus: ['com.app.product1'] }); const subs = await getSubscriptions({ skus: ['com.app.sub_monthly'] }); // Cleanup await endConnection(); ``` ### Product Types ```typescript interface Product { productId: string; price: string; currency: string; localizedPrice: string; title: string; description: string; type: 'inapp' | 'subs'; // iOS introductoryPrice?: string; introductoryPriceAsAmountIOS?: string; introductoryPricePaymentModeIOS?: string; introductoryPriceNumberOfPeriodsIOS?: string; introductoryPriceSubscriptionPeriodIOS?: string; subscriptionPeriodNumberIOS?: string; subscriptionPeriodUnitIOS?: string; discounts?: Discount[]; // Android subscriptionOfferDetails?: SubscriptionOffer[]; oneTimePurchaseOfferDetails?: OneTimePurchaseOffer; } interface SubscriptionOffer { basePlanId: string; offerId?: string; offerToken: string; offerTags: string[]; pricingPhases: PricingPhase[]; } interface PricingPhase { formattedPrice: string; priceCurrencyCode: string; priceAmountMicros: string; billingPeriod: string; billingCycleCount: number; recurrenceMode: number; } ``` ### Purchase Operations ```typescript import { requestPurchase, requestSubscription, finishTransaction, getAvailablePurchases, getPurchaseHistory, } from 'react-native-iap'; // Purchase consumable/non-consumable await requestPurchase({ sku: 'com.app.product1' }); // Purchase subscription (Android with offer token) await requestSubscription({ sku: 'com.app.sub_monthly', subscriptionOffers: [{ sku: 'com.app.sub_monthly', offerToken: 'token' }], }); // Finish transaction await finishTransaction({ purchase, isConsumable: true }); // Get available purchases (restore) const available = await getAvailablePurchases(); // Get purchase history const history = await getPurchaseHistory(); ``` ### Purchase Type ```typescript interface Purchase { productId: string; transactionId?: string; transactionDate: number; transactionReceipt: string; purchaseToken?: string; quantityIOS?: number; originalTransactionDateIOS?: number; originalTransactionIdentifierIOS?: string; verificationResultIOS?: string; appAccountToken?: string; // Android purchaseStateAndroid?: PurchaseStateAndroid; isAcknowledgedAndroid?: boolean; packageNameAndroid?: string; developerPayloadAndroid?: string; obfuscatedAccountIdAndroid?: string; obfuscatedProfileIdAndroid?: string; autoRenewingAndroid?: boolean; } ``` ## Event Listeners ```typescript import { purchaseUpdatedListener, purchaseErrorListener, } from 'react-native-iap'; // Purchase updates const purchaseUpdateSubscription = purchaseUpdatedListener( async (purchase: Purchase) => { const receipt = purchase.transactionReceipt; if (receipt) { // Verify with server await finishTransaction({ purchase }); } } ); // Purchase errors const purchaseErrorSubscription = purchaseErrorListener( (error: PurchaseError) => { console.warn('purchaseErrorListener', error); } ); // Cleanup purchaseUpdateSubscription.remove(); purchaseErrorSubscription.remove(); ``` ## iOS-Specific Functions ```typescript import { clearTransactionIOS, clearProductsIOS, getReceiptIOS, getPendingPurchasesIOS, getPromotedProductIOS, buyPromotedProductIOS, presentCodeRedemptionSheetIOS, validateReceiptIos, } from 'react-native-iap'; // Clear finished transactions await clearTransactionIOS(); // Clear cached products await clearProductsIOS(); // Get receipt for validation const receipt = await getReceiptIOS(); // Get pending purchases const pending = await getPendingPurchasesIOS(); // Handle promoted products const promotedProduct = await getPromotedProductIOS(); if (promotedProduct) { await buyPromotedProductIOS(); } // Show offer code redemption await presentCodeRedemptionSheetIOS(); ``` ## Android-Specific Functions ```typescript import { acknowledgePurchaseAndroid, consumePurchaseAndroid, flushFailedPurchasesCachedAsPendingAndroid, getPackageNameAndroid, isFeatureSupported, getBillingConfigAndroid, } from 'react-native-iap'; // Acknowledge purchase (non-consumables, subscriptions) await acknowledgePurchaseAndroid({ token: purchase.purchaseToken }); // Consume purchase (consumables) await consumePurchaseAndroid({ token: purchase.purchaseToken }); // Clear failed pending purchases await flushFailedPurchasesCachedAsPendingAndroid(); // Get package name const packageName = getPackageNameAndroid(); // Check feature support const supported = await isFeatureSupported('subscriptions'); // Get billing config const config = await getBillingConfigAndroid(); ``` ## Subscription Status (iOS) ```typescript import { getSubscriptionStatusIOS, getSubscriptionStatusesIOS, } from 'react-native-iap'; // Get status for single product const status = await getSubscriptionStatusIOS('com.app.sub_monthly'); // Get status for multiple products const statuses = await getSubscriptionStatusesIOS(); ``` ## Error Handling ```typescript import { IapIosSk2, ErrorCode } from 'react-native-iap'; try { await requestPurchase({ sku: 'com.app.product1' }); } catch (err) { if (err.code === ErrorCode.E_USER_CANCELLED) { // User cancelled } else if (err.code === ErrorCode.E_ITEM_UNAVAILABLE) { // Item not available } else if (err.code === ErrorCode.E_ALREADY_OWNED) { // Already owned } else { // Other error } } ``` ### Error Codes | Code | Description | |------|-------------| | `E_UNKNOWN` | Unknown error | | `E_USER_CANCELLED` | User cancelled | | `E_ITEM_UNAVAILABLE` | Item not available | | `E_NETWORK_ERROR` | Network error | | `E_SERVICE_ERROR` | Store service error | | `E_ALREADY_OWNED` | Item already owned | | `E_REMOTE_ERROR` | Remote error | | `E_NOT_PREPARED` | Not initialized | | `E_NOT_ENDED` | Not ended | | `E_DEVELOPER_ERROR` | Developer error | | `E_BILLING_RESPONSE_JSON_PARSE_ERROR` | JSON parse error | | `E_DEFERRED_PAYMENT` | Deferred payment | ## Complete Usage Example ```typescript import React, { useEffect } from 'react'; import { withIAPContext, useIAP, requestPurchase, finishTransaction, purchaseUpdatedListener, purchaseErrorListener, ProductPurchase, } from 'react-native-iap'; const productIds = ['com.app.product1']; const subscriptionIds = ['com.app.sub_monthly']; function Store() { const { connected, products, subscriptions, getProducts, getSubscriptions, } = useIAP(); useEffect(() => { if (connected) { getProducts({ skus: productIds }); getSubscriptions({ skus: subscriptionIds }); } }, [connected]); useEffect(() => { const purchaseSub = purchaseUpdatedListener( async (purchase: ProductPurchase) => { await finishTransaction({ purchase, isConsumable: false }); } ); const errorSub = purchaseErrorListener((error) => { console.error('Purchase error:', error); }); return () => { purchaseSub.remove(); errorSub.remove(); }; }, []); const handlePurchase = async (sku: string) => { try { await requestPurchase({ sku }); } catch (err) { console.error(err); } }; return (/* Render products and subscriptions */); } export default withIAPContext(Store); ``` ## Platform Differences | Feature | iOS | Android | |---------|-----|---------| | Subscription offers | Introductory price, Discounts | Offer tokens, Pricing phases | | Acknowledge | Automatic | Required within 3 days | | Consume | finishTransaction | consumePurchaseAndroid | | Receipt | getReceiptIOS | transactionReceipt in Purchase | | Promoted products | Supported | Not supported | | Offer codes | Supported | Promo codes via Play Store | --- # StoreKit 2 API Reference This document provides external API reference for Apple's StoreKit 2 framework. ## iOS 18+ Features | Feature | iOS Version | Description | |---------|-------------|-------------| | Win-back offers | iOS 18.0 | Re-engage churned subscribers | | Consumable transaction history | iOS 18.0 | History includes finished consumables | | Billing issue messages | iOS 18.0 | Automatic billing issue notifications via StoreKit Message | | UI context for purchases | iOS 18.2 | Required for proper payment sheet display | | External purchase notice | iOS 18.2 | `presentExternalPurchaseNoticeSheetIOS` | | `appTransactionID` | iOS 18.4 | Globally unique app transaction identifier (back-deployed to iOS 15) | | `originalPlatform` | iOS 18.4 | Original purchase platform (back-deployed to iOS 15) | | `Offer.Period` | iOS 18.4 | Offer period information | | `advancedCommerceInfo` | iOS 18.4 | Advanced Commerce API data | | Expanded offer codes | iOS 18.4 | For consumables/non-consumables | | JWS promotional offers | WWDC 2025 | New `promotionalOffer` purchase option with JWS format | | `introductoryOfferEligibility` | WWDC 2025 | Set eligibility via purchase option | ### WWDC 2025 Updates - **SubscriptionStatus by Transaction ID**: Query subscription status using any transaction ID - **JWS-based promotional offers**: New `promotionalOffer` purchase option with compact JWS string - **Introductory offer eligibility**: Override eligibility check with `introductoryOfferEligibility` purchase option - Both new purchase options are back-deployed to iOS 15 ## Product A type that describes an in-app purchase product. ### Properties ```swift let id: String // The product identifier let type: Product.ProductType // The type of product let displayName: String // Localized display name let description: String // Localized description let displayPrice: String // Localized price string let price: Decimal // Price as decimal let subscription: Product.SubscriptionInfo? // Subscription details ``` ### Methods #### products(for:) ```swift static func products(for identifiers: [String]) async throws -> [Product] ``` Fetches products from the App Store. #### purchase(options:) ```swift func purchase(options: Set = []) async throws -> Product.PurchaseResult ``` Initiates a purchase for this product. ## Transaction Represents a completed purchase transaction. ### Properties ```swift let id: UInt64 // Unique transaction ID let originalID: UInt64 // Original transaction ID let productID: String // Product identifier let purchaseDate: Date // When the purchase occurred let expirationDate: Date? // Subscription expiration date let revocationDate: Date? // When the transaction was revoked let isUpgraded: Bool // Whether this subscription was upgraded let environment: AppStore.Environment // sandbox or production ``` ### Methods #### currentEntitlements ```swift static var currentEntitlements: Transaction.Entitlements ``` A sequence of the customer's current entitlements. #### latest(for:) ```swift static func latest(for productID: String) async -> VerificationResult? ``` Gets the latest transaction for a product. #### finish() ```swift func finish() async ``` Marks the transaction as finished. ## AppStore Provides access to App Store functionality. ### Methods #### sync() ```swift static func sync() async throws ``` Syncs transactions with the App Store. #### showManageSubscriptions(in:) ```swift static func showManageSubscriptions(in scene: UIWindowScene) async throws ``` Shows the subscription management UI. #### beginRefundRequest(for:in:) ```swift static func beginRefundRequest(for transactionID: UInt64, in scene: UIWindowScene) async throws -> Transaction.RefundRequestStatus ``` Begins a refund request for a transaction. ## Win-Back Offers (iOS 18+) Win-back offers are a new offer type to re-engage churned subscribers. ### Automatic Presentation StoreKit Message automatically presents win-back offers when a user is eligible: ```swift // Message reason for win-back offers StoreKit.Message.Reason.winBackOffer ``` ### Manual Application Apply a win-back offer during purchase: ```swift let product: Product let winBackOffer: Product.SubscriptionOffer let result = try await product.purchase(options: [ .winBackOffer(winBackOffer) ]) ``` ### Checking Eligibility ```swift // Win-back offers are available in subscription.promotionalOffers // with type == .winBack let winBackOffers = product.subscription?.promotionalOffers.filter { $0.type == .winBack } ``` ### RenewalInfo Win-back offer information is available in renewal info: ```swift let renewalInfo: Product.SubscriptionInfo.RenewalInfo // Check if win-back offer is applied to next renewal if renewalInfo.renewalOfferType == .winBack { // Win-back offer will be applied } ``` ## UI Context for Purchases (iOS 18.2+) Beginning in iOS 18.2, purchase methods require a UI context to properly display payment sheets: ```swift // iOS/iPadOS/tvOS/visionOS: UIViewController let result = try await product.purchase(confirmIn: viewController) // macOS: NSWindow let result = try await product.purchase(confirmIn: window) // watchOS: No UI context required ``` > **OpenIAP Note**: UI context is handled automatically in OpenIAP using the active window scene. ## AppTransaction Updates (iOS 18.4+) ```swift let appTransaction = try await AppTransaction.shared // New in iOS 18.4 (back-deployed to iOS 15) let appTransactionID = appTransaction.appTransactionID // Globally unique per Apple Account let originalPlatform = appTransaction.originalPlatform // Original purchase platform ``` ### appTransactionID - Globally unique identifier for each Apple Account that downloads your app - Remains consistent across redownloads, refunds, repurchases, and storefront changes - Works with Family Sharing (each family member gets unique ID) - Back-deployed to iOS 15 ## Advanced Commerce API (iOS 18.4+) For apps with large product catalogs: ```swift // Check if product has advanced commerce info if let advancedInfo = product.advancedCommerceInfo { // Handle large catalog monetization } ``` ## External Purchase Support (iOS 18.2+) ### Present External Purchase Notice ```swift // Check if external purchase notice can be presented if await ExternalPurchase.canPresent { let result = try await ExternalPurchase.presentNoticeSheet() switch result { case .continue: // User wants to continue to external purchase case .dismissed: // User dismissed the notice } } ``` ### Present External Purchase Link ```swift let result = try await ExternalPurchase.open(url: externalURL) ``` > **OpenIAP Note**: `presentExternalPurchaseNoticeSheetIOS` and `presentExternalPurchaseLinkIOS` are available in the iOS package. --- ## Links & Resources - Documentation: https://openiap.dev/docs - Types Reference: https://openiap.dev/docs/types - APIs Reference: https://openiap.dev/docs/apis - Error Codes: https://openiap.dev/docs/errors - GitHub: https://github.com/hyodotdev/openiap ### Ecosystem Libraries - expo-iap: https://github.com/hyochan/expo-iap - react-native-iap: https://github.com/dooboolab-community/react-native-iap - flutter_inapp_purchase: https://github.com/dooboolab-community/flutter_inapp_purchase - godot-iap: https://github.com/hyochan/godot-iap - kmp-iap: https://github.com/nicoseng/kmp-iap