TSPurchaseManager.swift 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515
  1. //
  2. // TSPurchaseManager.swift
  3. // TSLiveWallpaper
  4. //
  5. // Created by 100Years on 2025/1/13.
  6. //
  7. import Foundation
  8. import StoreKit
  9. typealias PurchaseStateChangeHandler = (_ manager: PurchaseManager, _ state: PremiumRequestState, _ object: Any?) -> Void
  10. private let kPremiumExpiredInfoKey = "premiumExpiredInfoKey"
  11. public class PurchaseManager: NSObject {
  12. @objc public static let `default` = PurchaseManager()
  13. //苹果共享密钥
  14. var password:String = "155c8104e2b041c0abae43ace199124c"
  15. //商品信息
  16. public var purchaseProducts:[PurchaseProduct] = []
  17. struct Config {
  18. static let verifyUrl = "https://buy.itunes.apple.com/verifyReceipt"
  19. static let sandBoxUrl = "https://sandbox.itunes.apple.com/verifyReceipt"
  20. }
  21. lazy var products: [SKProduct] = []
  22. var onPurchaseStateChanged: PurchaseStateChangeHandler?
  23. // 会员信息
  24. var vipInformation: [String: Any] = [:]
  25. //原始订单交易id dict
  26. var originalTransactionIdentifierDict:[String:String] = [:]
  27. override init() {
  28. super.init()
  29. SKPaymentQueue.default().add(self)
  30. if let info = UserDefaults.standard.object(forKey: kPremiumExpiredInfoKey) as? [String: Any] {
  31. vipInformation = info
  32. }
  33. }
  34. public var expiredDate: Date? {
  35. guard let time = vipInformation["expireTime"] as? String else {
  36. return nil
  37. }
  38. return convertExpireDate(from: time)
  39. }
  40. public var expiredDateString: String {
  41. if vipType == .lifetime{
  42. return "Lifetime"
  43. } else {
  44. if let expDate = expiredDate {
  45. let format = DateFormatter()
  46. format.locale = .current
  47. format.dateFormat = "yyyy-MM-dd"
  48. return format.string(from: expDate)
  49. } else {
  50. return "--"
  51. }
  52. }
  53. }
  54. private func convertExpireDate(from string: String) -> Date? {
  55. if let ts = TimeInterval(string) {
  56. let date = Date(timeIntervalSince1970: ts / 1000)
  57. return date
  58. }
  59. return nil
  60. }
  61. @objc public var isVip: Bool {
  62. #if DEBUG
  63. return true
  64. #endif
  65. guard let expiresDate = expiredDate else {
  66. return false
  67. }
  68. let todayStart = Calendar.current.startOfDay(for: Date())
  69. let todayStartTs = todayStart.timeIntervalSince1970
  70. let expiresTs = expiresDate.timeIntervalSince1970
  71. return expiresTs > todayStartTs
  72. }
  73. public var vipType: PremiumPeriod {
  74. guard isVip, let type = vipInformation["type"] as? String else {
  75. return .none
  76. }
  77. return PremiumPeriod(rawValue: type) ?? .none
  78. }
  79. /// 过期时间: 1683277585000 毫秒
  80. func updateExpireTime(_ timeInterval: String,
  81. for productId: String) {
  82. vipInformation.removeAll()
  83. vipInformation["expireTime"] = timeInterval
  84. vipInformation["productId"] = productId
  85. vipInformation["type"] = period(for: productId).rawValue
  86. UserDefaults.standard.set(vipInformation, forKey: kPremiumExpiredInfoKey)
  87. UserDefaults.standard.synchronize()
  88. NotificationCenter.default.post(name: .kPurchaseDidChanged, object: nil)
  89. }
  90. // 商品id对应的时间周期
  91. func period(for productId: String) -> PremiumPeriod {
  92. return purchaseProducts.first(where: { $0.productId == productId })?.period ?? .none
  93. }
  94. // 时间周期对应的商品id
  95. func productId(for period: PremiumPeriod) -> String? {
  96. return purchaseProducts.first(where: { $0.period == period })?.productId
  97. }
  98. }
  99. // MARK: 商品信息
  100. extension PurchaseManager {
  101. public func product(for period: PremiumPeriod) -> SKProduct? {
  102. return products.first(where: { $0.productIdentifier == productId(for: period) })
  103. }
  104. // 商品价格
  105. public func price(for period: PremiumPeriod) -> String? {
  106. guard let product = product(for: period) else {
  107. return nil
  108. }
  109. let formatter = NumberFormatter()
  110. formatter.formatterBehavior = NumberFormatter.Behavior.behavior10_4
  111. formatter.numberStyle = .currency
  112. formatter.locale = product.priceLocale
  113. return formatter.string(from: product.price)
  114. }
  115. // public func originalPrice(for period: PremiumPeriod) -> String? {
  116. // guard let product = product(for: period) else {
  117. // return nil
  118. // }
  119. // switch period {
  120. // case .year, .lifetime:
  121. // // 5折
  122. // let price = product.price.doubleValue
  123. // let calculatePrice = price * 2
  124. // let originStr = String(format: "%.2f", calculatePrice)
  125. // let originPrice = NSDecimalNumber(string: originStr, locale: product.priceLocale)
  126. //
  127. // let formatter = NumberFormatter()
  128. // formatter.formatterBehavior = NumberFormatter.Behavior.behavior10_4
  129. // formatter.numberStyle = .currency
  130. // formatter.locale = product.priceLocale
  131. // return formatter.string(from: originPrice)
  132. // default:
  133. // return nil
  134. // }
  135. // }
  136. // 平局每周的金额
  137. public func averageWeekly(for period: PremiumPeriod) -> String? {
  138. guard let product = product(for: period) else {
  139. return nil
  140. }
  141. var originPrice = product.price
  142. let price = originPrice.doubleValue
  143. if period == .year {
  144. originPrice = NSDecimalNumber(string: String(format: "%.2f", price / 52.0), locale: nil)
  145. } else if period == .month {
  146. originPrice = NSDecimalNumber(string: String(format: "%.2f", price / 4.0), locale: nil)
  147. }
  148. let formatter = NumberFormatter()
  149. formatter.formatterBehavior = NumberFormatter.Behavior.behavior10_4
  150. formatter.numberStyle = .currency
  151. formatter.locale = product.priceLocale
  152. return formatter.string(from: originPrice)
  153. }
  154. // 平均每天的金额
  155. public func averageDay(for period: PremiumPeriod) -> String? {
  156. guard let product = product(for: period) else {
  157. return nil
  158. }
  159. var originPrice = product.price
  160. let price = originPrice.doubleValue
  161. if period == .year {
  162. originPrice = NSDecimalNumber(string: String(format: "%.2f", price / 365.0), locale: nil)
  163. } else if period == .month {
  164. originPrice = NSDecimalNumber(string: String(format: "%.2f", price / 30.0), locale: nil)
  165. }
  166. let formatter = NumberFormatter()
  167. formatter.formatterBehavior = NumberFormatter.Behavior.behavior10_4
  168. formatter.numberStyle = .currency
  169. formatter.locale = product.priceLocale
  170. return formatter.string(from: originPrice)
  171. }
  172. }
  173. // MARK: 商品 & 订阅请求
  174. extension PurchaseManager {
  175. ///请求商品
  176. public func requestProducts() {
  177. if !products.isEmpty {
  178. purchase(self, didChaged: .loadSuccess, object: nil)
  179. }
  180. purchase(self, didChaged: .loading, object: nil)
  181. let productIdentifiers = Set(purchaseProducts.map({ $0.productId }))
  182. debugPrint("PurchaseManager requestProducts = \(productIdentifiers)")
  183. let request = SKProductsRequest(productIdentifiers: productIdentifiers)
  184. request.delegate = self
  185. request.start()
  186. }
  187. public func restorePremium() {
  188. purchase(self, didChaged: .restoreing, object: nil)
  189. SKPaymentQueue.default().restoreCompletedTransactions()
  190. debugPrint("PurchaseManager restoreCompletedTransactions")
  191. }
  192. /// 购买支付
  193. public func pay(for period: PremiumPeriod) {
  194. guard SKPaymentQueue.canMakePayments() else {
  195. purchase(self, didChaged: .payFail, object: "Payment failed, please check your payment account")
  196. return
  197. }
  198. guard SKPaymentQueue.default().transactions.count <= 0 else {
  199. purchase(self, didChaged: .payFail, object: "You have outstanding orders that must be paid for before a new subscription can be placed.")
  200. restorePremium()
  201. return
  202. }
  203. if let product = product(for: period) {
  204. purchase(self, didChaged: .paying, object: nil)
  205. let payment = SKPayment(product: product)
  206. SKPaymentQueue.default().add(payment)
  207. debugPrint("PurchaseManager pay period = \(period)")
  208. }else{
  209. purchase(self, didChaged: .payFail, object: "Payment failed, no this item")
  210. }
  211. }
  212. }
  213. // MARK: 商品回调
  214. extension PurchaseManager: SKProductsRequestDelegate {
  215. public func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
  216. let products = response.products
  217. self.products = products
  218. purchase(self, didChaged: .loadSuccess, object: nil)
  219. NotificationCenter.default.post(name: .kPurchasePrepared, object: nil)
  220. debugPrint("PurchaseManager productsRequest didReceive = \(products)")
  221. }
  222. public func request(_ request: SKRequest, didFailWithError error: Error) {
  223. debugPrint("PurchaseManager productsRequest error = \(error)")
  224. purchase(self, didChaged: .loadFail, object: error.localizedDescription)
  225. }
  226. }
  227. // MARK: 订阅回调
  228. extension PurchaseManager: SKPaymentTransactionObserver {
  229. public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
  230. debugPrint("PurchaseManager paymentQueue transactions.count = \(transactions.count)")
  231. // debugPrint("PurchaseManager paymentQueue transactions = \(transactions)")
  232. originalTransactionIdentifierDict.removeAll()
  233. // 因为只有订阅类的购买项
  234. for transaction in transactions {
  235. // debugPrint("PurchaseManager paymentQueue transactions transactionIdentifier original= \(transaction.original?.transactionIdentifier)")
  236. // debugPrint("PurchaseManager paymentQueue transactions transactionIdentifier = \(transaction.transactionIdentifier)")
  237. // debugPrint("PurchaseManager paymentQueue transactions transactionIdentifier productIdentifier = \(transaction.payment.productIdentifier)")
  238. switch transaction.transactionState {
  239. case .purchasing:
  240. // Transaction is being added to the server queue.
  241. purchase(self, didChaged: .paying, object: nil)
  242. case .purchased:
  243. SKPaymentQueue.default().finishTransaction(transaction)
  244. //同样的原始订单,只处理一次.
  245. guard judgeWhether(transaction: transaction) else {
  246. break
  247. }
  248. // Transaction is in queue, user has been charged. Client should complete the transaction.
  249. #if DEBUG
  250. verifyPayResult(transaction: transaction, useSandBox: true)
  251. #else
  252. verifyPayResult(transaction: transaction, useSandBox: false)
  253. #endif
  254. case .failed:
  255. SKPaymentQueue.default().finishTransaction(transaction)
  256. // Transaction was cancelled or failed before being added to the server queue.
  257. if let error = transaction.error as NSError? {
  258. // 1. 检查内层错误
  259. if let underlyingError = error.userInfo[NSUnderlyingErrorKey] as? NSError {
  260. if underlyingError.domain == "ASDServerErrorDomain" && underlyingError.code == 3532 {
  261. print("用户已订阅,禁止重复购买")
  262. restorePremium()
  263. return
  264. }
  265. }
  266. // 2. 检查外层 SKError
  267. else if error.domain == SKErrorDomain {
  268. switch SKError.Code(rawValue: error.code) {
  269. case .unknown:
  270. print("未知错误,可能是服务器问题")
  271. default:
  272. break
  273. }
  274. }
  275. }
  276. var message = "Payment Failed".localized
  277. if let error = transaction.error as? SKError{
  278. if error.code == SKError.paymentCancelled {
  279. message = "The subscription was canceled".localized
  280. }
  281. }
  282. purchase(self, didChaged: .payFail, object: message)
  283. case .restored:
  284. SKPaymentQueue.default().finishTransaction(transaction)
  285. //同样的原始订单,只处理一次.
  286. guard judgeWhether(transaction: transaction) else {
  287. break
  288. }
  289. // Transaction was restored from user's purchase history. Client should complete the transaction.
  290. if let original = transaction.original,
  291. original.transactionState == .purchased {
  292. #if DEBUG
  293. verifyPayResult(transaction: transaction, useSandBox: true)
  294. #else
  295. verifyPayResult(transaction: transaction, useSandBox: false)
  296. #endif
  297. } else {
  298. purchase(self, didChaged: .restoreFail, object: "Failed to restore subscribe, please try again")
  299. }
  300. case .deferred: // The transaction is in the queue, but its final status is pending external action.
  301. break
  302. @unknown default:
  303. SKPaymentQueue.default().finishTransaction(transaction)
  304. }
  305. }
  306. }
  307. public func paymentQueue(_ queue: SKPaymentQueue, restoreCompletedTransactionsFailedWithError error: Error) {
  308. purchase(self, didChaged: .restoreFail, object: nil)
  309. }
  310. public func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
  311. if let trans = queue.transactions.first(where: { $0.transactionState == .purchased }) {
  312. verifyPayResult(transaction: trans, useSandBox: false)
  313. } else if queue.transactions.isEmpty {
  314. purchase(self, didChaged: .restoreFail, object: "You don't have an active subscription")
  315. }
  316. }
  317. func judgeWhether(transaction:SKPaymentTransaction) -> Bool {
  318. let id = transaction.original?.transactionIdentifier
  319. if let id = id {
  320. if let value = originalTransactionIdentifierDict[id] {
  321. return false
  322. }
  323. originalTransactionIdentifierDict[id] = "1"
  324. }
  325. return true
  326. }
  327. }
  328. extension PurchaseManager {
  329. func verifyPayResult(transaction: SKPaymentTransaction, useSandBox: Bool) {
  330. purchase(self, didChaged: .verifying, object: nil)
  331. guard let url = Bundle.main.appStoreReceiptURL,
  332. let receiptData = try? Data(contentsOf: url) else {
  333. purchase(self, didChaged: .verifyFail, object: "凭证文件为空")
  334. return
  335. }
  336. let requestContents = [
  337. "receipt-data": receiptData.base64EncodedString(),
  338. "password": password,
  339. ]
  340. guard let requestData = try? JSONSerialization.data(withJSONObject: requestContents) else {
  341. purchase(self, didChaged: .verifyFail, object: "凭证文件为空")
  342. return
  343. }
  344. let verifyUrlString = useSandBox ? Config.sandBoxUrl : Config.verifyUrl
  345. TSNetworkManager.postRequest(urlString: verifyUrlString, httpBody: requestData) { [weak self] data, error in
  346. guard let self = self else { return }
  347. if let data = data,
  348. let jsonResponse = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
  349. // debugPrint("PurchaseManager verifyPayResult = \(jsonResponse)")
  350. let status = jsonResponse["status"]
  351. if let status = status as? String, status == "21007" {
  352. self.verifyPayResult(transaction: transaction, useSandBox: true)
  353. } else if let status = status as? Int, status == 21007 {
  354. self.verifyPayResult(transaction: transaction, useSandBox: true)
  355. } else if let status = status as? String, status == "0" {
  356. self.handlerPayResult(transaction: transaction, resp: jsonResponse)
  357. } else if let status = status as? Int, status == 0 {
  358. self.handlerPayResult(transaction: transaction, resp: jsonResponse)
  359. } else {
  360. self.purchase(self, didChaged: .verifyFail, object: "验证结果状态码错误:\(status.debugDescription)")
  361. }
  362. } else {
  363. self.purchase(self, didChaged: .verifyFail, object: "验证结果为空")
  364. debugPrint("PurchaseManager 验证结果为空")
  365. }
  366. }
  367. /*
  368. 21000 App Store无法读取你提供的JSON数据
  369. 21002 收据数据不符合格式
  370. 21003 收据无法被验证
  371. 21004 你提供的共享密钥和账户的共享密钥不一致
  372. 21005 收据服务器当前不可用
  373. 21006 收据是有效的,但订阅服务已经过期。当收到这个信息时,解码后的收据信息也包含在返回内容中
  374. 21007 收据信息是测试用(sandbox),但却被发送到产品环境中验证
  375. 21008 收据信息是产品环境中使用,但却被发送到测试环境中验证
  376. */
  377. }
  378. func handlerPayResult(transaction: SKPaymentTransaction, resp: [String: Any]) {
  379. var isLifetime = false
  380. // 终生会员
  381. if let receipt = resp["receipt"] as? [String: Any],
  382. let in_app = receipt["in_app"] as? [[String: Any]] {
  383. if let lifetimeProductId = purchaseProducts.first(where: { $0.period == .lifetime })?.productId,
  384. let _ = in_app.filter({ ($0["product_id"] as? String) == lifetimeProductId }).first(where: { item in
  385. if let purchase_date = item["purchase_date"] as? String,
  386. !purchase_date.isEmpty {
  387. return true
  388. } else if let purchase_date_ms = item["purchase_date_ms"] as? String,
  389. !purchase_date_ms.isEmpty {
  390. return true
  391. }
  392. return false
  393. }) {
  394. updateExpireTime(lifetimeExpireTime, for: lifetimeProductId)
  395. isLifetime = true
  396. }
  397. }
  398. if !isLifetime {
  399. let info = resp["latest_receipt_info"] as? [[String: Any]]
  400. if let firstItem = info?.first,
  401. let expires_date_ms = firstItem["expires_date_ms"] as? String,
  402. let productId = firstItem["product_id"] as? String {
  403. updateExpireTime(expires_date_ms, for: productId)
  404. }
  405. }
  406. DispatchQueue.main.async {
  407. if transaction.transactionState == .restored {
  408. self.purchase(self, didChaged: .restoreSuccess, object: nil)
  409. } else {
  410. self.purchase(self, didChaged: .paySuccess, object: nil)
  411. }
  412. }
  413. }
  414. // 终生会员过期时间:100年
  415. var lifetimeExpireTime: String {
  416. let date = Date().addingTimeInterval(100 * 365 * 24 * 60 * 60)
  417. return "\(date.timeIntervalSince1970 * 1000)"
  418. }
  419. }
  420. public extension PurchaseManager {
  421. func canContinue(_ requireVip: Bool) -> Bool {
  422. guard requireVip else {
  423. return true
  424. }
  425. return isVip
  426. }
  427. func purchase(_ manager: PurchaseManager, didChaged state: PremiumRequestState, object: Any?){
  428. onPurchaseStateChanged?(manager,state,object)
  429. }
  430. }
  431. /*
  432. 首先,创建SKProductsRequest对象并使用init(productIdentifiers:)初始化,传入要查询的产品标识符。
  433. 然后,调用start()方法开始请求产品信息。
  434. 当请求成功时,productsRequest(_:didReceive:)方法会被调用,在这里可以获取产品详细信息并展示给用户(如在界面上显示产品价格、名称等)。如果请求失败,productsRequest(_:didFailWithError:)方法会被调用来处理错误。
  435. 当用户决定购买某个产品后,根据产品信息(SKProduct对象)创建SKPayment对象,然后使用SKPaymentQueue的add(_:)方法将支付请求添加到支付队列。
  436. 同时,在应用启动等合适的时机,通过SKPaymentQueue的addTransactionObserver(_:)方法添加交易观察者。当支付状态发生变化时,paymentQueue(_:updatedTransactions:)方法会被调用,在这里可以根据交易状态(如购买成功、失败、恢复等)进行相应的处理。
  437. */