|
|
@@ -6,6 +6,7 @@
|
|
|
//
|
|
|
|
|
|
import Foundation
|
|
|
+import StoreKit
|
|
|
|
|
|
|
|
|
var myWalletInfo: LNUserWalletInfo {
|
|
|
@@ -15,6 +16,36 @@ var myWalletInfo: LNUserWalletInfo {
|
|
|
|
|
|
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 errorDescription: String? {
|
|
|
+ switch self {
|
|
|
+ case .productNotFound: return "未找到对应的充值套餐"
|
|
|
+ case .createOrderfailed: return "订单创建失败"
|
|
|
+ case .paymentInvalid: return "当前设备不支持内购(请检查App Store账号)"
|
|
|
+ case .paymentCancelled: return "你取消了支付"
|
|
|
+ case .paymentFailed(let error): return "支付失败:\(error.localizedDescription)"
|
|
|
+ case .receiptVerifyFailed: return "订单验证失败"
|
|
|
+ case .receiptParseFailed: return "订单信息解析失败"
|
|
|
+ case .unknownError(let error): return "未知错误: \(error?.localizedDescription ?? "")"
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -24,12 +55,15 @@ class LNPurchaseManager {
|
|
|
private(set) var myWalletInfo: LNUserWalletInfo = LNUserWalletInfo()
|
|
|
|
|
|
private let lock = NSLock()
|
|
|
- private var goodsCache: [LNCurrencyType: [LNPurchaseGoodVO]] = [:]
|
|
|
+ private var goodsCache: [LNCurrencyType: [LNPurchaseGoodsVO]] = [:]
|
|
|
+ private var productMap: [String: Product] = [:]
|
|
|
+ private var transactionListeningTask: Task<Void, Never>?
|
|
|
|
|
|
private var exchangeConfig: LNCurrencyExchangeConfig?
|
|
|
|
|
|
private init() {
|
|
|
LNEventDeliver.addObserver(self)
|
|
|
+ refreshReceipt()
|
|
|
}
|
|
|
|
|
|
func reloadWalletInfo() {
|
|
|
@@ -45,7 +79,7 @@ class LNPurchaseManager {
|
|
|
|
|
|
func loadGoodsList(currencyType: LNCurrencyType,
|
|
|
queue: DispatchQueue = .main,
|
|
|
- handler: @escaping ([LNPurchaseGoodVO]?) -> Void)
|
|
|
+ handler: @escaping ([LNPurchaseGoodsVO]?) -> Void)
|
|
|
{
|
|
|
lock.lock()
|
|
|
let cache = goodsCache[currencyType]
|
|
|
@@ -60,9 +94,15 @@ class LNPurchaseManager {
|
|
|
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 {
|
|
|
// 如果前面没有缓存,则在更新时触发回调
|
|
|
@@ -72,7 +112,9 @@ class LNPurchaseManager {
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
+}
|
|
|
+
|
|
|
+extension LNPurchaseManager {
|
|
|
func exchangeCoinToDiamond(
|
|
|
amount: Double, queue: DispatchQueue = .main,
|
|
|
handler: @escaping (Bool) -> Void)
|
|
|
@@ -152,10 +194,180 @@ extension LNPurchaseManager: LNAccountManagerNotify {
|
|
|
|
|
|
// 拉取转换配置
|
|
|
getExchangeConfig()
|
|
|
+
|
|
|
+// 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?.errorDescription))")
|
|
|
+ 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?.errorDescription))")
|
|
|
+ notifyPurchaseResult(err: err == nil ? nil : .receiptVerifyFailed)
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private func refreshReceipt() {
|
|
|
+ // 沙盒环境需指定测试账号(可选,部分场景需要)
|
|
|
+ let request = SKReceiptRefreshRequest(receiptProperties: [:])
|
|
|
+
|
|
|
+ request.start()
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -164,4 +376,16 @@ extension LNPurchaseManager {
|
|
|
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)
|
|
|
+ }
|
|
|
}
|