// // ServicesMAStoreManager.swift // Mobile Music Assistant // // Created by Sven Hanold on 06.04.26. // import StoreKit @Observable @MainActor final class MAStoreManager { enum PurchaseResult: Equatable { case success case cancelled case failed(String) } static let productIDs: Set = [ "donate.song", "donate.album", "donate.anthology" ] var products: [Product] = [] var isPurchasing = false var purchaseResult: PurchaseResult? private var transactionListener: Task? init() { transactionListener = listenForTransactions() } // MARK: - Load Products func loadProducts() async { guard products.isEmpty else { return } do { let storeProducts = try await Product.products(for: Self.productIDs) products = storeProducts.sorted { $0.price < $1.price } } catch { print("Failed to load products: \(error)") } } // MARK: - Purchase func purchase(_ product: Product) async { isPurchasing = true purchaseResult = nil do { let result = try await product.purchase() switch result { case .success(let verification): let transaction = try checkVerified(verification) await transaction.finish() isPurchasing = false purchaseResult = .success case .userCancelled: isPurchasing = false purchaseResult = .cancelled case .pending: isPurchasing = false @unknown default: isPurchasing = false } } catch { isPurchasing = false purchaseResult = .failed(error.localizedDescription) } } // MARK: - Transaction Listener private func listenForTransactions() -> Task { Task.detached { for await verificationResult in Transaction.updates { if case .verified(let transaction) = verificationResult { await transaction.finish() } } } } // MARK: - Helpers private func checkVerified(_ result: VerificationResult) throws -> T { switch result { case .unverified(_, let error): throw error case .verified(let safe): return safe } } /// Returns the SF Symbol icon name for a given product ID func iconName(for productID: String) -> String { switch productID { case "donate.song": return "music.note" case "donate.album": return "opticaldisc" case "donate.anthology": return "music.note.list" default: return "gift" } } /// Returns a friendly tier name for a given product ID func tierName(for productID: String) -> String { switch productID { case "donate.song": return "Song" case "donate.album": return "Album" case "donate.anthology": return "Anthology" default: return "Donation" } } }