Use this page with AI

Copy this into your coding assistant and add your request.

Read https://openiap.dev/docs/setup/expo and https://openiap.dev/llms.txt. Follow the reading instructions, detailed reference, and linked guides relevant to my task before making changes.
Inspect my existing project and reuse its framework and conventions. Ask me for missing product decisions. Implement the requested behavior and run the applicable checks.
Show the working result, the commands and actual test results, and any remaining limitations. Keep your explanation brief.

My request: [describe what customers should be able to do]
See an example request →

Expo Setup

expo-iap provides in-app purchase support for Expo apps — both the managed workflow and bare apps that prefer the Expo Modules stack. For bare React Native we recommend react-native-iap (same API); see React Native CLI Projects for using expo-iap in bare apps.

Prerequisites#

RequirementDetails
Expo SDK53+; SDK 57 / React Native 0.86 is the validated baseline
iOSFollow the Expo SDK baseline (SDK 57: iOS 16.4+; SDK 53: iOS 15.1+)
AndroidAndroid 7 / API 24+ for supported Expo SDKs (native module minimum: API 23)
Node.jsFollow the Expo SDK baseline (SDK 57: Node.js 22.13.x; SDK 54–56: Node.js 20.19.x; SDK 53: Node.js 20.18.x)

Installation#

sh
npx expo install expo-iap

Android Kotlin Version#

expo-iap uses OpenIAP Android artifacts backed by Google Play Billing Library v9.1.0. Keep the Kotlin version that matches your Expo SDK instead of copying the newer compiler used to publish the standalone OpenIAP Android library.

  • Expo SDK 57: use Kotlin 2.1.20, which matches Expo and React Native 0.86. The default toolchain is sufficient; if you set kotlinVersion explicitly, use the value below.
  • Earlier supported Expo SDKs: keep their documented Kotlin version. Do not force the standalone Kotlin 2.4.x compiler into the Expo build, because Expo Gradle plugins compiled with an older Kotlin line cannot load that metadata.
json
{
  "expo": {
    "plugins": [
      "expo-iap",
      [
        "expo-build-properties",
        {
          "android": {
            "kotlinVersion": "2.1.20"
          }
        }
      ]
    ]
  }
}

Prebuild & Development Build#

After installing, generate native projects and create a development build:

sh
# Generate native iOS and Android directories
npx expo prebuild --clean

# Option A: Build with EAS
npm install -g eas-cli  # if not installed
eas build --platform ios --profile development
eas build --platform android --profile development

# Option B: Run locally
npx expo run:ios --device
npx expo run:android

iOS Configuration#

Match your Expo SDK deployment target. The validated SDK 57 setup requires iOS 16.4+:

ts
// app.json
{
  "expo": {
    "ios": {
      "deploymentTarget": "16.4"
    }
  }
}

// or app.config.ts
export default {
  expo: {
    ios: {
      deploymentTarget: '16.4',
    },
  },
};

Enable In-App Purchase capability in Xcode: Target > Signing & Capabilities > + Capability > In-App Purchase (after running npx expo prebuild).

Android Configuration#

  • Requires minSdkVersion 23+ and compileSdkVersion 36+
  • No additional configuration needed for Expo managed workflow

React Native CLI Projects#

If using React Native CLI (not Expo), install expo-modules-core first:

sh
npx install-expo-modules@latest
cd ios && pod install

Config Plugin Options#

The expo-iap config plugin does two things: it wires your IAPKit publishable key into the app for hosted purchase verification, and it enables optional store modules — Onside (an iOS alternative marketplace), Horizon OS (Meta Quest), and Amazon (Fire OS devices and the Vega OS runtime). All modules are off by default; enable only the stores you ship to.

json
{
  "expo": {
    "plugins": [
      [
        "expo-iap",
        {
          "iapkitApiKey": "openiap-kit_pk_<your-publishable-key>",
          "modules": {
            "onside": true,
            "horizon": true,
            "amazon": {
              "fireOS": false,
              "vegaOS": false
            }
          },
          "android": {
            "horizon": {
              "appId": "YOUR_HORIZON_APP_ID"
            }
          }
        }
      ]
    ]
  }
}

Use this page for the Expo plugin shape. Store-specific values — required developer-console fields, supported targets, and artifact rules — live in each store's setup page linked above.

Module enable flags live under modules; platform-specific values live under android or ios. For Amazon, modules.amazon.fireOS and modules.amazon.vegaOS toggle each target; the separate android.amazon.vegaOS block is only needed when your Vega OS build requires different values (app id, artifacts) than your regular Android config — see Amazon Store Setup.

Usage#

Under the hood, the typical flow is set up purchaseUpdatedListener and purchaseErrorListener initConnection fetchProducts requestPurchase finishTransaction, with endConnection on teardown. The useIAP hook manages the connection and listener steps for you. See the Purchase Guide for the complete flow.

useIAP Hook (Recommended)#

expo-iap provides the same useIAP hook as react-native-iap. It manages connection, state, and errors automatically.

ts
import React, { useEffect } from 'react';
import { Alert, Button, FlatList } from 'react-native';
import { useIAP, ErrorCode, finishTransaction } from 'expo-iap';

function Store() {
  const {
    connected,
    products,
    fetchProducts,
    requestPurchase,
  } = useIAP({
    onPurchaseSuccess: (purchase) => {
      // 1. Validate receipt with your backend or IAPKit
      // 2. Grant entitlement
      // 3. CRITICAL: Finish the transaction
      //    (Android auto-refunds after 3 days if not called!)
      void finishTransaction({
        purchase,
        isConsumable: false, // true for consumables
      }).catch((error) => {
        console.warn('Transaction finalization failed:', error);
      });
    },
    onPurchaseError: (error) => {
      if (error.code === ErrorCode.UserCancelled) return;
      Alert.alert('Purchase Failed', error.message);
    },
  });

  useEffect(() => {
    if (!connected) return;
    void fetchProducts({ skus: ['premium'] }).catch((error) => {
      console.warn('Product fetch failed:', error);
    });
  }, [connected, fetchProducts]);

  return (
    <FlatList
      data={products}
      keyExtractor={(product) => product.id}
      renderItem={({ item }) => (
        <Button
          title={`${item.title} - ${item.displayPrice}`}
          disabled={!connected}
          onPress={() => {
            void requestPurchase({
              request: {
                apple: { sku: item.id },
                google: { skus: [item.id] },
              },
              type: 'in-app',
            }).catch((error) =>
              console.warn('Purchase request failed:', error),
            );
          }}
        />
      )}
    />
  );
}

Each call here has a full reference — see fetchProducts, requestPurchase, and finishTransaction for parameters and per-store behavior, and ErrorCode for the full error reference.

The useIAP hook API is identical to react-native-iap: methods return Promise<void> and update internal state — use onPurchaseSuccess for purchase results.

Differences from react-native-iap#

expo-iap and react-native-iap share the same OpenIAP API; they differ only in tooling:

  • Uses npx expo install instead of npm install
  • Supports Expo managed workflow (no manual native code needed)
  • Built on the Expo Modules architecture instead of Nitro Modules, the C++/JSI binding layer react-native-iap uses

Error Handling#

Errors are automatically normalized to the ErrorCode enum. Use the provided helper functions:

ts
import {
  ErrorCode,
  isUserCancelledError,
  getUserFriendlyErrorMessage,
} from 'expo-iap';

// In useIAP onPurchaseError callback:
if (isUserCancelledError(error)) return;

const message = getUserFriendlyErrorMessage(error);
Alert.alert('Error', message);

// Or use switch for specific handling:
switch (error.code) {
  case ErrorCode.NetworkError:
    showRetryDialog();
    break;
  case ErrorCode.ItemUnavailable:
    showUnavailableMessage();
    break;
}

tvOS Support#

expo-iap supports Apple TV (tvOS) through react-native-tvos. Requires tvOS 16.0+.

Configuration#

Replace react-native with react-native-tvos in your package.json:

json
{
  "dependencies": {
    "react-native": "npm:react-native-tvos@0.86.2-0",
    "@react-native-tvos/config-tv": "^0.1.6",
    "expo-iap": "^5.6.3"
  }
}

Then configure your app.config.ts conditionally using the EXPO_TV environment variable:

ts
import type { ConfigContext, ExpoConfig } from '@expo/config';

export default ({ config }: ConfigContext): ExpoConfig => {
  const isTV = process.env.EXPO_TV === '1';

  return {
    ...config,
    name: 'my-app',
    slug: 'my-app',
    plugins: [
      ...(isTV
        ? [['@react-native-tvos/config-tv', { isTV: true }] as [string, any]]
        : []),
      ['expo-iap', {}],
      [
        'expo-build-properties',
        {
          ios: {
            deploymentTarget: isTV ? '16.0' : '16.4',
          },
        },
      ],
    ],
  };
};

Build for tvOS:

sh
# Prebuild for tvOS
EXPO_TV=1 npx expo prebuild --platform ios --clean

# Run on simulator
EXPO_TV=1 npx expo run:ios --device "Apple TV 4K (3rd generation)"

Legacy Expo SDKs#

Do not force an older Google Play Billing dependency with a config plugin. The OpenIAP Android artifact, its Kotlin metadata, and the Billing dependency are released and tested together; replacing only Billing does not make an older Expo toolchain compatible.

Troubleshooting#

Products not found#

  • Ensure all agreements are signed in App Store Connect / Google Play Console
  • Verify banking, legal, and tax information is complete and approved
  • Check that bundle ID / package name matches exactly
  • Products must be in "Ready to Submit" status (Apple) or "Active" (Google)
  • Wait 15-30 minutes after creating products before testing

Build issues#

  • Clear and reinstall: rm -rf node_modules && npm install
  • For iOS, clean pods: cd ios && rm -rf Pods Podfile.lock && pod install
  • For Expo projects: npx expo prebuild --clean
  • Reset Metro cache: npx react-native start --reset-cache

Next Steps#