/
StoreManager.swift
166 lines (147 loc) · 5.39 KB
/
StoreManager.swift
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
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
//
// StoreManager.swift
// Shotty Bird iOS
//
// Created by Jorge Tapia on 7/28/23.
// Copyright © 2023 Komodo Life. All rights reserved.
//
import StoreKit
/// In-app purchase transation error.
enum StoreKitError: Error {
case failedVerification
case unableToSync
case unknownError
}
/// In-app purchase transaction status.
enum PurchaseStatus {
case success(String)
case pending
case cancelled
case failed(Error)
case unknown
}
/// Footprint for store manager implementations.
protocol StoreKitManageable {
func retrieveProducts() async
func purchase(_ item: Product) async
func verifyPurchase<T>(_ verificationResult: VerificationResult<T>) throws -> T
func transactionStatusStream() -> Task<Void, Error>
}
/// Manager class for processing in-app purchases.
final class StoreManager: ObservableObject {
/// Shared manager instance.
static let shared = StoreManager()
/// The Remove Ads product ID.
static let noAdsProductID = "life.komodo.shottybird.noads"
@Published private(set) var items = [Product]()
@Published var transactionCompletionStatus = false
private let productIds = [StoreManager.noAdsProductID]
private(set) var purchaseStatus: PurchaseStatus = .unknown
private(set) var transactionListener: Task<Void, Error>?
init() {
transactionListener = transactionStatusStream()
Task {
await retrieveProducts()
}
}
deinit {
transactionListener?.cancel()
}
/// Retrieves all of the in-app products.
func retrieveProducts() async {
do {
let products = try await Product.products(for: productIds)
items = products.sorted(by: { $0.price < $1.price })
} catch {
print(error)
}
}
/// Purchases the in-app product.
/// - Parameter item: The product to purchase.
func purchase(_ item: Product) async {
do {
let result = try await item.purchase()
switch result {
case .success(let verification):
print("Purchase was a success, now it can be verified.")
do {
let verificationResult = try verifyPurchase(verification)
purchaseStatus = .success(verificationResult.productID)
await verificationResult.finish()
transactionCompletionStatus = true
} catch {
purchaseStatus = .failed(error)
transactionCompletionStatus = true
}
case .pending:
print("Transaction is pending for some action from the users related to the account")
purchaseStatus = .pending
transactionCompletionStatus = false
case .userCancelled:
print("Use cancelled the transaction")
purchaseStatus = .cancelled
transactionCompletionStatus = false
default:
print("Unknown error")
purchaseStatus = .failed(StoreKitError.unknownError)
transactionCompletionStatus = false
}
} catch {
print(error)
purchaseStatus = .failed(error)
transactionCompletionStatus = false
}
}
/// Verifies a purchase.
/// - Parameter verificationResult: The verification result.
/// - Returns: A result with the verification status or an error.
func verifyPurchase<T>(_ verificationResult: VerificationResult<T>) throws -> T {
switch verificationResult {
case .unverified(_, let error):
throw error // Successful purchase but transaction/receipt can't be verified due to some conditions like jailbroken phone
case .verified(let result):
return result // Successful purchase
}
}
/// Handles Interruptions.
func transactionStatusStream() -> Task<Void, Error> {
Task.detached(priority: .background) { @MainActor [weak self] in
do {
for await result in Transaction.updates {
let transaction = try self?.verifyPurchase(result)
self?.purchaseStatus = .success(transaction?.productID ?? "Unknown product ID")
self?.transactionCompletionStatus = true
await transaction?.finish()
}
} catch {
self?.transactionCompletionStatus = true
self?.purchaseStatus = .failed(error)
}
}
}
/// Unlock in-app features.
func unlockRemoveAds() async -> Bool {
for await result in Transaction.currentEntitlements {
guard case .verified(let transaction) = result else {
return false
}
return transaction.ownershipType == .purchased
}
return false
}
/// Listents to purchase intents and attempts to purchase the product.
func listenToPurchaseIntents() async {
for await intent in PurchaseIntent.intents {
await purchase(intent.product)
}
}
/// Attempts to restore purchases by syncing transactions.
func restorePurchases() async -> Result<Bool, Error> {
do {
try await AppStore.sync()
return .success(true)
} catch {
return .failure(error)
}
}
}