123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622 |
- //
- // 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:)方法会被调用,在这里可以根据交易状态(如购买成功、失败、恢复等)进行相应的处理。
-
- */
|