δΈ­

Apple In-App Purchase Setup

This guide covers setting up Apple In-App Purchases (IAP) using StoreKit for iOS and macOS applications.

Prerequisites

  • Apple Developer account ($99/year)
  • App registered in App Store Connect
  • Access to App Store Connect
  • Paid Applications agreement completed

Step 1: Complete Agreements

  1. Go to App Store Connect
  2. Click Agreements, Tax, and Banking
  3. Complete the Paid Applications agreement
  4. Add bank account and tax information

Important: You cannot test purchases until agreements are completed.

Step 2: Create In-App Purchases

  1. Go to your app in App Store Connect
  2. Navigate to Features > In-App Purchases
  3. Click + to add a new product

Product Types

Type Description Use Case
Consumable Can be purchased multiple times Virtual currency, lives
Non-Consumable One-time purchase Premium unlock, remove ads
Auto-Renewable Subscription Recurring billing Monthly/yearly membership
Non-Renewing Subscription Fixed-period access Season pass

Create a Subscription

  1. Select Auto-Renewable Subscription
  2. Create a Subscription Group:
  • Group name: "Pro Plans"
  • Reference name: Internal identifier
  1. Add subscription details:
Field Description Example
Reference Name Internal name Pro Monthly
Product ID Unique identifier com.app.pro.monthly
Subscription Duration Billing period 1 Month
Price Subscription price $9.99
  1. Add localization:
  • Display Name
  • Description
  1. Click Save

Configure Subscription Features

  1. Free Trial: Optional free trial period
  2. Introductory Offer: Discounted first period
  3. Promotional Offer: Special pricing for existing users
  4. Offer Codes: Redeemable codes for free access

Step 3: Configure Server Notifications

App Store Server Notifications V2:

  1. In App Store Connect, go to App Information
  2. Find App Store Server Notifications
  3. Enter your server URL:
https://yourdomain.com/v1/payment/webhook/apple
  1. Select Version 2 (recommended)
  2. Save changes

Notification Types

Type Description
SUBSCRIBED New subscription
DID_RENEW Subscription renewed
DIDCHANGERENEWAL_PREF Plan change scheduled
EXPIRED Subscription expired
DIDFAILTO_RENEW Payment failed
REFUND Purchase refunded

Step 4: Set Up Shared Secret

For receipt validation:

  1. Go to your app in App Store Connect
  2. Navigate to App Information
  3. Find App-Specific Shared Secret
  4. Click Manage and generate a secret
  5. Copy and save the shared secret securely

Step 5: Configure in OpenDev

  1. Log in to OpenDev Platform
  2. Go to your application's Payment Configuration
  3. Add Apple configuration:
{
  "platform": "apple",
  "enabled": true,
  "config": {
    "bundleId": "com.yourcompany.myapp",
    "sharedSecret": "your_shared_secret",
    "environment": "production",
    "notificationUrl": "https://yourdomain.com/v1/payment/webhook/apple"
  }
}

Configuration Fields

Field Required Description
Bundle ID Yes Your app's bundle identifier
Shared Secret Yes App-specific shared secret
Environment Yes sandbox or production
Notification URL Yes Server notification endpoint

Step 6: Configure Product Tiers

Link Apple Product IDs:

  1. Go to Product Tiers in OpenDev
  2. For each tier, add Apple product ID:
{
  "productId": "pro_monthly",
  "name": "Pro Monthly",
  "platformProductIds": {
    "apple": "com.yourcompany.myapp.pro.monthly"
  }
}

Step 7: Implement StoreKit 2

Add StoreKit Capability

  1. Open Xcode project
  2. Select target > Signing & Capabilities
  3. Click + Capability
  4. Add In-App Purchase

Fetch Products

import StoreKit

class StoreManager: ObservableObject {
    @Published var products: [Product] = []
    
    func fetchProducts() async {
        do {
            let productIds = ["com.app.pro.monthly", "com.app.pro.yearly"]
            products = try await Product.products(for: productIds)
        } catch {
            print("Failed to fetch products: \(error)")
        }
    }
}

Purchase Product

func purchase(_ product: Product) async throws -> Transaction? {
    let result = try await product.purchase()
    
    switch result {
    case .success(let verification):
        let transaction = try checkVerified(verification)
        
        // Send to server for verification
        await sendToServer(transaction: transaction)
        
        // Finish the transaction
        await transaction.finish()
        
        return transaction
        
    case .pending:
        // Transaction pending (e.g., Ask to Buy)
        return nil
        
    case .userCancelled:
        return nil
        
    @unknown default:
        return nil
    }
}

func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
    switch result {
    case .unverified:
        throw StoreError.failedVerification
    case .verified(let signedType):
        return signedType
    }
}

Handle Transactions

func listenForTransactions() -> Task<Void, Error> {
    return Task.detached {
        for await result in Transaction.updates {
            do {
                let transaction = try self.checkVerified(result)
                
                // Update entitlements
                await self.updateEntitlements(transaction)
                
                await transaction.finish()
            } catch {
                print("Transaction verification failed")
            }
        }
    }
}

Restore Purchases

func restorePurchases() async {
    for await result in Transaction.currentEntitlements {
        if case .verified(let transaction) = result {
            // Restore entitlement
            await updateEntitlements(transaction)
        }
    }
}

Step 8: Server-Side Verification

Verify Receipt (Legacy)

const axios = require('axios');

async function verifyReceipt(receiptData, isProduction = true) {
    const url = isProduction
        ? 'https://buy.itunes.apple.com/verifyReceipt'
        : 'https://sandbox.itunes.apple.com/verifyReceipt';
    
    const response = await axios.post(url, {
        'receipt-data': receiptData,
        'password': process.env.APPLE_SHARED_SECRET,
    });
    
    return response.data;
}
const { SignedDataVerifier } = require('@apple/app-store-server-library');

async function verifyTransaction(signedTransaction) {
    const verifier = new SignedDataVerifier(
        appleRootCAs,
        true, // Enable online checks
        Environment.PRODUCTION,
        bundleId
    );
    
    const transaction = await verifier.verifyAndDecodeTransaction(signedTransaction);
    return transaction;
}

Step 9: Handle Notifications

const { SignedDataVerifier } = require('@apple/app-store-server-library');

app.post('/webhook/apple', async (req, res) => {
    try {
        const verifier = new SignedDataVerifier(/*...*/);
        
        const notification = await verifier.verifyAndDecodeNotification(
            req.body.signedPayload
        );
        
        switch (notification.notificationType) {
            case 'SUBSCRIBED':
                await handleNewSubscription(notification);
                break;
            case 'DID_RENEW':
                await handleRenewal(notification);
                break;
            case 'EXPIRED':
            case 'DID_FAIL_TO_RENEW':
                await handleExpiration(notification);
                break;
            case 'REFUND':
                await handleRefund(notification);
                break;
        }
        
        res.status(200).send();
    } catch (error) {
        res.status(400).send();
    }
});

Step 10: Test the Integration

Sandbox Testing

  1. Create Sandbox tester in App Store Connect:
  • Go to Users and Access > Sandbox > Testers
  • Add test accounts
  1. On device:
  • Sign out of regular App Store account
  • Use Sandbox account for testing
  • Purchases are free in sandbox

Test Scenarios

Scenario How to Test
New purchase Buy with sandbox account
Subscription renewal Sandbox renews faster (see below)
Failed payment Interrupt transaction
Restore purchases Delete and reinstall app

Sandbox Time Acceleration

Real Duration Sandbox Duration
1 week 3 minutes
1 month 5 minutes
2 months 10 minutes
1 year 1 hour

Troubleshooting

Products Not Loading

Solutions:

  • Ensure Paid Applications agreement is complete
  • Product must be in "Ready to Submit" state
  • Bundle ID must match exactly
  • Wait 15-30 minutes after creating products

Purchase Not Completing

Solutions:

  • Check sandbox account is valid
  • Verify In-App Purchase capability is enabled
  • Check for parental controls or restrictions

Server Notifications Not Received

Solutions:

  • Verify URL is HTTPS and publicly accessible
  • Check URL returns 200 status
  • Verify using correct notification version

Security Best Practices

  1. Verify on Server - Never trust client-side verification alone
  2. Use Server Notifications - For reliable subscription status
  3. Store Transaction IDs - Prevent duplicate processing
  4. Handle Grace Period - Don't immediately revoke access
  5. Secure Shared Secret - Never expose in client code