Sen descrición

PremiumPlansWindowController.swift 22KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515
  1. import Cocoa
  2. final class PremiumPlansWindowController: NSWindowController {
  3. init() {
  4. let viewController = PremiumPlansViewController()
  5. let window = NSWindow(contentViewController: viewController)
  6. window.title = "Premium Plans"
  7. window.styleMask = [.titled, .closable, .miniaturizable, .resizable]
  8. window.styleMask.insert(.fullSizeContentView)
  9. window.titlebarAppearsTransparent = true
  10. window.titleVisibility = .hidden
  11. window.isOpaque = false
  12. window.backgroundColor = .clear
  13. window.setContentSize(NSSize(width: 1160, height: 760))
  14. window.minSize = NSSize(width: 980, height: 680)
  15. window.center()
  16. super.init(window: window)
  17. if let frameView = window.contentView?.superview {
  18. frameView.wantsLayer = true
  19. frameView.layer?.cornerRadius = 18
  20. frameView.layer?.masksToBounds = true
  21. }
  22. }
  23. @available(*, unavailable)
  24. required init?(coder: NSCoder) {
  25. nil
  26. }
  27. }
  28. private final class PremiumPlansViewController: NSViewController {
  29. private struct Plan {
  30. let id: String
  31. let title: String
  32. let subtitle: String
  33. let price: String
  34. let period: String
  35. let billedPill: String
  36. let billedLine: String
  37. let crossedPrice: String?
  38. let savingsText: String?
  39. let features: [String]
  40. let iconName: String
  41. let iconTint: NSColor
  42. let highlight: Bool
  43. }
  44. private enum Theme {
  45. static let pageStart = NSColor(srgbRed: 249 / 255, green: 252 / 255, blue: 255 / 255, alpha: 1)
  46. static let pageEnd = NSColor(srgbRed: 238 / 255, green: 244 / 255, blue: 255 / 255, alpha: 1)
  47. static let cardBackground = NSColor.white
  48. static let primaryText = NSColor(srgbRed: 27 / 255, green: 38 / 255, blue: 79 / 255, alpha: 1)
  49. static let secondaryText = NSColor(srgbRed: 108 / 255, green: 120 / 255, blue: 157 / 255, alpha: 1)
  50. static let cardBorder = NSColor(srgbRed: 198 / 255, green: 216 / 255, blue: 255 / 255, alpha: 1)
  51. static let accent = NSColor(srgbRed: 55 / 255, green: 128 / 255, blue: 255 / 255, alpha: 1)
  52. static let accentHover = NSColor(srgbRed: 38 / 255, green: 108 / 255, blue: 232 / 255, alpha: 1)
  53. static let mutedButtonFill = NSColor(srgbRed: 238 / 255, green: 243 / 255, blue: 252 / 255, alpha: 1)
  54. static let bottomStrip = NSColor(srgbRed: 244 / 255, green: 248 / 255, blue: 255 / 255, alpha: 1)
  55. static let divider = NSColor(srgbRed: 218 / 255, green: 228 / 255, blue: 247 / 255, alpha: 1)
  56. static let successText = NSColor(srgbRed: 21 / 255, green: 154 / 255, blue: 220 / 255, alpha: 1)
  57. static let iconTint = NSColor(srgbRed: 47 / 255, green: 136 / 255, blue: 255 / 255, alpha: 1)
  58. }
  59. private let plans: [Plan] = [
  60. Plan(
  61. id: "weekly",
  62. title: "Weekly",
  63. subtitle: "Flexible and commitment-free",
  64. price: "$9.99",
  65. period: "/ week",
  66. billedPill: "",
  67. billedLine: "",
  68. crossedPrice: nil,
  69. savingsText: nil,
  70. features: [
  71. "All premium features",
  72. "Perfect for short-term goals",
  73. "Cancel anytime"
  74. ],
  75. iconName: "paperplane.fill",
  76. iconTint: Theme.iconTint,
  77. highlight: false
  78. ),
  79. Plan(
  80. id: "monthly",
  81. title: "Monthly",
  82. subtitle: "Balanced for regular productivity",
  83. price: "$19.99",
  84. period: "/ month",
  85. billedPill: "",
  86. billedLine: "",
  87. crossedPrice: nil,
  88. savingsText: nil,
  89. features: [
  90. "All premium features",
  91. "Best value for regular users",
  92. "Priority support"
  93. ],
  94. iconName: "bolt.fill",
  95. iconTint: Theme.accent,
  96. highlight: true
  97. ),
  98. Plan(
  99. id: "yearly",
  100. title: "Yearly",
  101. subtitle: "Best value for long-term users",
  102. price: "$39.99",
  103. period: "/ year",
  104. billedPill: "3-day free trial",
  105. billedLine: "",
  106. crossedPrice: nil,
  107. savingsText: nil,
  108. features: [
  109. "All premium features",
  110. "Lowest effective monthly cost",
  111. "Ideal for long-term use"
  112. ],
  113. iconName: "crown.fill",
  114. iconTint: Theme.successText,
  115. highlight: false
  116. )
  117. ]
  118. private let pageGradient = CAGradientLayer()
  119. override func viewDidLayout() {
  120. super.viewDidLayout()
  121. pageGradient.frame = view.bounds
  122. }
  123. override func loadView() {
  124. view = NSView()
  125. view.wantsLayer = true
  126. view.layer?.cornerRadius = 18
  127. view.layer?.masksToBounds = true
  128. pageGradient.colors = [Theme.pageStart.cgColor, Theme.pageEnd.cgColor]
  129. pageGradient.startPoint = CGPoint(x: 0, y: 1)
  130. pageGradient.endPoint = CGPoint(x: 1, y: 0)
  131. view.layer?.addSublayer(pageGradient)
  132. setupLayout()
  133. }
  134. private func setupLayout() {
  135. let closeButton = NSButton(title: "", target: self, action: #selector(didTapClose))
  136. closeButton.translatesAutoresizingMaskIntoConstraints = false
  137. closeButton.isBordered = false
  138. closeButton.wantsLayer = true
  139. closeButton.layer?.cornerRadius = 15
  140. closeButton.layer?.backgroundColor = NSColor.white.withAlphaComponent(0.92).cgColor
  141. closeButton.layer?.borderWidth = 1
  142. closeButton.layer?.borderColor = Theme.divider.cgColor
  143. closeButton.contentTintColor = Theme.secondaryText
  144. closeButton.image = NSImage(systemSymbolName: "xmark", accessibilityDescription: "Close")
  145. closeButton.imageScaling = .scaleProportionallyDown
  146. closeButton.bezelStyle = .regularSquare
  147. let crownIcon = NSImageView()
  148. crownIcon.translatesAutoresizingMaskIntoConstraints = false
  149. crownIcon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 18, weight: .semibold)
  150. crownIcon.image = NSImage(systemSymbolName: "crown.fill", accessibilityDescription: nil)
  151. crownIcon.contentTintColor = NSColor(srgbRed: 254 / 255, green: 214 / 255, blue: 92 / 255, alpha: 1)
  152. let title = NSTextField(labelWithString: "Upgrade to Pro")
  153. title.font = .systemFont(ofSize: 40, weight: .semibold)
  154. title.textColor = Theme.primaryText
  155. title.alignment = .center
  156. let subtitle = NSTextField(labelWithString: "Unlock unlimited access to premium tools and boost your productivity.")
  157. subtitle.font = .systemFont(ofSize: 14, weight: .regular)
  158. subtitle.textColor = Theme.secondaryText
  159. subtitle.alignment = .center
  160. let cardsRow = NSStackView(views: plans.map(makePricingCard(_:)))
  161. cardsRow.orientation = .horizontal
  162. cardsRow.spacing = 14
  163. cardsRow.alignment = .top
  164. cardsRow.distribution = .fillEqually
  165. cardsRow.translatesAutoresizingMaskIntoConstraints = false
  166. let trustRow = makeTrustRow()
  167. let footerRow = makeFooterRow()
  168. let root = NSStackView(views: [crownIcon, title, subtitle, cardsRow, trustRow, footerRow])
  169. root.orientation = .vertical
  170. root.spacing = 18
  171. root.alignment = .centerX
  172. root.translatesAutoresizingMaskIntoConstraints = false
  173. view.addSubview(root)
  174. view.addSubview(closeButton)
  175. NSLayoutConstraint.activate([
  176. root.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 24),
  177. root.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -24),
  178. root.topAnchor.constraint(equalTo: view.topAnchor, constant: 20),
  179. root.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -16),
  180. closeButton.topAnchor.constraint(equalTo: view.topAnchor, constant: 14),
  181. closeButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -14),
  182. closeButton.widthAnchor.constraint(equalToConstant: 30),
  183. closeButton.heightAnchor.constraint(equalToConstant: 30),
  184. cardsRow.widthAnchor.constraint(equalTo: root.widthAnchor),
  185. cardsRow.heightAnchor.constraint(equalToConstant: 420),
  186. trustRow.widthAnchor.constraint(equalTo: root.widthAnchor),
  187. footerRow.widthAnchor.constraint(equalTo: root.widthAnchor),
  188. crownIcon.heightAnchor.constraint(equalToConstant: 20)
  189. ])
  190. }
  191. private func makePricingCard(_ plan: Plan) -> NSView {
  192. let card = NSView()
  193. card.translatesAutoresizingMaskIntoConstraints = false
  194. card.wantsLayer = true
  195. card.layer?.backgroundColor = Theme.cardBackground.cgColor
  196. card.layer?.cornerRadius = 16
  197. card.layer?.borderWidth = plan.highlight ? 2 : 1
  198. card.layer?.borderColor = (plan.highlight ? Theme.accent : Theme.cardBorder).cgColor
  199. let iconWell = NSView()
  200. iconWell.translatesAutoresizingMaskIntoConstraints = false
  201. iconWell.wantsLayer = true
  202. iconWell.layer?.cornerRadius = 10
  203. iconWell.layer?.backgroundColor = Theme.bottomStrip.cgColor
  204. iconWell.widthAnchor.constraint(equalToConstant: 24).isActive = true
  205. iconWell.heightAnchor.constraint(equalToConstant: 24).isActive = true
  206. let icon = NSImageView()
  207. icon.translatesAutoresizingMaskIntoConstraints = false
  208. icon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 11, weight: .bold)
  209. icon.image = NSImage(systemSymbolName: plan.iconName, accessibilityDescription: nil)
  210. icon.contentTintColor = plan.iconTint
  211. iconWell.addSubview(icon)
  212. NSLayoutConstraint.activate([
  213. icon.centerXAnchor.constraint(equalTo: iconWell.centerXAnchor),
  214. icon.centerYAnchor.constraint(equalTo: iconWell.centerYAnchor)
  215. ])
  216. let titleLabel = NSTextField(labelWithString: plan.title)
  217. titleLabel.font = .systemFont(ofSize: 20, weight: .semibold)
  218. titleLabel.textColor = Theme.primaryText
  219. titleLabel.alignment = .center
  220. let subtitleLabel = NSTextField(labelWithString: plan.subtitle)
  221. subtitleLabel.font = .systemFont(ofSize: 12, weight: .regular)
  222. subtitleLabel.textColor = Theme.secondaryText
  223. subtitleLabel.alignment = .center
  224. let billingPill = pillLabel(text: plan.billedPill, tint: Theme.bottomStrip, textColor: Theme.iconTint)
  225. billingPill.isHidden = plan.billedPill.isEmpty
  226. let priceLabel = NSTextField(labelWithString: plan.price)
  227. priceLabel.font = .systemFont(ofSize: 18, weight: .semibold)
  228. priceLabel.textColor = Theme.primaryText
  229. let periodLabel = NSTextField(labelWithString: plan.period)
  230. periodLabel.font = .systemFont(ofSize: 13, weight: .medium)
  231. periodLabel.textColor = Theme.secondaryText
  232. let priceRow = NSStackView(views: [priceLabel, periodLabel])
  233. priceRow.orientation = .horizontal
  234. priceRow.spacing = 4
  235. priceRow.alignment = .firstBaseline
  236. let billingLabel = NSTextField(labelWithString: plan.billedLine)
  237. billingLabel.font = .systemFont(ofSize: 13, weight: .medium)
  238. billingLabel.textColor = Theme.secondaryText
  239. billingLabel.isHidden = plan.billedLine.isEmpty
  240. let inlinePriceInfo = inlinePriceInfoLabel(oldPrice: plan.crossedPrice, newPrice: plan.savingsText)
  241. inlinePriceInfo.isHidden = (plan.crossedPrice == nil || plan.savingsText == nil)
  242. let divider = NSBox()
  243. divider.boxType = .separator
  244. divider.translatesAutoresizingMaskIntoConstraints = false
  245. divider.borderColor = Theme.divider
  246. let featuresStack = NSStackView(views: plan.features.map(makeFeatureRow(_:)))
  247. featuresStack.orientation = .vertical
  248. featuresStack.spacing = 9
  249. featuresStack.alignment = .leading
  250. let selectButton = NSButton(title: "Get \(plan.title)", target: self, action: #selector(didTapSelectPlan))
  251. selectButton.identifier = NSUserInterfaceItemIdentifier(plan.id)
  252. selectButton.isBordered = false
  253. selectButton.bezelStyle = .rounded
  254. selectButton.font = .systemFont(ofSize: 14, weight: .semibold)
  255. selectButton.contentTintColor = plan.highlight ? .white : Theme.primaryText
  256. selectButton.wantsLayer = true
  257. selectButton.layer?.cornerRadius = 12
  258. selectButton.layer?.borderWidth = 1
  259. selectButton.layer?.borderColor = (plan.highlight ? Theme.accent : Theme.divider).cgColor
  260. selectButton.layer?.backgroundColor = (plan.highlight
  261. ? NSColor(srgbRed: 189 / 255, green: 52 / 255, blue: 255 / 255, alpha: 1)
  262. : Theme.mutedButtonFill).cgColor
  263. selectButton.translatesAutoresizingMaskIntoConstraints = false
  264. selectButton.heightAnchor.constraint(equalToConstant: 58).isActive = true
  265. let spacer = NSView()
  266. spacer.setContentHuggingPriority(.defaultLow, for: .vertical)
  267. let content = NSStackView(views: [iconWell, titleLabel, subtitleLabel, billingPill, priceRow, billingLabel, inlinePriceInfo, divider, featuresStack, spacer, selectButton])
  268. content.orientation = .vertical
  269. content.spacing = 10
  270. content.alignment = .centerX
  271. content.translatesAutoresizingMaskIntoConstraints = false
  272. card.addSubview(content)
  273. NSLayoutConstraint.activate([
  274. divider.widthAnchor.constraint(equalTo: content.widthAnchor),
  275. content.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 18),
  276. content.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -18),
  277. content.topAnchor.constraint(equalTo: card.topAnchor, constant: 14),
  278. content.bottomAnchor.constraint(equalTo: card.bottomAnchor, constant: -16),
  279. selectButton.widthAnchor.constraint(equalTo: content.widthAnchor)
  280. ])
  281. return card
  282. }
  283. private func makeFeatureRow(_ text: String) -> NSView {
  284. let icon = NSImageView()
  285. icon.translatesAutoresizingMaskIntoConstraints = false
  286. icon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 11, weight: .bold)
  287. icon.image = NSImage(systemSymbolName: "checkmark.circle.fill", accessibilityDescription: nil)
  288. icon.contentTintColor = Theme.iconTint
  289. icon.widthAnchor.constraint(equalToConstant: 14).isActive = true
  290. let label = NSTextField(labelWithString: text)
  291. label.font = .systemFont(ofSize: 14, weight: .medium)
  292. label.textColor = Theme.primaryText
  293. let row = NSStackView(views: [icon, label])
  294. row.orientation = .horizontal
  295. row.spacing = 8
  296. row.alignment = .centerY
  297. row.distribution = .fill
  298. return row
  299. }
  300. private func inlinePriceInfoLabel(oldPrice: String?, newPrice: String?) -> NSTextField {
  301. guard let oldPrice, let newPrice else {
  302. return NSTextField(labelWithString: "")
  303. }
  304. let full = NSMutableAttributedString()
  305. let oldAttributes: [NSAttributedString.Key: Any] = [
  306. .font: NSFont.systemFont(ofSize: 12, weight: .semibold),
  307. .foregroundColor: Theme.secondaryText,
  308. .strikethroughStyle: NSUnderlineStyle.single.rawValue
  309. ]
  310. let newAttributes: [NSAttributedString.Key: Any] = [
  311. .font: NSFont.systemFont(ofSize: 12, weight: .bold),
  312. .foregroundColor: Theme.successText
  313. ]
  314. full.append(NSAttributedString(string: "\(oldPrice) ", attributes: oldAttributes))
  315. full.append(NSAttributedString(string: newPrice, attributes: newAttributes))
  316. let label = NSTextField(labelWithAttributedString: full)
  317. return label
  318. }
  319. private func pillLabel(text: String, tint: NSColor, textColor: NSColor) -> NSTextField {
  320. let pill = NSTextField(labelWithString: text)
  321. pill.font = .systemFont(ofSize: 10, weight: .semibold)
  322. pill.textColor = textColor
  323. pill.alignment = .center
  324. pill.wantsLayer = true
  325. pill.layer?.backgroundColor = tint.cgColor
  326. pill.layer?.cornerRadius = 9
  327. pill.translatesAutoresizingMaskIntoConstraints = false
  328. pill.heightAnchor.constraint(equalToConstant: 18).isActive = true
  329. pill.widthAnchor.constraint(greaterThanOrEqualToConstant: 95).isActive = true
  330. return pill
  331. }
  332. private func makeTrustRow() -> NSView {
  333. let badges = NSStackView(views: [
  334. trustBadge(icon: "shield.fill", title: "Secure Payments", subtitle: "Your payment is 100% secure."),
  335. trustBadge(icon: "arrow.counterclockwise", title: "Cancel Anytime", subtitle: "No commitment, cancel anytime."),
  336. trustBadge(icon: "headphones", title: "24/7 Support", subtitle: "We're here to help you anytime."),
  337. trustBadge(icon: "lock.fill", title: "Privacy First", subtitle: "Your data is safe with us.")
  338. ])
  339. badges.orientation = .horizontal
  340. badges.alignment = .centerY
  341. badges.distribution = .fillEqually
  342. badges.spacing = 12
  343. badges.edgeInsets = NSEdgeInsets(top: 0, left: 8, bottom: 0, right: 8)
  344. badges.translatesAutoresizingMaskIntoConstraints = false
  345. badges.wantsLayer = true
  346. badges.layer?.backgroundColor = Theme.bottomStrip.cgColor
  347. badges.layer?.borderColor = Theme.divider.cgColor
  348. badges.layer?.borderWidth = 1
  349. badges.layer?.cornerRadius = 10
  350. badges.setHuggingPriority(.defaultLow, for: .horizontal)
  351. badges.heightAnchor.constraint(equalToConstant: 72).isActive = true
  352. return badges
  353. }
  354. private func makeFooterRow() -> NSView {
  355. let items = [
  356. "Manage Subscription",
  357. "Restore Purchase",
  358. "Privacy Policy",
  359. "Terms of Services",
  360. "Support"
  361. ]
  362. let cells = items.enumerated().map { index, text in
  363. footerCell(text: text, showsTrailingDivider: index < items.count - 1)
  364. }
  365. let links = NSStackView(views: cells)
  366. links.orientation = .horizontal
  367. links.distribution = .fillEqually
  368. links.spacing = 0
  369. links.alignment = .centerY
  370. links.translatesAutoresizingMaskIntoConstraints = false
  371. return links
  372. }
  373. private func footerCell(text: String, showsTrailingDivider: Bool) -> NSView {
  374. let container = NSView()
  375. container.translatesAutoresizingMaskIntoConstraints = false
  376. let label = footerLink(text)
  377. label.translatesAutoresizingMaskIntoConstraints = false
  378. label.alignment = .center
  379. container.addSubview(label)
  380. var constraints = [
  381. label.centerXAnchor.constraint(equalTo: container.centerXAnchor),
  382. label.centerYAnchor.constraint(equalTo: container.centerYAnchor)
  383. ]
  384. if showsTrailingDivider {
  385. let divider = footerDivider()
  386. container.addSubview(divider)
  387. constraints.append(contentsOf: [
  388. divider.trailingAnchor.constraint(equalTo: container.trailingAnchor),
  389. divider.centerYAnchor.constraint(equalTo: container.centerYAnchor)
  390. ])
  391. }
  392. NSLayoutConstraint.activate(constraints)
  393. return container
  394. }
  395. private func trustBadge(icon: String, title: String, subtitle: String) -> NSView {
  396. let image = NSImageView()
  397. image.translatesAutoresizingMaskIntoConstraints = false
  398. image.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 12, weight: .semibold)
  399. image.image = NSImage(systemSymbolName: icon, accessibilityDescription: nil)
  400. image.contentTintColor = Theme.primaryText
  401. let titleLabel = NSTextField(labelWithString: title)
  402. titleLabel.font = .systemFont(ofSize: 12, weight: .bold)
  403. titleLabel.textColor = Theme.primaryText
  404. let subtitleLabel = NSTextField(labelWithString: subtitle)
  405. subtitleLabel.font = .systemFont(ofSize: 10, weight: .medium)
  406. subtitleLabel.textColor = Theme.secondaryText
  407. let textStack = NSStackView(views: [titleLabel, subtitleLabel])
  408. textStack.orientation = .vertical
  409. textStack.spacing = 2
  410. textStack.alignment = .leading
  411. let stack = NSStackView(views: [image, textStack])
  412. stack.orientation = .horizontal
  413. stack.spacing = 8
  414. stack.alignment = .leading
  415. stack.edgeInsets = NSEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
  416. stack.wantsLayer = true
  417. stack.layer?.backgroundColor = NSColor.clear.cgColor
  418. return stack
  419. }
  420. private func footerLink(_ text: String) -> NSTextField {
  421. let label = NSTextField(labelWithString: text)
  422. label.font = .systemFont(ofSize: 12, weight: .medium)
  423. label.textColor = Theme.secondaryText
  424. return label
  425. }
  426. private func footerDivider() -> NSBox {
  427. let divider = NSBox()
  428. divider.boxType = .separator
  429. divider.borderColor = Theme.divider
  430. divider.translatesAutoresizingMaskIntoConstraints = false
  431. divider.widthAnchor.constraint(equalToConstant: 1).isActive = true
  432. divider.heightAnchor.constraint(equalToConstant: 14).isActive = true
  433. return divider
  434. }
  435. @objc private func didTapSelectPlan(_ sender: NSButton) {
  436. sender.layer?.backgroundColor = Theme.accentHover.cgColor
  437. let selectedPlan = sender.identifier?.rawValue ?? sender.title
  438. let alert = NSAlert()
  439. alert.messageText = "Premium checkout coming soon"
  440. alert.informativeText = "Plan selected: \(selectedPlan.capitalized). Payment flow can be connected next."
  441. alert.alertStyle = .informational
  442. alert.addButton(withTitle: "OK")
  443. if let window = view.window {
  444. alert.beginSheetModal(for: window)
  445. } else {
  446. alert.runModal()
  447. }
  448. }
  449. @objc private func didTapClose() {
  450. guard let window = view.window else { return }
  451. if let parent = window.sheetParent {
  452. parent.endSheet(window)
  453. return
  454. }
  455. window.close()
  456. }
  457. }