LNPurchaseManager.swift 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. //
  2. // LNPurchaseManager.swift
  3. // Lanu
  4. //
  5. // Created by OneeChan on 2025/11/6.
  6. //
  7. import Foundation
  8. import StoreKit
  9. var myWalletInfo: LNUserWalletInfo {
  10. LNPurchaseManager.shared.myWalletInfo
  11. }
  12. protocol LNPurchaseManagerNotify {
  13. func onUserWalletInfoChanged(info: LNUserWalletInfo)
  14. func onUserPurchaseResult(err: LNPurchaseError?)
  15. }
  16. extension LNPurchaseManagerNotify {
  17. func onUserWalletInfoChanged(info: LNUserWalletInfo) {}
  18. func onUserPurchaseResult(err: LNPurchaseError?) {}
  19. }
  20. enum LNPurchaseError: LocalizedError {
  21. case productNotFound
  22. case createOrderfailed
  23. case paymentInvalid
  24. case paymentCancelled
  25. case paymentFailed(Error)
  26. case receiptVerifyFailed
  27. case receiptParseFailed
  28. case unknownError(Error?)
  29. var errorDesc: String? {
  30. switch self {
  31. case .productNotFound: return .init(key: "A00195")
  32. case .createOrderfailed: return .init(key: "A00196")
  33. case .paymentInvalid: return .init(key: "A00281")
  34. case .paymentCancelled: return .init(key: "A00197")
  35. case .paymentFailed(let error): return .init(key: "A00198", error.localizedDescription)
  36. case .receiptVerifyFailed: return .init(key: "A00199")
  37. case .receiptParseFailed: return .init(key: "A00282")
  38. case .unknownError(let error): return .init(key: "A00283", error?.localizedDescription ?? "")
  39. }
  40. }
  41. }
  42. class LNPurchaseManager {
  43. static let shared = LNPurchaseManager()
  44. private(set) var myWalletInfo: LNUserWalletInfo = LNUserWalletInfo()
  45. private let lock = NSLock()
  46. private var goodsCache: [LNCurrencyType: [LNPurchaseGoodsVO]] = [:]
  47. private var productMap: [String: Product] = [:]
  48. private var transactionListeningTask: Task<Void, Never>?
  49. private init() {
  50. LNEventDeliver.addObserver(self)
  51. }
  52. func goodsFro(_ code: String) -> (LNCurrencyType, LNPurchaseGoodsVO)? {
  53. for (type, list) in goodsCache {
  54. if let goods = list.first(where: { $0.code == code }) {
  55. return (type, goods)
  56. }
  57. }
  58. return nil
  59. }
  60. func reloadWalletInfo() {
  61. LNHttpManager.shared.getWalletInfo { [weak self] res, err in
  62. guard let self else { return }
  63. guard let res, err == nil else { return }
  64. myWalletInfo = LNUserWalletInfo()
  65. myWalletInfo.diamond = res.diamond
  66. myWalletInfo.coin = res.goldCoin
  67. myWalletInfo.bean = res.beanTotal
  68. myWalletInfo.unsettledBean = res.unsettledBean
  69. myWalletInfo.availableBean = res.availableBean
  70. notifyWalletInfoChanged()
  71. }
  72. }
  73. func loadGoodsList(currencyType: LNCurrencyType,
  74. queue: DispatchQueue = .main,
  75. handler: @escaping ([LNPurchaseGoodsVO]?) -> Void)
  76. {
  77. lock.lock()
  78. let cache = goodsCache[currencyType]
  79. lock.unlock()
  80. var cacheAvailable = false
  81. if let cache, !cache.isEmpty {
  82. // 如果有缓存,先返回缓存,并且触发更新(但是不回调)
  83. cacheAvailable = true
  84. handler(cache)
  85. }
  86. LNHttpManager.shared.getGoodsList(currencyType: currencyType) { [weak self] res, err in
  87. guard let self else { return }
  88. if let res, err == nil {
  89. var codes = Set<String>()
  90. lock.lock()
  91. goodsCache[currencyType] = res.items
  92. res.items.forEach {
  93. codes.insert($0.code)
  94. }
  95. lock.unlock()
  96. getRechargeProductInfo(ids: codes, handler: nil)
  97. RechargeManager.shared.loadRechargeProducts(productIds: codes.sorted(), completion: nil)
  98. }
  99. if !cacheAvailable {
  100. // 如果前面没有缓存,则在更新时触发回调
  101. queue.asyncIfNotGlobal {
  102. handler(res?.items)
  103. }
  104. }
  105. }
  106. }
  107. }
  108. extension LNPurchaseManager {
  109. func exchangeCurrency(exchangeType: LNExchangeType,
  110. amount: Double, queue: DispatchQueue = .main,
  111. handler: @escaping (Bool) -> Void)
  112. {
  113. let (from, to) = exchangeType.exchangeType
  114. LNHttpManager.shared.exchangeCurrency(from: from, to: to, amount: amount) { [weak self] err in
  115. guard let self else { return }
  116. queue.asyncIfNotGlobal {
  117. handler(err == nil)
  118. }
  119. if let err {
  120. showToast(err.errorDesc)
  121. } else {
  122. reloadWalletInfo()
  123. }
  124. }
  125. }
  126. func expectResult(exchangeType: LNExchangeType,
  127. count: Double, queue: DispatchQueue = .main,
  128. handler: @escaping (Double?) -> Void) {
  129. let (from, to) = exchangeType.exchangeType
  130. LNHttpManager.shared.getExchangeExpectResult(from: from, to: to, count: count) { res, err in
  131. queue.asyncIfNotGlobal {
  132. handler(res?.amount)
  133. }
  134. if let err {
  135. showToast(err.errorDesc)
  136. }
  137. }
  138. }
  139. }
  140. extension LNPurchaseManager: LNAccountManagerNotify {
  141. func onUserLogin() {
  142. reloadWalletInfo()
  143. // 拉取新的商品列表
  144. loadGoodsList(currencyType: .coin) { _ in }
  145. loadGoodsList(currencyType: .diamond) { _ in }
  146. // restoreCompletedTransactions()
  147. // startObservingTransactionUpdates()
  148. }
  149. func onUserLogout() {
  150. myWalletInfo = LNUserWalletInfo()
  151. transactionListeningTask?.cancel()
  152. transactionListeningTask = nil
  153. }
  154. }
  155. extension LNPurchaseManager {
  156. func purchaseProduct(goods: LNPurchaseGoodsVO) {
  157. guard AppStore.canMakePayments else {
  158. log("In-app purchase is not enable")
  159. notifyPurchaseResult(err: .paymentInvalid)
  160. return
  161. }
  162. getRechargeProductInfo(ids: [goods.code]) { [weak self] list in
  163. guard let self else { return }
  164. guard let product = list.first else {
  165. log("product not found for \(goods.code)")
  166. notifyPurchaseResult(err: .productNotFound)
  167. return
  168. }
  169. LNHttpManager.shared.createPurchase(id: goods.id) { [weak self] res, err in
  170. guard let self else { return }
  171. guard let res, err == nil else {
  172. log("create order failed \(String(describing: err?.errorDesc))")
  173. notifyPurchaseResult(err: .createOrderfailed)
  174. return
  175. }
  176. doPurchase(orderId: res.result, product: product)
  177. }
  178. }
  179. }
  180. private func doPurchase(orderId: String, product: Product) {
  181. Task { [weak self] in
  182. guard let self else { return }
  183. do {
  184. log("start puchase order \(orderId) product \(product.id)")
  185. let uuid = Product.PurchaseOption.appAccountToken(UUID(uuidString: orderId)!)
  186. let result = try await product.purchase(options: [uuid])
  187. switch result {
  188. case .success(let verification):
  189. // 验证收据
  190. await checkTransationVerify(result: verification)
  191. case .userCancelled:
  192. log("puchase order \(orderId) product \(product.id) user cancelled")
  193. notifyPurchaseResult(err: .paymentCancelled)
  194. case .pending:
  195. break
  196. @unknown default:
  197. log("puchase order \(orderId) product \(product.id) return unknown err")
  198. notifyPurchaseResult(err: .unknownError(nil))
  199. }
  200. } catch (let err) {
  201. log("puchase order \(orderId) product \(product.id) return err \(err)")
  202. notifyPurchaseResult(err: .paymentFailed(err))
  203. }
  204. }
  205. }
  206. private func restoreCompletedTransactions() {
  207. Task { [weak self] in
  208. guard let self else { return }
  209. for await result in Transaction.currentEntitlements {
  210. // 验证收据
  211. await checkTransationVerify(result: result)
  212. }
  213. }
  214. }
  215. private func startObservingTransactionUpdates() {
  216. // 避免重复创建Task
  217. guard transactionListeningTask == nil else { return }
  218. transactionListeningTask = Task(priority: .background) { [weak self] in
  219. guard let self = self else { return }
  220. // 持续监听所有交易更新(异步序列)
  221. for await transactionUpdate in Transaction.updates {
  222. // 验证收据
  223. await checkTransationVerify(result: transactionUpdate)
  224. }
  225. }
  226. }
  227. private func getRechargeProductInfo(ids: Set<String>, handler: (([Product]) -> Void)?) {
  228. let validIds = ids.filter { !$0.isEmpty }
  229. guard !validIds.isEmpty else {
  230. handler?([])
  231. return
  232. }
  233. Task {
  234. guard AppStore.canMakePayments else {
  235. handler?([])
  236. return
  237. }
  238. // 检查缓存
  239. let cachedProducts = validIds.compactMap { productMap[$0] }
  240. if cachedProducts.count == validIds.count {
  241. handler?(cachedProducts)
  242. return
  243. }
  244. do {
  245. let products = try await Product.products(for: Set(ids))
  246. guard !products.isEmpty else {
  247. handler?([])
  248. return
  249. }
  250. products.forEach {
  251. productMap[$0.id] = $0
  252. }
  253. handler?(products)
  254. } catch (_) {
  255. handler?([])
  256. }
  257. }
  258. }
  259. private func checkTransationVerify(result: VerificationResult<Transaction>) async {
  260. switch result {
  261. case .unverified(let transaction, let error):
  262. if let orderId = transaction.appAccountToken?.uuidString {
  263. log("puchase order \(orderId) product \(transaction.productID) user unverified err \(error)")
  264. }
  265. await transaction.finish()
  266. notifyPurchaseResult(err: .receiptVerifyFailed)
  267. case .verified(let transaction):
  268. guard let orderId = transaction.appAccountToken?.uuidString else {
  269. notifyPurchaseResult(err: .receiptVerifyFailed)
  270. return
  271. }
  272. log("puchase order \(orderId) product \(transaction.productID) user verified")
  273. guard let receiptURL = Bundle.main.appStoreReceiptURL,
  274. FileManager.default.fileExists(atPath: receiptURL.path),
  275. let receiptData = try? Data(contentsOf: receiptURL)
  276. else {
  277. log("can't find appStore Receipt File data")
  278. notifyPurchaseResult(err: .receiptParseFailed)
  279. return
  280. }
  281. let receiptBase64 = receiptData.base64EncodedString()
  282. log("start verify order \(orderId)")
  283. LNHttpManager.shared.verifyPurchase(orderId: orderId, receipt: receiptBase64) { [weak self] err in
  284. guard let self else { return }
  285. log("verify order \(orderId) return err \(String(describing: err?.errorDesc))")
  286. notifyPurchaseResult(err: err == nil ? nil : .receiptVerifyFailed)
  287. }
  288. }
  289. }
  290. }
  291. extension LNPurchaseManager {
  292. private func notifyWalletInfoChanged() {
  293. let info = myWalletInfo
  294. LNEventDeliver.notifyEvent { ($0 as? LNPurchaseManagerNotify)?.onUserWalletInfoChanged(info: info) }
  295. }
  296. private func notifyPurchaseResult(err: LNPurchaseError?) {
  297. LNEventDeliver.notifyEvent {
  298. ($0 as? LNPurchaseManagerNotify)?.onUserPurchaseResult(err: err)
  299. }
  300. }
  301. }
  302. extension LNPurchaseManager {
  303. func log(_ items: Any...,) {
  304. Log.w("-----> LNPurchaseManager <----- ", items)
  305. }
  306. }