美文网首页
iOS的内购和订阅

iOS的内购和订阅

作者: 大成小栈 | 来源:发表于2025-08-07 22:41 被阅读0次

1. 苹果后台创建(App Store Connect)

  • 开发者需在 App Store Connect 后台手动创建商品

  • 路径:功能 → 应用内购买 → 创建(+)

  • 每个商品需填写:
    商品 ID:唯一标识符(如 com.czchat.CZChat01
    类型:消耗型/非消耗型/订阅
    定价:设置基准价格
    本地化信息:名称和描述(多语言)

2. App 中的使用方式

获取商品信息前,商品 ID 必须内置到 App 代码中(如示例代码所示),常见实现方式:

// 方式1:硬编码(不推荐)
NSArray *productIDs = @[@"com.czchat.CZChat01", @"com.czchat.CZChat02"];

// 方式2:从 Plist 文件读取(推荐)
NSString *path = [[NSBundle mainBundle] pathForResource:@"IAPProducts" ofType:@"plist"];
NSArray *productIDs = [NSArray arrayWithContentsOfFile:path];

// 方式3:从服务器动态获取(最佳实践)
[APIClient fetchProductIDs:^(NSArray *ids) {
    NSSet *productSet = [NSSet setWithArray:ids];
    SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:productSet];
    [request start];
}];

3. 交易处理流程

交易处理流程

4. 模拟内购和订阅处理逻辑

以下是用 Swift 5 和 StoreKit 2 实现的最新内购和订阅逻辑代码,采用现代 Swift 并发编程模式:

import SwiftUI
import StoreKit

// MARK: - 商品模型
struct StoreProduct: Identifiable {
    let id: String
    let displayName: String
    let description: String
    let price: String
    let skProduct: Product
}

// MARK: - 内购管理器
@MainActor
class StoreManager: ObservableObject {
    // 商品集合
    @Published var products: [StoreProduct] = []
    @Published var subscriptions: [StoreProduct] = []
    
    // 购买状态
    @Published var purchaseState: PurchaseState = .idle
    @Published var currentEntitlements: [Product] = []
    
    // 错误处理
    @Published var errorMessage: String?
    
    // 状态枚举
    enum PurchaseState {
        case idle
        case inProgress
        case success(Product)
        case restored
        case failed
    }
    
    // 初始化
    init() {
        Task {
            // 设置交易监听
            _ = await Transaction.updates
                .map { await self.handle(transaction: $0) }
            
            // 加载商品
            await loadProducts()
            
            // 检查当前订阅状态
            await checkCurrentEntitlements()
        }
    }
    
    // 加载所有商品
    func loadProducts() async {
        do {
            // 从服务器获取商品ID列表(实际项目中)
            let productIDs = try await fetchProductIDsFromServer()
            
            // 请求商品信息
            let storeProducts = try await Product.products(for: productIDs)
            
            // 分类处理
            self.products = storeProducts
                .filter { $0.type == .consumable || $0.type == .nonConsumable }
                .map { StoreProduct(
                    id: $0.id,
                    displayName: $0.displayName,
                    description: $0.description,
                    price: $0.displayPrice,
                    skProduct: $0
                )}
            
            self.subscriptions = storeProducts
                .filter { $0.type == .autoRenewable }
                .map { StoreProduct(
                    id: $0.id,
                    displayName: $0.displayName,
                    description: $0.description,
                    price: $0.displayPrice,
                    skProduct: $0
                )}
            
        } catch {
            errorMessage = "加载商品失败: \(error.localizedDescription)"
        }
    }
    
    // 购买商品
    func purchase(_ product: Product) async {
        purchaseState = .inProgress
        
        do {
            let result = try await product.purchase()
            
            switch result {
            case .success(let verification):
                // 验证交易结果
                if case .verified(let transaction) = verification {
                    // 通知服务器验证交易
                    await verifyTransactionWithServer(transaction)
                    
                    // 更新本地状态
                    await transaction.finish()
                    purchaseState = .success(product)
                    
                    // 更新权益
                    await checkCurrentEntitlements()
                } else {
                    purchaseState = .failed
                    errorMessage = "交易验证失败"
                }
                
            case .pending:
                errorMessage = "交易等待处理"
                purchaseState = .idle
                
            case .userCancelled:
                errorMessage = "用户取消交易"
                purchaseState = .idle
                
            @unknown default:
                purchaseState = .idle
            }
        } catch {
            errorMessage = "购买失败: \(error.localizedDescription)"
            purchaseState = .failed
        }
    }
    
    // 恢复购买
    func restorePurchases() async {
        do {
            try await AppStore.sync()
            await checkCurrentEntitlements()
            purchaseState = .restored
        } catch {
            errorMessage = "恢复购买失败: \(error.localizedDescription)"
        }
    }
    
    // 检查当前权益
    func checkCurrentEntitlements() async {
        var entitlements: [Product] = []
        
        // 检查所有交易
        for await result in Transaction.currentEntitlements {
            if case .verified(let transaction) = result {
                // 获取关联的商品
                if let product = products.first(where: { $0.id == transaction.productID })?.skProduct {
                    entitlements.append(product)
                } else if let subscription = subscriptions.first(where: { $0.id == transaction.productID })?.skProduct {
                    entitlements.append(subscription)
                }
            }
        }
        
        self.currentEntitlements = entitlements
    }
    
    // MARK: - 服务器交互
    // 从服务器获取商品ID(示例)
    private func fetchProductIDsFromServer() async throws -> [String] {
        // 实际项目中替换为网络请求
        return [
            "com.yourcompany.consumable_item",
            "com.yourcompany.premium_upgrade",
            "com.yourcompany.monthly_subscription"
        ]
    }
    
    // 向服务器验证交易
    private func verifyTransactionWithServer(_ transaction: Transaction) async {
        guard let appStoreReceiptURL = Bundle.main.appStoreReceiptURL,
              FileManager.default.fileExists(atPath: appStoreReceiptURL.path) else {
            errorMessage = "收据文件不存在"
            return
        }
        
        do {
            let receiptData = try Data(contentsOf: appStoreReceiptURL)
            let base64Receipt = receiptData.base64EncodedString()
            
            // 发送到服务器验证
            let verificationResult = try await sendReceiptToServer(
                receiptData: base64Receipt,
                productID: transaction.productID,
                transactionID: String(transaction.id)
            
            if verificationResult.success {
                print("服务器验证成功")
            } else {
                errorMessage = "服务器验证失败: \(verificationResult.message ?? "")"
            }
        } catch {
            errorMessage = "收据处理错误: \(error.localizedDescription)"
        }
    }
    
    // 发送收据到服务器(示例)
    private func sendReceiptToServer(receiptData: String, productID: String, transactionID: String) async throws -> VerificationResponse {
        // 实际项目中替换为真实网络请求
        struct VerificationResponse: Decodable {
            let success: Bool
            let message: String?
        }
        
        // 模拟网络请求
        await Task.sleep(1_000_000_000) // 1秒延迟
        
        // 模拟成功响应
        return VerificationResponse(success: true, message: "验证成功")
    }
    
    // 处理交易更新
    private func handle(transaction: VerificationResult<Transaction>) async {
        if case .verified(let verifiedTransaction) = transaction {
            // 解锁内容
            await checkCurrentEntitlements()
            
            // 完成交易
            await verifiedTransaction.finish()
        }
    }
}

// MARK: - 用户界面
struct StoreView: View {
    @StateObject private var storeManager = StoreManager()
    
    var body: some View {
        NavigationView {
            List {
                // 消耗型/非消耗型商品
                Section(header: Text("商品")) {
                    ForEach(storeManager.products) { product in
                        productRow(for: product)
                    }
                }
                
                // 订阅商品
                Section(header: Text("订阅")) {
                    ForEach(storeManager.subscriptions) { product in
                        productRow(for: product)
                    }
                }
                
                // 恢复购买按钮
                Section {
                    Button("恢复购买") {
                        Task { await storeManager.restorePurchases() }
                    }
                    .frame(maxWidth: .infinity, alignment: .center)
                }
                
                // 当前权益
                if !storeManager.currentEntitlements.isEmpty {
                    Section(header: Text("当前权益")) {
                        ForEach(storeManager.currentEntitlements, id: \.id) { product in
                            Text(product.displayName)
                        }
                    }
                }
            }
            .navigationTitle("应用内购买")
            .alert(isPresented: .constant(storeManager.errorMessage != nil)) {
                Alert(
                    title: Text("错误"),
                    message: Text(storeManager.errorMessage ?? "未知错误"),
                    dismissButton: .default(Text("确定")) {
                        storeManager.errorMessage = nil
                    }
                )
            }
            .overlay {
                if case .inProgress = storeManager.purchaseState {
                    ProgressView("处理中...")
                        .padding()
                        .background(Color.secondary.opacity(0.8))
                        .cornerRadius(10)
                }
            }
        }
    }
    
    // 商品行视图
    private func productRow(for product: StoreProduct) -> some View {
        HStack {
            VStack(alignment: .leading) {
                Text(product.displayName)
                    .font(.headline)
                Text(product.description)
                    .font(.subheadline)
                    .foregroundColor(.secondary)
            }
            
            Spacer()
            
            Button(action: {
                Task { await storeManager.purchase(product.skProduct) }
            }) {
                Text(product.price)
                    .padding(.horizontal, 12)
                    .padding(.vertical, 8)
                    .background(Color.blue)
                    .foregroundColor(.white)
                    .cornerRadius(10)
            }
        }
        .padding(.vertical, 8)
    }
}

// MARK: - 预览
struct StoreView_Previews: PreviewProvider {
    static var previews: some View {
        StoreView()
    }
}
  1. 使用 StoreKit 2 全新 API

    • 采用 Swift 并发 (async/await)
    • 使用 ProductTransaction 对象模型
    • 通过 Transaction.currentEntitlements 管理权益
  2. 商品管理优化

    • 动态从服务器获取商品ID
    • 自动分类消耗型/非消耗型/订阅商品
    • 实时更新当前权益状态
  3. 订阅管理改进

    • 自动续期订阅状态跟踪
    • 通过 Transaction.updates 监听实时变化
    • 使用 AppStore.sync() 恢复购买
  4. 错误处理

    • 集中式错误处理
    • 状态机管理 (PurchaseState)
    • 用户友好的错误提示
  5. SwiftUI 集成

    • 响应式数据绑定 (@Published)
    • 主线程安全 (@MainActor)
    • 现代化 UI 组件
最佳实践建议:
  1. 服务器验证强化

    private func verifyTransactionWithServer(_ transaction: Transaction) async {
        // 获取收据
        guard let receiptURL = Bundle.main.appStoreReceiptURL,
              let receiptData = try? Data(contentsOf: receiptURL) else { return }
        
        // 准备验证数据
        let requestData: [String: Any] = [
            "receipt": receiptData.base64EncodedString(),
            "product_id": transaction.productID,
            "transaction_id": String(transaction.id),
            "environment": transaction.environment.rawValue
        ]
        
        // 发送验证请求
        do {
            let result = try await serverAPI.verifyReceipt(requestData)
            if result.isValid {
                // 发放权益
            } else {
                // 处理无效交易
            }
        } catch {
            // 处理网络错误
        }
    }
    
  2. 订阅状态管理

    func subscriptionStatus(for productID: String) async -> Product.SubscriptionInfo.Status? {
        guard let product = subscriptions.first(where: { $0.id == productID })?.skProduct,
              let status = try? await product.subscription?.status else {
            return nil
        }
        
        // 返回最高优先级状态
        return status.max(by: { $0.state < $1.state })
    }
    
  3. 沙盒测试处理

    private func verificationURL(for environment: Environment) -> URL {
        switch environment {
        case .production:
            return URL(string: "https://buy.itunes.apple.com/verifyReceipt")!
        case .sandbox, .xcode:
            return URL(string: "https://sandbox.itunes.apple.com/verifyReceipt")!
        @unknown default:
            return URL(string: "https://sandbox.itunes.apple.com/verifyReceipt")!
        }
    }
    
  4. 促销优惠处理

    func checkForPromotionalOffer(for product: Product) async {
        guard let subscription = product.subscription,
              let offers = await subscription.promotionalOffers else { return }
        
        // 显示可用的促销优惠
        for offer in offers {
            print("促销优惠: \(offer.id) - \(offer.type)")
        }
    }
    

这个实现完全采用 Swift 现代编程范式:

  • 使用 SwiftUI 构建声明式 UI
  • 采用 Swift 并发处理异步操作
  • 利用 StoreKit 2 的新特性
  • 符合苹果最新审核指南要求
  • 包含完整的错误处理和状态管理

实际部署时,需要:

  1. 在 App Store Connect 配置商品
  2. 实现真实的服务器验证端点
  3. 添加适当的本地化内容
  4. 配置沙盒测试环境
  5. 处理订阅续期和生命周期事件

5. 兼容 iOS 14 及以上版本

结合了 StoreKit 1 和 StoreKit 2 的最佳实践:

import SwiftUI
import StoreKit

// MARK: - 商品模型
struct StoreProduct: Identifiable {
    let id: String
    let displayName: String
    let description: String
    let price: String
    let skProduct: SKProduct?
    @available(iOS 15.0, *)
    var product: Product? {
        get { nil }
        set { }
    }
}

// MARK: - 内购管理器
class StoreManager: NSObject, ObservableObject, SKProductsRequestDelegate, SKPaymentTransactionObserver {
    
    // 商品集合
    @Published var products: [StoreProduct] = []
    @Published var subscriptions: [StoreProduct] = []
    
    // 购买状态
    @Published var purchaseState: PurchaseState = .idle
    @Published var currentEntitlements: [String] = []
    
    // 错误处理
    @Published var errorMessage: String?
    
    // 状态枚举
    enum PurchaseState {
        case idle
        case inProgress
        case success(String)
        case restored
        case failed
    }
    
    // StoreKit 1 变量
    private var productRequest: SKProductsRequest?
    private var pendingProduct: SKProduct?
    
    // 初始化
    override init() {
        super.init()
        // 添加交易观察者
        SKPaymentQueue.default().add(self)
        
        // 加载商品
        loadProducts()
    }
    
    deinit {
        SKPaymentQueue.default().remove(self)
    }
    
    // MARK: - 公共方法
    
    // 加载所有商品
    func loadProducts() {
        // 从服务器获取商品ID列表(实际项目中)
        let productIDs = [
            "com.yourcompany.consumable_item",
            "com.yourcompany.premium_upgrade",
            "com.yourcompany.monthly_subscription"
        ]
        
        if #available(iOS 15.0, *) {
            Task {
                await loadProductsWithStoreKit2(for: productIDs)
            }
        } else {
            loadProductsWithStoreKit1(for: productIDs)
        }
    }
    
    // 购买商品
    func purchase(_ product: StoreProduct) {
        purchaseState = .inProgress
        
        if #available(iOS 15.0, *) {
            Task {
                await purchaseWithStoreKit2(product)
            }
        } else {
            purchaseWithStoreKit1(product)
        }
    }
    
    // 恢复购买
    func restorePurchases() {
        if #available(iOS 15.0, *) {
            Task {
                await restoreWithStoreKit2()
            }
        } else {
            restoreWithStoreKit1()
        }
    }
    
    // MARK: - StoreKit 2 方法 (iOS 15+)
    @available(iOS 15.0, *)
    private func loadProductsWithStoreKit2(for productIDs: [String]) async {
        do {
            let storeProducts = try await Product.products(for: productIDs)
            
            await MainActor.run {
                // 分类处理
                self.products = storeProducts
                    .filter { $0.type == .consumable || $0.type == .nonConsumable }
                    .map { StoreProduct(
                        id: $0.id,
                        displayName: $0.displayName,
                        description: $0.description,
                        price: $0.displayPrice,
                        skProduct: nil
                    )}
                
                self.subscriptions = storeProducts
                    .filter { $0.type == .autoRenewable }
                    .map { StoreProduct(
                        id: $0.id,
                        displayName: $0.displayName,
                        description: $0.description,
                        price: $0.displayPrice,
                        skProduct: nil
                    )}
            }
            
            // 检查当前权益
            await checkCurrentEntitlements()
            
        } catch {
            await MainActor.run {
                errorMessage = "加载商品失败: \(error.localizedDescription)"
            }
        }
    }
    
    @available(iOS 15.0, *)
    private func purchaseWithStoreKit2(_ product: StoreProduct) async {
        guard let sk2Product = try? await Product.products(for: [product.id]).first else {
            await MainActor.run {
                errorMessage = "商品不可用"
                purchaseState = .failed
            }
            return
        }
        
        do {
            let result = try await sk2Product.purchase()
            
            switch result {
            case .success(let verification):
                // 验证交易结果
                if case .verified(let transaction) = verification {
                    // 通知服务器验证交易
                    await verifyTransactionWithServer(
                        productID: transaction.productID,
                        transactionID: String(transaction.id)
                    )
                    
                    // 更新本地状态
                    await transaction.finish()
                    await MainActor.run {
                        purchaseState = .success(product.displayName)
                    }
                    
                    // 更新权益
                    await checkCurrentEntitlements()
                } else {
                    await MainActor.run {
                        purchaseState = .failed
                        errorMessage = "交易验证失败"
                    }
                }
                
            case .pending:
                await MainActor.run {
                    errorMessage = "交易等待处理"
                    purchaseState = .idle
                }
                
            case .userCancelled:
                await MainActor.run {
                    errorMessage = "用户取消交易"
                    purchaseState = .idle
                }
                
            @unknown default:
                await MainActor.run {
                    purchaseState = .idle
                }
            }
        } catch {
            await MainActor.run {
                errorMessage = "购买失败: \(error.localizedDescription)"
                purchaseState = .failed
            }
        }
    }
    
    @available(iOS 15.0, *)
    private func restoreWithStoreKit2() async {
        do {
            try await AppStore.sync()
            await checkCurrentEntitlements()
            await MainActor.run {
                purchaseState = .restored
            }
        } catch {
            await MainActor.run {
                errorMessage = "恢复购买失败: \(error.localizedDescription)"
            }
        }
    }
    
    @available(iOS 15.0, *)
    private func checkCurrentEntitlements() async {
        var entitlements: [String] = []
        
        // 检查所有交易
        for await result in Transaction.currentEntitlements {
            if case .verified(let transaction) = result {
                entitlements.append(transaction.productID)
            }
        }
        
        await MainActor.run {
            self.currentEntitlements = entitlements
        }
    }
    
    // MARK: - StoreKit 1 方法 (iOS 14 兼容)
    private func loadProductsWithStoreKit1(for productIDs: [String]) {
        let request = SKProductsRequest(productIdentifiers: Set(productIDs))
        request.delegate = self
        productRequest = request
        request.start()
    }
    
    private func purchaseWithStoreKit1(_ product: StoreProduct) {
        guard let skProduct = product.skProduct else {
            errorMessage = "商品信息不完整"
            purchaseState = .failed
            return
        }
        
        let payment = SKPayment(product: skProduct)
        SKPaymentQueue.default().add(payment)
    }
    
    private func restoreWithStoreKit1() {
        SKPaymentQueue.default().restoreCompletedTransactions()
    }
    
    // MARK: - SKProductsRequestDelegate (StoreKit 1)
    func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
        // 处理无效商品
        if !response.invalidProductIdentifiers.isEmpty {
            print("无效商品ID: \(response.invalidProductIdentifiers)")
        }
        
        // 处理有效商品
        var allProducts: [StoreProduct] = []
        var subscriptionProducts: [StoreProduct] = []
        
        for product in response.products {
            let storeProduct = StoreProduct(
                id: product.productIdentifier,
                displayName: product.localizedTitle,
                description: product.localizedDescription,
                price: formattedPrice(for: product),
                skProduct: product
            )
            
            // 判断是否为订阅
            if product.subscriptionPeriod != nil {
                subscriptionProducts.append(storeProduct)
            } else {
                allProducts.append(storeProduct)
            }
        }
        
        DispatchQueue.main.async {
            self.products = allProducts
            self.subscriptions = subscriptionProducts
        }
    }
    
    func request(_ request: SKRequest, didFailWithError error: Error) {
        DispatchQueue.main.async {
            self.errorMessage = "商品请求失败: \(error.localizedDescription)"
        }
    }
    
    // MARK: - SKPaymentTransactionObserver (StoreKit 1)
    func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
        for transaction in transactions {
            switch transaction.transactionState {
            case .purchasing:
                // 交易正在进行中
                break
                
            case .purchased, .restored:
                // 交易完成或恢复
                handleCompletedTransaction(transaction)
                queue.finishTransaction(transaction)
                
            case .failed:
                // 交易失败
                handleFailedTransaction(transaction)
                queue.finishTransaction(transaction)
                
            case .deferred:
                // 交易需要家长批准
                DispatchQueue.main.async {
                    self.purchaseState = .idle
                    self.errorMessage = "购买需要家长批准"
                }
                
            @unknown default:
                break
            }
        }
    }
    
    func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
        DispatchQueue.main.async {
            self.purchaseState = .restored
        }
    }
    
    func paymentQueue(_ queue: SKPaymentQueue, restoreCompletedTransactionsFailedWithError error: Error) {
        DispatchQueue.main.async {
            self.errorMessage = "恢复购买失败: \(error.localizedDescription)"
        }
    }
    
    // MARK: - 交易处理
    private func handleCompletedTransaction(_ transaction: SKPaymentTransaction) {
        let productID = transaction.payment.productIdentifier
        
        // 验证收据
        verifyReceiptForTransaction(transaction) { [weak self] success in
            guard let self = self else { return }
            
            DispatchQueue.main.async {
                if success {
                    self.purchaseState = .success(transaction.payment.productIdentifier)
                    self.currentEntitlements.append(productID)
                } else {
                    self.errorMessage = "交易验证失败"
                    self.purchaseState = .failed
                }
            }
        }
    }
    
    private func handleFailedTransaction(_ transaction: SKPaymentTransaction) {
        DispatchQueue.main.async {
            if let error = transaction.error as? SKError {
                switch error.code {
                case .paymentCancelled:
                    self.errorMessage = "用户取消交易"
                case .paymentNotAllowed:
                    self.errorMessage = "设备不允许支付"
                case .paymentInvalid:
                    self.errorMessage = "支付参数无效"
                case .clientInvalid:
                    self.errorMessage = "客户端未授权"
                default:
                    self.errorMessage = "交易失败: \(error.localizedDescription)"
                }
            } else {
                self.errorMessage = transaction.error?.localizedDescription ?? "未知错误"
            }
            self.purchaseState = .failed
        }
    }
    
    // MARK: - 收据验证
    private func verifyReceiptForTransaction(_ transaction: SKPaymentTransaction, completion: @escaping (Bool) -> Void) {
        // 获取收据数据
        guard let receiptURL = Bundle.main.appStoreReceiptURL,
              let receiptData = try? Data(contentsOf: receiptURL) else {
            completion(false)
            return
        }
        
        let base64Receipt = receiptData.base64EncodedString()
        
        // 确定验证环境
        let isSandbox = isSandboxReceipt(receiptData)
        let verifyURL = isSandbox ?
        "https://sandbox.itunes.apple.com/verifyReceipt" :
        "https://buy.itunes.apple.com/verifyReceipt"
        
        // 准备请求数据
        let requestData: [String: Any] = [
            "receipt-data": base64Receipt,
            "password": "你的共享密钥", // 仅用于订阅
            "exclude-old-transactions": true
        ]
        
        // 发送验证请求
        sendReceiptVerificationRequest(to: verifyURL, data: requestData) { result in
            switch result {
            case .success(let response):
                // 验证响应状态
                guard let status = response["status"] as? Int, status == 0 else {
                    completion(false)
                    return
                }
                
                // 检查是否包含当前交易
                if let receiptInfo = response["receipt"] as? [String: Any],
                   let inApp = receiptInfo["in_app"] as? [[String: Any]] {
                    let transactionIDs = inApp.compactMap { $0["transaction_id"] as? String }
                    let originalIDs = inApp.compactMap { $0["original_transaction_id"] as? String }
                    
                    let allIDs = transactionIDs + originalIDs
                    let transactionID = String(transaction.transactionIdentifier ?? "")
                    
                    completion(allIDs.contains(transactionID))
                } else {
                    completion(false)
                }
                
            case .failure:
                completion(false)
            }
        }
    }
    
    private func verifyTransactionWithServer(productID: String, transactionID: String) async {
        // 获取收据数据
        guard let receiptURL = Bundle.main.appStoreReceiptURL,
              let receiptData = try? Data(contentsOf: receiptURL) else {
            await MainActor.run {
                errorMessage = "收据文件不存在"
            }
            return
        }
        
        let base64Receipt = receiptData.base64EncodedString()
        
        // 确定验证环境
        let isSandbox = isSandboxReceipt(receiptData)
        
        // 发送到服务器验证
        do {
            let success = try await sendReceiptToServer(
                receiptData: base64Receipt,
                productID: productID,
                transactionID: transactionID,
                isSandbox: isSandbox
            )
            
            if !success {
                await MainActor.run {
                    errorMessage = "服务器验证失败"
                }
            }
        } catch {
            await MainActor.run {
                errorMessage = "验证错误: \(error.localizedDescription)"
            }
        }
    }
    
    private func isSandboxReceipt(_ receiptData: Data) -> Bool {
        let receiptString = String(data: receiptData, encoding: .ascii) ?? ""
        return receiptString.contains("Sandbox") || receiptString.contains("Environment=Sandbox")
    }
    
    // MARK: - 服务器交互
    private func sendReceiptVerificationRequest(to urlString: String, data: [String: Any], completion: @escaping (Result<[String: Any], Error>) -> Void) {
        guard let url = URL(string: urlString),
              let jsonData = try? JSONSerialization.data(withJSONObject: data) else {
            completion(.failure(NSError(domain: "InvalidRequest", code: 0)))
            return
        }
        
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.httpBody = jsonData
        request.timeoutInterval = 20
        
        URLSession.shared.dataTask(with: request) { data, response, error in
            if let error = error {
                completion(.failure(error))
                return
            }
            
            guard let data = data,
                  let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
                completion(.failure(NSError(domain: "InvalidResponse", code: 0)))
                return
            }
            
            completion(.success(json))
        }.resume()
    }
    
    private func sendReceiptToServer(receiptData: String, productID: String, transactionID: String, isSandbox: Bool) async throws -> Bool {
        // 实际项目中替换为真实网络请求
        // 这里使用延迟模拟网络请求
        try await Task.sleep(nanoseconds: 1_000_000_000) // 1秒延迟
        
        // 模拟成功响应
        return true
    }
    
    // MARK: - 辅助方法
    private func formattedPrice(for product: SKProduct) -> String {
        let formatter = NumberFormatter()
        formatter.numberStyle = .currency
        formatter.locale = product.priceLocale
        return formatter.string(from: product.price) ?? "\(product.price)"
    }
}

// MARK: - 用户界面
struct StoreView: View {
    @StateObject private var storeManager = StoreManager()
    
    var body: some View {
        NavigationView {
            List {
                // 消耗型/非消耗型商品
                Section(header: Text("商品")) {
                    ForEach(storeManager.products) { product in
                        productRow(for: product)
                    }
                }
                
                // 订阅商品
                if !storeManager.subscriptions.isEmpty {
                    Section(header: Text("订阅")) {
                        ForEach(storeManager.subscriptions) { product in
                            productRow(for: product)
                        }
                    }
                }
                
                // 恢复购买按钮
                Section {
                    Button("恢复购买") {
                        storeManager.restorePurchases()
                    }
                    .frame(maxWidth: .infinity, alignment: .center)
                }
                
                // 当前权益
                if !storeManager.currentEntitlements.isEmpty {
                    Section(header: Text("当前权益")) {
                        ForEach(storeManager.currentEntitlements, id: \.self) { productID in
                            Text(productID)
                        }
                    }
                }
            }
            .navigationTitle("应用内购买")
            .alert(isPresented: .constant(storeManager.errorMessage != nil)) {
                Alert(
                    title: Text("错误"),
                    message: Text(storeManager.errorMessage ?? "未知错误"),
                    dismissButton: .default(Text("确定")) {
                        storeManager.errorMessage = nil
                    }
                )
            }
            .overlay {
                if case .inProgress = storeManager.purchaseState {
                    ProgressView("处理中...")
                        .padding()
                        .background(Color.secondary.opacity(0.8))
                        .cornerRadius(10)
                }
            }
        }
    }
    
    // 商品行视图
    private func productRow(for product: StoreProduct) -> some View {
        HStack {
            VStack(alignment: .leading) {
                Text(product.displayName)
                    .font(.headline)
                Text(product.description)
                    .font(.subheadline)
                    .foregroundColor(.secondary)
            }
            
            Spacer()
            
            Button(action: {
                storeManager.purchase(product)
            }) {
                Text(product.price)
                    .padding(.horizontal, 12)
                    .padding(.vertical, 8)
                    .background(Color.blue)
                    .foregroundColor(.white)
                    .cornerRadius(10)
            }
        }
        .padding(.vertical, 8)
    }
}

// MARK: - 预览
struct StoreView_Previews: PreviewProvider {
    static var previews: some View {
        StoreView()
    }
}
  1. StoreKit 1 实现细节
  • 商品加载:使用 SKProductsRequestDelegate 获取商品信息
  • 交易处理:通过 SKPaymentTransactionObserver 监听交易状态
  • 收据验证:使用传统方法获取并验证收据
  • 恢复购买:调用 restoreCompletedTransactions()
  1. StoreKit 2 实现细节 (iOS 15+)
  • 商品加载:使用 Product.products(for:) 异步方法
  • 交易处理:通过 Transaction.updates 监听交易状态
  • 收据验证:自动管理,简化流程
  • 恢复购买:使用 AppStore.sync() 方法
  1. 统一接口设计
// 公共方法保持统一
func loadProducts()
func purchase(_ product: StoreProduct)
func restorePurchases()
  1. 收据验证兼容方案
private func verifyReceiptForTransaction(_ transaction: SKPaymentTransaction, completion: @escaping (Bool) -> Void) {
    // 获取收据数据
    guard let receiptURL = Bundle.main.appStoreReceiptURL,
          let receiptData = try? Data(contentsOf: receiptURL) else {
        completion(false)
        return
    }
    
    // 确定环境(沙盒或生产)
    let isSandbox = isSandboxReceipt(receiptData)
    let verifyURL = isSandbox ?
    "https://sandbox.itunes.apple.com/verifyReceipt" :
    "https://buy.itunes.apple.com/verifyReceipt"
    
    // 发送验证请求...
}
  1. 错误处理与状态管理
  • 统一的状态枚举 PurchaseState
  • 统一的错误消息展示
  • 主线程安全的更新机制

使用说明

  1. 配置商品

    • 在 App Store Connect 中添加商品
    • 设置唯一的商品 ID(如 "com.yourcompany.premium")
  2. 初始化管理器

    @StateObject private var storeManager = StoreManager()
    
  3. 加载商品

    // 在视图出现时加载
    .onAppear {
        storeManager.loadProducts()
    }
    
  4. 处理购买

    Button(action: {
        storeManager.purchase(product)
    }) {
        // ...
    }
    
  5. 处理恢复购买

    Button("恢复购买") {
        storeManager.restorePurchases()
    }
    
  6. 服务器验证

    • 实现 sendReceiptToServer 方法
    • 在服务器端处理苹果的收据验证

测试注意事项

  1. 沙盒测试

    • 在 App Store Connect 创建沙盒测试账号
    • 使用沙盒环境验证收据
  2. 收据刷新

    // 强制刷新收据
    let request = SKReceiptRefreshRequest()
    request.start()
    
  3. 交易完成

    • 确保调用 finishTransaction() 完成交易
    • 避免重复处理同一交易
  4. 订阅测试

    • 在 App Store Connect 修改订阅续订设置
    • 测试不同续订状态的处理

此实现方案完全兼容 iOS 14 及以上版本,在 iOS 15+ 设备上使用现代 StoreKit 2 API,在 iOS 14 设备上回退到稳定可靠的 StoreKit 1 API,提供了无缝的用户体验和开发者接口。

相关文章

  • iOS 苹果内购流程

    本文参考: iOS开发之内购完全笔记 iOS开发内购全套图文教程 iOS应用程序内购/内付费(一) 代码...

  • iOS内购流程文档-Lion

    iOS内购流程: iOS内购 什么时候用到呢? 虚拟产品就需要用到iOS内购;购买的商品,是在本app中...

  • ios内购IAP相关内容

    ios内购IAP相关内容 iOS IAP应用内购详细步骤和问题总结指南 - 简书https://www.jians...

  • iOS内购:自动续期订阅总结

    前言:内购类型有四种:消耗型商品,非消耗型商品,非续期订阅,自动续期订阅. 顾名思义,从中最有难度的就是自动续期订...

  • iOS-内购

    前言:关于iOS内购,参考两篇博文 iOS-iOS内购流程(手把手图文教程)iOS内购你看我就够了(埋坑篇) 我自...

  • ios内购注意事项

    内购两种方式 ios内购及一些常用的破解手段 iap内购破解原理 苹果官方内购demo 内购的消耗性和非消耗性购买说明

  • WWDC2020-StoreKit Testing

    前几天WWDC2020, 里面提高了一个关于内购的新特性 StoreKit Testing 开发过iOS订阅的同学...

  • Unity 接入IAP(上)Android篇

    很多项目都会遇到内购和订阅相关模块,这里我总结一下内购接入的时候遇到的各种坑,以及内购测试的时候,有什么比较好的方...

  • Apple内购审核总结

    内购流程 1 创建内购商品 我的APP-功能-选择对应的内购类型-订阅群组(群组说明)- 本地化信息 - 审核信息...

  • iOS内购一条龙------项目代码 (4)

    步骤一 iOS内购一条龙------账户信息填写(1)步骤二 iOS内购一条龙------配置内购产品ID (2)...

网友评论

      本文标题:iOS的内购和订阅

      本文链接:https://www.haomeiwen.com/subject/jcdeojtx.html