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