118 lines
3.5 KiB
Swift
118 lines
3.5 KiB
Swift
//
|
|
// ServicesMAStoreManager.swift
|
|
// Mobile Music Assistant
|
|
//
|
|
// Created by Sven Hanold on 09.04.26.
|
|
//
|
|
|
|
import StoreKit
|
|
import OSLog
|
|
import SwiftUI
|
|
|
|
private let logger = Logger(subsystem: "com.musicassistant.mobile", category: "StoreManager")
|
|
|
|
enum PurchaseResult: Equatable {
|
|
case success(Product)
|
|
case cancelled
|
|
case failed(String)
|
|
}
|
|
|
|
@Observable @MainActor final class MAStoreManager {
|
|
var products: [Product] = []
|
|
var purchaseResult: PurchaseResult?
|
|
var isPurchasing = false
|
|
var loadError: String?
|
|
|
|
private static let productIDs: Set<String> = [
|
|
"donatesong",
|
|
"donatealbum",
|
|
"donateanthology"
|
|
]
|
|
|
|
private var updateListenerTask: Task<Void, Never>?
|
|
|
|
init() {
|
|
updateListenerTask = listenForTransactions()
|
|
}
|
|
|
|
func loadProducts() async {
|
|
loadError = nil
|
|
do {
|
|
let fetched = try await Product.products(for: Self.productIDs)
|
|
products = fetched.sorted { $0.price < $1.price }
|
|
if fetched.isEmpty {
|
|
loadError = "No products returned. Make sure the StoreKit configuration is active in the scheme (Edit Scheme → Run → Options → StoreKit Configuration)."
|
|
logger.warning("Product.products(for:) returned 0 results for IDs: \(Self.productIDs)")
|
|
}
|
|
} catch {
|
|
loadError = error.localizedDescription
|
|
logger.error("Failed to load products: \(error.localizedDescription)")
|
|
}
|
|
}
|
|
|
|
func purchase(_ product: Product) async {
|
|
isPurchasing = true
|
|
defer { isPurchasing = false }
|
|
|
|
do {
|
|
let result = try await product.purchase()
|
|
switch result {
|
|
case .success(let verification):
|
|
let transaction = try checkVerified(verification)
|
|
await transaction.finish()
|
|
purchaseResult = .success(product)
|
|
case .userCancelled:
|
|
purchaseResult = .cancelled
|
|
case .pending:
|
|
break
|
|
@unknown default:
|
|
break
|
|
}
|
|
} catch {
|
|
purchaseResult = .failed(error.localizedDescription)
|
|
}
|
|
}
|
|
|
|
private func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
|
|
switch result {
|
|
case .unverified(_, let error):
|
|
throw error
|
|
case .verified(let value):
|
|
return value
|
|
}
|
|
}
|
|
|
|
private func listenForTransactions() -> Task<Void, Never> {
|
|
Task(priority: .background) { [weak self] in
|
|
for await result in Transaction.updates {
|
|
do {
|
|
let transaction = try self?.checkVerified(result)
|
|
await transaction?.finish()
|
|
} catch {
|
|
logger.error("Transaction verification failed: \(error.localizedDescription)")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Helpers
|
|
|
|
func iconName(for product: Product) -> String {
|
|
switch product.id {
|
|
case "donatesong": return "music.note"
|
|
case "donatealbum": return "opticaldisc"
|
|
case "donateanthology": return "music.note.list"
|
|
default: return "heart.fill"
|
|
}
|
|
}
|
|
|
|
func tierName(for product: Product) -> LocalizedStringKey {
|
|
switch product.id {
|
|
case "donatesong": return "Song"
|
|
case "donatealbum": return "Album"
|
|
case "donateanthology": return "Anthology"
|
|
default: return LocalizedStringKey(product.displayName)
|
|
}
|
|
}
|
|
}
|