// // KMWinBackWindowController.swift // PDF Reader Pro // // Created by User-Tangchao on 2025/1/13. // import Cocoa import StoreKit import SwiftUI //class KMWinBackModel: NSObject { // var originalPrice: Double = 0 // //} @objcMembers class KMIAPTransaction: NSObject { var transactionIdentifier: String? var productIdentifier: String? override init() { super.init() self.transactionIdentifier = "" self.productIdentifier = "" } } let KMWinBackIAPProductPurchasedNotificationName = "KMWinBackIAPProductPurchasedNotification" class KMWinBackWindowController: KMBaseWindowController { static let shared = KMWinBackWindowController(windowNibName: "KMWinBackWindowController") @IBOutlet weak var contentBox: NSBox! @IBOutlet weak var backgroundIv: NSImageView! @IBOutlet weak var iconIv: NSImageView! @IBOutlet weak var titleLabel: NSTextField! @IBOutlet weak var subTitleLabel: NSTextField! @IBOutlet weak var despBox: NSBox! @IBOutlet weak var buttonBox: NSBox! private lazy var despView_: KMWinBackDespView = { let view = KMWinBackDespView() return view }() private lazy var buttonView_: KMWinBackButtonView = { let view = KMWinBackButtonView() return view }() private let showCountKey_ = "WinBackWindowShowCount" private let lastShowTimeKey_ = "WinBackWindowLastShowTime" private var origialPrice_: String = "" private var offerId_: String? private var displayPriceString_: String = "" override func windowDidLoad() { super.windowDidLoad() window?.standardWindowButton(.zoomButton)?.isHidden = true window?.standardWindowButton(.miniaturizeButton)?.isHidden = true window?.delegate = self despBox.borderWidth = 0 despBox.cornerRadius = 4 despBox.contentView = despView_ buttonBox.borderWidth = 0 buttonBox.cornerRadius = 4 buttonBox.contentView = buttonView_ backgroundIv.image = NSImage(named: "KMImageNameWinBackBg") backgroundIv.image?.size = window?.frame.size ?? NSMakeSize(520, 540) iconIv.image = NSImage(named: "KMImageNameWinBackIcon") titleLabel.stringValue = NSLocalizedString("PDF Reader Pro", comment: "") titleLabel.font = .SFProTextRegularFont(20) despView_.titleLabel.stringValue = NSLocalizedString("PDF Reader Pro Advanced\n - 6 Months Plan", comment: "") despView_.tipLabel.stringValue = NSLocalizedString("Because you were previously subscribed to PDF Reader Pro, we are now delivering a special offer to you. ", comment: "") despView_.iconIv.image = NSImage(named: "KMImageNameWinBackDespIcon") buttonView_.backgroundView.xRadius = 4 buttonView_.backgroundView.yRadius = 4 buttonView_.button.font = .SFProTextRegularFont(16) buttonView_.itemClick = { [weak self] idx, _ in self?.purchase() } origialPrice_ = _fetchOrigialPrice() ?? "" interfaceThemeDidChanged(self.window?.appearance?.name ?? .aqua) if #available(macOS 15.0, *) { var rootView = CPDFOfferSubscriptionStoreView() rootView.eligibleOffersCallback = { offertIds in KMPrint("你符合赢回的优惠卷:") KMPrint(offertIds) if offertIds.isEmpty == false { if let data = offertIds.first, data.isEmpty == false { if let data = self.offerId_, data.isEmpty == false { return } if self.needShow() { self._selectOffer(offerId: data) self.showWindow(nil) self._saveRecord() self.window?.makeKeyAndOrderFront(nil) } else { KMPrint("Win Back:不需要显示") self.window?.close() } } } } let hostingView = NSHostingView(rootView: rootView) hostingView.frame = NSRect(x: 0, y: 0, width: 1000, height: 1000) contentBox.addSubview(hostingView, positioned: .below, relativeTo: backgroundIv) hostingView.isHidden = true } else { self.window?.close() } // https://api.storekit.itunes.apple.com/inApps/v1/subscriptions/{transactionId} // https://api.storekit-sandbox.itunes.apple.com/inApps/v1/subscriptions/{transactionId} // /2000000828945111 // let url = "https://api.storekit-sandbox.itunes.apple.com/inApps/v1/subscriptions" //// let urlString = KMMemberCenterConfig().activityBaseURL() + url // // ["transactionId" : "2000000828945111"] // KMRequestServer.requestServer.request(urlString: url, method: .get, params: nil) { requestSerializer in //// requestSerializer.setValue("Bearer " + token, forHTTPHeaderField: "Authorization") // } completion: { task, responseObject, error in // guard let dict = responseObject as? [String : Any] else { //// callback(false, nil, error) // return // } // // let model = KMRequestResultModel(dict: dict) //// callback(model.isSuccess(), model, error) // } // KMRequestServer.requestServer.request(urlString: url, method: .get, params: nil, completion:?) // Self.GET(urlString: url, parameter: nil) { success , result , err in // KMPrint("") // } } public class func GET(urlString: String, parameter: [String : String]? = nil, headers: [String : String]? = nil, callback:@escaping ((Bool, [String : Any]?, String?)->Void)) { // var _urlString = "\(self.baseUrl)"+urlString var _urlString = urlString if let data = parameter, !data.isEmpty { _urlString.append("?") var i = 0 for (key, value) in data { _urlString.append("\(key)=\(value)") if (data.count > 1 && i != data.count-1) { _urlString.append("&") } i += 1 } } let url: URL = URL(string: _urlString)! let session = URLSession.shared var request = URLRequest(url: url) request.httpMethod = "GET" request.setValue("application/json; charset=UTF-8", forHTTPHeaderField: "Content-Type") if let _headers = headers { for (key, value) in _headers { request.setValue(value, forHTTPHeaderField: key) } } request.timeoutInterval = 60.0 session.configuration.timeoutIntervalForRequest = 30.0 let task: URLSessionDataTask = session.dataTask(with: request) { data , response, error in DispatchQueue.main.async { if let _ = error { callback(false, nil, error.debugDescription) return } guard let _data = data else { callback(false, nil, error.debugDescription) return } if let result = self.JsonDataParse(data: _data) { // let resultMap = KMRequestResultModel(dict: result) // var dataDict: [String : Any] = [:] // if let data = resultMap.data as? [String : Any] { // dataDict = data // } else { // if let dataArray = resultMap.data as? [Any] { // var i = 0 // for dict in dataArray { // dataDict["\(i)"] = dict // i += 1 // } // } // } // // callback(resultMap.isSuccess(), dataDict, error.debugDescription) // return } // callback(false, nil, error.debugDescription) } } task.resume() } private class func JsonDataParse(data: Data) -> Dictionary? { let result = try?JSONSerialization.jsonObject(with: data, options: .mutableContainers) return result as? Dictionary } override func interfaceThemeDidChanged(_ appearance: NSAppearance.Name) { super.interfaceThemeDidChanged(appearance) KMMainThreadExecute { if KMAppearance.isDarkMode() { self.titleLabel.textColor = .white let attri = NSMutableAttributedString(string: NSLocalizedString("Special Offer - ", comment: ""), attributes: [.font : NSFont.SFProTextRegularFont(32), .foregroundColor : NSColor.white]) attri.append(.init(string: self.displayPriceString_, attributes: [.font : NSFont.SFProTextRegularFont(32), .foregroundColor : NSColor(hex: "#FD7272")])) attri.append(.init(string: self.origialPrice_, attributes: [.font : NSFont.SFProTextRegularFont(24), .foregroundColor : NSColor(hex: "#7E7F85"), .strikethroughStyle : NSUnderlineStyle.single.rawValue])) let style = NSMutableParagraphStyle() style.alignment = .center attri.addAttribute(.paragraphStyle, value: style, range: NSMakeRange(0, attri.length)) self.subTitleLabel.attributedStringValue = attri self.despBox.fillColor = NSColor.white.withAlphaComponent(0.15) self.despView_.titleLabel.textColor = .white let despAttri = NSMutableAttributedString(string: NSLocalizedString("First 6 months", comment: "")+" ", attributes: [.font : NSFont.SFProTextRegularFont(14), .foregroundColor : NSColor.white]) despAttri.append(.init(string: self.displayPriceString_, attributes: [.font : NSFont.SFProTextRegularFont(14), .foregroundColor : NSColor(hex: "#227AFF")])) let string = ", " + String(format: NSLocalizedString("then %@ for 6 months.", comment: ""), self.origialPrice_) despAttri.append(.init(string: string, attributes: [.font : NSFont.SFProTextRegularFont(14), .foregroundColor : NSColor.white])) self.despView_.subTitleLabel.attributedStringValue = despAttri self.despView_.hLine.layer?.backgroundColor = NSColor.white.withAlphaComponent(0.15).cgColor self.despView_.tipLabel.textColor = NSColor(hex: "#C8C9CC") } else { self.titleLabel.textColor = NSColor(hex: "#0E1114") let attri = NSMutableAttributedString(string: NSLocalizedString("Special Offer - ", comment: ""), attributes: [.font : NSFont.SFProTextRegularFont(32), .foregroundColor : NSColor(hex: "#000150")]) attri.append(.init(string: self.displayPriceString_, attributes: [.font : NSFont.SFProTextRegularFont(32), .foregroundColor : NSColor(hex: "#FD7272")])) attri.append(.init(string: self.origialPrice_, attributes: [.font : NSFont.SFProTextRegularFont(24), .foregroundColor : NSColor(hex: "#757780"), .strikethroughStyle : NSUnderlineStyle.single.rawValue])) let style = NSMutableParagraphStyle() style.alignment = .center attri.addAttribute(.paragraphStyle, value: style, range: NSMakeRange(0, attri.length)) self.subTitleLabel.attributedStringValue = attri self.despBox.fillColor = .white self.despView_.titleLabel.textColor = NSColor(hex: "#0E1114") let despAttri = NSMutableAttributedString(string: NSLocalizedString("First 6 months", comment: "")+" ", attributes: [.font : NSFont.SFProTextRegularFont(14), .foregroundColor : NSColor(hex: "#000150")]) despAttri.append(.init(string: self.displayPriceString_, attributes: [.font : NSFont.SFProTextRegularFont(14), .foregroundColor : NSColor(hex: "#4982E6")])) let string = ", " + String(format: NSLocalizedString("then %@ for 6 months.", comment: ""), self.origialPrice_) despAttri.append(.init(string: string, attributes: [.font : NSFont.SFProTextRegularFont(14), .foregroundColor : NSColor(hex: "#000150")])) self.despView_.subTitleLabel.attributedStringValue = despAttri self.despView_.hLine.layer?.backgroundColor = NSColor.black.withAlphaComponent(0.15).cgColor self.despView_.tipLabel.textColor = NSColor(hex: "#757780") } self.buttonView_.backgroundView.colors = [NSColor(hex: "#F8965A"), NSColor(hex: "#FD7171")] self.buttonView_.button.title = NSLocalizedString("Get Special Offer - Advanced 6 Mos", comment: "") self.buttonView_.button.setTitleColor(.white) } } // MARK: - Private Methods private func _showCenter(animate: Bool){ guard let screenFrame = NSScreen.main?.frame else { return } guard let win = self.window else { return } var frame = win.frame frame.origin.y = (screenFrame.size.height-frame.size.height)*0.5 frame.origin.x = (screenFrame.size.width-frame.size.width)*0.5 win.setFrame(frame, display: true, animate: animate) } // MARK: - Private Methods private func _saveRecord() { let lastShowTime = KMDataManager.ud_double(forKey: lastShowTimeKey_) if lastShowTime <= 0 { let cnt = KMDataManager.ud_integer(forKey: showCountKey_) KMDataManager.ud_set(cnt+1, forKey: showCountKey_) let date = Date().timeIntervalSince1970 KMDataManager.ud_set(date, forKey: lastShowTimeKey_) return } let date = Date(timeIntervalSince1970: lastShowTime) let calendar = Calendar.current let unit: Set = [.day,.month,.year] let nowComps = calendar.dateComponents(unit, from: Date()) let selfCmps = calendar.dateComponents(unit, from: date) let theYear = selfCmps.year ?? 0 let theMonth = selfCmps.month ?? 0 let theDay = selfCmps.day ?? 0 let otherYear = nowComps.year ?? 0 let otherMonth = nowComps.month ?? 0 let otherDay = nowComps.day ?? 0 if otherYear > theYear || otherMonth > theMonth { let cnt = KMDataManager.ud_integer(forKey: showCountKey_) KMDataManager.ud_set(cnt+1, forKey: showCountKey_) let date = Date().timeIntervalSince1970 KMDataManager.ud_set(date, forKey: lastShowTimeKey_) return } if otherDay - theDay >= 1 { let cnt = KMDataManager.ud_integer(forKey: showCountKey_) KMDataManager.ud_set(cnt+1, forKey: showCountKey_) let date = Date().timeIntervalSince1970 KMDataManager.ud_set(date, forKey: lastShowTimeKey_) return } } private func _fetchOrigialPrice() -> String? { return IAPProductsManager.default().fourDevicesAllAccessPackNew6Months_lite?.price() } private func _selectOffer(offerId: String) { offerId_ = offerId if #available(macOS 12.0, *) { winbackOffers(productId: "com.pdfreaderpro.mac_free.member.all_access_pack_advanced_6months.001") { offsers in for offse in offsers { let data = offse.id if data == offerId { self.displayPriceString_ = offse.displayPrice self.interfaceThemeDidChanged(self.window?.appearance?.name ?? .aqua) } } } } else { } } private func _showAlert(message: String) { KMMainThreadExecute { let alert = NSAlert() alert.alertStyle = .critical alert.messageText = message alert.addButton(withTitle: NSLocalizedString("OK", comment: "")) alert.runModal() } } private func _beginLoading() { self.window?.contentView?.beginLoading() } private func _endLoading() { self.window?.contentView?.endLoading() } // MARK: - Public Methods public func openWindow() { // self.showWindow(nil) if #available(macOS 12.0, *) { winbackOffers(productId: "com.pdfreaderpro.mac_free.member.all_access_pack_advanced_6months.001") { [weak self] offers in if offers.isEmpty { // 商品没有配置 Win Back 优惠卷 self?.window?.setIsVisible(false) self?.window?.close() } else { self?.showWindow(nil) self?.window?.setIsVisible(false) self?.window?.close() } } } else { window?.setIsVisible(false) window?.close() } } public func needShow() -> Bool { if #available(macOS 15.0, iOS 18.0, *) { if KMDataManager.ud_integer(forKey: showCountKey_) >= 3 { #if DEBUG return true #else return false #endif } let lastShowTime = KMDataManager.ud_double(forKey: lastShowTimeKey_) if lastShowTime > 0 { let date = Date(timeIntervalSince1970: lastShowTime) if date.isToday() { #if DEBUG #else return false #endif } } if KMNewUserGiftManager.default.loginProgressState == .none || KMNewUserGiftManager.default.fetchReceiptProgressState == .none { return false } let member = KMMemberInfo.shared // if member.isLogin { // if member.is_advanced() && member.is_subscribe() && (member.is_year() || member.is_half_year()) { // return false // } // } if member.isMemberAllFunction { // 有本地或账号权益 return false } return true } return false } public func clearRecord() { KMDataManager.ud_set(0, forKey: showCountKey_) KMDataManager.ud_set(0, forKey: lastShowTimeKey_) } @available(macOS 12.0, *) public func winbackOffers(productId: String?, callback: (([Product.SubscriptionOffer])->Void)?) { if #available(macOS 15.0, iOS 18.0, *) { guard let theProductId = productId else { callback?([]) return } Task { let products = try await Product.products(for: [theProductId]) guard let product = products.first else { callback?([]) return } guard let subscriptionInfo = product.subscription else { callback?([]) return } callback?(subscriptionInfo.winBackOffers) } } else { callback?([]) } } public func purchase() { Task { @MainActor in // 加载产品 if #available(macOS 15.0, *) { let products = try? await Product.products(for: ["com.pdfreaderpro.mac_free.member.all_access_pack_advanced_6months.001"]) guard let product = products?.first else { _showAlert(message: "未找到指定的产品") return } // 确保产品为订阅类型 guard let subscriptionInfo = product.subscription else { _showAlert(message: "该产品不是订阅类型") return } // 查找可用的 Win-Back Offer KMPrint("product:\(product)") KMPrint("subscriptionInfo.winBackOffers:\(subscriptionInfo.winBackOffers)") let offers = subscriptionInfo.winBackOffers var off: Product.SubscriptionOffer? for offer in offers { if self.offerId_ == offer.id { off = offer break } } guard let winBackOffer = off else { KMPrint("没有找到适合用户的 Win-Back Offer") _showAlert(message: "No Offer") return } // 使用 Win-Back Offer 创建购买选项 let purchaseOption = Product.PurchaseOption.winBackOffer(winBackOffer) self._beginLoading() // 发起购买 let purchaseResult = try await product.purchase(options: [purchaseOption]) switch purchaseResult { case .success(let verificationResult): self._endLoading() switch verificationResult { case .verified(let transaction): KMPrint("购买成功:\(transaction.id)") await transaction.finish() DispatchQueue.main.asyncAfter(deadline: .now()+1) { self.window?.close() let man = IAPProductsManager.default() man?.winbackIAPPurchased(withProdcutId: transaction.productID, transactionId: "\(transaction.id)") } case .unverified(_, let error): KMPrint("验证失败:\(error)") } case .userCancelled: KMPrint("用户取消了购买") self._endLoading() case .pending: KMPrint("购买失败") @unknown default: KMPrint("购买失败") } } } } } // MARK: - NSWindowDelegate extension KMWinBackWindowController: NSWindowDelegate { func windowDidResize(_ notification: Notification) { guard let data = window?.isEqual(to: notification.object), data == true else { return } backgroundIv.image?.size = window?.frame.size ?? NSMakeSize(520, 540) } }