// // TSPurchaseManager.swift // TSLiveWallpaper // // Created by 100Years on 2025/1/13. // import Foundation import StoreKit public enum PremiumPeriod{ case none case month case year case lifetime case week(WeekType) public enum WeekType:String, CaseIterable{ case week = "week" case week1 = "week1" case weekPromotional1 = "weekPromotional1" } } extension PremiumPeriod:CaseIterable ,RawRepresentable { /// 所有可迭代的 case(包括 week 的所有子类型) public static var allCases: [PremiumPeriod] { var cases: [PremiumPeriod] = [.none, .month, .year, .lifetime] cases.append(contentsOf: WeekType.allCases.map { .week($0) }) return cases } public var rawValue: String{ switch self { case .week(let type): type.rawValue case .month: "Monthly" case .year: "Yearly" case .lifetime: "Lifetime" default: "" } } public init?(rawValue: String) { // 先尝试匹配基础类型 switch rawValue { case "Monthly": self = .month case "Yearly": self = .year case "Lifetime": self = .lifetime default: if let weekType = WeekType(rawValue: rawValue){ self = .week(weekType) }else{ self = .none } } } var isWeekType:Bool { switch self { case .week(_): return true default: return false } } } extension PremiumPeriod { /// 对应vip类型,可以免费使用次数 var freeNumber: Int { switch self { case .week(_): return 30 case .month: return 30 case .year: return 60 case .none: return 0 default: return 30 } } var saveString: String { switch self { case .none: return "80%"//"40%" 增加月付费 default: return "80%" } } /* 1. 一年(非闰年) ​365 天​ = 365 × 24 × 60 × 60 × 1000 = ​31,536,000,000 毫秒 (若闰年 366 天 = 31,622,400,000 毫秒) ​2. 一个月(平均) ​30.44 天​(按 365 天/12 个月计算)≈ 30.44 × 24 × 60 × 60 × 1000 ≈ ​2,629,746,000 毫秒 (实际月份天数不同,如 28/30/31 天需单独计算) ​3. 一周 ​7 天​ = 7 × 24 × 60 × 60 × 1000 = ​604,800,000 毫秒 */ var milliseconds:Int { switch self { case .year: return 365 * 24 * 60 * 60 * 1000 case .month: return 30 * 24 * 60 * 60 * 1000 case .week(_): return 7 * 24 * 60 * 60 * 1000 default: return 0 } } } public enum VipFreeNumType: String, CaseIterable { case none = "kNone" case generatePic = "kGeneratePicFreeNum" case aichat = "kAIChatFreeNum" case textGeneratePic = "kTextGeneratePicFreeNum" case picToPic = "kPicToPicFreeNum" case aiGenerate = "kAIGenerateFreeNum" } 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("kPurchaseProductPrepared") static let kPurchaseDidChanged = Self("kPurchaseDidChanged") } private let kFreeNumKey = "kFreeNumKey" private let kTotalUseNumKey = "kTotalUseNumKey" 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] = { [ PurchaseProduct(productId: "101", period: .month),//增加月付费 PurchaseProduct(productId: "102", period: .year), PurchaseProduct(productId: "103", period: .week(.week)), PurchaseProduct(productId: "113", period: .week(.weekPromotional1)) ] }() 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] = [:] public var totalUsedTimes: Int = 0 public var isOverTotalTimes: Bool { if isVip { loadTotalUse() // #if DEBUG // return false // #endif return totalUsedTimes >= vipType.freeNumber } return false } 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 vipType != .none //#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 { //#if DEBUG // return PremiumPeriod.none //#endif guard isVip, let type = vipInformation["type"] as? String else { return .none } debugPrint("PurchaseManager get vipType = \(type)") 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 dePrint("vipInformation = \(vipInformation)") 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 introductoryPrice(for period: PremiumPeriod) -> String? { guard let product = product(for: period) else { return nil } guard let introductoryPrice = product.introductoryPrice else { return nil } let formatter = NumberFormatter() formatter.formatterBehavior = NumberFormatter.Behavior.behavior10_4 formatter.numberStyle = .currency formatter.locale = product.priceLocale return formatter.string(from: introductoryPrice.price) } // 平局每周的金额 public func averageWeekly(for period: PremiumPeriod) -> String? { guard let product = product(for: period) else { return nil } var originPrice = product.price let price = originPrice.doubleValue if period == .year { originPrice = NSDecimalNumber(string: String(format: "%.2f", price / 52.0), locale: nil) } else if period == .month { originPrice = NSDecimalNumber(string: String(format: "%.2f", price / 4.0), locale: nil) } let formatter = NumberFormatter() formatter.formatterBehavior = NumberFormatter.Behavior.behavior10_4 formatter.numberStyle = .currency formatter.locale = product.priceLocale return formatter.string(from: originPrice) } // 平均每天的金额 public func averageDay(for period: PremiumPeriod) -> String? { guard let product = product(for: period) else { return nil } var originPrice = product.price let price = originPrice.doubleValue if period == .year { originPrice = NSDecimalNumber(string: String(format: "%.2f", price / 365.0), locale: nil) } else if period == .month { originPrice = NSDecimalNumber(string: String(format: "%.2f", price / 30.0), locale: nil) } let formatter = NumberFormatter() formatter.formatterBehavior = NumberFormatter.Behavior.behavior10_4 formatter.numberStyle = .currency formatter.locale = product.priceLocale return formatter.string(from: originPrice) } // 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 restorePremium") subscriptionApple(type: .created, jsonString: "Payment restore") } /// 购买支付 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.") debugPrint("PurchaseManager pay period restorePremium = \(period)") restorePremium() return } if let product = product(for: period) { purchase(self, didChaged: .paying, object: nil) let payment = SKPayment(product: product) debugPrint("PurchaseManager pay product = \(product.localizedDescription)") SKPaymentQueue.default().add(payment) debugPrint("PurchaseManager pay period = \(period)") subscriptionApple(type: .created, jsonString: "Payment period = \(product)") } 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)") for product in products { print("请求到商品ID: \(product.productIdentifier)") // 获取促销价格 if let introductoryPrice = product.introductoryPrice { print("新用户促销价格: \(introductoryPrice.price) \(introductoryPrice.priceLocale.currencySymbol ?? "")") print("新用户促销周期: \(introductoryPrice.subscriptionPeriod.numberOfUnits) \(introductoryPrice.subscriptionPeriod.unit)") } // 获取促销价格 for discounts in product.discounts { print("老用户促销价格: \(discounts.price) \(discounts.priceLocale.currencySymbol ?? "")") print("老用户促销周期: \(discounts.subscriptionPeriod.numberOfUnits)") } } } 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) if let error = transaction.error as NSError? { // 1. 检查内层错误 if let underlyingError = error.userInfo[NSUnderlyingErrorKey] as? NSError { if underlyingError.domain == "ASDServerErrorDomain" && underlyingError.code == 3532 { print("用户已订阅,禁止重复购买") restorePremium() return } } // 2. 检查外层 SKError else if error.domain == SKErrorDomain { switch SKError.Code(rawValue: error.code) { case .unknown: print("未知错误,可能是服务器问题") default: break } } } // 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) subscriptionApple(type: .result, jsonString: 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") subscriptionApple(type: .result, jsonString: "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, _ 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 { if upgradePendingRenewalInfo(resp) == false { 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) } } subscriptionApple(type: .result, jsonString: simplifyVerifyPayResult(resp: resp)) } func upgradePendingRenewalInfo(_ resp: [String: Any]) -> Bool { guard !resp.isEmpty else { return false } /* pending_renewal_info={ "auto_renew_product_id" = 102; "auto_renew_status" = 1; "original_transaction_id" = 2000000929272571; "product_id" = 101; }*/ let info = resp["pending_renewal_info"] as? [[String: Any]] if let firstItem = info?.first, let auto_renew_product_id = firstItem["auto_renew_product_id"] as? String, let auto_product_id = firstItem["product_id"] as? String { if auto_renew_product_id != auto_product_id {//拿到待生效的和当前的对比不一样,以待生效的为主 //取当前的过期时间+加上待生效的会员过期时长 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 expiresms = Int(expires_date_ms) ?? 0 + period(for: auto_renew_product_id).milliseconds updateExpireTime(String(expiresms), for: auto_renew_product_id) return true } } } return false } // 终生会员过期时间: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, _, 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 { saveForTotalUse() } if isVip { return } var freeNum = freeDict[type.rawValue] ?? 0 if freeNum > 0 { freeNum -= 1 } if freeNum < 0 { freeNum = 0 } freeDict[type.rawValue] = freeNum saveForFree() NotificationCenter.default.post(name: .kVipFreeNumChanged, object: nil, userInfo: ["VipFreeNumType": type]) } 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 saveForTotalUse() { // 先加载当前记录(确保日期正确) loadTotalUse() // 增加使用次数 totalUsedTimes += 1 // 保存新的记录 let dict: [String: Any] = ["date": Date().dateDayString, "times": totalUsedTimes] UserDefaults.standard.set(dict, forKey: kTotalUseNumKey) UserDefaults.standard.synchronize() } func loadTotalUse() { // 当天没记录,设置默认次数 guard let dict = UserDefaults.standard.dictionary(forKey: kTotalUseNumKey), dict.safeString(forKey: "date") == Date().dateDayString else { totalUsedTimes = 0 return } // 有记录,设置已经使用次数 totalUsedTimes = dict.safeInt(forKey: "times") } 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, VipFreeNumType.aiGenerate.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 } } extension PurchaseManager { func checkLocalReceiptForIntroOffer(type:PremiumPeriod) -> Bool { guard let productId = productId(for: type) else { return false } guard let receiptURL = Bundle.main.appStoreReceiptURL, FileManager.default.fileExists(atPath: receiptURL.path) else { return false // 无收据,用户未订阅过 } // 如果有收据,进一步解析(需实现收据解析逻辑) if let receiptData = try? Data(contentsOf: receiptURL) { let receiptString = receiptData.base64EncodedString() return parseReceiptForIntroOffer(receiptString: receiptString,productId:productId) } return false } func parseReceiptForIntroOffer(receiptString: String,productId:String) -> Bool { // 这里需要解析收据的 JSON 数据(实际开发建议使用开源库如 SwiftyStoreKit) guard let receiptData = Data(base64Encoded: receiptString), let receiptJSON = try? JSONSerialization.jsonObject(with: receiptData, options: []) as? [String: Any], let latestReceiptInfo = receiptJSON["latest_receipt_info"] as? [[String: Any]] else { return false } // 检查是否有购买过首月优惠商品(例如 product_id = "monthly_intro") for receipt in latestReceiptInfo { if let productID = receipt["product_id"] as? String, productID == "productId" { return true // 用户已订阅过首月优惠 } } return false } } /* 首先,创建SKProductsRequest对象并使用init(productIdentifiers:)初始化,传入要查询的产品标识符。 然后,调用start()方法开始请求产品信息。 当请求成功时,productsRequest(_:didReceive:)方法会被调用,在这里可以获取产品详细信息并展示给用户(如在界面上显示产品价格、名称等)。如果请求失败,productsRequest(_:didFailWithError:)方法会被调用来处理错误。 当用户决定购买某个产品后,根据产品信息(SKProduct对象)创建SKPayment对象,然后使用SKPaymentQueue的add(_:)方法将支付请求添加到支付队列。 同时,在应用启动等合适的时机,通过SKPaymentQueue的addTransactionObserver(_:)方法添加交易观察者。当支付状态发生变化时,paymentQueue(_:updatedTransactions:)方法会被调用,在这里可以根据交易状态(如购买成功、失败、恢复等)进行相应的处理。 */