UTMDonateStore.swift 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. //
  2. // Copyright © 2024 osy. All rights reserved.
  3. //
  4. // Licensed under the Apache License, Version 2.0 (the "License");
  5. // you may not use this file except in compliance with the License.
  6. // You may obtain a copy of the License at
  7. //
  8. // http://www.apache.org/licenses/LICENSE-2.0
  9. //
  10. // Unless required by applicable law or agreed to in writing, software
  11. // distributed under the License is distributed on an "AS IS" BASIS,
  12. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. // See the License for the specific language governing permissions and
  14. // limitations under the License.
  15. //
  16. import Foundation
  17. import StoreKit
  18. @available(iOS 15, *)
  19. class UTMDonateStore: ObservableObject {
  20. typealias RenewalState = StoreKit.Product.SubscriptionInfo.RenewalState
  21. enum StoreError: Error {
  22. case failedVerification
  23. }
  24. let productImages: [String: String] = [
  25. "consumable.small": "switch.2",
  26. "consumable.medium": "memorychip",
  27. "consumable.large": "pc",
  28. "subscription.small": "sum",
  29. "subscription.medium": "function",
  30. "subscription.large": "opticaldisc",
  31. ]
  32. @Published private(set) var consumables: [Product]
  33. @Published private(set) var subscriptions: [Product]
  34. @Published private(set) var purchasedSubscriptions: [Product] = []
  35. @Published private(set) var subscriptionGroupStatus: RenewalState?
  36. @Published private(set) var isLoaded: Bool = false
  37. @Published private(set) var id: UUID = UUID()
  38. var updateListenerTask: Task<Void, Error>? = nil
  39. init() {
  40. //Initialize empty products, and then do a product request asynchronously to fill them in.
  41. consumables = []
  42. subscriptions = []
  43. //Start a transaction listener as close to app launch as possible so you don't miss any transactions.
  44. updateListenerTask = listenForTransactions()
  45. Task {
  46. //During store initialization, request products from the App Store.
  47. await requestProducts()
  48. //Deliver products that the customer purchases.
  49. await updateCustomerProductStatus()
  50. }
  51. }
  52. deinit {
  53. updateListenerTask?.cancel()
  54. }
  55. func listenForTransactions() -> Task<Void, Error> {
  56. return Task.detached {
  57. //Iterate through any transactions that don't come from a direct call to `purchase()`.
  58. for await result in Transaction.updates {
  59. do {
  60. let transaction = try self.checkVerified(result)
  61. //Deliver products to the user.
  62. await self.updateCustomerProductStatus()
  63. //Always finish a transaction.
  64. await transaction.finish()
  65. } catch {
  66. //StoreKit has a transaction that fails verification. Don't deliver content to the user.
  67. logger.error("Transaction failed verification")
  68. }
  69. }
  70. }
  71. }
  72. @MainActor
  73. func requestProducts() async {
  74. isLoaded = false
  75. do {
  76. let storeProducts = try await Product.products(for: productImages.keys)
  77. var newConsumables: [Product] = []
  78. var newSubscriptions: [Product] = []
  79. //Filter the products into categories based on their type.
  80. for product in storeProducts {
  81. switch product.type {
  82. case .consumable:
  83. newConsumables.append(product)
  84. case .autoRenewable:
  85. newSubscriptions.append(product)
  86. default:
  87. //Ignore this product.
  88. logger.error("Unknown product: \(product)")
  89. }
  90. }
  91. //Sort each product category by price, lowest to highest, to update the store.
  92. consumables = sortByPrice(newConsumables)
  93. subscriptions = sortByPrice(newSubscriptions)
  94. } catch {
  95. logger.error("Failed product request from the App Store server: \(error)")
  96. }
  97. isLoaded = true
  98. }
  99. func purchase(with action: () async throws -> Product.PurchaseResult) async throws -> Transaction? {
  100. //Begin purchasing the `Product` the user selects.
  101. let result = try await action()
  102. switch result {
  103. case .success(let verification):
  104. //Check whether the transaction is verified. If it isn't,
  105. //this function rethrows the verification error.
  106. let transaction = try checkVerified(verification)
  107. //The transaction is verified. Deliver content to the user.
  108. await updateCustomerProductStatus()
  109. //Always finish a transaction.
  110. await transaction.finish()
  111. return transaction
  112. case .userCancelled, .pending:
  113. return nil
  114. default:
  115. return nil
  116. }
  117. }
  118. func isPurchased(_ product: Product) async throws -> Bool {
  119. //Determine whether the user purchases a given product.
  120. switch product.type {
  121. case .autoRenewable:
  122. return purchasedSubscriptions.contains(product)
  123. default:
  124. return false
  125. }
  126. }
  127. func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
  128. //Check whether the JWS passes StoreKit verification.
  129. switch result {
  130. case .unverified:
  131. //StoreKit parses the JWS, but it fails verification.
  132. throw StoreError.failedVerification
  133. case .verified(let safe):
  134. //The result is verified. Return the unwrapped value.
  135. return safe
  136. }
  137. }
  138. @MainActor
  139. func updateCustomerProductStatus() async {
  140. var purchasedSubscriptions: [Product] = []
  141. //Iterate through all of the user's purchased products.
  142. for await result in Transaction.currentEntitlements {
  143. do {
  144. //Check whether the transaction is verified. If it isn’t, catch `failedVerification` error.
  145. let transaction = try checkVerified(result)
  146. //Check the `productType` of the transaction and get the corresponding product from the store.
  147. switch transaction.productType {
  148. case .autoRenewable:
  149. if let subscription = subscriptions.first(where: { $0.id == transaction.productID }) {
  150. purchasedSubscriptions.append(subscription)
  151. }
  152. default:
  153. break
  154. }
  155. } catch {
  156. logger.error("failed to update product status: \(error)")
  157. }
  158. }
  159. //Update the store information with auto-renewable subscription products.
  160. self.purchasedSubscriptions = purchasedSubscriptions
  161. //Check the `subscriptionGroupStatus` to learn the auto-renewable subscription state to determine whether the customer
  162. //is new (never subscribed), active, or inactive (expired subscription). This app has only one subscription
  163. //group, so products in the subscriptions array all belong to the same group. The statuses that
  164. //`product.subscription.status` returns apply to the entire subscription group.
  165. subscriptionGroupStatus = try? await subscriptions.first?.subscription?.status.first?.state
  166. }
  167. func sortByPrice(_ products: [Product]) -> [Product] {
  168. products.sorted(by: { return $0.price < $1.price })
  169. }
  170. }