123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565 |
- //
- // 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<String,Any>? {
- let result = try?JSONSerialization.jsonObject(with: data, options: .mutableContainers)
- return result as? Dictionary<String, Any>
- }
-
- 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<Calendar.Component> = [.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)
- }
- }
|