Skip to main content

Working with In-App Purchases

1. Configuring Products in App Store Connect

Product Type Description Notes
Consumable Product that can be purchased multiple times Must be consumed after purchase to allow re-purchase
Non-Consumable Product that is purchased once and retained Can be used indefinitely, not tied to user account
Auto-Renewable Subscription Subscription that renews automatically Requires a subscription group and can be managed in App Store Connect
Non-Renewing Subscription Subscription that does not auto-renew Must be managed manually by the app developer

Example: Product ID Configuration

let productIdentifier = "com.example.myapp.premium"
Note: Product identifiers must be unique and follow reverse domain name notation.
Warning: Once a product is created in App Store Connect, it cannot be deleted or modified.
Field Value Description
Product ID com.example.myapp.premium Unique identifier for the product in App Store Connect
Reference Name Premium Access Internal name used in App Store Connect
Product Type Non-Consumable Type of in-app purchase (e.g., Consumable, Non-Consumable)
Price Tier $4.99 Price set in App Store Connect (can be localized)

2. Using StoreKit for Purchases

Example: Requesting Product Information

import StoreKit

class ProductManager: ObservableObject {
    @Published var products: [Product] = []
    
    func fetchProducts() {
        Task {
            do {
                let storeProducts = try await Product.fetch(
                    .subscription("premium_subscription")
                )
                self.products = storeProducts
            } catch {
                print("Failed to fetch products: \(error)")
            }
        }
    }
}
Property Description Notes
Product.id String Unique identifier for the product in App Store Connect
Product.name String Name of the product as displayed in App Store
Product.description String Description of the product as displayed in App Store
Product.price String Formatted price in the user's local currency
Product.priceLocale Locale Locale used to format the price

Example: Initiating a Purchase

import StoreKit

func purchase(_ product: Product) {
    Task {
        do {
            let result = try await product.purchase()
            switch result {
            case .success(let verification):
                // Handle successful purchase
                print("Purchase verified: \(verification)")
            case .userCancelled:
                print("User cancelled purchase")
            case .pending:
                print("Purchase is pending")
            @unknown default:
                print("Unknown purchase result")
            }
        } catch {
            print("Purchase failed: \(error)")
        }
    }
}
Note: Always verify purchase receipts on your server for security.

Example: Handling Purchase Verification

import StoreKit

func verifyPurchase(_ verification: VerificationResult<Transaction>) {
    guard case .success = verification else { return }
    
    // Send receipt to your server for validation
    Task {
        let receipt = await Transaction.currentReceipt
        // Upload receipt to your server for verification
    }
}

3. Fetching Product Information

Property Description Notes
productIdentifier Unique identifier for the product Must match the identifier in App Store Connect
localizedTitle Title of the product in the user's language Read-only property
localizedDescription Description of the product in the user's language Read-only property
price Price of the product in local currency Read-only property
currencyCode ISO 4217 currency code (e.g., USD, EUR) Read-only property

Example: Fetching Product Information

import StoreKit

class ProductManager: ObservableObject {
    @Published var products: [SKProduct] = []
    
    func fetchProducts() {
        let productIdentifiers = ["com.example.product1", "com.example.product2"]
        let set = Set(productIdentifiers)
        
        SKProductsRequest(productsRequest: set).start { [weak self] response in
            guard let products = response.products else { return }
            self?.products = products
        }
    }
}
Note: The SKProductsRequest must be called from the main thread, and it's recommended to handle errors gracefully in production apps.

4. Processing Transactions

Property Description Notes
transactionIdentifier Unique identifier for the transaction Used to identify and verify transactions
payment Payment object associated with the transaction Contains product identifier and quantity
transactionDate Date and time when the transaction occurred UTC date format
transactionState Current state of the transaction Can be .purchased, .failed, .restored, or .deferred

Example: Handling Transaction States

func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
    for transaction in transactions {
        switch transaction.transactionState {
        case .purchased:
            print("Purchase successful: \(transaction.payment.productIdentifier)")
            // Deliver content to user
        case .failed:
            print("Purchase failed: \(transaction.error?.localizedDescription ?? "")")
        case .restored:
            print("Purchase restored: \(transaction.payment.productIdentifier)")
        case .deferred:
            print("Purchase deferred")
        default:
            break
        }
    }
}
Note: Always verify transactions with Apple's servers before delivering content to ensure authenticity.
Warning: Never deliver content immediately after a purchase. Always validate the receipt on Apple's servers first.

5. Restoring Purchases

Restoring purchases allows users to recover previously bought in-app items on a new device or after reinstalling the app. This is essential for non-consumable and auto-renewable subscriptions.

Method Description Notes
restoreCompletedTransactions() Restores all completed transactions for the current user. Must be called to restore non-consumable purchases and subscriptions.
restoreCompletedTransactions(with:) Restores transactions with a specific receipt. Useful for restoring purchases from a specific device or session.

Example: Restoring Purchases

import StoreKit

class PurchaseManager: NSObject, SKPaymentTransactionObserver {
    func restorePurchases() {
        SKPaymentQueue.default().restoreCompletedTransactions()
    }

    func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
        for transaction in transactions {
            switch transaction.transactionState {
            case .restored:
                // Handle restored purchase
                print("Restored: \(transaction.payment.productIdentifier)")
            case .failed:
                // Handle failed restore
                print("Restore failed: \(transaction.error?.localizedDescription ?? "")")
            default:
                break
            }
        }
    }
}
Note: Users must be logged into their Apple ID to restore purchases. The system will automatically handle restoring all eligible purchases for the user's account.
Warning: Do not call restoreCompletedTransactions() in a loop or repeatedly. Apple's StoreKit may reject repeated restore requests.

Key Points for Restore Purchases

  • Only non-consumable and auto-renewable subscription purchases can be restored.
  • Consumable purchases cannot be restored and must be repurchased.
  • Restoring is handled through SKPaymentQueue.
  • Implement paymentQueue(_:updatedTransactions:) to handle restored transactions.

6. Implementing Subscription Management

Example: Requesting Subscription Products

import StoreKit

class SubscriptionManager: ObservableObject {
    @Published var products: [Product] = []
    
    func fetchProducts() async throws {
        let products = try await Product.fetchAll()
        self.products = products.filter { $0.type == .autoRenewable }
    }
}
Property Description Notes
productID String Unique identifier for the subscription product
displayName String Name of the subscription tier
description String Human-readable description of the subscription
price String Formatted price in user's local currency
type Product.Type Subscription type (e.g., .autoRenewable)

Example: Purchasing a Subscription

func purchaseSubscription(_ product: Product) async throws {
    let result = try await product.purchase()
    
    switch result {
    case .success(let transaction):
        // Handle successful purchase
        await transaction.finish()
    case .userCancelled:
        // Handle user cancellation
        break
    default:
        // Handle other cases
        break
    }
}
Note: Always call transaction.finish() to complete the transaction after handling it.

Example: Managing Auto-Renewal

func manageSubscription() {
    Task {
        do {
            let updates = try await StoreKit.Transaction.currentEntitlements
            for update in updates {
                if update.productType == .autoRenewable {
                    // Check renewal status
                    let renewalInfo = update.renewalInfo
                    if renewalInfo.isInGracePeriod {
                        // Handle grace period logic
                    }
                }
            }
        } catch {
            print("Failed to fetch entitlements: \(error)")
        }
    }
}

Subscription Management Workflow

  • Fetch available subscription products using Product.fetchAll()
  • Display product details to the user
  • Initiate purchase with product.purchase()
  • Handle transaction result and call transaction.finish()
  • Monitor entitlements for renewal status

7. Handling Receipt Validation

Example: Basic Receipt Validation

import StoreKit

class ReceiptValidator: ObservableObject {
    @Published var receiptStatus: String = "Not validated"
    
    func validateReceipt() {
        guard let appStoreReceiptURL = Bundle.main.appStoreReceiptURL else {
            receiptStatus = "No receipt found"
            return
        }
        
        do {
            let receiptData = try Data(contentsOf: appStoreReceiptURL)
            let receiptString = receiptData.base64EncodedString()
            
            // Send to your server for validation
            // Example API call:
            // sendToServer(receiptString)
            
            receiptStatus = "Receipt validated successfully"
        } catch {
            receiptStatus = "Failed to read receipt: \(error.localizedDescription)"
        }
    }
}
Property Description Notes
appStoreReceiptURL URL to the app's receipt file Available in iOS 2.0+
base64EncodedString() Encodes data to base64 string Used for sending receipt to server
Note: The receipt is automatically refreshed by the App Store when needed. You should validate receipts on your server to ensure they are legitimate.
Warning: Never validate receipts locally on the device. Always send to your server for validation to prevent tampering.

Receipt Validation Steps

  • Retrieve the receipt file from Bundle.main.appStoreReceiptURL
  • Read the receipt data as Data
  • Encode the data to base64 string
  • Send to your server for validation
  • Handle the response from your server

8. Using Server-Side Receipt Verification

Example: Verifying Receipt with Server

import Foundation

struct ReceiptVerificationRequest: Codable {
    let receiptData: String
    let password: String?
}

struct ReceiptVerificationResponse: Codable {
    let status: Int
    let receipt: Receipt?
    let latestReceipt: String?
}

struct Receipt: Codable {
    let bundleId: String
    let applicationVersion: String
    let inAppPurchases: [InAppPurchase]?
}

struct InAppPurchase: Codable {
    let productId: String
    let transactionId: String
    let originalTransactionId: String?
    let purchaseDate: Date
    let originalPurchaseDate: Date?
}

func verifyReceiptWithServer(
    receiptData: Data,
    appBundleId: String,
    appVersion: String,
    serverURL: URL,
    completion: @escaping (Result<ReceiptVerificationResponse, Error>) -> Void
) {
    guard let receiptString = receiptData.base64EncodedString().addingPercentEncoding(withAllowedCharacters: .alphanumerics) else {
        completion(.failure(NSError(domain: "ReceiptEncodingError", code: 0)))
        return
    }

    let request = ReceiptVerificationRequest(
        receiptData: receiptString,
        password: "your-shared-secret"
    )

    var urlRequest = URLRequest(url: serverURL)
    urlRequest.httpMethod = "POST"
    urlRequest.httpBody = try? JSONEncoder().encode(request)
    urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")

    let task = URLSession.shared.dataTask(with: urlRequest) { data, response, error in
        if let error = error {
            completion(.failure(error))
            return
        }

        guard let data = data else {
            completion(.failure(NSError(domain: "NoDataError", code: 0)))
            return
        }

        do {
            let result = try JSONDecoder().decode(ReceiptVerificationResponse.self, from: data)
            completion(.success(result))
        } catch {
            completion(.failure(error))
        }
    }

    task.resume()
}
Property Description Notes
receiptData Base64 encoded receipt data from the device Required for verification
password Shared secret from App Store Connect (optional) Used for auto-renewable subscriptions
status Response status code from Apple's server 0 = success, other values indicate errors
receipt Decoded receipt object with purchase details Contains in-app purchases, bundle ID, version
latestReceipt Latest receipt for auto-renewable subscriptions May be present when subscription is renewed
Note: Server-side verification is more secure than client-side and helps prevent fraud.
Warning: Never hardcode your shared secret in the app. Use a secure backend service.

Server-Side Receipt Verification Steps

  • Collect receipt data from the device
  • Send it to your backend server securely
  • Verify with Apple's servers using shared secret
  • Validate receipt contents (bundle ID, purchase dates)
  • Grant or deny access based on valid receipts

9. Implementing Promotional Offers

Property Description Notes
promotionalOffer SKProductDiscount? Represents a promotional offer for a product
offerIdentifier String? Unique identifier for the promotional offer
price NSDecimalNumber? Price of the promotional offer
paymentMode SKProductDiscount.PaymentMode Payment mode (e.g., freeTrial, payAsYouGo)
subscriptionPeriod SKProductSubscriptionPeriod? Duration of the subscription period

Example: Fetching Promotional Offers

import StoreKit

class ProductManager: ObservableObject {
    @Published var products: [SKProduct] = []
    
    func fetchProducts() {
        let productIdentifiers = Set(["com.example.product1", "com.example.product2"])
        let request = SKProductsRequest(productIdentifiers: productIdentifiers)
        request.delegate = self
        request.start()
    }
}

extension ProductManager: SKProductsRequestDelegate {
    func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
        self.products = response.products
    }
}

Example: Applying Promotional Offer

import StoreKit

func purchaseProduct(_ product: SKProduct) {
    guard let discount = product.priceLabeledDiscount else { return }
    
    let payment = SKPayment(product: product)
    payment.applicationUsername = "user123"
    payment.paymentDiscount = discount
    
    SKPaymentQueue.default().add(payment)
}
Note: Promotional offers must be approved in App Store Connect before they can be used.
Warning: Ensure that the user's account is properly authenticated before applying promotional offers.
Payment Mode Description Use Case
freeTrial Free trial period for subscription products Offering free access to premium content
payAsYouGo Reduced price for a limited time Discounted pricing for promotional campaigns

10. Testing In-App Purchases

Note: Testing in-app purchases requires an Apple Developer account and Xcode 15 or later.
Environment Description Usage
Sandbox Simulated environment for testing in-app purchases without real transactions. Use during development and testing phases.
Production Live environment where actual purchases occur. Only used after app approval and deployment.

Example: Setting up SKTestSession

import StoreKit

class PurchaseManager: ObservableObject {
    @Published var products: [SKProduct] = []
    
    func loadProducts() async throws {
        let productIdentifiers = ["com.example.product1", "com.example.product2"]
        let products = try await SKProductsRequest(products: productIdentifiers).products
        self.products = products
    }
    
    func purchase(_ product: SKProduct) async throws {
        let result = try await SKPaymentQueue.default().addPayment(product)
    }
}

Example: Simulating Purchase Flow

import StoreKit

func simulatePurchase() {
    let payment = SKPayment(product: product)
    SKPaymentQueue.default().add(payment)
    
    // Handle transaction in delegate
    func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
        for transaction in transactions {
            switch transaction.transactionState {
            case .purchased:
                print("Purchase successful")
                SKPaymentQueue.default().finishTransaction(transaction)
            case .failed:
                print("Purchase failed")
                SKPaymentQueue.default().finishTransaction(transaction)
            default:
                break
            }
        }
    }
}

Testing Steps

  • Create a test user in App Store Connect.
  • Use the sandbox environment for all testing.
  • Ensure all product identifiers match those in App Store Connect.
  • Simulate various transaction states (purchased, failed, restored).

Was this article helpful?