Implementing In-App Purchases
1. Setting Up In-App Purchase Libraries
Setting up in-app purchase libraries in React Native involves integrating platform-specific solutions for iOS (StoreKit) and Android (Google Play Billing). The most popular cross-platform solution is react-native-iap, which provides a unified API for both platforms.
| Library | Platform Support | Installation Command | Notes |
|---|---|---|---|
| react-native-iap | Android, iOS | npm install react-native-iap |
Officially supported by the React Native community; requires native setup for iOS and Android |
| react-native-store | Android, iOS | npm install react-native-store |
Lightweight alternative, less actively maintained |
Note: For iOS, you must also configure App Store Connect and set up the in-app purchase products in the Apple Developer portal before using this library.
2. Configuring Products
| Property | Description | Values |
|---|---|---|
| productId | Unique identifier for the product | string |
| type | Product type (consumable, non-consumable, subscription) | consumable, non-consumable, subscription |
| title | Product title displayed to the user | string |
| description | Product description displayed to the user | string |
| price | Price of the product in local currency | string |
| currency | ISO currency code (e.g., USD, EUR) | string |
Example: Product Configuration
const products = [
{
productId: 'com.example.product1',
type: 'consumable',
title: 'Extra Life',
description: 'Restore one life in the game',
price: '$1.99',
currency: 'USD'
},
{
productId: 'com.example.subscription',
type: 'subscription',
title: 'Premium Membership',
description: 'Unlimited access to all features',
price: '$9.99',
currency: 'USD'
}
];
Note: Product IDs must be unique and match exactly what is configured in the app store (Apple App Store or Google Play Console).
3. Fetching Available Products
Example: Fetching Product Details
import { getProducts } from 'react-native-iap';
const fetchProducts = async () => {
try {
const products = await getProducts(['product_1', 'product_2']);
console.log('Fetched products:', products);
} catch (error) {
console.error('Error fetching products:', error);
}
};
fetchProducts();
| Property | Description | Notes |
|---|---|---|
| productIdentifier | Unique identifier for the product | Must match store listing |
| title | Product title as shown in store | Localized string |
| description | Product description in store | Localized string |
| price | Price in local currency | Formatted string |
| currency | ISO currency code | E.g., USD, EUR |
| type | Product type (consumable, non-consumable) | See product types table |
Note: Ensure all product identifiers are registered in the app store before fetching.
Warning: Do not hardcode product IDs; fetch them dynamically to support localization and updates.
Product Types Overview
- Consumable: Can be purchased multiple times (e.g., in-game currency)
- Non-Consumable: Purchased once, persists across app reinstalls (e.g., premium features)
- Subscription: Recurring payment for access (e.g., monthly magazine)
4. Initiating Purchase Flow
| Method | Description | Parameters | Notes |
|---|---|---|---|
| purchaseItem() | Initiates the purchase flow for a specific item | productId, quantity |
Must be called after getProducts() to ensure item is valid |
| purchaseItemWithQuantity() | Initiates the purchase flow with a specified quantity | productId, quantity |
Supports in-app items with quantity selection |
Example: Initiating Purchase Flow
import { purchaseItem } from 'react-native-in-app-purchase';
const handlePurchase = async () => {
try {
const result = await purchaseItem('com.example.product1', 1);
console.log('Purchase successful:', result);
} catch (error) {
console.error('Purchase failed:', error);
}
};
Note: Ensure the user has sufficient funds and the item is available before initiating purchase.
5. Handling Purchase Verification
| Property | Description | Value |
|---|---|---|
| transactionId | Unique identifier for the transaction | string |
| productId | Identifier of the purchased product | string |
| transactionDate | Date and time of the transaction | string |
| transactionReceipt | Receipt data for verification | string |
Example: Verify Purchase
import { purchaseUpdated, purchaseError } from 'react-native-iap';
const verifyPurchase = async (transaction) => {
const receipt = transaction.transactionReceipt;
const response = await fetch('https://your-server.com/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ receipt })
});
const verified = await response.json();
return verified;
};
purchaseUpdated(async (transaction) => {
const isVerified = await verifyPurchase(transaction);
if (isVerified.valid) {
// Grant product access
} else {
// Handle invalid purchase
}
});
Note: Always verify purchases on your server to prevent fraud.
Warning: Do not rely solely on client-side verification.
6. 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 |
|---|---|---|
| restorePurchases | Method to restore previously purchased items | Only available on iOS; Android returns empty list |
| productIdentifier | Unique identifier for the product to restore | Required for restoring specific purchases |
Example: Restore Purchases
import { restorePurchases } from 'react-native-iap';
const handleRestore = async () => {
try {
const purchases = await restorePurchases();
console.log('Restored purchases:', purchases);
} catch (error) {
console.error('Restore failed:', error);
}
};
Note: On Android,
restorePurchases only works for consumable items and returns a list of purchases that were made in the last 30 days.
Warning: On iOS, restoring purchases does not automatically re-deliver content. You must implement logic to verify purchase receipts and restore user entitlements manually.
7. Implementing Subscriptions
| Property | Description | Notes |
|---|---|---|
| productType | subscription |
Indicates the product is a subscription |
| subscriptionPeriod | day, week, month, year |
Duration of the subscription period |
| gracePeriodDays | 7 |
Number of days a user can still access content after subscription expires |
Example: Subscription Product Configuration
const subscriptionProduct = {
id: "premium_monthly",
type: "subscription",
period: "month",
price: "$9.99",
currency: "USD",
description: "Monthly premium access"
};
Example: Purchase Subscription
import { purchase } from 'react-native-iap';
const handlePurchase = async () => {
try {
const purchaseResult = await purchase({
productType: "subscription",
productId: "premium_monthly"
});
console.log("Purchase successful:", purchaseResult);
} catch (error) {
console.error("Purchase failed:", error);
}
};
Note: Subscriptions must be managed through the respective app store (Apple App Store or Google Play Store) and require proper configuration in the developer console.
8. Handling Subscription Status
| Property | Description | Values |
|---|---|---|
| status | Current status of the subscription | active, expired, grace_period, inactive |
| renewalDate | Date when the subscription will renew | ISO 8601 date string |
| isInGracePeriod | Indicates if the subscription is in a grace period | true, false |
Example: Check Subscription Status
import { purchaseManager } from 'react-native-iap';
const checkSubscriptionStatus = async () => {
try {
const purchases = await purchaseManager.getAvailablePurchases();
const subscription = purchases.find(purchase => purchase.productId.includes('premium'));
if (subscription) {
console.log('Subscription Status:', subscription.status);
console.log('Renewal Date:', subscription.renewalDate);
console.log('Grace Period:', subscription.isInGracePeriod);
}
} catch (error) {
console.error('Error checking subscription status:', error);
}
};
Note: The subscription status can change based on user actions or external events like payment failures.
9. Testing In-App Purchases
Example: Setting up Purchase Testing
import { Platform } from 'react-native';
import { RNIap, purchaseUpdatedListener, purchaseErrorListener } from 'react-native-iap';
const itemSkus = Platform.select({
ios: ['com.example.product1'],
android: ['com.example.product1'],
});
const initIAP = async () => {
try {
await RNIap.initConnection();
const products = await RNIap.getProducts(itemSkus);
console.log('Products:', products);
} catch (error) {
console.error('IAP init error:', error);
}
};
initIAP();
| Platform | Testing Method | Notes |
|---|---|---|
| iOS | StoreKit Test Configuration |
Use Xcode's StoreKit configuration to simulate purchases |
| Android | Google Play Console Internal Testing |
Use internal test track to test purchases before release |
Note: Ensure all test products are configured in the respective app stores before testing.
Testing Workflow
- Set up test user accounts in the app store (e.g., Google Play Console or Apple Developer)
- Configure test products in the store dashboard
- Use
RNIap.initConnection()to connect to the store - Retrieve product information using
RNIap.getProducts() - Simulate purchase flow with test users
Testing vs Production
- Testing: Use sandbox environments and test users
- Production: Real payments, live user accounts
10. Managing Purchase Errors
| Error Code | Description | Handling Recommendation |
|---|---|---|
| 1 | PURCHASING_ERROR |
General purchasing error. Log the error and notify user to try again. |
| 2 | NETWORK_ERROR |
Network connectivity issue. Prompt user to check internet connection. |
| 3 | INVALID_PRODUCT_ERROR |
Product ID not found. Validate product IDs in app and backend. |
| 4 | USER_CANCELLED_ERROR |
User canceled the purchase. No action required, but can log for analytics. |
| 5 | USER_NOT_ALLOWED_ERROR |
Parental controls or user restrictions. Inform user about settings. |
| 6 | STORE_ERROR |
Store-specific error. Retry purchase or show store status page. |
Example: Error Handling in Purchase Flow
import { purchase } from 'react-native-in-app purchases';
const handlePurchase = async (productId) => {
try {
const result = await purchase(productId);
if (result && result.transactionId) {
// Successful purchase logic
}
} catch (error) {
switch (error.code) {
case 1:
console.error('General purchasing error:', error.message);
// Notify user to try again
break;
case 2:
console.error('Network error:', error.message);
// Prompt user to check internet
break;
case 3:
console.error('Invalid product:', error.message);
// Validate and log product ID
break;
case 4:
console.log('User cancelled purchase');
// Log for analytics
break;
case 5:
console.error('User not allowed:', error.message);
// Inform about parental controls
break;
case 6:
console.error('Store error:', error.message);
// Show store status or retry
break;
default:
console.error('Unexpected error:', error);
}
}
};
Note: Always implement retry logic for
NETWORK_ERROR and STORE_ERROR to improve user experience.