KMWinBackWindowController.swift 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565
  1. //
  2. // KMWinBackWindowController.swift
  3. // PDF Reader Pro
  4. //
  5. // Created by User-Tangchao on 2025/1/13.
  6. //
  7. import Cocoa
  8. import StoreKit
  9. import SwiftUI
  10. //class KMWinBackModel: NSObject {
  11. // var originalPrice: Double = 0
  12. //
  13. //}
  14. @objcMembers class KMIAPTransaction: NSObject {
  15. var transactionIdentifier: String?
  16. var productIdentifier: String?
  17. override init() {
  18. super.init()
  19. self.transactionIdentifier = ""
  20. self.productIdentifier = ""
  21. }
  22. }
  23. let KMWinBackIAPProductPurchasedNotificationName = "KMWinBackIAPProductPurchasedNotification"
  24. class KMWinBackWindowController: KMBaseWindowController {
  25. static let shared = KMWinBackWindowController(windowNibName: "KMWinBackWindowController")
  26. @IBOutlet weak var contentBox: NSBox!
  27. @IBOutlet weak var backgroundIv: NSImageView!
  28. @IBOutlet weak var iconIv: NSImageView!
  29. @IBOutlet weak var titleLabel: NSTextField!
  30. @IBOutlet weak var subTitleLabel: NSTextField!
  31. @IBOutlet weak var despBox: NSBox!
  32. @IBOutlet weak var buttonBox: NSBox!
  33. private lazy var despView_: KMWinBackDespView = {
  34. let view = KMWinBackDespView()
  35. return view
  36. }()
  37. private lazy var buttonView_: KMWinBackButtonView = {
  38. let view = KMWinBackButtonView()
  39. return view
  40. }()
  41. private let showCountKey_ = "WinBackWindowShowCount"
  42. private let lastShowTimeKey_ = "WinBackWindowLastShowTime"
  43. private var origialPrice_: String = ""
  44. private var offerId_: String?
  45. private var displayPriceString_: String = ""
  46. override func windowDidLoad() {
  47. super.windowDidLoad()
  48. window?.standardWindowButton(.zoomButton)?.isHidden = true
  49. window?.standardWindowButton(.miniaturizeButton)?.isHidden = true
  50. window?.delegate = self
  51. despBox.borderWidth = 0
  52. despBox.cornerRadius = 4
  53. despBox.contentView = despView_
  54. buttonBox.borderWidth = 0
  55. buttonBox.cornerRadius = 4
  56. buttonBox.contentView = buttonView_
  57. backgroundIv.image = NSImage(named: "KMImageNameWinBackBg")
  58. backgroundIv.image?.size = window?.frame.size ?? NSMakeSize(520, 540)
  59. iconIv.image = NSImage(named: "KMImageNameWinBackIcon")
  60. titleLabel.stringValue = NSLocalizedString("PDF Reader Pro", comment: "")
  61. titleLabel.font = .SFProTextRegularFont(20)
  62. despView_.titleLabel.stringValue = NSLocalizedString("PDF Reader Pro Advanced\n - 6 Months Plan", comment: "")
  63. despView_.tipLabel.stringValue = NSLocalizedString("Because you were previously subscribed to PDF Reader Pro, we are now delivering a special offer to you. ", comment: "")
  64. despView_.iconIv.image = NSImage(named: "KMImageNameWinBackDespIcon")
  65. buttonView_.backgroundView.xRadius = 4
  66. buttonView_.backgroundView.yRadius = 4
  67. buttonView_.button.font = .SFProTextRegularFont(16)
  68. buttonView_.itemClick = { [weak self] idx, _ in
  69. self?.purchase()
  70. }
  71. origialPrice_ = _fetchOrigialPrice() ?? ""
  72. interfaceThemeDidChanged(self.window?.appearance?.name ?? .aqua)
  73. if #available(macOS 15.0, *) {
  74. var rootView = CPDFOfferSubscriptionStoreView()
  75. rootView.eligibleOffersCallback = { offertIds in
  76. KMPrint("你符合赢回的优惠卷:")
  77. KMPrint(offertIds)
  78. if offertIds.isEmpty == false {
  79. if let data = offertIds.first, data.isEmpty == false {
  80. if let data = self.offerId_, data.isEmpty == false {
  81. return
  82. }
  83. if self.needShow() {
  84. self._selectOffer(offerId: data)
  85. self.showWindow(nil)
  86. self._saveRecord()
  87. self.window?.makeKeyAndOrderFront(nil)
  88. } else {
  89. KMPrint("Win Back:不需要显示")
  90. self.window?.close()
  91. }
  92. }
  93. }
  94. }
  95. let hostingView = NSHostingView(rootView: rootView)
  96. hostingView.frame = NSRect(x: 0, y: 0, width: 1000, height: 1000)
  97. contentBox.addSubview(hostingView, positioned: .below, relativeTo: backgroundIv)
  98. hostingView.isHidden = true
  99. } else {
  100. self.window?.close()
  101. }
  102. // https://api.storekit.itunes.apple.com/inApps/v1/subscriptions/{transactionId}
  103. // https://api.storekit-sandbox.itunes.apple.com/inApps/v1/subscriptions/{transactionId}
  104. // /2000000828945111
  105. // let url = "https://api.storekit-sandbox.itunes.apple.com/inApps/v1/subscriptions"
  106. //// let urlString = KMMemberCenterConfig().activityBaseURL() + url
  107. // // ["transactionId" : "2000000828945111"]
  108. // KMRequestServer.requestServer.request(urlString: url, method: .get, params: nil) { requestSerializer in
  109. //// requestSerializer.setValue("Bearer " + token, forHTTPHeaderField: "Authorization")
  110. // } completion: { task, responseObject, error in
  111. // guard let dict = responseObject as? [String : Any] else {
  112. //// callback(false, nil, error)
  113. // return
  114. // }
  115. //
  116. // let model = KMRequestResultModel(dict: dict)
  117. //// callback(model.isSuccess(), model, error)
  118. // }
  119. // KMRequestServer.requestServer.request(urlString: url, method: .get, params: nil, completion:?)
  120. // Self.GET(urlString: url, parameter: nil) { success , result , err in
  121. // KMPrint("")
  122. // }
  123. }
  124. public class func GET(urlString: String, parameter: [String : String]? = nil, headers: [String : String]? = nil, callback:@escaping ((Bool, [String : Any]?, String?)->Void)) {
  125. // var _urlString = "\(self.baseUrl)"+urlString
  126. var _urlString = urlString
  127. if let data = parameter, !data.isEmpty {
  128. _urlString.append("?")
  129. var i = 0
  130. for (key, value) in data {
  131. _urlString.append("\(key)=\(value)")
  132. if (data.count > 1 && i != data.count-1) {
  133. _urlString.append("&")
  134. }
  135. i += 1
  136. }
  137. }
  138. let url: URL = URL(string: _urlString)!
  139. let session = URLSession.shared
  140. var request = URLRequest(url: url)
  141. request.httpMethod = "GET"
  142. request.setValue("application/json; charset=UTF-8", forHTTPHeaderField: "Content-Type")
  143. if let _headers = headers {
  144. for (key, value) in _headers {
  145. request.setValue(value, forHTTPHeaderField: key)
  146. }
  147. }
  148. request.timeoutInterval = 60.0
  149. session.configuration.timeoutIntervalForRequest = 30.0
  150. let task: URLSessionDataTask = session.dataTask(with: request) { data , response, error in
  151. DispatchQueue.main.async {
  152. if let _ = error {
  153. callback(false, nil, error.debugDescription)
  154. return
  155. }
  156. guard let _data = data else {
  157. callback(false, nil, error.debugDescription)
  158. return
  159. }
  160. if let result = self.JsonDataParse(data: _data) {
  161. // let resultMap = KMRequestResultModel(dict: result)
  162. // var dataDict: [String : Any] = [:]
  163. // if let data = resultMap.data as? [String : Any] {
  164. // dataDict = data
  165. // } else {
  166. // if let dataArray = resultMap.data as? [Any] {
  167. // var i = 0
  168. // for dict in dataArray {
  169. // dataDict["\(i)"] = dict
  170. // i += 1
  171. // }
  172. // }
  173. // }
  174. //
  175. // callback(resultMap.isSuccess(), dataDict, error.debugDescription)
  176. // return
  177. }
  178. // callback(false, nil, error.debugDescription)
  179. }
  180. }
  181. task.resume()
  182. }
  183. private class func JsonDataParse(data: Data) -> Dictionary<String,Any>? {
  184. let result = try?JSONSerialization.jsonObject(with: data, options: .mutableContainers)
  185. return result as? Dictionary<String, Any>
  186. }
  187. override func interfaceThemeDidChanged(_ appearance: NSAppearance.Name) {
  188. super.interfaceThemeDidChanged(appearance)
  189. KMMainThreadExecute {
  190. if KMAppearance.isDarkMode() {
  191. self.titleLabel.textColor = .white
  192. let attri = NSMutableAttributedString(string: NSLocalizedString("Special Offer - ", comment: ""), attributes: [.font : NSFont.SFProTextRegularFont(32), .foregroundColor : NSColor.white])
  193. attri.append(.init(string: self.displayPriceString_, attributes: [.font : NSFont.SFProTextRegularFont(32), .foregroundColor : NSColor(hex: "#FD7272")]))
  194. attri.append(.init(string: self.origialPrice_, attributes: [.font : NSFont.SFProTextRegularFont(24), .foregroundColor : NSColor(hex: "#7E7F85"), .strikethroughStyle : NSUnderlineStyle.single.rawValue]))
  195. let style = NSMutableParagraphStyle()
  196. style.alignment = .center
  197. attri.addAttribute(.paragraphStyle, value: style, range: NSMakeRange(0, attri.length))
  198. self.subTitleLabel.attributedStringValue = attri
  199. self.despBox.fillColor = NSColor.white.withAlphaComponent(0.15)
  200. self.despView_.titleLabel.textColor = .white
  201. let despAttri = NSMutableAttributedString(string: NSLocalizedString("First 6 months", comment: "")+" ", attributes: [.font : NSFont.SFProTextRegularFont(14), .foregroundColor : NSColor.white])
  202. despAttri.append(.init(string: self.displayPriceString_, attributes: [.font : NSFont.SFProTextRegularFont(14), .foregroundColor : NSColor(hex: "#227AFF")]))
  203. let string = ", " + String(format: NSLocalizedString("then %@ for 6 months.", comment: ""), self.origialPrice_)
  204. despAttri.append(.init(string: string, attributes: [.font : NSFont.SFProTextRegularFont(14), .foregroundColor : NSColor.white]))
  205. self.despView_.subTitleLabel.attributedStringValue = despAttri
  206. self.despView_.hLine.layer?.backgroundColor = NSColor.white.withAlphaComponent(0.15).cgColor
  207. self.despView_.tipLabel.textColor = NSColor(hex: "#C8C9CC")
  208. } else {
  209. self.titleLabel.textColor = NSColor(hex: "#0E1114")
  210. let attri = NSMutableAttributedString(string: NSLocalizedString("Special Offer - ", comment: ""), attributes: [.font : NSFont.SFProTextRegularFont(32), .foregroundColor : NSColor(hex: "#000150")])
  211. attri.append(.init(string: self.displayPriceString_, attributes: [.font : NSFont.SFProTextRegularFont(32), .foregroundColor : NSColor(hex: "#FD7272")]))
  212. attri.append(.init(string: self.origialPrice_, attributes: [.font : NSFont.SFProTextRegularFont(24), .foregroundColor : NSColor(hex: "#757780"), .strikethroughStyle : NSUnderlineStyle.single.rawValue]))
  213. let style = NSMutableParagraphStyle()
  214. style.alignment = .center
  215. attri.addAttribute(.paragraphStyle, value: style, range: NSMakeRange(0, attri.length))
  216. self.subTitleLabel.attributedStringValue = attri
  217. self.despBox.fillColor = .white
  218. self.despView_.titleLabel.textColor = NSColor(hex: "#0E1114")
  219. let despAttri = NSMutableAttributedString(string: NSLocalizedString("First 6 months", comment: "")+" ", attributes: [.font : NSFont.SFProTextRegularFont(14), .foregroundColor : NSColor(hex: "#000150")])
  220. despAttri.append(.init(string: self.displayPriceString_, attributes: [.font : NSFont.SFProTextRegularFont(14), .foregroundColor : NSColor(hex: "#4982E6")]))
  221. let string = ", " + String(format: NSLocalizedString("then %@ for 6 months.", comment: ""), self.origialPrice_)
  222. despAttri.append(.init(string: string, attributes: [.font : NSFont.SFProTextRegularFont(14), .foregroundColor : NSColor(hex: "#000150")]))
  223. self.despView_.subTitleLabel.attributedStringValue = despAttri
  224. self.despView_.hLine.layer?.backgroundColor = NSColor.black.withAlphaComponent(0.15).cgColor
  225. self.despView_.tipLabel.textColor = NSColor(hex: "#757780")
  226. }
  227. self.buttonView_.backgroundView.colors = [NSColor(hex: "#F8965A"), NSColor(hex: "#FD7171")]
  228. self.buttonView_.button.title = NSLocalizedString("Get Special Offer - Advanced 6 Mos", comment: "")
  229. self.buttonView_.button.setTitleColor(.white)
  230. }
  231. }
  232. // MARK: - Private Methods
  233. private func _showCenter(animate: Bool){
  234. guard let screenFrame = NSScreen.main?.frame else {
  235. return
  236. }
  237. guard let win = self.window else {
  238. return
  239. }
  240. var frame = win.frame
  241. frame.origin.y = (screenFrame.size.height-frame.size.height)*0.5
  242. frame.origin.x = (screenFrame.size.width-frame.size.width)*0.5
  243. win.setFrame(frame, display: true, animate: animate)
  244. }
  245. // MARK: - Private Methods
  246. private func _saveRecord() {
  247. let lastShowTime = KMDataManager.ud_double(forKey: lastShowTimeKey_)
  248. if lastShowTime <= 0 {
  249. let cnt = KMDataManager.ud_integer(forKey: showCountKey_)
  250. KMDataManager.ud_set(cnt+1, forKey: showCountKey_)
  251. let date = Date().timeIntervalSince1970
  252. KMDataManager.ud_set(date, forKey: lastShowTimeKey_)
  253. return
  254. }
  255. let date = Date(timeIntervalSince1970: lastShowTime)
  256. let calendar = Calendar.current
  257. let unit: Set<Calendar.Component> = [.day,.month,.year]
  258. let nowComps = calendar.dateComponents(unit, from: Date())
  259. let selfCmps = calendar.dateComponents(unit, from: date)
  260. let theYear = selfCmps.year ?? 0
  261. let theMonth = selfCmps.month ?? 0
  262. let theDay = selfCmps.day ?? 0
  263. let otherYear = nowComps.year ?? 0
  264. let otherMonth = nowComps.month ?? 0
  265. let otherDay = nowComps.day ?? 0
  266. if otherYear > theYear || otherMonth > theMonth {
  267. let cnt = KMDataManager.ud_integer(forKey: showCountKey_)
  268. KMDataManager.ud_set(cnt+1, forKey: showCountKey_)
  269. let date = Date().timeIntervalSince1970
  270. KMDataManager.ud_set(date, forKey: lastShowTimeKey_)
  271. return
  272. }
  273. if otherDay - theDay >= 1 {
  274. let cnt = KMDataManager.ud_integer(forKey: showCountKey_)
  275. KMDataManager.ud_set(cnt+1, forKey: showCountKey_)
  276. let date = Date().timeIntervalSince1970
  277. KMDataManager.ud_set(date, forKey: lastShowTimeKey_)
  278. return
  279. }
  280. }
  281. private func _fetchOrigialPrice() -> String? {
  282. return IAPProductsManager.default().fourDevicesAllAccessPackNew6Months_lite?.price()
  283. }
  284. private func _selectOffer(offerId: String) {
  285. offerId_ = offerId
  286. if #available(macOS 12.0, *) {
  287. winbackOffers(productId: "com.pdfreaderpro.mac_free.member.all_access_pack_advanced_6months.001") { offsers in
  288. for offse in offsers {
  289. let data = offse.id
  290. if data == offerId {
  291. self.displayPriceString_ = offse.displayPrice
  292. self.interfaceThemeDidChanged(self.window?.appearance?.name ?? .aqua)
  293. }
  294. }
  295. }
  296. } else {
  297. }
  298. }
  299. private func _showAlert(message: String) {
  300. KMMainThreadExecute {
  301. let alert = NSAlert()
  302. alert.alertStyle = .critical
  303. alert.messageText = message
  304. alert.addButton(withTitle: NSLocalizedString("OK", comment: ""))
  305. alert.runModal()
  306. }
  307. }
  308. private func _beginLoading() {
  309. self.window?.contentView?.beginLoading()
  310. }
  311. private func _endLoading() {
  312. self.window?.contentView?.endLoading()
  313. }
  314. // MARK: - Public Methods
  315. public func openWindow() {
  316. // self.showWindow(nil)
  317. if #available(macOS 12.0, *) {
  318. winbackOffers(productId: "com.pdfreaderpro.mac_free.member.all_access_pack_advanced_6months.001") { [weak self] offers in
  319. if offers.isEmpty { // 商品没有配置 Win Back 优惠卷
  320. self?.window?.setIsVisible(false)
  321. self?.window?.close()
  322. } else {
  323. self?.showWindow(nil)
  324. self?.window?.setIsVisible(false)
  325. self?.window?.close()
  326. }
  327. }
  328. } else {
  329. window?.setIsVisible(false)
  330. window?.close()
  331. }
  332. }
  333. public func needShow() -> Bool {
  334. if #available(macOS 15.0, iOS 18.0, *) {
  335. if KMDataManager.ud_integer(forKey: showCountKey_) >= 3 {
  336. #if DEBUG
  337. return true
  338. #else
  339. return false
  340. #endif
  341. }
  342. let lastShowTime = KMDataManager.ud_double(forKey: lastShowTimeKey_)
  343. if lastShowTime > 0 {
  344. let date = Date(timeIntervalSince1970: lastShowTime)
  345. if date.isToday() {
  346. #if DEBUG
  347. #else
  348. return false
  349. #endif
  350. }
  351. }
  352. if KMNewUserGiftManager.default.loginProgressState == .none || KMNewUserGiftManager.default.fetchReceiptProgressState == .none {
  353. return false
  354. }
  355. let member = KMMemberInfo.shared
  356. // if member.isLogin {
  357. // if member.is_advanced() && member.is_subscribe() && (member.is_year() || member.is_half_year()) {
  358. // return false
  359. // }
  360. // }
  361. if member.isMemberAllFunction { // 有本地或账号权益
  362. return false
  363. }
  364. return true
  365. }
  366. return false
  367. }
  368. public func clearRecord() {
  369. KMDataManager.ud_set(0, forKey: showCountKey_)
  370. KMDataManager.ud_set(0, forKey: lastShowTimeKey_)
  371. }
  372. @available(macOS 12.0, *)
  373. public func winbackOffers(productId: String?, callback: (([Product.SubscriptionOffer])->Void)?) {
  374. if #available(macOS 15.0, iOS 18.0, *) {
  375. guard let theProductId = productId else {
  376. callback?([])
  377. return
  378. }
  379. Task {
  380. let products = try await Product.products(for: [theProductId])
  381. guard let product = products.first else {
  382. callback?([])
  383. return
  384. }
  385. guard let subscriptionInfo = product.subscription else {
  386. callback?([])
  387. return
  388. }
  389. callback?(subscriptionInfo.winBackOffers)
  390. }
  391. } else {
  392. callback?([])
  393. }
  394. }
  395. public func purchase() {
  396. Task { @MainActor in
  397. // 加载产品
  398. if #available(macOS 15.0, *) {
  399. let products = try? await Product.products(for: ["com.pdfreaderpro.mac_free.member.all_access_pack_advanced_6months.001"])
  400. guard let product = products?.first else {
  401. _showAlert(message: "未找到指定的产品")
  402. return
  403. }
  404. // 确保产品为订阅类型
  405. guard let subscriptionInfo = product.subscription else {
  406. _showAlert(message: "该产品不是订阅类型")
  407. return
  408. }
  409. // 查找可用的 Win-Back Offer
  410. KMPrint("product:\(product)")
  411. KMPrint("subscriptionInfo.winBackOffers:\(subscriptionInfo.winBackOffers)")
  412. let offers = subscriptionInfo.winBackOffers
  413. var off: Product.SubscriptionOffer?
  414. for offer in offers {
  415. if self.offerId_ == offer.id {
  416. off = offer
  417. break
  418. }
  419. }
  420. guard let winBackOffer = off else {
  421. KMPrint("没有找到适合用户的 Win-Back Offer")
  422. _showAlert(message: "No Offer")
  423. return
  424. }
  425. // 使用 Win-Back Offer 创建购买选项
  426. let purchaseOption = Product.PurchaseOption.winBackOffer(winBackOffer)
  427. self._beginLoading()
  428. // 发起购买
  429. let purchaseResult = try await product.purchase(options: [purchaseOption])
  430. switch purchaseResult {
  431. case .success(let verificationResult):
  432. self._endLoading()
  433. switch verificationResult {
  434. case .verified(let transaction):
  435. KMPrint("购买成功:\(transaction.id)")
  436. await transaction.finish()
  437. DispatchQueue.main.asyncAfter(deadline: .now()+1) {
  438. self.window?.close()
  439. let man = IAPProductsManager.default()
  440. man?.winbackIAPPurchased(withProdcutId: transaction.productID, transactionId: "\(transaction.id)")
  441. }
  442. case .unverified(_, let error):
  443. KMPrint("验证失败:\(error)")
  444. }
  445. case .userCancelled:
  446. KMPrint("用户取消了购买")
  447. self._endLoading()
  448. case .pending:
  449. KMPrint("购买失败")
  450. @unknown default:
  451. KMPrint("购买失败")
  452. }
  453. }
  454. }
  455. }
  456. }
  457. // MARK: - NSWindowDelegate
  458. extension KMWinBackWindowController: NSWindowDelegate {
  459. func windowDidResize(_ notification: Notification) {
  460. guard let data = window?.isEqual(to: notification.object), data == true else {
  461. return
  462. }
  463. backgroundIv.image?.size = window?.frame.size ?? NSMakeSize(520, 540)
  464. }
  465. }