説明なし

PremiumPlansWindowController.swift 25KB

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