Skip to content

Instantly share code, notes, and snippets.

@lienmt
Last active November 27, 2024 20:02
Show Gist options
  • Save lienmt/1f1c0d399e57a1c0eb636a60f7887e05 to your computer and use it in GitHub Desktop.
Save lienmt/1f1c0d399e57a1c0eb636a60f7887e05 to your computer and use it in GitHub Desktop.
PurchaseManager with SwiftUI & StoreKit
import Foundation
import StoreKit
enum ProductsId: String {
case subsWeekly24 = "WeeklyId"
case subsAnnual24 = "AnnualId"
}
@MainActor
class PurchaseManager: NSObject, ObservableObject {
private let productIds: [String] = [
ProductsId.subsWeekly24.rawValue,
ProductsId.subsAnnual24.rawValue
]
@Published
private(set) var products: [Product] = []
@Published
private(set) var purchasedProductIDs = Set<String>()
private var productsLoaded = false
private var updates: Task<Void, Never>? = nil
// MARK: Lifecycle methods
override init() {
super.init()
SKPaymentQueue.default().add(self)
updates = observeTransactionUpdates()
}
deinit {
updates?.cancel()
}
var isPremium: Bool {
!purchasedProductIDs.isEmpty
}
// MARK: Public methods
func loadProducts() async throws {
guard !self.productsLoaded else { return }
self.products = try await Product.products(for: productIds).sorted(by: { $0.price < $1.price })
self.productsLoaded = true
}
func purchase(_ product: Product) async throws {
let result = try await product.purchase()
switch result {
case let .success(.verified(transaction)):
// Successful purhcase
await transaction.finish()
await updatePurchasedProducts()
case .success(.unverified(_, _)):
// Successful purchase but transaction/receipt can't be verified
// Could be a jailbroken phone
break
case .pending:
// Transaction waiting on SCA (Strong Customer Authentication) or
// approval from Ask to Buy
break
case .userCancelled:
// ^^^
break
@unknown default:
break
}
}
func updatePurchasedProducts() async {
for await result in Transaction.currentEntitlements {
guard case .verified(let transaction) = result else {
continue
}
updatePurchasedList(transaction)
}
}
// MARK: Private methods
private func observeTransactionUpdates() -> Task<Void, Never> {
Task(priority: .background) { [unowned self] in
for await verificationResult in Transaction.updates {
guard case .verified(let transaction) = verificationResult else {
continue
}
updatePurchasedList(transaction)
}
}
}
private func updatePurchasedList(_ transaction: StoreKit.Transaction) {
if transaction.revocationDate == nil {
self.purchasedProductIDs.insert(transaction.productID)
} else {
self.purchasedProductIDs.remove(transaction.productID)
}
}
private func canMakePurchases() -> Bool { return SKPaymentQueue.canMakePayments() }
}
extension PurchaseManager: SKPaymentTransactionObserver {
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction: AnyObject in transactions {
if let trans = transaction as? SKPaymentTransaction {
switch trans.transactionState {
case .purchased:
if let transaction = transaction as? SKPaymentTransaction {
purchasedProductIDs.insert(transaction.payment.productIdentifier)
SKPaymentQueue.default().finishTransaction(transaction)
}
case .failed:
SKPaymentQueue.default().finishTransaction(transaction as! SKPaymentTransaction)
case .restored:
if let transaction = transaction as? SKPaymentTransaction {
SKPaymentQueue.default().finishTransaction(transaction)
}
default: break
}
}
}
}
func paymentQueue(_ queue: SKPaymentQueue, shouldAddStorePayment payment: SKPayment, for product: SKProduct) -> Bool {
if canMakePurchases() {
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(self)
SKPaymentQueue.default().add(payment)
return true
} else {
return false
}
}
}
import SwiftUI
@main
struct YourApp: App {
@StateObject
private var purchaseManager = PurchaseManager()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(purchaseManager)
.task {
await purchaseManager.updatePurchasedProducts()
}
}
}
}
struct ContentView: View {
@EnvironmentObject
private var purchaseManager: PurchaseManager
var body: some View {
VStack {
Text("Hello World!")
Text("ℹ️ is premium: \(purchaseManager.isPremium)")
}
.task {
do {
try await purchaseManager.loadProducts()
debugPrint("ℹ️ is premium \(purchaseManager.isPremium)")
} catch(let error) {
debugPrint("🚨 Error purchaseManager.loadProducts() \(error)")
}
}
}
}
#Preview {
ContentView()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment