LNPurchaseManager.swift 13 KB

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