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
renewalStateto determine subscription status - Use
autoRenewto 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 Gofor 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.