You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在飞行模式/离线状态下处理StoreKit 2内购逻辑?

解决StoreKit 2离线状态下非消耗型内购功能失效问题

问题背景

为iOS应用添加首个非消耗型内购时,发现StoreKit 2在离线/飞行模式下无法正常工作。原本通过store.purchasedItems.isEmpty判断是否展示高级功能,但该逻辑在离线时完全失效——核心原因是Transaction.currentEntitlements需要在线向App Store完成验证,离线状态下无法获取已购项目列表,导致purchasedItems为空。

解决方案:本地持久化+StoreKit验证结合

可以用@AppStorage(或UserDefaults)存储购买状态的本地缓存,同时保留StoreKit的在线验证逻辑,确保离线时能读取本地状态,在线时同步最新的官方验证结果。

具体实现步骤

  1. 添加本地状态存储
    在Store类中加入@AppStorage变量,用来缓存用户的购买状态:
import Foundation
import StoreKit
import SwiftUI // 需导入SwiftUI才能使用@AppStorage

typealias Transaction = StoreKit.Transaction
public enum StoreError: Error {
    case failedVerification
}

class Store: ObservableObject {
    @Published private(set) var items: [Product] = []
    @Published private(set) var purchasedItems: [Product] = []
    // 新增:本地缓存购买状态(替换为你的内购ID对应的标识)
    @AppStorage("hasPurchasedPremium") private var hasPurchasedPremium = false
    var updateListenerTask: Task<Void, Error>? = nil
    
    // 原有init、deinit等代码保持不变
}
  1. 在关键时机更新本地状态
    需要在以下场景同步更新hasPurchasedPremium:
  • 购买成功后
  • 启动时StoreKit验证完成后
  • 交易状态更新时

修改对应方法:

// 修改purchase方法,购买成功后同步本地状态
func purchase(_ product: Product) async throws -> Transaction? {
    let result = try await product.purchase()
    switch result {
    case .success(let verification):
        let transaction = try checkVerified(verification)
        await updateCustomerProductStatus()
        // 匹配你的内购ID,更新本地状态
        if transaction.productID == "你的内购ID" {
            hasPurchasedPremium = true
        }
        await transaction.finish()
        return transaction
    case .userCancelled, .pending:
        return nil
    default:
        return nil
    }
}

// 修改updateCustomerProductStatus方法,验证后同步本地状态
@MainActor
func updateCustomerProductStatus() async {
    var purchasedItems: [Product] = []
    var localPurchaseStatus = false
    for await result in Transaction.currentEntitlements {
        do {
            let transaction = try checkVerified(result)
            // 跳过已撤销的内购
            guard transaction.revocationDate == nil else { continue }
            if let product = items.first(where: { $0.id == transaction.productID }) {
                purchasedItems.append(product)
                if product.id == "你的内购ID" {
                    localPurchaseStatus = true
                }
            }
        } catch {
            print("验证失败: \(error)")
        }
    }
    self.purchasedItems = purchasedItems
    hasPurchasedPremium = localPurchaseStatus
}

// 修改listenForTransactions方法,交易更新时同步本地状态
func listenForTransactions() -> Task<Void, Error> {
    return Task.detached {
        for await result in Transaction.updates {
            do {
                let transaction = try self.checkVerified(result)
                await self.updateCustomerProductStatus()
                // 处理撤销场景
                if transaction.productID == "你的内购ID" {
                    await MainActor.run {
                        self.hasPurchasedPremium = transaction.revocationDate == nil
                    }
                }
                await transaction.finish()
            } catch {
                print("交易处理失败: \(error)")
            }
        }
    }
}
  1. 修改功能判断逻辑
    原有用store.purchasedItems.isEmpty的地方,改为优先判断本地缓存,同时保留在线验证的兜底:
// 在UI中判断是否展示高级功能
if store.hasPurchasedPremium || !store.purchasedItems.isEmpty {
    // 展示高级功能
} else {
    // 展示购买入口
}

注意事项

  • 防篡改:本地缓存仅用于离线场景,在线时必须以StoreKit的Transaction.currentEntitlements验证结果为准,避免用户通过修改本地数据获取权限。
  • 恢复购买:添加恢复购买方法,支持用户切换设备或重装APP后恢复权限:
func restorePurchases() async {
    await updateCustomerProductStatus()
}
  • 撤销处理:如果用户内购被退款或撤销,Transaction.revocationDate会有值,需同步将本地状态设为false。

内容的提问来源于stack exchange,提问作者Alexnnd

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.13 11:15:35