123 lines
3.2 KiB
Swift
123 lines
3.2 KiB
Swift
//
|
|
// 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<String> = [
|
|
"donate.song",
|
|
"donate.album",
|
|
"donate.anthology"
|
|
]
|
|
|
|
var products: [Product] = []
|
|
var isPurchasing = false
|
|
var purchaseResult: PurchaseResult?
|
|
|
|
private var transactionListener: Task<Void, Never>?
|
|
|
|
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<Void, Never> {
|
|
Task.detached {
|
|
for await verificationResult in Transaction.updates {
|
|
if case .verified(let transaction) = verificationResult {
|
|
await transaction.finish()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Helpers
|
|
|
|
private func checkVerified<T>(_ result: VerificationResult<T>) 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"
|
|
}
|
|
}
|
|
}
|