Skip to content

Instantly share code, notes, and snippets.

@uy
Last active April 5, 2025 10:14
Show Gist options
  • Save uy/8899059aa44e8e5892e34bfcd4b92d47 to your computer and use it in GitHub Desktop.
Save uy/8899059aa44e8e5892e34bfcd4b92d47 to your computer and use it in GitHub Desktop.
StoreKit2 properly working purchase manager observable.
//
// PurchaseManager.swift
// ...
//
// Created by Utku Yeğen on 4.04.2025.
//
import Foundation
import StoreKit
enum Subs: String, CaseIterable {
// ...
}
@MainActor
class PurchaseManager: ObservableObject {
private let productIds = Subs.allCases
@Published private(set) var products: [Product] = []
private var productsLoaded = false
@Published private(set) var purchasedProductIDs = Set<String>()
var hasUnlockedPro: Bool {
return !self.purchasedProductIDs.isEmpty
}
private var updates: Task<Void, Never>? = nil
init() {
updates = observeTransactionUpdates()
}
deinit {
updates?.cancel()
}
func loadProducts() async throws {
guard !self.productsLoaded else { return }
self.products = try await Product.products(for: Subs.getAllProductIds()).sorted(by: { $0.id > $1.id })
self.productsLoaded = true
}
func purchase(_ product: Product) async throws {
let result = try await product.purchase()
switch result {
case let .success(.verified(transaction)):
await transaction.finish()
await updatePurchasedProducts()
case let .success(.unverified(_, _)):
// Transaction can't be verified
break
case .pending:
// Waiting approval (Ask to Buy / SCA)
break
case .userCancelled:
// User cancelled purchase
break
@unknown default:
break
}
}
func restore() async throws {
try await AppStore.sync()
}
func updatePurchasedProducts() async {
var updatedPurchasedIDs = Set<String>()
for await result in Transaction.currentEntitlements {
guard case .verified(let transaction) = result else { continue }
if transaction.revocationDate == nil {
updatedPurchasedIDs.insert(transaction.productID)
}
await transaction.finish()
}
self.purchasedProductIDs = updatedPurchasedIDs
}
private func observeTransactionUpdates() -> Task<Void, Never> {
Task(priority: .background) { [unowned self] in
for await verificationResult in Transaction.updates {
switch verificationResult {
case .verified(let transaction):
await transaction.finish()
case .unverified(_, _):
break
}
await self.updatePurchasedProducts()
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment