// // LNPurchaseManager.swift // Lanu // // Created by OneeChan on 2025/11/6. // import Foundation import StoreKit var myWalletInfo: LNUserWalletInfo { LNPurchaseManager.shared.myWalletInfo } protocol LNPurchaseManagerNotify { func onUserWalletInfoChanged(info: LNUserWalletInfo) func onUserPurchaseResult(err: LNPurchaseError?) } extension LNPurchaseManagerNotify { func onUserWalletInfoChanged(info: LNUserWalletInfo) {} func onUserPurchaseResult(err: LNPurchaseError?) {} } enum LNPurchaseError: LocalizedError { case productNotFound case createOrderfailed case paymentInvalid case paymentCancelled case paymentFailed(Error) case receiptVerifyFailed case receiptParseFailed case unknownError(Error?) var errorDesc: String? { switch self { case .productNotFound: return .init(key: "A00195") case .createOrderfailed: return .init(key: "A00196") case .paymentInvalid: return .init(key: "A00281") case .paymentCancelled: return .init(key: "A00197") case .paymentFailed(let error): return .init(key: "A00198", error.localizedDescription) case .receiptVerifyFailed: return .init(key: "A00199") case .receiptParseFailed: return .init(key: "A00282") case .unknownError(let error): return .init(key: "A00283", error?.localizedDescription ?? "") } } } class LNPurchaseManager { static let shared = LNPurchaseManager() private(set) var myWalletInfo: LNUserWalletInfo = LNUserWalletInfo() private let lock = NSLock() private var goodsCache: [LNCurrencyType: [LNPurchaseGoodsVO]] = [:] private var productMap: [String: Product] = [:] private var transactionListeningTask: Task? private init() { LNEventDeliver.addObserver(self) } func goodsFro(_ code: String) -> (LNCurrencyType, LNPurchaseGoodsVO)? { for (type, list) in goodsCache { if let goods = list.first(where: { $0.code == code }) { return (type, goods) } } return nil } func reloadWalletInfo() { LNHttpManager.shared.getWalletInfo { [weak self] res, err in guard let self else { return } guard let res, err == nil else { return } myWalletInfo = LNUserWalletInfo() myWalletInfo.diamond = res.diamond myWalletInfo.coin = res.goldCoin myWalletInfo.bean = res.beanTotal myWalletInfo.unsettledBean = res.unsettledBean myWalletInfo.availableBean = res.availableBean notifyWalletInfoChanged() } } func updateWallet(diamond: Double? = nil, coin: Double? = nil) { if let diamond { myWalletInfo.diamond = diamond } if let coin { myWalletInfo.coin = coin } notifyWalletInfoChanged() } func loadGoodsList(currencyType: LNCurrencyType, queue: DispatchQueue = .main, handler: @escaping ([LNPurchaseGoodsVO]?) -> Void) { lock.lock() let cache = goodsCache[currencyType] lock.unlock() var cacheAvailable = false if let cache, !cache.isEmpty { // 如果有缓存,先返回缓存,并且触发更新(但是不回调) cacheAvailable = true handler(cache) } LNHttpManager.shared.getGoodsList(currencyType: currencyType) { [weak self] res, err in guard let self else { return } if let res, err == nil { var codes = Set() lock.lock() goodsCache[currencyType] = res.items res.items.forEach { codes.insert($0.code) } lock.unlock() getRechargeProductInfo(ids: codes, handler: nil) RechargeManager.shared.loadRechargeProducts(productIds: codes.sorted(), completion: nil) } if !cacheAvailable { // 如果前面没有缓存,则在更新时触发回调 queue.asyncIfNotGlobal { handler(res?.items) } } } } } extension LNPurchaseManager { func exchangeCurrency(exchangeType: LNExchangeType, amount: Double, queue: DispatchQueue = .main, handler: @escaping (Bool) -> Void) { let (from, to) = exchangeType.exchangeType LNHttpManager.shared.exchangeCurrency(from: from, to: to, amount: amount) { [weak self] err in guard let self else { return } queue.asyncIfNotGlobal { handler(err == nil) } if let err { showToast(err.errorDesc) } else { reloadWalletInfo() } } } func expectResult(exchangeType: LNExchangeType, count: Double, queue: DispatchQueue = .main, handler: @escaping (Double?) -> Void) { let (from, to) = exchangeType.exchangeType LNHttpManager.shared.getExchangeExpectResult(from: from, to: to, count: count) { res, err in queue.asyncIfNotGlobal { handler(res?.amount) } if let err { showToast(err.errorDesc) } } } } extension LNPurchaseManager: LNAccountManagerNotify { func onUserLogin() { reloadWalletInfo() // 拉取新的商品列表 loadGoodsList(currencyType: .coin) { _ in } loadGoodsList(currencyType: .diamond) { _ in } // restoreCompletedTransactions() // startObservingTransactionUpdates() } func onUserLogout() { myWalletInfo = LNUserWalletInfo() transactionListeningTask?.cancel() transactionListeningTask = nil } } extension LNPurchaseManager { func purchaseProduct(goods: LNPurchaseGoodsVO) { guard AppStore.canMakePayments else { log("In-app purchase is not enable") notifyPurchaseResult(err: .paymentInvalid) return } getRechargeProductInfo(ids: [goods.code]) { [weak self] list in guard let self else { return } guard let product = list.first else { log("product not found for \(goods.code)") notifyPurchaseResult(err: .productNotFound) return } LNHttpManager.shared.createPurchase(id: goods.id) { [weak self] res, err in guard let self else { return } guard let res, err == nil else { log("create order failed \(String(describing: err?.errorDesc))") notifyPurchaseResult(err: .createOrderfailed) return } doPurchase(orderId: res.result, product: product) } } } private func doPurchase(orderId: String, product: Product) { Task { [weak self] in guard let self else { return } do { log("start puchase order \(orderId) product \(product.id)") let uuid = Product.PurchaseOption.appAccountToken(UUID(uuidString: orderId)!) let result = try await product.purchase(options: [uuid]) switch result { case .success(let verification): // 验证收据 await checkTransationVerify(result: verification) case .userCancelled: log("puchase order \(orderId) product \(product.id) user cancelled") notifyPurchaseResult(err: .paymentCancelled) case .pending: break @unknown default: log("puchase order \(orderId) product \(product.id) return unknown err") notifyPurchaseResult(err: .unknownError(nil)) } } catch (let err) { log("puchase order \(orderId) product \(product.id) return err \(err)") notifyPurchaseResult(err: .paymentFailed(err)) } } } private func restoreCompletedTransactions() { Task { [weak self] in guard let self else { return } for await result in Transaction.currentEntitlements { // 验证收据 await checkTransationVerify(result: result) } } } private func startObservingTransactionUpdates() { // 避免重复创建Task guard transactionListeningTask == nil else { return } transactionListeningTask = Task(priority: .background) { [weak self] in guard let self = self else { return } // 持续监听所有交易更新(异步序列) for await transactionUpdate in Transaction.updates { // 验证收据 await checkTransationVerify(result: transactionUpdate) } } } private func getRechargeProductInfo(ids: Set, handler: (([Product]) -> Void)?) { let validIds = ids.filter { !$0.isEmpty } guard !validIds.isEmpty else { handler?([]) return } Task { guard AppStore.canMakePayments else { handler?([]) return } // 检查缓存 let cachedProducts = validIds.compactMap { productMap[$0] } if cachedProducts.count == validIds.count { handler?(cachedProducts) return } do { let products = try await Product.products(for: Set(ids)) guard !products.isEmpty else { handler?([]) return } products.forEach { productMap[$0.id] = $0 } handler?(products) } catch (_) { handler?([]) } } } private func checkTransationVerify(result: VerificationResult) async { switch result { case .unverified(let transaction, let error): if let orderId = transaction.appAccountToken?.uuidString { log("puchase order \(orderId) product \(transaction.productID) user unverified err \(error)") } await transaction.finish() notifyPurchaseResult(err: .receiptVerifyFailed) case .verified(let transaction): guard let orderId = transaction.appAccountToken?.uuidString else { notifyPurchaseResult(err: .receiptVerifyFailed) return } log("puchase order \(orderId) product \(transaction.productID) user verified") guard let receiptURL = Bundle.main.appStoreReceiptURL, FileManager.default.fileExists(atPath: receiptURL.path), let receiptData = try? Data(contentsOf: receiptURL) else { log("can't find appStore Receipt File data") notifyPurchaseResult(err: .receiptParseFailed) return } let receiptBase64 = receiptData.base64EncodedString() log("start verify order \(orderId)") LNHttpManager.shared.verifyPurchase(orderId: orderId, receipt: receiptBase64) { [weak self] err in guard let self else { return } log("verify order \(orderId) return err \(String(describing: err?.errorDesc))") notifyPurchaseResult(err: err == nil ? nil : .receiptVerifyFailed) } } } } extension LNPurchaseManager { private func notifyWalletInfoChanged() { let info = myWalletInfo LNEventDeliver.notifyEvent { ($0 as? LNPurchaseManagerNotify)?.onUserWalletInfoChanged(info: info) } } private func notifyPurchaseResult(err: LNPurchaseError?) { LNEventDeliver.notifyEvent { ($0 as? LNPurchaseManagerNotify)?.onUserPurchaseResult(err: err) } } } extension LNPurchaseManager { func log(_ items: Any...,) { Log.w("-----> LNPurchaseManager <----- ", items) } }