| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354 |
- //
- // 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<Void, Never>?
-
- 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 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<String>()
- 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<String>, 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<Transaction>) 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)
- }
- }
|