🛍️ SwiftUI 使用 StoreKit2 实现内购

2023/11/27

IAP 类型

  • Consumables - 消耗品, 购买一次、多次和批量购买的产品。比如用于游戏充钱提升等级。
  • Non-consumables - 非消耗品, 只能购买一次且不会过期的产品。一般用于永久解锁高级功能。👀
  • Auto-renewable subscriptions - 自动续期订阅, 用于自动订阅解锁功能,直至用户取消订阅。🚀
  • Non-renewing subscriptions - 非续订订阅,到期结束,不自动续订。 比如游戏的季卡等。

📢 IAP 需在 App Connect 创建内购,或订阅及分组。包括内购/订阅id、价格、信息描述(多语言)等。

内购配置:需要在connect上配置订阅项目,另外Xcode - Targets - Signing & Capabilities 上添加 In-App Purchase

StoreKit 配置文件

在 Xcode13 ,创建 .storekit 配置文件集成内购测试,填写配置完成后,设置 Debug .storekit 配置文件。

image

参考代码

import Foundation

import StoreKit

import SwiftUI

// 购买结果
enum PurchaseResult {
    case purchased
    case unverified(Error)
    case pending
    case cancelled
    case error
}

@MainActor
final class PurchaseViewModel: ObservableObject {
    
    // MARK: 产品 id
    private let productIds = ["pro_monthly", "pro_yearly", "pro_lifetime"]
    private var productsLoaded = false
    private var updates: Task<Void, Never>? = nil
    
    // MARK: 已购买的 product ids
    private(set) var purchasedProductIds = Set<String>()
    
    @Published
    private(set) var products: [Product] = []
    
    @Published
    private(set) var purchaseResult: PurchaseResult?
    
    @AppStorage("app.global.hasPro")
    private(set) var hasPro = false

    init() {
        self.updates = observeTransactionUpdates()
        
        Task {
            await self.getProducts()
        }
    }

    deinit {
        self.updates?.cancel()
    }
}

extension PurchaseViewModel {
    // MARK: 获取 IAP 列表
    func getProducts() async {
        do {
            guard !self.productsLoaded else { return }
        
            // 按价格排序
            let products = try await Product.products(for: productIds)
            let productsSorted = products.sorted { $0.price < $1.price }
            
            self.productsLoaded = true
            self.products = productsSorted
            
        } catch {
            self.purchaseResult = .error
        }
    }
    
    // MARK: 执行购买
    func purchase(_ product: Product) async {
        do {
            let result = try await product.purchase()
            switch result {
                case let .success(.verified(transaction)):
                    // 购买并且验证成功
                    await transaction.finish()
                    await self.updatePurchasedProducts()
                    self.purchaseResult = .purchased
                    
                case let .success(.unverified(_, error)):
                    self.purchaseResult = .unverified(error)
                
                case .pending:
                    self.purchaseResult = .pending
                
                case .userCancelled:
                    self.purchaseResult = .cancelled
                
                @unknown default:
                    self.purchaseResult = .error
                }
            } catch {
                self.purchaseResult = .error
            }
    }
    
    // MARK: 恢复购买
    func restorePurchase() async {
        try? await AppStore.sync()
    }
    
    // MARK: 更新已购买的列表Id
    func updatePurchasedProducts() async {
        for await result in Transaction.currentEntitlements {
            guard case .verified(let transaction) = result else { continue }
            
            if transaction.revocationDate == nil {
                self.purchasedProductIds.insert(transaction.productID)
            } else {
                self.purchasedProductIds.remove(transaction.productID)
            }
        }
        // 判断是否有购买,缓存到 UserDefaults
        self.hasPro = !self.purchasedProductIds.isEmpty
    }
    
    // MARK: 监听购买 (用于用户已经购买过,执行更新)
    func observeTransactionUpdates() -> Task<Void, Never> {
        Task(priority: .background) {
            for await _ in Transaction.updates {
                await self.updatePurchasedProducts()
            }
        }
    }
}

建议在 App 启动时候初始化内购代码,以便优先加载内购产品。

可能存在审核的坑:

1、苹果要求内购产品页要有使用条款的文本或链接,Apple 提供了适用于所有地区的标准最终用户许可协议(EULA),可以在页面引用。https://www.apple.com/legal/internet-services/itunes/dev/stdeula