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
}
}