Skip to content

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" --validation

One-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 --sandbox

What 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 UI

Purchase 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

  1. Create IAP products in App Store Connect
  2. Add products to your app
  3. 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

  1. Upload signed APK
  2. Create in-app products
  3. 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

  1. Always Validate: Never trust client-side validation alone
  2. Handle Edge Cases: Network failures, cancelled purchases
  3. Clear Pricing: Show local currency and billing periods
  4. Restore Function: Always provide restore functionality
  5. Test Thoroughly: Test all scenarios including refunds

Built with ❤️ for the AI Coding community, by Praney Behl