// // TSPurchaseManager.swift // TSLiveWallpaper // // Created by 100Years on 2025/1/13. // import Foundation import StoreKit public enum PremiumPeriod: String, CaseIterable { case none = "" case week = "Week" case month = "Monthly" case year = "Yearly" case lifetime = "Lifetime" } public enum VipFreeNumType: String, CaseIterable { case generatePic = "kGeneratePicFreeNum" case aichat = "kAIChatFreeNum" case textGeneratePic = "kTextGeneratePicFreeNum" case picToPic = "kPicToPicFreeNum" } public struct PurchaseProduct { public let productId: String public let period: PremiumPeriod public init(productId: String, period: PremiumPeriod) { self.productId = productId self.period = period } } public enum PremiumRequestState { case none case loading case loadSuccess case loadFail case paying case paySuccess case payFail case restoreing case restoreSuccess case restoreFail case verifying case verifySuccess case verifyFail } public extension Notification.Name { static let kPurchasePrepared = Self.init("kPurchaseProductPrepared") static let kPurchaseDidChanged = Self.init("kPurchaseDidChanged") } private let kFreeNumKey = "kFreeNumKey" private let kPremiumExpiredInfoKey = "premiumExpiredInfoKey" typealias PurchaseStateChangeHandler = (_ manager: PurchaseManager, _ state: PremiumRequestState, _ object: Any?) -> Void let kPurchaseDefault = PurchaseManager.default public class PurchaseManager: NSObject { @objc public static let `default` = PurchaseManager() //苹果共享密钥 private let AppleSharedKey:String = "7fa595ea66a54b16b14ca2e2bf40f276" //商品信息 public lazy var purchaseProducts:[PurchaseProduct] = { return [ PurchaseProduct(productId: "101", period:.month), PurchaseProduct(productId: "102", period:.year), PurchaseProduct(productId: "103", period:.week), //PurchaseProduct(productId: "003", period: .lifetime), ] }() struct Config { static let verifyUrl = "https://buy.itunes.apple.com/verifyReceipt" static let sandBoxUrl = "https://sandbox.itunes.apple.com/verifyReceipt" } lazy var products: [SKProduct] = [] var onPurchaseStateChanged: PurchaseStateChangeHandler? // 会员信息 var vipInformation: [String: Any] = [:] // 免费使用会员的次数 var freeDict:[String:Int] = [:] //原始订单交易id dict var originalTransactionIdentifierDict:[String:String] = [:] override init() { super.init() SKPaymentQueue.default().add(self) if let info = UserDefaults.standard.object(forKey: kPremiumExpiredInfoKey) as? [String: Any] { vipInformation = info } initializeForFree() } public var expiredDate: Date? { guard let time = vipInformation["expireTime"] as? String else { return nil } return convertExpireDate(from: time) } public var expiredDateString: String { if vipType == .lifetime{ return "Life Time" } else { if let expDate = expiredDate { let format = DateFormatter() format.locale = .current format.dateFormat = "yyyy-MM-dd" return format.string(from: expDate) } else { return "--" } } } private func convertExpireDate(from string: String) -> Date? { if let ts = TimeInterval(string) { let date = Date(timeIntervalSince1970: ts / 1000) return date } return nil } @objc public var isVip: Bool { // #if DEBUG // return true // #endif guard let expiresDate = expiredDate else { return false } let todayStart = Calendar.current.startOfDay(for: Date()) let todayStartTs = todayStart.timeIntervalSince1970 let expiresTs = expiresDate.timeIntervalSince1970 return expiresTs > todayStartTs } public var vipType: PremiumPeriod { guard isVip, let type = vipInformation["type"] as? String else { return .none } return PremiumPeriod(rawValue: type) ?? .none } /// 过期时间: 1683277585000 毫秒 func updateExpireTime(_ timeInterval: String, for productId: String) { vipInformation.removeAll() vipInformation["expireTime"] = timeInterval vipInformation["productId"] = productId vipInformation["type"] = period(for: productId).rawValue UserDefaults.standard.set(vipInformation, forKey: kPremiumExpiredInfoKey) UserDefaults.standard.synchronize() NotificationCenter.default.post(name: .kPurchaseDidChanged, object: nil) } // 商品id对应的时间周期 func period(for productId: String) -> PremiumPeriod { return purchaseProducts.first(where: { $0.productId == productId })?.period ?? .none } // 时间周期对应的商品id func productId(for period: PremiumPeriod) -> String? { return purchaseProducts.first(where: { $0.period == period })?.productId } } // MARK: 商品信息 extension PurchaseManager { public func product(for period: PremiumPeriod) -> SKProduct? { return products.first(where: { $0.productIdentifier == productId(for: period) }) } // 商品价格 public func price(for period: PremiumPeriod) -> String? { guard let product = product(for: period) else { return nil } let formatter = NumberFormatter() formatter.formatterBehavior = NumberFormatter.Behavior.behavior10_4 formatter.numberStyle = .currency formatter.locale = product.priceLocale return formatter.string(from: product.price) } // public func originalPrice(for period: PremiumPeriod) -> String? { // guard let product = product(for: period) else { // return nil // } // switch period { // case .year, .lifetime: // // 5折 // let price = product.price.doubleValue // let calculatePrice = price * 2 // let originStr = String(format: "%.2f", calculatePrice) // let originPrice = NSDecimalNumber(string: originStr, locale: product.priceLocale) // // let formatter = NumberFormatter() // formatter.formatterBehavior = NumberFormatter.Behavior.behavior10_4 // formatter.numberStyle = .currency // formatter.locale = product.priceLocale // return formatter.string(from: originPrice) // default: // return nil // } // } } // MARK: 商品 & 订阅请求 extension PurchaseManager { ///请求商品 public func requestProducts() { if !products.isEmpty { purchase(self, didChaged: .loadSuccess, object: nil) } purchase(self, didChaged: .loading, object: nil) let productIdentifiers = Set(purchaseProducts.map({ $0.productId })) debugPrint("PurchaseManager requestProducts = \(productIdentifiers)") let request = SKProductsRequest(productIdentifiers: productIdentifiers) request.delegate = self request.start() } public func restorePremium() { purchase(self, didChaged: .restoreing, object: nil) SKPaymentQueue.default().restoreCompletedTransactions() debugPrint("PurchaseManager restoreCompletedTransactions") } /// 购买支付 public func pay(for period: PremiumPeriod) { guard SKPaymentQueue.canMakePayments() else { purchase(self, didChaged: .payFail, object: "Payment failed, please check your payment account") return } guard SKPaymentQueue.default().transactions.count <= 0 else { purchase(self, didChaged: .payFail, object: "You have outstanding orders that must be paid for before a new subscription can be placed.") restorePremium() return } if let product = product(for: period) { purchase(self, didChaged: .paying, object: nil) let payment = SKPayment(product: product) SKPaymentQueue.default().add(payment) debugPrint("PurchaseManager pay period = \(period)") }else{ purchase(self, didChaged: .payFail, object: "Payment failed, no this item") } } } // MARK: 商品回调 extension PurchaseManager: SKProductsRequestDelegate { public func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) { let products = response.products self.products = products purchase(self, didChaged: .loadSuccess, object: nil) NotificationCenter.default.post(name: .kPurchasePrepared, object: nil) debugPrint("PurchaseManager productsRequest didReceive = \(products)") } public func request(_ request: SKRequest, didFailWithError error: Error) { debugPrint("PurchaseManager productsRequest error = \(error)") purchase(self, didChaged: .loadFail, object: error.localizedDescription) } } // MARK: 订阅回调 extension PurchaseManager: SKPaymentTransactionObserver { public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) { debugPrint("PurchaseManager paymentQueue transactions.count = \(transactions.count)") // debugPrint("PurchaseManager paymentQueue transactions = \(transactions)") originalTransactionIdentifierDict.removeAll() // 因为只有订阅类的购买项 for transaction in transactions { // debugPrint("PurchaseManager paymentQueue transactions transactionIdentifier original= \(transaction.original?.transactionIdentifier)") // debugPrint("PurchaseManager paymentQueue transactions transactionIdentifier = \(transaction.transactionIdentifier)") // debugPrint("PurchaseManager paymentQueue transactions transactionIdentifier productIdentifier = \(transaction.payment.productIdentifier)") switch transaction.transactionState { case .purchasing: // Transaction is being added to the server queue. purchase(self, didChaged: .paying, object: nil) case .purchased: SKPaymentQueue.default().finishTransaction(transaction) //同样的原始订单,只处理一次. guard judgeWhether(transaction: transaction) else { break } // Transaction is in queue, user has been charged. Client should complete the transaction. #if DEBUG verifyPayResult(transaction: transaction, useSandBox: true) #else verifyPayResult(transaction: transaction, useSandBox: false) #endif case .failed: SKPaymentQueue.default().finishTransaction(transaction) // Transaction was cancelled or failed before being added to the server queue. var message = "Payment Failed" if let error = transaction.error as? SKError, error.code == SKError.paymentCancelled { message = "The subscription was canceled" } purchase(self, didChaged: .payFail, object: message) case .restored: SKPaymentQueue.default().finishTransaction(transaction) //同样的原始订单,只处理一次. guard judgeWhether(transaction: transaction) else { break } // Transaction was restored from user's purchase history. Client should complete the transaction. if let original = transaction.original, original.transactionState == .purchased { #if DEBUG verifyPayResult(transaction: transaction, useSandBox: true) #else verifyPayResult(transaction: transaction, useSandBox: false) #endif } else { purchase(self, didChaged: .restoreFail, object: "Failed to restore subscribe, please try again") } case .deferred: // The transaction is in the queue, but its final status is pending external action. break @unknown default: SKPaymentQueue.default().finishTransaction(transaction) } } } public func paymentQueue(_ queue: SKPaymentQueue, restoreCompletedTransactionsFailedWithError error: Error) { purchase(self, didChaged: .restoreFail, object: nil) } public func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) { if let trans = queue.transactions.first(where: { $0.transactionState == .purchased }) { verifyPayResult(transaction: trans, useSandBox: false) } else if queue.transactions.isEmpty { purchase(self, didChaged: .restoreFail, object: "You don't have an active subscription") } } func judgeWhether(transaction:SKPaymentTransaction) -> Bool { let id = transaction.original?.transactionIdentifier if let id = id { if let value = originalTransactionIdentifierDict[id] { return false } originalTransactionIdentifierDict[id] = "1" } return true } } extension PurchaseManager { func verifyPayResult(transaction: SKPaymentTransaction, useSandBox: Bool) { purchase(self, didChaged: .verifying, object: nil) guard let url = Bundle.main.appStoreReceiptURL, let receiptData = try? Data(contentsOf: url) else { purchase(self, didChaged: .verifyFail, object: "凭证文件为空") return } let requestContents = [ "receipt-data": receiptData.base64EncodedString(), "password": AppleSharedKey, ] guard let requestData = try? JSONSerialization.data(withJSONObject: requestContents) else { purchase(self, didChaged: .verifyFail, object: "凭证文件为空") return } let verifyUrlString = useSandBox ? Config.sandBoxUrl : Config.verifyUrl postRequest(urlString: verifyUrlString, httpBody: requestData) { [weak self] data, error in guard let self = self else { return } if let data = data, let jsonResponse = try? JSONSerialization.jsonObject(with: data) as? [String: Any] { // debugPrint("PurchaseManager verifyPayResult = \(jsonResponse)") let status = jsonResponse["status"] if let status = status as? String, status == "21007" { self.verifyPayResult(transaction: transaction, useSandBox: true) } else if let status = status as? Int, status == 21007 { self.verifyPayResult(transaction: transaction, useSandBox: true) } else if let status = status as? String, status == "0" { self.handlerPayResult(transaction: transaction, resp: jsonResponse) } else if let status = status as? Int, status == 0 { self.handlerPayResult(transaction: transaction, resp: jsonResponse) } else { self.purchase(self, didChaged: .verifyFail, object: "验证结果状态码错误:\(status.debugDescription)") } } else { self.purchase(self, didChaged: .verifyFail, object: "验证结果为空") debugPrint("PurchaseManager 验证结果为空") } } /* 21000 App Store无法读取你提供的JSON数据 21002 收据数据不符合格式 21003 收据无法被验证 21004 你提供的共享密钥和账户的共享密钥不一致 21005 收据服务器当前不可用 21006 收据是有效的,但订阅服务已经过期。当收到这个信息时,解码后的收据信息也包含在返回内容中 21007 收据信息是测试用(sandbox),但却被发送到产品环境中验证 21008 收据信息是产品环境中使用,但却被发送到测试环境中验证 */ } func handlerPayResult(transaction: SKPaymentTransaction, resp: [String: Any]) { var isLifetime = false // 终生会员 if let receipt = resp["receipt"] as? [String: Any], let in_app = receipt["in_app"] as? [[String: Any]] { if let lifetimeProductId = purchaseProducts.first(where: { $0.period == .lifetime })?.productId, let _ = in_app.filter({ ($0["product_id"] as? String) == lifetimeProductId }).first(where: { item in if let purchase_date = item["purchase_date"] as? String, !purchase_date.isEmpty { return true } else if let purchase_date_ms = item["purchase_date_ms"] as? String, !purchase_date_ms.isEmpty { return true } return false }) { updateExpireTime(lifetimeExpireTime, for: lifetimeProductId) isLifetime = true } } if !isLifetime { let info = resp["latest_receipt_info"] as? [[String: Any]] if let firstItem = info?.first, let expires_date_ms = firstItem["expires_date_ms"] as? String, let productId = firstItem["product_id"] as? String { updateExpireTime(expires_date_ms, for: productId) } } DispatchQueue.main.async { if transaction.transactionState == .restored { self.purchase(self, didChaged: .restoreSuccess, object: nil) } else { self.purchase(self, didChaged: .paySuccess, object: nil) } } } // 终生会员过期时间:100年 var lifetimeExpireTime: String { let date = Date().addingTimeInterval(100 * 365 * 24 * 60 * 60) return "\(date.timeIntervalSince1970 * 1000)" } /// 发送 POST 请求 /// - Parameters: /// - urlString: 请求的 URL 字符串 /// - parameters: 请求的参数字典(将自动转换为 JSON) /// - timeout: 超时时间(默认 30 秒) /// - completion: 请求完成的回调,返回 `Data?` 和 `Error?` func postRequest( urlString: String, httpBody: Data?, timeout: TimeInterval = 90, completion: @escaping (Data?, Error?) -> Void ) { // 确保 URL 有效 guard let url = URL(string: urlString) else { completion(nil, NSError(domain: "Invalid URL", code: -1, userInfo: nil)) return } dePrint("postRequest urlString=\(urlString)") // 创建请求 var request = URLRequest(url: url) request.httpMethod = "POST" request.timeoutInterval = timeout request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.httpBody = httpBody // 创建数据任务 let task = URLSession.shared.dataTask(with: request) { data, response, error in completion(data, error) } // 启动任务 task.resume() } } public extension PurchaseManager { func canContinue(_ requireVip: Bool) -> Bool { guard requireVip else { return true } return isVip } func purchase(_ manager: PurchaseManager, didChaged state: PremiumRequestState, object: Any?){ onPurchaseStateChanged?(manager,state,object) } } /// 免费生成图片次数 extension PurchaseManager { /// 使用一次免费次数 func useOnceForFree(type:VipFreeNumType){ if isVip { return } var freeNum = freeDict[type.rawValue] ?? 0 if freeNum > 0 { freeNum-=1 } if freeNum < 0 { freeNum = 0 } freeDict[type.rawValue] = freeNum saveForFree() } func freeNum(type:VipFreeNumType) -> Int{ let freeNum = freeDict[type.rawValue] ?? 0 return freeNum } func saveForFree(){ UserDefaults.standard.set(freeDict, forKey: kFreeNumKey) UserDefaults.standard.synchronize() } func initializeForFree(){ if let dict = UserDefaults.standard.dictionary(forKey: kFreeNumKey) as? [String:Int]{ freeDict = dict }else{ freeDict = [ VipFreeNumType.generatePic.rawValue:1, VipFreeNumType.aichat.rawValue:1, VipFreeNumType.textGeneratePic.rawValue:1, VipFreeNumType.picToPic.rawValue:1 ] saveForFree() } } /// 免费次数是否可用 func freeNumAvailable(type:VipFreeNumType) -> Bool{ if isVip == true { return true }else{ if let freeNum = freeDict[type.rawValue],freeNum > 0 { return true } } return false } /// 是否展示生成类的会员图标 func generateVipShow(type:VipFreeNumType) -> Bool{ if isVip == false, freeNum(type: type) > 0 { return false } return true } } /* 首先,创建SKProductsRequest对象并使用init(productIdentifiers:)初始化,传入要查询的产品标识符。 然后,调用start()方法开始请求产品信息。 当请求成功时,productsRequest(_:didReceive:)方法会被调用,在这里可以获取产品详细信息并展示给用户(如在界面上显示产品价格、名称等)。如果请求失败,productsRequest(_:didFailWithError:)方法会被调用来处理错误。 当用户决定购买某个产品后,根据产品信息(SKProduct对象)创建SKPayment对象,然后使用SKPaymentQueue的add(_:)方法将支付请求添加到支付队列。 同时,在应用启动等合适的时机,通过SKPaymentQueue的addTransactionObserver(_:)方法添加交易观察者。当支付状态发生变化时,paymentQueue(_:updatedTransactions:)方法会被调用,在这里可以根据交易状态(如购买成功、失败、恢复等)进行相应的处理。 */