|
@@ -0,0 +1,612 @@
|
|
|
+//
|
|
|
+// 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 ringtones = "kRingtonesFreeNum"
|
|
|
+ case posetr = "kPosetrFreeNum"
|
|
|
+ case photo = "kPhotoFreeNum"
|
|
|
+}
|
|
|
+
|
|
|
+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 = "4ed5151881304025bfee0295ee459422"
|
|
|
+
|
|
|
+ //商品信息
|
|
|
+ public lazy var purchaseProducts:[PurchaseProduct] = {
|
|
|
+ return [
|
|
|
+ PurchaseProduct(productId: "01", 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.ringtones.rawValue:1,
|
|
|
+ VipFreeNumType.posetr.rawValue:1,
|
|
|
+ VipFreeNumType.photo.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
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+/*
|
|
|
+
|
|
|
+ 首先,创建SKProductsRequest对象并使用init(productIdentifiers:)初始化,传入要查询的产品标识符。
|
|
|
+ 然后,调用start()方法开始请求产品信息。
|
|
|
+ 当请求成功时,productsRequest(_:didReceive:)方法会被调用,在这里可以获取产品详细信息并展示给用户(如在界面上显示产品价格、名称等)。如果请求失败,productsRequest(_:didFailWithError:)方法会被调用来处理错误。
|
|
|
+ 当用户决定购买某个产品后,根据产品信息(SKProduct对象)创建SKPayment对象,然后使用SKPaymentQueue的add(_:)方法将支付请求添加到支付队列。
|
|
|
+ 同时,在应用启动等合适的时机,通过SKPaymentQueue的addTransactionObserver(_:)方法添加交易观察者。当支付状态发生变化时,paymentQueue(_:updatedTransactions:)方法会被调用,在这里可以根据交易状态(如购买成功、失败、恢复等)进行相应的处理。
|
|
|
+
|
|
|
+ */
|