Skip to main content

Implementing In-App Purchases

1. Installing IAP Library

Step Description Command
1 Install the Expo In-App Purchases package npx expo install @expo/apple-app-store
2 Install the Google Play Billing library (for Android) npx expo install @expo/google-play-billing
Note: Ensure that you have configured your app in the Apple App Store Connect and Google Play Console before proceeding with implementation.

2. Connecting to Store

Property Description Values
store Defines the store to connect to apple, google
productId Unique identifier for the product in the store com.example.product1
purchaseType Type of purchase to initiate consumable, non-consumable

Example: Connecting to Apple Store

import { connectToStore } from 'expo-in-app-purchases';

const storeConnection = await connectToStore({
  store: 'apple',
  productId: 'com.example.product1',
  purchaseType: 'consumable'
});
Note: Ensure the product ID matches exactly with the one registered in App Store Connect.

3. Getting Products

Property Description Example
productId Unique identifier for the product "com.example.product1"
title Title of the product as displayed to the user "Premium Feature"
description Longer description of the product "Unlock all premium features"
price Price in local currency "$4.99"
currency ISO currency code (e.g., USD, EUR) "USD"

Example: Fetching Product Info

import * as Store from 'expo-in-app-purchases';

const fetchProducts = async () => {
  try {
    const { products } = await Store.getProducts(['com.example.product1']);
    console.log(products[0].title); // "Premium Feature"
  } catch (error) {
    console.error('Failed to fetch products:', error);
  }
};
Note: Product IDs must be configured in the app store or Google Play Console before they can be retrieved.

4. Purchasing Products

Property Description Example
productIds Array of product identifiers to purchase ['com.example.product1', 'com.example.product2']
quantity Number of items to purchase (optional) 2

Example: Purchase a Product

import { purchaseProduct } from 'expo-in-app-purchases';

const productId = 'com.example.product1';
try {
  const purchase = await purchaseProduct(productId);
  console.log('Purchase successful:', purchase);
} catch (error) {
  console.error('Purchase failed:', error);
}
Note: Ensure the product ID matches exactly what is defined in the store.
Warning: Always handle purchase failures gracefully to maintain user trust.

5. Restoring Purchases

Restoring purchases allows users to recover previously bought in-app items, particularly useful for iOS devices where purchases are tied to the user's Apple ID.

Property Description Notes
restorePurchasesAsync() Restores purchases for the current user Only available on iOS and Android

Example: Restore Purchases

import { restorePurchasesAsync } from 'expo-in-app-purchases';

const restorePurchases = async () => {
  try {
    const { purchases, error } = await restorePurchasesAsync();
    if (error) {
      console.error('Restore failed:', error);
    } else {
      console.log('Restored purchases:', purchases);
    }
  } catch (err) {
    console.error('Unexpected error:', err);
  }
};
Note: On iOS, restoring purchases is only possible for purchases made with the same Apple ID. Users may need to sign in to their Apple ID on the device.

6. Handling Purchase Updates

Property Description Values
purchase Object containing purchase details { productId, transactionId, receipt }
status Current status of the purchase 'purchased', 'failed', 'deferred'

Example: Listen for purchase updates

import { usePurchaseUpdate } from 'expo-in-app-purchases';

const handlePurchaseUpdate = (purchase) => {
  if (purchase.status === 'purchased') {
    console.log('Purchase successful:', purchase.productId);
  } else if (purchase.status === 'failed') {
    console.log('Purchase failed:', purchase.productId);
  }
};

usePurchaseUpdate(handlePurchaseUpdate);
Note: Ensure that purchase updates are handled securely to prevent fraud.

7. Validating Receipts

Validating receipts ensures that in-app purchases are legitimate and have not been tampered with. This is crucial for preventing fraud and maintaining the integrity of your app's monetization system.

Receipt Field Description Validation Status
receipt The receipt data provided by the platform (iOS or Android) Required
signature Signature for verifying the receipt (iOS only) Required for iOS
transactionId Unique identifier for the transaction Required
productId The identifier of the purchased product Required
purchaseDate Date and time of the purchase Required

Example: Validating Receipt on iOS

import { validateReceipt } from 'expo-in-app-purchases';

const receiptData = '<receipt_data>';
const signature = '<signature>';

try {
  const isValid = await validateReceipt(receiptData, signature);
  if (isValid) {
    console.log('Receipt is valid');
  } else {
    console.log('Receipt is invalid');
  }
} catch (error) {
  console.error('Validation error:', error);
}
Note: For iOS, the receipt must be validated against Apple's servers to ensure it has not been tampered with.
Warning: Never rely solely on client-side validation. Always validate receipts on your server for security.
Validation Method Platform Description Status
Apple App Store iOS Receipts must be validated via Apple's server NEW
Google Play Console Android Receipts must be validated via Google's server NEW

8. Managing Subscriptions

Property Description Values
productId Unique identifier for the subscription product "com.example.subscription.monthly"
productType Type of in-app product "subscription"
renewalState Current renewal status of the subscription "active", "expired", "grace_period"
autoRenew Whether the subscription auto-renews true, false

Example: Check Subscription Status

import * as Store from 'expo-in-app-purchases';

const checkSubscriptionStatus = async () => {
  const purchases = await Store.getAvailablePurchases();
  const subscription = purchases.find(purchase => purchase.productType === 'subscription');

  if (subscription) {
    console.log('Subscription ID:', subscription.productId);
    console.log('Renewal State:', subscription.renewalState);
    console.log('Auto Renew:', subscription.autoRenew);
  }
};
Note: Subscriptions must be managed through the platform's app store (Apple App Store or Google Play Store) for renewal and cancellation.
Warning: Ensure that subscription status is verified server-side to prevent fraud.

Subscription Management Workflow

  • Fetch available purchases using getAvailablePurchases()
  • Identify subscription products by productType
  • Check renewalState to determine subscription status
  • Use autoRenew to manage renewal preferences

9. Handling Purchase Errors

Error Code Description Handling Recommendation
E_UNKNOWN Unexpected error occurred during purchase flow Log error details and notify user to try again later
E_USER_CANCELLED User cancelled the purchase flow Inform user that purchase was cancelled and no charge occurred
E_NETWORK_ERROR Network connectivity issue during purchase Display retry option or offline message to user
E_PURCHASE_NOT_ALLOWED Device or account restrictions prevent purchase Guide user to check device settings or parental controls
E_INVALID_PRODUCT Product ID is invalid or not found in store Verify product configuration and retry purchase
E_RECEIPT_VALIDATION_FAILED Receipt could not be validated by server Retry validation or contact support for manual verification

Example: Error Handling in Purchase Flow

import { purchaseAsync } from 'expo-in-app-purchases';

async function handlePurchase(productId) {
  try {
    const result = await purchaseAsync(productId);
    if (result.transactionId) {
      console.log('Purchase successful');
    }
  } catch (error) {
    switch (error.code) {
      case 'E_USER_CANCELLED':
        alert('Purchase was cancelled');
        break;
      case 'E_NETWORK_ERROR':
        alert('Network error, please try again');
        break;
      default:
        console.error('Purchase failed:', error);
        alert('An unexpected error occurred');
    }
  }
}
Note: Always implement graceful error handling to maintain a smooth user experience even when purchases fail.

10. Testing Purchases

Example: Setting up purchase testing in Expo

import * as StoreReview from 'expo-store-review';
import { Platform } from 'react-native';

if (Platform.OS === 'ios') {
  StoreReview.requestReview();
}
Environment Testing Method Notes
Expo Development expo start Use Expo Go app for testing purchases
Production App Store / Google Play Real purchases must be made through official stores
Note: In-app purchases must be tested in a sandbox environment before going live.
Warning: Do not use real payment methods during testing.

Testing Purchase Flow

  • Use Expo Go for local testing
  • Simulate purchase flows using sandbox environments
  • Ensure all transaction states are handled properly

11. Finishing Transactions

Property Description Values
transactionId Unique identifier for the transaction string
productId Identifier of the purchased product string
receipt Receipt data from the store object
transactionDate Date and time of the transaction string

Example: Finish a Transaction

import { finishTransactionAsync } from 'expo-in-app-purchases';

const transactionId = 'txn_123456789';
await finishTransactionAsync(transactionId);
Note: Always call finishTransactionAsync after successfully delivering the purchased content to the user to prevent duplicate purchases.

12. Handling Pending Purchases

When a user initiates an in-app purchase but does not complete it immediately, the purchase is marked as "pending." Handling these pending purchases is crucial for maintaining a seamless user experience and ensuring transactions are processed correctly.

Property Description Notes
purchaseState Indicates the state of the purchase Can be PURCHASED, PENDING, or UNSPECIFIED_STATE
orderId Unique identifier for the purchase order Used to verify and manage purchases
receipt Receipt data for the purchase Contains transaction details and is used for verification

Example: Handling Pending Purchases

import { getPendingPurchasesAsync } from 'expo-in-app-purchases';

const handlePendingPurchases = async () => {
  try {
    const pendingPurchases = await getPendingPurchasesAsync();
    
    if (pendingPurchases.length > 0) {
      console.log('Pending purchases found:', pendingPurchases);
      // Process each pending purchase
      for (const purchase of pendingPurchases) {
        if (purchase.purchaseState === 'PENDING') {
          // Re-verify or finalize the purchase
          console.log('Processing pending purchase:', purchase.orderId);
        }
      }
    } else {
      console.log('No pending purchases');
    }
  } catch (error) {
    console.error('Error handling pending purchases:', error);
  }
};

handlePendingPurchases();
Note: It's important to handle pending purchases on app startup or when the user returns to the app to ensure that no transactions are missed or duplicated.

Was this article helpful?