blob: bc909c16bd7c458c1ca7204dc8ba9100f08a9a59 (
plain) (
blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
|
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>?
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: 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 {
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):
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 .pending:
statusMessage = "Purchase is pending approval."
case .userCancelled:
break
@unknown default:
errorMessage = "The App Store returned an unknown purchase result."
}
} catch {
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
}
}
|