add-in-app-purchase
Implement in-app purchases for iOS App Store and Google Play Store.
Overview
The add-in-app-purchase command sets up in-app purchases (IAP) for your React Native app, supporting subscriptions, one-time purchases, and consumables on both iOS and Android platforms.
Usage
bash
/template add-in-app-purchase <purchase-type> [options]Parameters
<purchase-type>- Type of purchase:subscription,one-time,consumable
Options
--products- Comma-separated list of product IDs--restore- Enable purchase restoration--validation- Server-side receipt validation--sandbox- Enable sandbox testing
Examples
Subscription Setup
bash
/template add-in-app-purchase subscription --products "monthly,yearly" --validationOne-time Purchase
bash
/template add-in-app-purchase one-time --products "remove_ads,unlock_premium"Full IAP System
bash
/template add-in-app-purchase subscription --restore --validation --sandboxWhat It Creates
IAP Structure
src/
├── services/
│ ├── iap/
│ │ ├── config.ts # Product configuration
│ │ ├── PurchaseManager.ts # Main purchase logic
│ │ ├── validation.ts # Receipt validation
│ │ ├── products.ts # Product definitions
│ │ ├── restoration.ts # Restore purchases
│ │ └── types.ts # TypeScript types
│ └── IAPService.ts # Service facade
├── screens/
│ └── StoreScreen.tsx # Purchase UIPurchase Manager
typescript
// src/services/iap/PurchaseManager.ts
import {
Purchase,
PurchaseError,
purchaseErrorListener,
purchaseUpdatedListener,
finishTransaction,
getProducts,
requestPurchase,
validateReceiptIos,
acknowledgePurchaseAndroid,
} from 'react-native-iap';
export class PurchaseManager {
private purchaseUpdateSubscription: EmitterSubscription | null = null;
private purchaseErrorSubscription: EmitterSubscription | null = null;
async initialize() {
try {
await initConnection();
// Set up listeners
this.purchaseUpdateSubscription = purchaseUpdatedListener(
this.handlePurchaseUpdate.bind(this)
);
this.purchaseErrorSubscription = purchaseErrorListener(
this.handlePurchaseError.bind(this)
);
// Load products
await this.loadProducts();
// Check pending purchases
await this.checkPendingPurchases();
} catch (error) {
console.error('IAP initialization failed:', error);
}
}
async loadProducts() {
const products = await getProducts({
skus: Platform.select({
ios: IOS_PRODUCT_IDS,
android: ANDROID_PRODUCT_IDS,
}),
});
store.setProducts(products);
return products;
}
async purchaseProduct(productId: string) {
try {
const purchase = await requestPurchase({
sku: productId,
andDangerouslyFinishTransactionAutomaticallyIOS: false,
});
return purchase;
} catch (error) {
throw new PurchaseError(error);
}
}
private async handlePurchaseUpdate(purchase: Purchase) {
const receipt = purchase.transactionReceipt;
if (receipt) {
// Validate receipt
const isValid = await this.validateReceipt(receipt);
if (isValid) {
// Grant access
await this.grantAccess(purchase.productId);
// Finish transaction
await finishTransaction({
purchase,
isConsumable: this.isConsumable(purchase.productId),
});
// Track analytics
this.trackPurchase(purchase);
}
}
}
}Product Configuration
typescript
// src/services/iap/products.ts
export const PRODUCTS = {
// Subscriptions
monthly_subscription: {
id: Platform.select({
ios: 'com.myapp.monthly',
android: 'monthly_subscription',
}),
type: 'subscription',
price: '$9.99',
duration: 'month',
features: ['unlimited_access', 'no_ads', 'premium_content'],
},
yearly_subscription: {
id: Platform.select({
ios: 'com.myapp.yearly',
android: 'yearly_subscription',
}),
type: 'subscription',
price: '$99.99',
duration: 'year',
features: ['everything_in_monthly', 'exclusive_content', 'priority_support'],
savings: '17%',
},
// One-time purchases
remove_ads: {
id: Platform.select({
ios: 'com.myapp.remove_ads',
android: 'remove_ads',
}),
type: 'one_time',
price: '$4.99',
features: ['no_ads_forever'],
},
// Consumables
coins_100: {
id: Platform.select({
ios: 'com.myapp.coins_100',
android: 'coins_100',
}),
type: 'consumable',
price: '$0.99',
amount: 100,
},
};Receipt Validation
typescript
// src/services/iap/validation.ts
export async function validateReceipt(
receipt: string,
platform: 'ios' | 'android'
): Promise<boolean> {
try {
const response = await fetch(`${API_URL}/validate-receipt`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${getAuthToken()}`,
},
body: JSON.stringify({
receipt,
platform,
sandbox: __DEV__,
}),
});
const result = await response.json();
if (platform === 'ios') {
return result.status === 0;
} else {
return result.purchaseState === 0;
}
} catch (error) {
console.error('Receipt validation failed:', error);
return false;
}
}
// Server-side validation (Node.js example)
export async function validateAppleReceipt(receipt: string, sandbox: boolean) {
const url = sandbox
? 'https://sandbox.itunes.apple.com/verifyReceipt'
: 'https://buy.itunes.apple.com/verifyReceipt';
const response = await fetch(url, {
method: 'POST',
body: JSON.stringify({
'receipt-data': receipt,
password: process.env.APPLE_SHARED_SECRET,
}),
});
return response.json();
}Store Screen UI
typescript
// src/screens/StoreScreen.tsx
export function StoreScreen() {
const { products, purchaseProduct, isLoading } = useIAP();
const [selectedProduct, setSelectedProduct] = useState(null);
const handlePurchase = async (productId: string) => {
try {
setSelectedProduct(productId);
await purchaseProduct(productId);
showToast('Purchase successful!');
navigation.goBack();
} catch (error) {
showAlert('Purchase failed', error.message);
} finally {
setSelectedProduct(null);
}
};
return (
<ScrollView>
<Text style={styles.title}>Choose Your Plan</Text>
{/* Subscriptions */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Subscriptions</Text>
{products.subscriptions.map((product) => (
<ProductCard
key={product.productId}
product={product}
onPress={() => handlePurchase(product.productId)}
loading={selectedProduct === product.productId}
recommended={product.productId === 'yearly_subscription'}
/>
))}
</View>
{/* One-time purchases */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>One-time Purchases</Text>
{products.oneTime.map((product) => (
<ProductCard
key={product.productId}
product={product}
onPress={() => handlePurchase(product.productId)}
loading={selectedProduct === product.productId}
/>
))}
</View>
<RestorePurchasesButton />
<TermsAndPrivacy />
</ScrollView>
);
}Platform Setup
iOS Configuration
App Store Connect
- Create IAP products in App Store Connect
- Add products to your app
- Submit for review with app
Capabilities
xml
<!-- ios/MyApp.xcodeproj -->
<!-- Enable In-App Purchase capability in Xcode -->StoreKit Configuration
swift
// ios/StoreKitConfig.storekit
{
"products": [
{
"id": "com.myapp.monthly",
"type": "subscription",
"referenceName": "Monthly Subscription",
"price": 9.99
}
]
}Android Configuration
Google Play Console
- Upload signed APK
- Create in-app products
- Add testers for sandbox testing
Billing Permission
xml
<!-- android/app/src/main/AndroidManifest.xml -->
<uses-permission android:name="com.android.vending.BILLING" />Restore Purchases
Implementation
typescript
// src/services/iap/restoration.ts
export async function restorePurchases(): Promise<Purchase[]> {
try {
const purchases = await getAvailablePurchases();
for (const purchase of purchases) {
// Validate each purchase
const isValid = await validateReceipt(
purchase.transactionReceipt,
Platform.OS
);
if (isValid) {
// Restore access
await grantAccess(purchase.productId);
}
}
return purchases;
} catch (error) {
console.error('Restore failed:', error);
throw error;
}
}
// UI Component
export function RestorePurchasesButton() {
const [isRestoring, setIsRestoring] = useState(false);
const handleRestore = async () => {
setIsRestoring(true);
try {
const purchases = await restorePurchases();
if (purchases.length > 0) {
showAlert('Success', `Restored ${purchases.length} purchases`);
} else {
showAlert('No Purchases', 'No previous purchases found');
}
} catch (error) {
showAlert('Error', 'Failed to restore purchases');
} finally {
setIsRestoring(false);
}
};
return (
<TouchableOpacity onPress={handleRestore} disabled={isRestoring}>
<Text>Restore Purchases</Text>
</TouchableOpacity>
);
}Subscription Management
Check Subscription Status
typescript
export async function checkSubscriptionStatus(): Promise<SubscriptionStatus> {
const purchases = await getAvailablePurchases();
const activeSubscriptions = purchases.filter((purchase) => {
// Check if subscription is still valid
if (Platform.OS === 'ios') {
return purchase.transactionDate &&
new Date(purchase.transactionDate) > new Date();
} else {
return purchase.purchaseStateAndroid === 1;
}
});
return {
isActive: activeSubscriptions.length > 0,
subscriptions: activeSubscriptions,
expiryDate: getLatestExpiryDate(activeSubscriptions),
};
}Handle Subscription Changes
typescript
// Upgrade/downgrade subscriptions
export async function changeSubscription(
currentProductId: string,
newProductId: string
) {
if (Platform.OS === 'android') {
// Android handles upgrades/downgrades
await requestPurchase({
sku: newProductId,
purchaseTokenAndroid: getCurrentPurchaseToken(),
prorationModeAndroid: PRORATION_MODE.IMMEDIATE_WITH_TIME_PRORATION,
});
} else {
// iOS handles automatically
await requestPurchase({ sku: newProductId });
}
}Testing
Sandbox Testing
typescript
// Enable sandbox mode for testing
export function configureSandbox() {
if (__DEV__) {
// iOS: Use sandbox tester accounts
// Android: Use test cards
console.log('IAP: Sandbox mode enabled');
}
}
// Mock purchases for development
export const mockPurchase = async (productId: string) => {
console.log(`Mock purchase: ${productId}`);
await grantAccess(productId);
return createMockPurchase(productId);
};Unit Tests
typescript
describe('PurchaseManager', () => {
it('validates receipts correctly', async () => {
const mockReceipt = 'mock-receipt-data';
fetchMock.mockResponseOnce(JSON.stringify({ status: 0 }));
const isValid = await validateReceipt(mockReceipt, 'ios');
expect(isValid).toBe(true);
});
it('handles purchase errors gracefully', async () => {
const error = new Error('Purchase cancelled');
const handled = await handlePurchaseError(error);
expect(handled).toBe(true);
});
});Analytics
Track Purchase Events
typescript
export function trackPurchase(purchase: Purchase) {
analytics.track('purchase_completed', {
productId: purchase.productId,
price: purchase.localizedPrice,
currency: purchase.currency,
platform: Platform.OS,
transactionId: purchase.transactionId,
});
// Track revenue
analytics.revenue({
amount: parseFloat(purchase.price),
currency: purchase.currency,
productId: purchase.productId,
});
}
export function trackPurchaseError(error: PurchaseError) {
analytics.track('purchase_failed', {
error: error.message,
code: error.code,
productId: error.productId,
});
}Best Practices
- Always Validate: Never trust client-side validation alone
- Handle Edge Cases: Network failures, cancelled purchases
- Clear Pricing: Show local currency and billing periods
- Restore Function: Always provide restore functionality
- Test Thoroughly: Test all scenarios including refunds
Related Commands
add-screen- Create store screenssetup-shared-backend- Server-side validationsetup-crash-reporting- Track IAP errors
