How to deal with StoreKit 2 on Airplane mode or offline?

Ale*_*nnd 4 storekit in-app-purchase app-store-connect swiftui storekit2

I'm trying to add/manage my first in-app purchase (non-consumable) on my iOS app and I just discovered that StoreKit 2 doesn't work well offline.

These past days, I used to display (or not) the premium features based on store.purchasedItems.isEmpty but this doesn't work at all on Airplane mode.

I mean, I understand that some parts of my Store file can't be accessible offline. The fetch request from the App Store can only works online, for example. But I didn't expected to be the same about the purchasedItems.

So, I'm wondering what should I do instead? Maybe displaying (or not) the premium features based on an @AppStorage variable? If yes, where should I toggle it? So many questions, I'm quite lost.

Here's my Store file, it's a "light" version from the WWDC21 StoreKit 2 Demo:

import Foundation
import StoreKit

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] = []

    var updateListenerTask: Task<Void, Error>? = nil

    init() {
        items = []
        updateListenerTask = listenForTransactions()
        Task {
            await requestProducts()
            await updateCustomerProductStatus()
        }
    }

    deinit {
        updateListenerTask?.cancel()
    }

    func listenForTransactions() -> Task<Void, Error> {
        return Task.detached {
            for await result in Transaction.updates {
                do {
                    let transaction = try self.checkVerified(result)
                    await self.updateCustomerProductStatus()
                    await transaction.finish()
                } catch {
                    print()
                }
            }
        }
    }

    @MainActor
    func requestProducts() async {
        do {
            let storeItems = try await Product.products(for: [ /* IAP */ ])
            var newItems: [Product] = []
            for item in storeItems {
                newItems.append(item)
            }
            items = newItems
        } catch {
            print()
        }
    }

    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()
            await transaction.finish()
            return transaction
        case .userCancelled, .pending:
            return nil
        default:
            return nil
        }
    }

    func isPurchased(_ product: Product) async throws -> Bool {
        purchasedItems.contains(product)
    }

    func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
        switch result {
        case .verified(let safe):
            return safe
        case .unverified:
            throw StoreError.failedVerification
        }
    }

    @MainActor
    func updateCustomerProductStatus() async {
        var purchasedItems: [Product] = []
        for await result in Transaction.currentEntitlements {
            do {
                let transaction = try checkVerified(result)
                if let product = products.first(where: { $0.id == transaction.productID }) {
                    purchasedItems.append(product)
                }
            } catch {
                print()
            }
        }
        self.purchasedItems = purchasedItems
    }

}
Run Code Online (Sandbox Code Playgroud)

Joe*_* C. 7

我在使用 StoreKit 2 实现订阅时遇到了同样的问题。示例代码是面向对象的Product,看起来访问这些对象的唯一方法是使用耗时的异步请求从 App Store 获取它们。我真的不想通过等待此请求完成来阻止我的应用程序的关键功能。

到目前为止,我发现保证离线访问客户购买状态的最佳方法是忽略示例代码并Transaction使用Product. 离线时,您仍然可以访问Transaction.currentEntitlements. 不幸的是,currentEntitlements仅包含有效订阅的交易,并且当订阅过期、撤销等时为空。

例如

@MainActor
func updateCustomerProductStatus() async {
    var purchasedSubscriptions: [Transaction] = []

    // Iterate through all of the user's purchased products.
    for await result in Transaction.currentEntitlements {
        do {
            let transaction = try checkVerified(result)

            switch transaction.productType {
            case .autoRenewable:
                purchasedSubscriptions.append(transaction)
            default:
                continue
            }
        } catch {
            DLog("Failed to verify entitlement transaction")
        }
    }

    self.purchasedSubscriptions = purchasedSubscriptions
}

// Use Transaction.productID to determine tier of service. e.g.
purchasedSubscriptions.first?.productID == "your.pro.identifier"
Run Code Online (Sandbox Code Playgroud)