Skip to content

astralchen/IAPKit

Repository files navigation

Swift IAP Framework

Swift iOS macOS License SPM

A modern, comprehensive In-App Purchase framework for iOS and macOS applications, built with Swift Concurrency and designed for reliability, ease of use, and cross-version compatibility.

✨ Features

πŸ”„ Cross-Version Compatibility

  • Automatic StoreKit Version Detection: Seamlessly switches between StoreKit 1 and StoreKit 2 based on system availability
  • Unified API: Single interface works across iOS 13+ and macOS 10.15+
  • Future-Proof: Ready for new StoreKit versions with minimal code changes

πŸ›‘οΈ Anti-Loss Mechanism

  • Transaction Recovery: Automatically recovers incomplete transactions on app launch
  • Real-Time Monitoring: Continuously monitors transaction queue for changes
  • Smart Retry Logic: Exponential backoff retry mechanism for failed transactions
  • State Persistence: Critical transaction states are persisted locally

πŸ“‹ Order Management

  • Server-Side Orders: Create orders on your server before processing payments
  • Purchase Attribution: Track purchase sources, campaigns, and user context
  • Enhanced Analytics: Rich data for business intelligence and reporting
  • Order Validation: Additional security layer with order-receipt matching
  • Flexible UserInfo: Associate custom data with every purchase

⚑ Performance & Reliability

  • Intelligent Caching: Smart product information caching with configurable expiration
  • Concurrency Safe: Built with Swift Concurrency (async/await, @MainActor)
  • Memory Efficient: Automatic cleanup of expired data and resources
  • Network Resilient: Handles network interruptions gracefully

🌍 Internationalization

  • Multi-Language Support: English, Chinese (Simplified), Japanese, French
  • Localized Error Messages: User-friendly error messages in user's language
  • Accessibility Ready: Full VoiceOver and accessibility support

πŸ§ͺ Testing & Development

  • Comprehensive Test Suite: 95%+ code coverage with unit and integration tests
  • Mock Support: Complete mock implementations for testing
  • Debug Tools: Detailed logging and debugging utilities
  • CI/CD Ready: Automated testing and validation

πŸ“‹ Requirements

  • iOS 13.0+ or macOS 10.15+
  • Swift 6.0+
  • Xcode 15.0+

πŸ“¦ Installation

Swift Package Manager

Add the following to your Package.swift file:

dependencies: [
    .package(url: "https://github.com/yourusername/swift-iap-framework.git", from: "1.0.0")
]

Or add it through Xcode:

  1. File β†’ Add Package Dependencies
  2. Enter the repository URL
  3. Select the version and add to your target

Requirements

  • iOS 13.0+ or macOS 10.15+
  • Swift 6.0+
  • Xcode 15.0+
  • StoreKit framework (automatically linked)

πŸš€ Quick Start

Basic Setup

import IAPKit

class AppDelegate: UIApplicationDelegate {
    func application(_ application: UIApplication, 
                    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        
        // Initialize the IAP framework with network configuration
        Task {
            let networkBaseURL = URL(string: "https://your-api.com")!
            try await IAPManager.shared.initialize(networkBaseURL: networkBaseURL)
        }
        
        return true
    }
    
    func applicationWillTerminate(_ application: UIApplication) {
        // Clean up resources
        IAPManager.shared.cleanup()
    }
}

Custom Configuration

import IAPKit

// Create custom configuration
let networkConfig = NetworkConfiguration.default(baseURL: URL(string: "https://your-api.com")!)
let config = IAPConfiguration(
    enableDebugLogging: true,
    autoFinishTransactions: true,
    maxRetryAttempts: 5,
    productCacheExpiration: 600, // 10 minutes
    autoRecoverTransactions: true,
    networkConfiguration: networkConfig
)

// Initialize with custom configuration
let manager = IAPManager(configuration: config)
try await manager.initialize(configuration: nil) // Uses constructor config

Loading Products

import IAPKit

class StoreViewController: UIViewController {
    private let iapManager = IAPManager.shared
    
    override func viewDidLoad() {
        super.viewDidLoad()
        loadProducts()
    }
    
    private func loadProducts() {
        Task {
            do {
                let productIDs: Set<String> = [
                    "com.yourapp.premium",
                    "com.yourapp.coins_100",
                    "com.yourapp.monthly_subscription"
                ]
                
                let products = try await iapManager.loadProducts(productIDs: productIDs)
                
                await MainActor.run {
                    updateUI(with: products)
                }
                
            } catch {
                await MainActor.run {
                    showError(error)
                }
            }
        }
    }
}

Making Purchases

Basic Purchase

private func purchaseProduct(_ product: IAPProduct) {
    Task {
        do {
            let result = try await iapManager.purchase(product)
            
            await MainActor.run {
                switch result {
                case .success(let transaction, let order):
                    showSuccess("Purchase completed successfully!")
                    print("Order ID: \(order.id)")
                    activateFeature(for: transaction.productID)
                    
                case .pending(let transaction, let order):
                    showInfo("Purchase is pending approval")
                    print("Order ID: \(order.id)")
                    
                case .cancelled(let order):
                    showInfo("Purchase was cancelled")
                    if let order = order {
                        print("Cancelled Order ID: \(order.id)")
                    }
                    
                case .failed(let error, let order):
                    showError("Purchase failed: \(error.localizedDescription)")
                    if let order = order {
                        print("Failed Order ID: \(order.id)")
                    }
                }
            }
            
        } catch {
            await MainActor.run {
                showError(error)
            }
        }
    }
}

Purchase with User Information

private func purchaseWithUserInfo(_ product: IAPProduct) {
    Task {
        do {
            // Include user context and attribution data
            let userInfo: [String: Any] = [
                "userID": "user_12345",
                "campaign": "summer_sale_2024",
                "source": "push_notification",
                "screen": "premium_features",
                "experiment": "pricing_test_v2",
                "variant": "discount_20_percent"
            ]
            
            let result = try await iapManager.purchase(product, userInfo: userInfo)
            
            await MainActor.run {
                switch result {
                case .success(let transaction, let order):
                    showSuccess("Purchase completed successfully!")
                    print("Transaction ID: \(transaction.id)")
                    print("Order ID: \(order.id)")
                    print("Server Order ID: \(order.serverOrderID ?? "none")")
                    
                    // Activate feature with order context
                    activateFeatureWithOrder(for: transaction.productID, order: order)
                    
                case .pending(let transaction, let order):
                    showInfo("Purchase pending approval")
                    trackPendingPurchase(order: order)
                    
                case .cancelled(let order):
                    if let order = order {
                        trackCancelledPurchase(order: order)
                    }
                    
                case .failed(let error, let order):
                    showError("Purchase failed: \(error.localizedDescription)")
                    if let order = order {
                        trackFailedPurchase(order: order, error: error)
                    }
                }
            }
            
        } catch {
            await MainActor.run {
                showError(error)
            }
        }
    }
}

Restoring Purchases

private func restorePurchases() {
    Task {
        do {
            let transactions = try await iapManager.restorePurchases()
            
            await MainActor.run {
                showSuccess("Restored \(transactions.count) purchases")
                
                for transaction in transactions {
                    activateFeature(for: transaction.productID)
                }
            }
            
        } catch {
            await MainActor.run {
                showError(error)
            }
        }
    }
}

πŸ“± SwiftUI Integration

ObservableObject Wrapper

import SwiftUI
import IAPKit

@MainActor
class IAPStore: ObservableObject {
    @Published var products: [IAPProduct] = []
    @Published var isLoading = false
    @Published var errorMessage: String?
    
    private let iapManager = IAPManager.shared
    
    func loadProducts() {
        isLoading = true
        errorMessage = nil
        
        Task {
            do {
                let productIDs: Set<String> = ["com.app.premium", "com.app.coins"]
                products = try await iapManager.loadProducts(productIDs: productIDs)
            } catch {
                errorMessage = error.localizedDescription
            }
            
            isLoading = false
        }
    }
    
    func purchase(_ product: IAPProduct, userInfo: [String: Any]? = nil) {
        Task {
            do {
                let result = try await iapManager.purchase(product, userInfo: userInfo)
                
                switch result {
                case .success(let transaction, let order):
                    // Handle successful purchase
                    print("Purchase successful! Order: \(order.id)")
                case .pending(let transaction, let order):
                    // Handle pending purchase
                    print("Purchase pending. Order: \(order.id)")
                case .cancelled(let order):
                    // Handle cancellation
                    if let order = order {
                        print("Purchase cancelled. Order: \(order.id)")
                    }
                case .failed(let error, let order):
                    errorMessage = error.localizedDescription
                    if let order = order {
                        print("Purchase failed. Order: \(order.id)")
                    }
                }
            } catch {
                errorMessage = error.localizedDescription
            }
        }
    }
}

SwiftUI View

struct StoreView: View {
    @StateObject private var store = IAPStore()
    
    var body: some View {
        NavigationView {
            List(store.products) { product in
                ProductRow(product: product) {
                    store.purchase(product)
                }
            }
            .navigationTitle("Store")
            .onAppear {
                store.loadProducts()
            }
            .overlay {
                if store.isLoading {
                    ProgressView("Loading products...")
                }
            }
            .alert("Error", isPresented: .constant(store.errorMessage != nil)) {
                Button("OK") {
                    store.errorMessage = nil
                }
            } message: {
                Text(store.errorMessage ?? "")
            }
        }
    }
}

βš™οΈ Configuration

Advanced Configuration

// Create network configuration
let networkConfig = NetworkConfiguration.default(baseURL: URL(string: "https://your-api.com")!)

// Create IAP configuration
let config = IAPConfiguration(
    enableDebugLogging: true,
    autoFinishTransactions: false,
    maxRetryAttempts: 5,
    baseRetryDelay: 2.0,
    productCacheExpiration: 3600, // 1 hour
    autoRecoverTransactions: true,
    receiptValidation: .default,
    networkConfiguration: networkConfig
)

let customManager = IAPManager(configuration: config)
try await customManager.initialize(configuration: nil)

Receipt Validation

// Local validation (basic)
let result = try await iapManager.validateReceipt(receiptData)

// Validation with order information (enhanced security)
let result = try await iapManager.validateReceipt(receiptData, with: order)

// Custom remote validation configuration
let remoteValidationConfig = ReceiptValidationConfiguration.remote(
    serverURL: URL(string: "https://your-validation-server.com/validate")!,
    timeout: 30.0,
    cacheExpiration: 300.0
)

let networkConfig = NetworkConfiguration.default(baseURL: URL(string: "https://your-api.com")!)
let config = IAPConfiguration(
    receiptValidation: remoteValidationConfig,
    networkConfiguration: networkConfig
)

// Custom receipt validator implementation
class CustomReceiptValidator: ReceiptValidatorProtocol {
    func validateReceipt(_ receiptData: Data) async throws -> IAPReceiptValidationResult {
        // Implement your server-side validation
        // Send receiptData to your server
        // Return validation result
        return IAPReceiptValidationResult(isValid: true, error: nil)
    }
}

let customValidator = CustomReceiptValidator()
let manager = IAPManager(configuration: config, receiptValidator: customValidator)

Network Customization

IAPKit provides flexible network layer customization for advanced use cases:

// Basic network configuration
let networkConfig = NetworkConfiguration.default(baseURL: URL(string: "https://your-api.com")!)

let config = IAPConfiguration(
    enableDebugLogging: false,
    autoFinishTransactions: true,
    networkConfiguration: networkConfig
)

Custom Authentication

// Add authentication to all requests
let authConfig = NetworkConfiguration.withAuthentication(
    baseURL: URL(string: "https://secure-api.com")!,
    authTokenProvider: {
        return await AuthService.shared.getToken()
    }
)

let config = IAPConfiguration(networkConfiguration: authConfig)

Custom Request/Response Processing

// Custom request builder with encryption
let secureBuilder = SecureNetworkRequestBuilder(
    additionalHeaders: ["X-API-Key": "your-key"],
    encryptBody: { data in
        return try await CryptoService.encrypt(data)
    }
)

// Custom response parser with validation
let validatingParser = ValidatingNetworkResponseParser { data, response in
    try await validateResponseSignature(data, response)
}

let customComponents = NetworkCustomComponents(
    requestExecutor: nil,
    responseParser: validatingParser,
    requestBuilder: secureBuilder,
    endpointBuilder: nil
)

let networkConfig = NetworkConfiguration(
    baseURL: URL(string: "https://api.com")!,
    customComponents: customComponents
)

Standalone Network Client Usage

The NetworkClient can be used independently for general API operations:

// Create standalone network client
let networkConfig = NetworkConfiguration.default(baseURL: URL(string: "https://your-api.com")!)
let networkClient = NetworkClient.default(configuration: networkConfig)

// Create and manage orders
let order = IAPOrder.created(id: "order-123", productID: "product-456", userInfo: nil, serverOrderID: nil)
let response = try await networkClient.createOrder(order)

// Query order status
let statusResponse = try await networkClient.queryOrderStatus(response.serverOrderID)

For detailed network customization guide, see Network Customization Guide.

Order Management

// Create order before purchase (optional)
let userInfo = ["userID": "12345", "campaign": "summer_sale"]
let order = try await iapManager.createOrder(for: product, userInfo: userInfo)
print("Order created: \(order.id)")

// Query order status
let status = try await iapManager.queryOrderStatus(order.id)
print("Order status: \(status.localizedDescription)")

// Monitor order until completion
func monitorOrder(_ order: IAPOrder) async {
    var currentOrder = order
    
    while !currentOrder.status.isTerminal {
        let newStatus = try await iapManager.queryOrderStatus(currentOrder.id)
        
        if newStatus != currentOrder.status {
            print("Order status changed: \(currentOrder.status) -> \(newStatus)")
            currentOrder = currentOrder.withStatus(newStatus)
            
            // Handle status changes
            switch newStatus {
            case .completed:
                print("Order completed successfully!")
                return
            case .failed:
                print("Order failed")
                return
            case .cancelled:
                print("Order was cancelled")
                return
            default:
                break
            }
        }
        
        // Wait before next check
        try await Task.sleep(nanoseconds: 5_000_000_000) // 5 seconds
    }
}

πŸ“‹ Order Management Deep Dive

Why Use Order Management?

Order management provides several key benefits:

  • Enhanced Analytics: Track purchase attribution, user behavior, and campaign effectiveness
  • Fraud Prevention: Additional validation layer with order-receipt matching
  • Business Intelligence: Rich data for reporting, A/B testing, and user segmentation
  • Compliance: Meet regulatory requirements for purchase tracking and auditing
  • Customer Support: Better tools for handling purchase-related inquiries

Order Lifecycle

// 1. Order Creation (automatic with purchase)
let userInfo = [
    "userID": "user_12345",
    "campaign": "summer_sale_2024",
    "source": "push_notification",
    "experiment": "pricing_test_v2"
]

let result = try await iapManager.purchase(product, userInfo: userInfo)

// 2. Order Status Tracking
switch result {
case .success(let transaction, let order):
    // Order completed successfully
    print("Order \(order.id) completed")
    
case .pending(let transaction, let order):
    // Order is pending (e.g., awaiting parental approval)
    await monitorOrderUntilCompletion(order)
    
case .cancelled(let order):
    // Order was cancelled by user
    if let order = order {
        logOrderCancellation(order)
    }
    
case .failed(let error, let order):
    // Order failed for some reason
    if let order = order {
        logOrderFailure(order, error: error)
    }
}

Advanced Order Features

// Manual order creation (for complex flows)
let order = try await iapManager.createOrder(for: product, userInfo: userInfo)

// Order status queries
let currentStatus = try await iapManager.queryOrderStatus(order.id)

// Order properties
print("Order ID: \(order.id)")
print("Product ID: \(order.productID)")
print("Created: \(order.createdAt)")
print("Status: \(order.status.localizedDescription)")
print("Is Active: \(order.isActive)")
print("Is Expired: \(order.isExpired)")

// User information access
if let userInfo = order.userInfo {
    print("User ID: \(userInfo["userID"] ?? "unknown")")
    print("Campaign: \(userInfo["campaign"] ?? "none")")
}

Order Analytics Integration

class OrderAnalytics {
    func trackPurchaseFlow(_ product: IAPProduct, userInfo: [String: Any]) async {
        // Track purchase initiation
        logEvent("purchase_initiated", parameters: [
            "product_id": product.id,
            "price": product.price.doubleValue,
            "user_id": userInfo["userID"] as? String ?? "",
            "campaign": userInfo["campaign"] as? String ?? ""
        ])
        
        do {
            let result = try await iapManager.purchase(product, userInfo: userInfo)
            
            switch result {
            case .success(let transaction, let order):
                // Track successful purchase with rich context
                logEvent("purchase_completed", parameters: [
                    "transaction_id": transaction.id,
                    "order_id": order.id,
                    "server_order_id": order.serverOrderID ?? "",
                    "product_id": product.id,
                    "price": product.price.doubleValue,
                    "completion_time": Date().timeIntervalSince(order.createdAt)
                ])
                
            case .failed(let error, let order):
                // Track failure with order context
                logEvent("purchase_failed", parameters: [
                    "product_id": product.id,
                    "order_id": order?.id ?? "",
                    "error_type": String(describing: type(of: error)),
                    "error_message": error.localizedDescription
                ])
            }
        } catch {
            // Track errors
            logEvent("purchase_error", parameters: [
                "product_id": product.id,
                "error": error.localizedDescription
            ])
        }
    }
    
    private func logEvent(_ name: String, parameters: [String: Any]) {
        // Send to your analytics service
        print("Analytics: \(name) - \(parameters)")
    }
}

Migration from Basic Purchases

If you're currently using basic purchases without orders, migration is straightforward:

// Before (basic purchase)
let result = try await iapManager.purchase(product)

// After (order-based purchase)
let result = try await iapManager.purchase(product, userInfo: userInfo)

// The result structure changes slightly:
switch result {
case .success(let transaction, let order):  // Now includes order
    // Handle success with order context
case .pending(let transaction, let order):  // Now includes order
    // Handle pending with order context
case .cancelled(let order):                 // Now includes optional order
    // Handle cancellation with order context
case .failed(let error, let order):         // Now includes optional order
    // Handle failure with order context
}

The framework maintains backward compatibility - you can still call purchase(product) without userInfo, and it will work as before but with enhanced result information.

πŸ”§ Advanced Usage

Transaction Monitoring

// The framework automatically monitors transactions, but you can also
// manually check for pending transactions
await iapManager.recoverTransactions { result in
    switch result {
    case .success(let count):
        print("Recovered \(count) transactions")
    case .failure(let error):
        print("Recovery failed: \(error)")
    case .alreadyInProgress:
        print("Recovery already in progress")
    }
}

Product Caching

// Preload products for better performance
await iapManager.preloadProducts(productIDs: ["com.app.premium"])

// Clear cache when needed
await iapManager.clearProductCache()

// Get cache statistics
let stats = await iapManager.getCacheStats()
print("Cached products: \(stats.validItems)")

Error Handling

do {
    let result = try await iapManager.purchase(product)
} catch let error as IAPError {
    switch error {
    case .productNotFound:
        showAlert("Product not available")
    case .purchaseCancelled:
        // User cancelled, no action needed
        break
    case .networkError:
        showAlert("Please check your internet connection")
    case .paymentNotAllowed:
        showAlert("Purchases are disabled in Settings")
    default:
        showAlert("Purchase failed: \(error.localizedDescription)")
    }
} catch {
    showAlert("Unexpected error: \(error.localizedDescription)")
}

πŸ§ͺ Testing

Unit Testing

import XCTest
@testable import IAPKit

class IAPManagerTests: XCTestCase {
    private var mockAdapter: MockStoreKitAdapter!
    private var manager: IAPManager!
    
    override func setUp() async throws {
        mockAdapter = MockStoreKitAdapter()
        let config = IAPConfiguration.default
        manager = IAPManager(configuration: config, adapter: mockAdapter)
        await manager.initialize()
    }
    
    func testLoadProducts() async throws {
        // Given
        let expectedProducts = [IAPProduct.mock(id: "test_product")]
        mockAdapter.mockProducts = expectedProducts
        
        // When
        let products = try await manager.loadProducts(productIDs: ["test_product"])
        
        // Then
        XCTAssertEqual(products.count, 1)
        XCTAssertEqual(products.first?.id, "test_product")
    }
}

Integration Testing

class IAPIntegrationTests: XCTestCase {
    func testPurchaseFlow() async throws {
        let manager = IAPManager.shared
        await manager.initialize()
        
        // Test with StoreKit Testing framework
        let products = try await manager.loadProducts(productIDs: ["com.test.product"])
        XCTAssertFalse(products.isEmpty)
        
        let result = try await manager.purchase(products.first!)
        // Verify purchase result
    }
}

πŸ› Troubleshooting

Common Issues

Products Not Loading

// Check product IDs in App Store Connect
// Ensure products are approved and available
// Verify bundle ID matches

let validation = productService.validateProductIDs(productIDs)
if !validation.isAllValid {
    print("Invalid product IDs: \(validation.invalidIDs)")
}

Purchases Not Working

// Check device settings
if !SKPaymentQueue.canMakePayments() {
    showAlert("Purchases are disabled on this device")
    return
}

// Verify network connection
// Check App Store Connect configuration
// Review sandbox vs production environment

Transaction Recovery Issues

// Enable debug logging
var config = IAPConfiguration.default
config.enableDebugLogging = true

// Check recovery statistics
let stats = await iapManager.getRecoveryStats()
print("Recovery attempts: \(stats.totalAttempts)")
print("Successful recoveries: \(stats.successfulRecoveries)")

Debug Information

#if DEBUG
let debugInfo = iapManager.getDebugInfo()
print("Debug Info: \(debugInfo)")

// Test localization
let tester = LocalizationTester()
let report = tester.validateAllLocalizations()
print(report.summary)
#endif

πŸ“š API Reference

Core Classes

  • IAPManager: Main interface for all IAP operations with order management
  • IAPProduct: Represents an App Store product with pricing and subscription info
  • IAPTransaction: Represents a purchase transaction with state tracking
  • IAPOrder: Server-side order with user context and analytics data
  • IAPError: Comprehensive error types including order-specific errors
  • NetworkClient: Standalone HTTP client for order management APIs

Configuration

  • IAPConfiguration: Framework configuration with network settings
  • NetworkConfiguration: Network layer configuration with custom components
  • ReceiptValidationConfiguration: Receipt validation settings (local/remote/hybrid)
  • NetworkCustomComponents: Custom network component implementations

Services

  • ProductService: Product loading, caching, and validation
  • PurchaseService: Purchase processing with order-based flow
  • OrderService: Server-side order creation, tracking, and management
  • TransactionMonitor: Real-time transaction monitoring and recovery
  • ReceiptValidator: Receipt validation with order correlation
  • RetryManager: Configurable retry logic for network operations

Network Components

  • NetworkRequestExecutor: HTTP request execution (customizable)
  • NetworkResponseParser: Response parsing and validation (customizable)
  • NetworkRequestBuilder: Request construction and signing (customizable)
  • NetworkEndpointBuilder: API endpoint construction (customizable)

🀝 Contributing

We welcome contributions! Please see our Contributing Guide for details.

Development Setup

  1. Clone the repository
  2. Open in Xcode 15+
  3. Run tests: ⌘+U
  4. Build documentation: ⌘+Shift+D

Code Style

  • Follow Swift API Design Guidelines
  • Use SwiftLint for code formatting
  • Write comprehensive tests for new features
  • Update documentation for API changes

πŸ“„ License

This project is licensed under the MIT License - see the LICENSE file for details.

πŸ™ Acknowledgments

  • Apple's StoreKit framework
  • Swift Concurrency community
  • Contributors and testers

πŸ“ž Support


Made with ❀️ by [Your Name/Company]

About

IAPFramework

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages