LNPurchaseManager.swift 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  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. func getBankAccountStatus(queue: DispatchQueue = .main, handler: @escaping(Bool) -> Void) {
  149. LNHttpManager.shared.getBankAccountStatus { res, err in
  150. queue.asyncIfNotGlobal {
  151. handler(res?.verifiedIdentity == true)
  152. }
  153. }
  154. }
  155. }
  156. extension LNPurchaseManager: LNAccountManagerNotify {
  157. func onUserLogin() {
  158. reloadWalletInfo()
  159. // 拉取新的商品列表
  160. loadGoodsList(currencyType: .coin) { _ in }
  161. loadGoodsList(currencyType: .diamond) { _ in }
  162. // restoreCompletedTransactions()
  163. // startObservingTransactionUpdates()
  164. }
  165. func onUserLogout() {
  166. myWalletInfo = LNUserWalletInfo()
  167. transactionListeningTask?.cancel()
  168. transactionListeningTask = nil
  169. }
  170. }
  171. extension LNPurchaseManager {
  172. func purchaseProduct(goods: LNPurchaseGoodsVO) {
  173. guard AppStore.canMakePayments else {
  174. log("In-app purchase is not enable")
  175. notifyPurchaseResult(err: .paymentInvalid)
  176. return
  177. }
  178. getRechargeProductInfo(ids: [goods.code]) { [weak self] list in
  179. guard let self else { return }
  180. guard let product = list.first else {
  181. log("product not found for \(goods.code)")
  182. notifyPurchaseResult(err: .productNotFound)
  183. return
  184. }
  185. LNHttpManager.shared.createPurchase(id: goods.id) { [weak self] res, err in
  186. guard let self else { return }
  187. guard let res, err == nil else {
  188. log("create order failed \(String(describing: err?.errorDesc))")
  189. notifyPurchaseResult(err: .createOrderfailed)
  190. return
  191. }
  192. doPurchase(orderId: res.result, product: product)
  193. }
  194. }
  195. }
  196. private func doPurchase(orderId: String, product: Product) {
  197. Task { [weak self] in
  198. guard let self else { return }
  199. do {
  200. log("start puchase order \(orderId) product \(product.id)")
  201. let uuid = Product.PurchaseOption.appAccountToken(UUID(uuidString: orderId)!)
  202. let result = try await product.purchase(options: [uuid])
  203. switch result {
  204. case .success(let verification):
  205. // 验证收据
  206. await checkTransationVerify(result: verification)
  207. case .userCancelled:
  208. log("puchase order \(orderId) product \(product.id) user cancelled")
  209. notifyPurchaseResult(err: .paymentCancelled)
  210. case .pending:
  211. break
  212. @unknown default:
  213. log("puchase order \(orderId) product \(product.id) return unknown err")
  214. notifyPurchaseResult(err: .unknownError(nil))
  215. }
  216. } catch (let err) {
  217. log("puchase order \(orderId) product \(product.id) return err \(err)")
  218. notifyPurchaseResult(err: .paymentFailed(err))
  219. }
  220. }
  221. }
  222. private func restoreCompletedTransactions() {
  223. Task { [weak self] in
  224. guard let self else { return }
  225. for await result in Transaction.currentEntitlements {
  226. // 验证收据
  227. await checkTransationVerify(result: result)
  228. }
  229. }
  230. }
  231. private func startObservingTransactionUpdates() {
  232. // 避免重复创建Task
  233. guard transactionListeningTask == nil else { return }
  234. transactionListeningTask = Task(priority: .background) { [weak self] in
  235. guard let self = self else { return }
  236. // 持续监听所有交易更新(异步序列)
  237. for await transactionUpdate in Transaction.updates {
  238. // 验证收据
  239. await checkTransationVerify(result: transactionUpdate)
  240. }
  241. }
  242. }
  243. private func getRechargeProductInfo(ids: Set<String>, handler: (([Product]) -> Void)?) {
  244. let validIds = ids.filter { !$0.isEmpty }
  245. guard !validIds.isEmpty else {
  246. handler?([])
  247. return
  248. }
  249. Task {
  250. guard AppStore.canMakePayments else {
  251. handler?([])
  252. return
  253. }
  254. // 检查缓存
  255. let cachedProducts = validIds.compactMap { productMap[$0] }
  256. if cachedProducts.count == validIds.count {
  257. handler?(cachedProducts)
  258. return
  259. }
  260. do {
  261. let products = try await Product.products(for: Set(ids))
  262. guard !products.isEmpty else {
  263. handler?([])
  264. return
  265. }
  266. products.forEach {
  267. productMap[$0.id] = $0
  268. }
  269. handler?(products)
  270. } catch (_) {
  271. handler?([])
  272. }
  273. }
  274. }
  275. private func checkTransationVerify(result: VerificationResult<Transaction>) async {
  276. switch result {
  277. case .unverified(let transaction, let error):
  278. if let orderId = transaction.appAccountToken?.uuidString {
  279. log("puchase order \(orderId) product \(transaction.productID) user unverified err \(error)")
  280. }
  281. await transaction.finish()
  282. notifyPurchaseResult(err: .receiptVerifyFailed)
  283. case .verified(let transaction):
  284. guard let orderId = transaction.appAccountToken?.uuidString else {
  285. notifyPurchaseResult(err: .receiptVerifyFailed)
  286. return
  287. }
  288. log("puchase order \(orderId) product \(transaction.productID) user verified")
  289. guard let receiptURL = Bundle.main.appStoreReceiptURL,
  290. FileManager.default.fileExists(atPath: receiptURL.path),
  291. let receiptData = try? Data(contentsOf: receiptURL)
  292. else {
  293. log("can't find appStore Receipt File data")
  294. notifyPurchaseResult(err: .receiptParseFailed)
  295. return
  296. }
  297. let receiptBase64 = receiptData.base64EncodedString()
  298. log("start verify order \(orderId)")
  299. LNHttpManager.shared.verifyPurchase(orderId: orderId, receipt: receiptBase64) { [weak self] err in
  300. guard let self else { return }
  301. log("verify order \(orderId) return err \(String(describing: err?.errorDesc))")
  302. notifyPurchaseResult(err: err == nil ? nil : .receiptVerifyFailed)
  303. }
  304. }
  305. }
  306. }
  307. extension LNPurchaseManager {
  308. private func notifyWalletInfoChanged() {
  309. let info = myWalletInfo
  310. LNEventDeliver.notifyEvent { ($0 as? LNPurchaseManagerNotify)?.onUserWalletInfoChanged(info: info) }
  311. }
  312. private func notifyPurchaseResult(err: LNPurchaseError?) {
  313. LNEventDeliver.notifyEvent {
  314. ($0 as? LNPurchaseManagerNotify)?.onUserPurchaseResult(err: err)
  315. }
  316. }
  317. }
  318. extension LNPurchaseManager {
  319. func log(_ items: Any...,) {
  320. Log.w("-----> LNPurchaseManager <----- ", items)
  321. }
  322. }