LNPurchaseManager.swift 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  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 updateWallet(diamond: Double? = nil, coin: Double? = nil) {
  74. if let diamond {
  75. myWalletInfo.diamond = diamond
  76. }
  77. if let coin {
  78. myWalletInfo.coin = coin
  79. }
  80. notifyWalletInfoChanged()
  81. }
  82. func loadGoodsList(currencyType: LNCurrencyType,
  83. queue: DispatchQueue = .main,
  84. handler: @escaping ([LNPurchaseGoodsVO]?) -> Void)
  85. {
  86. lock.lock()
  87. let cache = goodsCache[currencyType]
  88. lock.unlock()
  89. var cacheAvailable = false
  90. if let cache, !cache.isEmpty {
  91. // 如果有缓存,先返回缓存,并且触发更新(但是不回调)
  92. cacheAvailable = true
  93. handler(cache)
  94. }
  95. LNHttpManager.shared.getGoodsList(currencyType: currencyType) { [weak self] res, err in
  96. guard let self else { return }
  97. if let res, err == nil {
  98. var codes = Set<String>()
  99. lock.lock()
  100. goodsCache[currencyType] = res.items
  101. res.items.forEach {
  102. codes.insert($0.code)
  103. }
  104. lock.unlock()
  105. getRechargeProductInfo(ids: codes, handler: nil)
  106. RechargeManager.shared.loadRechargeProducts(productIds: codes.sorted(), completion: nil)
  107. }
  108. if !cacheAvailable {
  109. // 如果前面没有缓存,则在更新时触发回调
  110. queue.asyncIfNotGlobal {
  111. handler(res?.items)
  112. }
  113. }
  114. }
  115. }
  116. }
  117. extension LNPurchaseManager {
  118. func exchangeCurrency(exchangeType: LNExchangeType,
  119. amount: Double, queue: DispatchQueue = .main,
  120. handler: @escaping (Bool) -> Void)
  121. {
  122. let (from, to) = exchangeType.exchangeType
  123. LNHttpManager.shared.exchangeCurrency(from: from, to: to, amount: amount) { [weak self] err in
  124. guard let self else { return }
  125. queue.asyncIfNotGlobal {
  126. handler(err == nil)
  127. }
  128. if let err {
  129. showToast(err.errorDesc)
  130. } else {
  131. reloadWalletInfo()
  132. }
  133. }
  134. }
  135. func expectResult(exchangeType: LNExchangeType,
  136. count: Double, queue: DispatchQueue = .main,
  137. handler: @escaping (Double?) -> Void) {
  138. let (from, to) = exchangeType.exchangeType
  139. LNHttpManager.shared.getExchangeExpectResult(from: from, to: to, count: count) { res, err in
  140. queue.asyncIfNotGlobal {
  141. handler(res?.amount)
  142. }
  143. if let err {
  144. showToast(err.errorDesc)
  145. }
  146. }
  147. }
  148. }
  149. extension LNPurchaseManager: LNAccountManagerNotify {
  150. func onUserLogin() {
  151. reloadWalletInfo()
  152. // 拉取新的商品列表
  153. loadGoodsList(currencyType: .coin) { _ in }
  154. loadGoodsList(currencyType: .diamond) { _ in }
  155. // restoreCompletedTransactions()
  156. // startObservingTransactionUpdates()
  157. }
  158. func onUserLogout() {
  159. myWalletInfo = LNUserWalletInfo()
  160. transactionListeningTask?.cancel()
  161. transactionListeningTask = nil
  162. }
  163. }
  164. extension LNPurchaseManager {
  165. func purchaseProduct(goods: LNPurchaseGoodsVO) {
  166. guard AppStore.canMakePayments else {
  167. log("In-app purchase is not enable")
  168. notifyPurchaseResult(err: .paymentInvalid)
  169. return
  170. }
  171. getRechargeProductInfo(ids: [goods.code]) { [weak self] list in
  172. guard let self else { return }
  173. guard let product = list.first else {
  174. log("product not found for \(goods.code)")
  175. notifyPurchaseResult(err: .productNotFound)
  176. return
  177. }
  178. LNHttpManager.shared.createPurchase(id: goods.id) { [weak self] res, err in
  179. guard let self else { return }
  180. guard let res, err == nil else {
  181. log("create order failed \(String(describing: err?.errorDesc))")
  182. notifyPurchaseResult(err: .createOrderfailed)
  183. return
  184. }
  185. doPurchase(orderId: res.result, product: product)
  186. }
  187. }
  188. }
  189. private func doPurchase(orderId: String, product: Product) {
  190. Task { [weak self] in
  191. guard let self else { return }
  192. do {
  193. log("start puchase order \(orderId) product \(product.id)")
  194. let uuid = Product.PurchaseOption.appAccountToken(UUID(uuidString: orderId)!)
  195. let result = try await product.purchase(options: [uuid])
  196. switch result {
  197. case .success(let verification):
  198. // 验证收据
  199. await checkTransationVerify(result: verification)
  200. case .userCancelled:
  201. log("puchase order \(orderId) product \(product.id) user cancelled")
  202. notifyPurchaseResult(err: .paymentCancelled)
  203. case .pending:
  204. break
  205. @unknown default:
  206. log("puchase order \(orderId) product \(product.id) return unknown err")
  207. notifyPurchaseResult(err: .unknownError(nil))
  208. }
  209. } catch (let err) {
  210. log("puchase order \(orderId) product \(product.id) return err \(err)")
  211. notifyPurchaseResult(err: .paymentFailed(err))
  212. }
  213. }
  214. }
  215. private func restoreCompletedTransactions() {
  216. Task { [weak self] in
  217. guard let self else { return }
  218. for await result in Transaction.currentEntitlements {
  219. // 验证收据
  220. await checkTransationVerify(result: result)
  221. }
  222. }
  223. }
  224. private func startObservingTransactionUpdates() {
  225. // 避免重复创建Task
  226. guard transactionListeningTask == nil else { return }
  227. transactionListeningTask = Task(priority: .background) { [weak self] in
  228. guard let self = self else { return }
  229. // 持续监听所有交易更新(异步序列)
  230. for await transactionUpdate in Transaction.updates {
  231. // 验证收据
  232. await checkTransationVerify(result: transactionUpdate)
  233. }
  234. }
  235. }
  236. private func getRechargeProductInfo(ids: Set<String>, handler: (([Product]) -> Void)?) {
  237. let validIds = ids.filter { !$0.isEmpty }
  238. guard !validIds.isEmpty else {
  239. handler?([])
  240. return
  241. }
  242. Task {
  243. guard AppStore.canMakePayments else {
  244. handler?([])
  245. return
  246. }
  247. // 检查缓存
  248. let cachedProducts = validIds.compactMap { productMap[$0] }
  249. if cachedProducts.count == validIds.count {
  250. handler?(cachedProducts)
  251. return
  252. }
  253. do {
  254. let products = try await Product.products(for: Set(ids))
  255. guard !products.isEmpty else {
  256. handler?([])
  257. return
  258. }
  259. products.forEach {
  260. productMap[$0.id] = $0
  261. }
  262. handler?(products)
  263. } catch (_) {
  264. handler?([])
  265. }
  266. }
  267. }
  268. private func checkTransationVerify(result: VerificationResult<Transaction>) async {
  269. switch result {
  270. case .unverified(let transaction, let error):
  271. if let orderId = transaction.appAccountToken?.uuidString {
  272. log("puchase order \(orderId) product \(transaction.productID) user unverified err \(error)")
  273. }
  274. await transaction.finish()
  275. notifyPurchaseResult(err: .receiptVerifyFailed)
  276. case .verified(let transaction):
  277. guard let orderId = transaction.appAccountToken?.uuidString else {
  278. notifyPurchaseResult(err: .receiptVerifyFailed)
  279. return
  280. }
  281. log("puchase order \(orderId) product \(transaction.productID) user verified")
  282. guard let receiptURL = Bundle.main.appStoreReceiptURL,
  283. FileManager.default.fileExists(atPath: receiptURL.path),
  284. let receiptData = try? Data(contentsOf: receiptURL)
  285. else {
  286. log("can't find appStore Receipt File data")
  287. notifyPurchaseResult(err: .receiptParseFailed)
  288. return
  289. }
  290. let receiptBase64 = receiptData.base64EncodedString()
  291. log("start verify order \(orderId)")
  292. LNHttpManager.shared.verifyPurchase(orderId: orderId, receipt: receiptBase64) { [weak self] err in
  293. guard let self else { return }
  294. log("verify order \(orderId) return err \(String(describing: err?.errorDesc))")
  295. notifyPurchaseResult(err: err == nil ? nil : .receiptVerifyFailed)
  296. }
  297. }
  298. }
  299. }
  300. extension LNPurchaseManager {
  301. private func notifyWalletInfoChanged() {
  302. let info = myWalletInfo
  303. LNEventDeliver.notifyEvent { ($0 as? LNPurchaseManagerNotify)?.onUserWalletInfoChanged(info: info) }
  304. }
  305. private func notifyPurchaseResult(err: LNPurchaseError?) {
  306. LNEventDeliver.notifyEvent {
  307. ($0 as? LNPurchaseManagerNotify)?.onUserPurchaseResult(err: err)
  308. }
  309. }
  310. }
  311. extension LNPurchaseManager {
  312. func log(_ items: Any...,) {
  313. Log.w("-----> LNPurchaseManager <----- ", items)
  314. }
  315. }