diff options
| author | Christian Cleberg <[email protected]> | 2026-04-19 15:02:43 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-04-19 15:02:43 -0500 |
| commit | 2199384f887cf5634f3fdcf651eeb9c2c7ed457a (patch) | |
| tree | 20953cc3be5e628618c121ffdd77775f1fbbddee /Hutch/Views/More/TipStoreViewModel.swift | |
| parent | 7d5ca0607c091f76aa29cd6fa8c76cfeb1e02ac8 (diff) | |
| download | hutch-2199384f887cf5634f3fdcf651eeb9c2c7ed457a.tar.gz hutch-2199384f887cf5634f3fdcf651eeb9c2c7ed457a.tar.bz2 hutch-2199384f887cf5634f3fdcf651eeb9c2c7ed457a.zip | |
fix: finish half-completed storekit implementation
Diffstat (limited to 'Hutch/Views/More/TipStoreViewModel.swift')
| -rw-r--r-- | Hutch/Views/More/TipStoreViewModel.swift | 118 |
1 files changed, 105 insertions, 13 deletions
diff --git a/Hutch/Views/More/TipStoreViewModel.swift b/Hutch/Views/More/TipStoreViewModel.swift index 54868ba..bc909c1 100644 --- a/Hutch/Views/More/TipStoreViewModel.swift +++ b/Hutch/Views/More/TipStoreViewModel.swift @@ -2,43 +2,135 @@ import StoreKit @Observable final class TipStoreViewModel { + enum TipProduct: String, CaseIterable { + case small + case medium + case large + + var id: String { + "net.cleberg.hutch.tip.\(rawValue)" + } + + var displayName: String { + switch self { + case .small: + "Small Tip" + case .medium: + "Medium Tip" + case .large: + "Large Tip" + } + } + } + + static let productIDs = TipProduct.allCases.map(\.id) + var products: [Product] = [] var isLoading = false + var isRestoringPurchases = false + var purchasingProductID: String? var errorMessage: String? + var statusMessage: String? + + private var transactionUpdatesTask: Task<Void, Never>? - private let productIDs = [ - "net.cleberg.hutch.tip.small", - "net.cleberg.hutch.tip.medium", - "net.cleberg.hutch.tip.large" - ] + init() { + transactionUpdatesTask = Task.detached(priority: .background) { + for await verification in Transaction.updates { + guard case .verified(let transaction) = verification else { continue } + await transaction.finish() + } + } + } + + deinit { + transactionUpdatesTask?.cancel() + } + @MainActor func loadProducts() async { + guard !isLoading else { return } + isLoading = true + errorMessage = nil defer { isLoading = false } + do { - let fetched = try await Product.products(for: productIDs) - // Sort by price ascending to maintain small/medium/large order - products = fetched.sorted { $0.price < $1.price } + let fetched = try await Product.products(for: Self.productIDs) + let productsByID = Dictionary(uniqueKeysWithValues: fetched.map { ($0.id, $0) }) + let orderedProducts = TipProduct.allCases.compactMap { productsByID[$0.id] } + let missingProducts = TipProduct.allCases.filter { productsByID[$0.id] == nil } + + products = orderedProducts + + if !missingProducts.isEmpty { + let missingNames = missingProducts.map(\.displayName).joined(separator: ", ") + errorMessage = "Missing products from the App Store response: \(missingNames). Confirm the product identifiers match App Store Connect exactly and that each item is approved or available in sandbox." + } } catch { - errorMessage = error.localizedDescription + products = [] + errorMessage = "Couldn't load tips from the App Store. \(error.localizedDescription)" } } + @MainActor func purchase(_ product: Product) async { + guard purchasingProductID == nil else { return } + + purchasingProductID = product.id + errorMessage = nil + statusMessage = nil + defer { purchasingProductID = nil } + do { let result = try await product.purchase() switch result { case .success(let verification): - if case .verified(let transaction) = verification { + switch verification { + case .verified(let transaction): await transaction.finish() + statusMessage = "Purchase completed successfully." + case .unverified(_, let error): + errorMessage = "The App Store returned an unverified transaction. \(error.localizedDescription)" } - case .userCancelled, .pending: + + case .pending: + statusMessage = "Purchase is pending approval." + + case .userCancelled: break + @unknown default: - break + errorMessage = "The App Store returned an unknown purchase result." } } catch { - errorMessage = error.localizedDescription + errorMessage = "Purchase failed. \(error.localizedDescription)" + } + } + + @MainActor + func restorePurchases() async { + guard !isRestoringPurchases else { return } + + isRestoringPurchases = true + errorMessage = nil + defer { isRestoringPurchases = false } + + do { + try await AppStore.sync() + statusMessage = "Purchase history synced with the App Store." + await loadProducts() + } catch { + errorMessage = "Couldn't sync purchases. \(error.localizedDescription)" } } + + @MainActor + func clearStatusMessage() { + statusMessage = nil + } + + func isPurchasing(_ product: Product) -> Bool { + purchasingProductID == product.id + } } |
