import Cocoa import StoreKit final class PremiumPlansWindowController: NSWindowController { /// Matches `PremiumPlansViewController.Theme.pageStart` so the window backing fills sheet corners. static let paywallSheetBackground = NSColor(srgbRed: 249 / 255, green: 252 / 255, blue: 255 / 255, alpha: 1) init() { let viewController = PremiumPlansViewController() let window = NSWindow(contentViewController: viewController) window.title = "Premium Plans" // Borderless avoids titled-window chrome: its rounded titlebar frame often leaves dark wedges at // the corners when combined with a custom full-bleed paywall (this window is only shown as a sheet). window.styleMask = [.borderless, .closable, .resizable] window.isOpaque = true window.backgroundColor = Self.paywallSheetBackground window.setContentSize(NSSize(width: 1160, height: 760)) window.minSize = NSSize(width: 980, height: 680) window.isRestorable = false window.center() super.init(window: window) } @available(*, unavailable) required init?(coder: NSCoder) { nil } } private final class PremiumPlansViewController: NSViewController { private final class HoverPricingCardView: NSView { private let baseBorderColor: NSColor private let hoverBorderColor: NSColor private var trackingAreaRef: NSTrackingArea? init(baseBorderColor: NSColor, hoverBorderColor: NSColor) { self.baseBorderColor = baseBorderColor self.hoverBorderColor = hoverBorderColor super.init(frame: .zero) wantsLayer = true layer?.cornerRadius = 16 applyHoverStyle(isHovered: false, animated: false) } @available(*, unavailable) required init?(coder: NSCoder) { nil } override func updateTrackingAreas() { super.updateTrackingAreas() if let trackingAreaRef { removeTrackingArea(trackingAreaRef) } let options: NSTrackingArea.Options = [.activeInActiveApp, .mouseEnteredAndExited, .inVisibleRect] let area = NSTrackingArea(rect: .zero, options: options, owner: self, userInfo: nil) addTrackingArea(area) trackingAreaRef = area } override func mouseEntered(with event: NSEvent) { super.mouseEntered(with: event) applyHoverStyle(isHovered: true, animated: true) } override func mouseExited(with event: NSEvent) { super.mouseExited(with: event) applyHoverStyle(isHovered: false, animated: true) } private func applyHoverStyle(isHovered: Bool, animated: Bool) { guard let layer else { return } let updates = { layer.borderWidth = isHovered ? 2 : 1 layer.borderColor = (isHovered ? self.hoverBorderColor : self.baseBorderColor).cgColor layer.shadowColor = self.hoverBorderColor.withAlphaComponent(0.35).cgColor layer.shadowOpacity = isHovered ? 0.22 : 0 layer.shadowRadius = isHovered ? 14 : 0 layer.shadowOffset = .init(width: 0, height: -2) } if animated { NSAnimationContext.runAnimationGroup { context in context.duration = 0.16 updates() } } else { updates() } } } /// Footer text actions: accent color + pointing hand on hover (matches pricing-card tracking behavior). private final class FooterLinkButton: NSButton { private var trackingAreaRef: NSTrackingArea? private var didPushCursor = false override func updateTrackingAreas() { super.updateTrackingAreas() if let trackingAreaRef { removeTrackingArea(trackingAreaRef) } let options: NSTrackingArea.Options = [.activeInActiveApp, .mouseEnteredAndExited, .inVisibleRect] let area = NSTrackingArea(rect: .zero, options: options, owner: self, userInfo: nil) addTrackingArea(area) trackingAreaRef = area } override func mouseEntered(with event: NSEvent) { super.mouseEntered(with: event) setHoverVisuals(hovered: true) if !didPushCursor { NSCursor.pointingHand.push() didPushCursor = true } } override func mouseExited(with event: NSEvent) { super.mouseExited(with: event) setHoverVisuals(hovered: false) if didPushCursor { NSCursor.pop() didPushCursor = false } } override func viewWillMove(toWindow newWindow: NSWindow?) { super.viewWillMove(toWindow: newWindow) if newWindow == nil, didPushCursor { NSCursor.pop() didPushCursor = false } if newWindow == nil { setHoverVisuals(hovered: false, animated: false) } } private func setHoverVisuals(hovered: Bool, animated: Bool = true) { let color = hovered ? Theme.accent : Theme.secondaryText if animated { NSAnimationContext.runAnimationGroup { context in context.duration = 0.15 self.animator().contentTintColor = color } } else { contentTintColor = color } } } /// Purchase CTAs: fill/border/shadow + pointing hand on hover. private final class PlanPurchaseHoverButton: NSButton { private var trackingAreaRef: NSTrackingArea? private var didPushCursor = false private let isPrimaryStyle: Bool private static let primaryFill = NSColor(srgbRed: 189 / 255, green: 52 / 255, blue: 255 / 255, alpha: 1) private static let primaryFillHover = NSColor(srgbRed: 205 / 255, green: 88 / 255, blue: 255 / 255, alpha: 1) init(planId: String, title: String, isPrimaryStyle: Bool, target: AnyObject?, action: Selector) { self.isPrimaryStyle = isPrimaryStyle super.init(frame: .zero) identifier = NSUserInterfaceItemIdentifier(planId) self.title = title self.target = target self.action = action isBordered = false bezelStyle = .rounded font = .systemFont(ofSize: 14, weight: .semibold) wantsLayer = true layer?.cornerRadius = 12 focusRingType = .none translatesAutoresizingMaskIntoConstraints = false applyBaseStyle(hovered: false) } @available(*, unavailable) required init?(coder: NSCoder) { nil } override var isEnabled: Bool { didSet { if !isEnabled { applyBaseStyle(hovered: false, animated: true) if didPushCursor { NSCursor.pop() didPushCursor = false } } } } override func updateTrackingAreas() { super.updateTrackingAreas() if let trackingAreaRef { removeTrackingArea(trackingAreaRef) } let options: NSTrackingArea.Options = [.activeInActiveApp, .mouseEnteredAndExited, .inVisibleRect] let area = NSTrackingArea(rect: .zero, options: options, owner: self, userInfo: nil) addTrackingArea(area) trackingAreaRef = area } override func mouseEntered(with event: NSEvent) { super.mouseEntered(with: event) guard isEnabled else { return } applyBaseStyle(hovered: true, animated: true) if !didPushCursor { NSCursor.pointingHand.push() didPushCursor = true } } override func mouseExited(with event: NSEvent) { super.mouseExited(with: event) applyBaseStyle(hovered: false, animated: true) if didPushCursor { NSCursor.pop() didPushCursor = false } } override func viewWillMove(toWindow newWindow: NSWindow?) { super.viewWillMove(toWindow: newWindow) if newWindow == nil, didPushCursor { NSCursor.pop() didPushCursor = false } if newWindow == nil { applyBaseStyle(hovered: false, animated: false) } } private func applyBaseStyle(hovered: Bool, animated: Bool = true) { let updates = { if self.isPrimaryStyle { self.layer?.backgroundColor = (hovered ? Self.primaryFillHover : Self.primaryFill).cgColor self.layer?.borderColor = Theme.accent.cgColor self.layer?.borderWidth = hovered ? 2 : 1 self.contentTintColor = .white self.layer?.shadowColor = Self.primaryFill.cgColor self.layer?.shadowOpacity = hovered ? 0.28 : 0 self.layer?.shadowRadius = hovered ? 12 : 0 self.layer?.shadowOffset = CGSize(width: 0, height: -2) } else { let baseFill = Theme.mutedButtonFill let hoverFill = baseFill.blended(withFraction: 0.22, of: Theme.accent) ?? baseFill self.layer?.backgroundColor = (hovered ? hoverFill : baseFill).cgColor self.layer?.borderColor = (hovered ? Theme.accent : Theme.divider).cgColor self.layer?.borderWidth = hovered ? 2 : 1 self.contentTintColor = Theme.primaryText self.layer?.shadowColor = Theme.accent.withAlphaComponent(0.35).cgColor self.layer?.shadowOpacity = hovered ? 0.18 : 0 self.layer?.shadowRadius = hovered ? 10 : 0 self.layer?.shadowOffset = CGSize(width: 0, height: -2) } } if animated { NSAnimationContext.runAnimationGroup { context in context.duration = 0.16 updates() } } else { updates() } } } /// Close control: subtle lift + accent tint on hover. private final class PremiumCloseHoverButton: NSButton { private var trackingAreaRef: NSTrackingArea? private var didPushCursor = false init(target: AnyObject?, action: Selector) { super.init(frame: .zero) self.target = target self.action = action isBordered = false wantsLayer = true layer?.cornerRadius = 15 bezelStyle = .regularSquare image = NSImage(systemSymbolName: "xmark", accessibilityDescription: "Close") imageScaling = .scaleProportionallyDown focusRingType = .none translatesAutoresizingMaskIntoConstraints = false applyStyle(hovered: false, animated: false) } @available(*, unavailable) required init?(coder: NSCoder) { nil } override func updateTrackingAreas() { super.updateTrackingAreas() if let trackingAreaRef { removeTrackingArea(trackingAreaRef) } let options: NSTrackingArea.Options = [.activeInActiveApp, .mouseEnteredAndExited, .inVisibleRect] let area = NSTrackingArea(rect: .zero, options: options, owner: self, userInfo: nil) addTrackingArea(area) trackingAreaRef = area } override func mouseEntered(with event: NSEvent) { super.mouseEntered(with: event) applyStyle(hovered: true, animated: true) if !didPushCursor { NSCursor.pointingHand.push() didPushCursor = true } } override func mouseExited(with event: NSEvent) { super.mouseExited(with: event) applyStyle(hovered: false, animated: true) if didPushCursor { NSCursor.pop() didPushCursor = false } } override func viewWillMove(toWindow newWindow: NSWindow?) { super.viewWillMove(toWindow: newWindow) if newWindow == nil, didPushCursor { NSCursor.pop() didPushCursor = false } if newWindow == nil { applyStyle(hovered: false, animated: false) } } private func applyStyle(hovered: Bool, animated: Bool) { let updates = { self.layer?.backgroundColor = (hovered ? NSColor.white.withAlphaComponent(0.98) : NSColor.white.withAlphaComponent(0.92)).cgColor self.layer?.borderColor = (hovered ? Theme.accent.withAlphaComponent(0.45) : Theme.divider).cgColor self.layer?.borderWidth = hovered ? 1.5 : 1 self.contentTintColor = hovered ? Theme.accent : Theme.secondaryText self.layer?.shadowColor = Theme.accent.withAlphaComponent(0.25).cgColor self.layer?.shadowOpacity = hovered ? 0.2 : 0 self.layer?.shadowRadius = hovered ? 8 : 0 self.layer?.shadowOffset = CGSize(width: 0, height: -1) } if animated { NSAnimationContext.runAnimationGroup { context in context.duration = 0.15 updates() } } else { updates() } } } /// Shown until StoreKit returns localized `Product.displayPrice` (never use hardcoded currency amounts). private static let unloadedPricePlaceholder = "—" private struct Plan { let id: String let title: String let subtitle: String let period: String let billedPill: String let billedLine: String let crossedPrice: String? let savingsText: String? let features: [String] let iconName: String let iconTint: NSColor let highlight: Bool } private enum Theme { static let pageStart = NSColor(srgbRed: 249 / 255, green: 252 / 255, blue: 255 / 255, alpha: 1) static let pageEnd = NSColor(srgbRed: 238 / 255, green: 244 / 255, blue: 255 / 255, alpha: 1) static let cardBackground = NSColor.white static let primaryText = NSColor(srgbRed: 27 / 255, green: 38 / 255, blue: 79 / 255, alpha: 1) static let secondaryText = NSColor(srgbRed: 108 / 255, green: 120 / 255, blue: 157 / 255, alpha: 1) static let cardBorder = NSColor(srgbRed: 198 / 255, green: 216 / 255, blue: 255 / 255, alpha: 1) static let accent = NSColor(srgbRed: 55 / 255, green: 128 / 255, blue: 255 / 255, alpha: 1) static let accentHover = NSColor(srgbRed: 38 / 255, green: 108 / 255, blue: 232 / 255, alpha: 1) static let mutedButtonFill = NSColor(srgbRed: 238 / 255, green: 243 / 255, blue: 252 / 255, alpha: 1) static let bottomStrip = NSColor(srgbRed: 244 / 255, green: 248 / 255, blue: 255 / 255, alpha: 1) static let divider = NSColor(srgbRed: 218 / 255, green: 228 / 255, blue: 247 / 255, alpha: 1) static let successText = NSColor(srgbRed: 21 / 255, green: 154 / 255, blue: 220 / 255, alpha: 1) static let iconTint = NSColor(srgbRed: 47 / 255, green: 136 / 255, blue: 255 / 255, alpha: 1) } private enum FeatureListMetrics { static let spacing = CGFloat(10) static let edgeInsets = NSEdgeInsets(top: 21, left: 37, bottom: 21, right: 0) } private let subscriptionStore = SubscriptionStore.shared private var planPriceFields: [String: (price: NSTextField, period: NSTextField)] = [:] private var planPurchaseButtons: [String: NSButton] = [:] private var subscriptionPrimaryFooterButton: NSButton? private var premiumCloseButton: NSButton? private var subscriptionStatusObservation: NSObjectProtocol? private let plans: [Plan] = [ Plan( id: "weekly", title: "Weekly", subtitle: "Flexible and commitment-free", period: "/ week", billedPill: "", billedLine: "", crossedPrice: nil, savingsText: nil, features: [ "All premium features", "Perfect for short-term goals", "Cancel anytime" ], iconName: "paperplane.fill", iconTint: Theme.iconTint, highlight: false ), Plan( id: "monthly", title: "Monthly", subtitle: "Balanced for regular productivity", period: "/ month", billedPill: "", billedLine: "", crossedPrice: nil, savingsText: nil, features: [ "All premium features", "Best value for regular users", "Priority support" ], iconName: "bolt.fill", iconTint: Theme.accent, highlight: true ), Plan( id: "yearly", title: "Yearly", subtitle: "Best value for long-term users", period: "/ year", billedPill: "3 days free trial", billedLine: "", crossedPrice: nil, savingsText: nil, features: [ "All premium features", "Lowest effective monthly cost", "Ideal for long-term use" ], iconName: "crown.fill", iconTint: Theme.successText, highlight: false ) ] private let pageGradient = CAGradientLayer() deinit { if let subscriptionStatusObservation { NotificationCenter.default.removeObserver(subscriptionStatusObservation) } } override func viewDidLoad() { super.viewDidLoad() subscriptionStatusObservation = NotificationCenter.default.addObserver( forName: .subscriptionStatusDidChange, object: nil, queue: .main ) { [weak self] _ in Task { @MainActor in await self?.subscriptionStore.loadProducts() self?.applyStorePricing() self?.updateSubscriptionPrimaryFooter() self?.updatePremiumCloseButtonVisibility() } } Task { @MainActor in await loadStoreProducts() } } override func viewDidLayout() { super.viewDidLayout() pageGradient.frame = view.bounds } override func loadView() { view = NSView() view.wantsLayer = true pageGradient.colors = [Theme.pageStart.cgColor, Theme.pageEnd.cgColor] pageGradient.startPoint = CGPoint(x: 0, y: 1) pageGradient.endPoint = CGPoint(x: 1, y: 0) view.layer?.addSublayer(pageGradient) setupLayout() } private func setupLayout() { let closeButton = PremiumCloseHoverButton(target: self, action: #selector(didTapClose)) let crownIcon = NSImageView() crownIcon.translatesAutoresizingMaskIntoConstraints = false crownIcon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 18, weight: .semibold) crownIcon.image = NSImage(systemSymbolName: "crown.fill", accessibilityDescription: nil) crownIcon.contentTintColor = NSColor(srgbRed: 254 / 255, green: 214 / 255, blue: 92 / 255, alpha: 1) let title = NSTextField(labelWithString: "Upgrade to Pro") title.font = .systemFont(ofSize: 40, weight: .semibold) title.textColor = Theme.primaryText title.alignment = .center let subtitle = NSTextField(labelWithString: "Unlock unlimited access to premium tools and boost your productivity.") subtitle.font = .systemFont(ofSize: 14, weight: .regular) subtitle.textColor = Theme.secondaryText subtitle.alignment = .center let cardsRow = NSStackView(views: plans.map(makePricingCard(_:))) cardsRow.orientation = .horizontal cardsRow.spacing = 14 cardsRow.alignment = .top cardsRow.distribution = .fillEqually cardsRow.translatesAutoresizingMaskIntoConstraints = false for card in cardsRow.arrangedSubviews { card.heightAnchor.constraint(equalTo: cardsRow.heightAnchor).isActive = true } let trustRow = makeTrustRow() let footerRow = makeFooterRow() let root = NSStackView(views: [crownIcon, title, subtitle, cardsRow, trustRow, footerRow]) root.orientation = .vertical root.spacing = 18 root.alignment = .centerX root.translatesAutoresizingMaskIntoConstraints = false view.addSubview(root) view.addSubview(closeButton) NSLayoutConstraint.activate([ root.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 24), root.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -24), root.topAnchor.constraint(equalTo: view.topAnchor, constant: 20), root.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -16), closeButton.topAnchor.constraint(equalTo: view.topAnchor, constant: 14), closeButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -14), closeButton.widthAnchor.constraint(equalToConstant: 30), closeButton.heightAnchor.constraint(equalToConstant: 30), cardsRow.widthAnchor.constraint(equalTo: root.widthAnchor), cardsRow.heightAnchor.constraint(equalToConstant: 420), trustRow.widthAnchor.constraint(equalTo: root.widthAnchor), footerRow.widthAnchor.constraint(equalTo: root.widthAnchor), crownIcon.heightAnchor.constraint(equalToConstant: 20) ]) premiumCloseButton = closeButton updatePremiumCloseButtonVisibility() } private func updatePremiumCloseButtonVisibility() { premiumCloseButton?.isHidden = !subscriptionStore.isProActive } private func makePricingCard(_ plan: Plan) -> NSView { let card = HoverPricingCardView(baseBorderColor: Theme.cardBorder, hoverBorderColor: Theme.accent) card.translatesAutoresizingMaskIntoConstraints = false card.layer?.backgroundColor = Theme.cardBackground.cgColor let iconWell = NSView() iconWell.translatesAutoresizingMaskIntoConstraints = false iconWell.wantsLayer = true iconWell.layer?.cornerRadius = 10 iconWell.layer?.backgroundColor = Theme.bottomStrip.cgColor iconWell.widthAnchor.constraint(equalToConstant: 24).isActive = true iconWell.heightAnchor.constraint(equalToConstant: 24).isActive = true let icon = NSImageView() icon.translatesAutoresizingMaskIntoConstraints = false icon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 11, weight: .bold) icon.image = NSImage(systemSymbolName: plan.iconName, accessibilityDescription: nil) icon.contentTintColor = plan.iconTint iconWell.addSubview(icon) NSLayoutConstraint.activate([ icon.centerXAnchor.constraint(equalTo: iconWell.centerXAnchor), icon.centerYAnchor.constraint(equalTo: iconWell.centerYAnchor) ]) let titleLabel = NSTextField(labelWithString: plan.title) titleLabel.font = .systemFont(ofSize: 20, weight: .semibold) titleLabel.textColor = Theme.primaryText titleLabel.alignment = .center let subtitleLabel = NSTextField(labelWithString: plan.subtitle) subtitleLabel.font = .systemFont(ofSize: 12, weight: .regular) subtitleLabel.textColor = Theme.secondaryText subtitleLabel.alignment = .center let topRightTag = pillLabel(text: plan.billedPill, tint: Theme.bottomStrip, textColor: Theme.iconTint) topRightTag.isHidden = plan.billedPill.isEmpty topRightTag.font = .systemFont(ofSize: 10, weight: .bold) let priceLabel = NSTextField(labelWithString: Self.unloadedPricePlaceholder) priceLabel.font = .systemFont(ofSize: 18, weight: .semibold) priceLabel.textColor = Theme.primaryText let periodLabel = NSTextField(labelWithString: plan.period) periodLabel.font = .systemFont(ofSize: 13, weight: .medium) periodLabel.textColor = Theme.secondaryText let priceRow = NSStackView(views: [priceLabel, periodLabel]) priceRow.orientation = .horizontal priceRow.spacing = 4 priceRow.alignment = .firstBaseline let billingLabel = NSTextField(labelWithString: plan.billedLine) billingLabel.font = .systemFont(ofSize: 13, weight: .medium) billingLabel.textColor = Theme.secondaryText let inlinePriceInfo = inlinePriceInfoLabel(oldPrice: plan.crossedPrice, newPrice: plan.savingsText) let divider = NSBox() divider.boxType = .separator divider.translatesAutoresizingMaskIntoConstraints = false divider.borderColor = Theme.divider let featuresStack = NSStackView(views: plan.features.map(makeFeatureRow(_:))) featuresStack.orientation = .vertical featuresStack.spacing = FeatureListMetrics.spacing featuresStack.alignment = .leading featuresStack.edgeInsets = FeatureListMetrics.edgeInsets let selectButton = PlanPurchaseHoverButton( planId: plan.id, title: "Get \(plan.title)", isPrimaryStyle: plan.highlight, target: self, action: #selector(didTapSelectPlan) ) planPurchaseButtons[plan.id] = selectButton planPriceFields[plan.id] = (priceLabel, periodLabel) selectButton.heightAnchor.constraint(equalToConstant: 50).isActive = true var contentViews: [NSView] = [iconWell, titleLabel, subtitleLabel, priceRow] if !plan.billedLine.isEmpty { contentViews.append(billingLabel) } if plan.crossedPrice != nil, plan.savingsText != nil { contentViews.append(inlinePriceInfo) } contentViews.append(contentsOf: [divider, featuresStack]) let verticalFlex = NSView() verticalFlex.translatesAutoresizingMaskIntoConstraints = false verticalFlex.setContentHuggingPriority(.defaultLow, for: .vertical) verticalFlex.setContentCompressionResistancePriority(.defaultLow, for: .vertical) let column = NSStackView(views: contentViews + [verticalFlex, selectButton]) column.orientation = .vertical column.spacing = 10 column.alignment = .centerX column.distribution = .fill column.translatesAutoresizingMaskIntoConstraints = false card.addSubview(column) card.addSubview(topRightTag) NSLayoutConstraint.activate([ divider.widthAnchor.constraint(equalTo: column.widthAnchor), featuresStack.widthAnchor.constraint(equalTo: column.widthAnchor), column.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 18), column.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -18), column.topAnchor.constraint(equalTo: card.topAnchor, constant: 14), column.bottomAnchor.constraint(equalTo: card.bottomAnchor, constant: -12), selectButton.widthAnchor.constraint(equalTo: column.widthAnchor), topRightTag.topAnchor.constraint(equalTo: card.topAnchor, constant: 12), topRightTag.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -12) ]) return card } private func makeFeatureRow(_ text: String) -> NSView { let icon = NSImageView() icon.translatesAutoresizingMaskIntoConstraints = false icon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 11, weight: .bold) icon.image = NSImage(systemSymbolName: "checkmark.circle.fill", accessibilityDescription: nil) icon.contentTintColor = Theme.iconTint icon.widthAnchor.constraint(equalToConstant: 14).isActive = true let label = NSTextField(labelWithString: text) label.font = .systemFont(ofSize: 14, weight: .medium) label.textColor = Theme.primaryText let row = NSStackView(views: [icon, label]) row.orientation = .horizontal row.spacing = FeatureListMetrics.spacing row.alignment = .centerY row.distribution = .fill return row } private func inlinePriceInfoLabel(oldPrice: String?, newPrice: String?) -> NSTextField { guard let oldPrice, let newPrice else { return NSTextField(labelWithString: "") } let full = NSMutableAttributedString() let oldAttributes: [NSAttributedString.Key: Any] = [ .font: NSFont.systemFont(ofSize: 12, weight: .semibold), .foregroundColor: Theme.secondaryText, .strikethroughStyle: NSUnderlineStyle.single.rawValue ] let newAttributes: [NSAttributedString.Key: Any] = [ .font: NSFont.systemFont(ofSize: 12, weight: .bold), .foregroundColor: Theme.successText ] full.append(NSAttributedString(string: "\(oldPrice) ", attributes: oldAttributes)) full.append(NSAttributedString(string: newPrice, attributes: newAttributes)) let label = NSTextField(labelWithAttributedString: full) return label } private func pillLabel(text: String, tint: NSColor, textColor: NSColor) -> NSTextField { let pill = NSTextField(labelWithString: text) pill.font = .systemFont(ofSize: 10, weight: .semibold) pill.textColor = textColor pill.alignment = .center pill.wantsLayer = true pill.layer?.backgroundColor = tint.cgColor pill.layer?.cornerRadius = 9 pill.translatesAutoresizingMaskIntoConstraints = false pill.heightAnchor.constraint(equalToConstant: 18).isActive = true pill.widthAnchor.constraint(greaterThanOrEqualToConstant: 95).isActive = true return pill } private func makeTrustRow() -> NSView { let badges = NSStackView(views: [ trustBadge(icon: "shield.fill", title: "Secure Payments", subtitle: "Your payment is 100% secure."), trustBadge(icon: "arrow.counterclockwise", title: "Cancel Anytime", subtitle: "No commitment, cancel anytime."), trustBadge(icon: "headphones", title: "24/7 Support", subtitle: "We're here to help you anytime."), trustBadge(icon: "lock.fill", title: "Privacy First", subtitle: "Your data is safe with us.") ]) badges.orientation = .horizontal badges.alignment = .centerY badges.distribution = .fillEqually badges.spacing = 12 badges.edgeInsets = NSEdgeInsets(top: 0, left: 8, bottom: 0, right: 8) badges.translatesAutoresizingMaskIntoConstraints = false badges.wantsLayer = true badges.layer?.backgroundColor = Theme.bottomStrip.cgColor badges.layer?.borderColor = Theme.divider.cgColor badges.layer?.borderWidth = 1 badges.layer?.cornerRadius = 10 badges.setHuggingPriority(.defaultLow, for: .horizontal) badges.heightAnchor.constraint(equalToConstant: 72).isActive = true return badges } private func makeFooterRow() -> NSView { let primary = footerActionCell( title: subscriptionPrimaryFooterTitle(), action: #selector(didTapPrimaryFooterSubscriptionAction), showsTrailingDivider: true ) subscriptionPrimaryFooterButton = primary.button let entries: [(text: String, action: Selector)] = [ ("Restore Purchase", #selector(didTapRestorePurchases)), ("Privacy Policy", #selector(didTapFooterPrivacyPolicy)), ("Terms of Use", #selector(didTapFooterTermsOfServices)), ("Support", #selector(didTapFooterSupport)) ] let cells = [primary.container] + entries.enumerated().map { index, entry in footerActionCell(title: entry.text, action: entry.action, showsTrailingDivider: index < entries.count - 1).container } let links = NSStackView(views: cells) links.orientation = .horizontal links.distribution = .fillEqually links.spacing = 0 links.alignment = .centerY links.translatesAutoresizingMaskIntoConstraints = false return links } private func footerActionCell(title: String, action: Selector, showsTrailingDivider: Bool) -> (container: NSView, button: NSButton) { let container = NSView() container.translatesAutoresizingMaskIntoConstraints = false let button = FooterLinkButton(title: title, target: self, action: action) button.isBordered = false button.bezelStyle = .rounded button.font = .systemFont(ofSize: 12, weight: .medium) button.contentTintColor = Theme.secondaryText button.focusRingType = .none button.translatesAutoresizingMaskIntoConstraints = false container.addSubview(button) var constraints: [NSLayoutConstraint] = [ button.centerXAnchor.constraint(equalTo: container.centerXAnchor), button.centerYAnchor.constraint(equalTo: container.centerYAnchor) ] if showsTrailingDivider { let divider = footerDivider() container.addSubview(divider) constraints.append(contentsOf: [ divider.trailingAnchor.constraint(equalTo: container.trailingAnchor), divider.centerYAnchor.constraint(equalTo: container.centerYAnchor) ]) } NSLayoutConstraint.activate(constraints) return (container, button) } private enum PrimaryFooterSubscriptionTitle { static let manage = "Manage Subscription" static let continueFree = "Continue with free plan" } private func subscriptionPrimaryFooterTitle() -> String { subscriptionStore.isProActive ? PrimaryFooterSubscriptionTitle.manage : PrimaryFooterSubscriptionTitle.continueFree } private func updateSubscriptionPrimaryFooter() { subscriptionPrimaryFooterButton?.title = subscriptionPrimaryFooterTitle() } private func trustBadge(icon: String, title: String, subtitle: String) -> NSView { let image = NSImageView() image.translatesAutoresizingMaskIntoConstraints = false image.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 12, weight: .semibold) image.image = NSImage(systemSymbolName: icon, accessibilityDescription: nil) image.contentTintColor = Theme.primaryText let titleLabel = NSTextField(labelWithString: title) titleLabel.font = .systemFont(ofSize: 12, weight: .bold) titleLabel.textColor = Theme.primaryText let subtitleLabel = NSTextField(labelWithString: subtitle) subtitleLabel.font = .systemFont(ofSize: 10, weight: .medium) subtitleLabel.textColor = Theme.secondaryText let textStack = NSStackView(views: [titleLabel, subtitleLabel]) textStack.orientation = .vertical textStack.spacing = 2 textStack.alignment = .leading let stack = NSStackView(views: [image, textStack]) stack.orientation = .horizontal stack.spacing = 8 stack.alignment = .leading stack.edgeInsets = NSEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) stack.wantsLayer = true stack.layer?.backgroundColor = NSColor.clear.cgColor return stack } private func footerDivider() -> NSBox { let divider = NSBox() divider.boxType = .separator divider.borderColor = Theme.divider divider.translatesAutoresizingMaskIntoConstraints = false divider.widthAnchor.constraint(equalToConstant: 1).isActive = true divider.heightAnchor.constraint(equalToConstant: 14).isActive = true return divider } @objc private func didTapSelectPlan(_ sender: NSButton) { guard let planKey = sender.identifier?.rawValue else { return } Task { await purchasePlan(planKey: planKey) } } @objc private func didTapPrimaryFooterSubscriptionAction(_ sender: NSButton) { let userTappedManage = (sender.title == PrimaryFooterSubscriptionTitle.manage) Task { @MainActor [weak self] in guard let self else { return } await subscriptionStore.refreshEntitlements(deep: true) updateSubscriptionPrimaryFooter() let active = subscriptionStore.isProActive if active || userTappedManage { guard let url = URL(string: "https://apps.apple.com/account/subscriptions") else { return } NSWorkspace.shared.open(url) return } // Non-pro: dismiss paywall and return to the home (dashboard) window. dismissPremiumSheetFromParentIfNeeded() } } @objc private func didTapRestorePurchases() { Task { await restorePurchases() } } @objc private func didTapFooterPrivacyPolicy() { AppLegalURLs.openInSafari(AppLegalURLs.privacyPolicy) } @objc private func didTapFooterTermsOfServices() { AppLegalURLs.openInSafari(AppLegalURLs.termsOfUse) } @objc private func didTapFooterSupport() { AppLegalURLs.openInSafari(AppLegalURLs.support) } private func loadStoreProducts() async { await subscriptionStore.refreshEntitlements(deep: true) await subscriptionStore.loadProducts() applyStorePricing() updateSubscriptionPrimaryFooter() updatePremiumCloseButtonVisibility() } private func applyStorePricing() { for plan in plans { guard let fields = planPriceFields[plan.id] else { continue } guard let product = subscriptionStore.product(forPlanKey: plan.id) else { fields.price.stringValue = Self.unloadedPricePlaceholder continue } fields.price.stringValue = product.displayPrice if let period = product.subscription?.subscriptionPeriod { fields.period.stringValue = periodSuffix(for: period) } } } private func periodSuffix(for period: Product.SubscriptionPeriod) -> String { let value = period.value switch period.unit { case .day: return value == 1 ? "/ day" : "/ \(value) days" case .week: return value == 1 ? "/ week" : "/ \(value) weeks" case .month: return value == 1 ? "/ month" : "/ \(value) months" case .year: return value == 1 ? "/ year" : "/ \(value) years" @unknown default: return "" } } private func setPurchasing(_ isPurchasing: Bool) { for button in planPurchaseButtons.values { button.isEnabled = !isPurchasing } } private func purchasePlan(planKey: String) async { setPurchasing(true) defer { setPurchasing(false) } do { let completed = try await subscriptionStore.purchase(planKey: planKey) guard completed else { return } AppRatingCoordinator.shared.scheduleReviewAfterSubscriptionPurchase() let alert = NSAlert() alert.messageText = "You're subscribed" alert.informativeText = "Thank you — Pro features are now available." alert.alertStyle = .informational alert.addButton(withTitle: "OK") if let window = view.window { alert.beginSheetModal(for: window) { [weak self] _ in self?.dismissPremiumSheetFromParentIfNeeded() } } else { alert.runModal() dismissPremiumSheetFromParentIfNeeded() } } catch { await MainActor.run { self.presentPurchaseError(error) } } } private func restorePurchases() async { setPurchasing(true) defer { setPurchasing(false) } do { try await subscriptionStore.restorePurchases() let active = subscriptionStore.isProActive let alert = NSAlert() if active { alert.messageText = "Purchases restored" alert.informativeText = "Your subscription is active." } else { alert.messageText = "No subscription found" alert.informativeText = "There was nothing to restore for this Apple ID." } alert.alertStyle = .informational alert.addButton(withTitle: "OK") if let window = view.window { alert.beginSheetModal(for: window) { [weak self] _ in if active { self?.dismissPremiumSheetFromParentIfNeeded() } } } else { alert.runModal() if active { dismissPremiumSheetFromParentIfNeeded() } } } catch { await MainActor.run { self.presentPurchaseError(error) } } } private func presentPurchaseError(_ error: Error) { let alert = NSAlert() alert.messageText = "Something went wrong" if let localized = error as? LocalizedError { var parts: [String] = [] if let description = localized.errorDescription { parts.append(description) } if let recovery = localized.recoverySuggestion { parts.append(recovery) } alert.informativeText = parts.isEmpty ? error.localizedDescription : parts.joined(separator: "\n\n") } else { alert.informativeText = error.localizedDescription } alert.alertStyle = .warning alert.addButton(withTitle: "OK") if let window = view.window { alert.beginSheetModal(for: window) } else { alert.runModal() } } private func dismissPremiumSheetFromParentIfNeeded() { guard let sheet = view.window, let parent = sheet.sheetParent else { return } parent.endSheet(sheet) } @objc private func didTapClose() { guard let window = view.window else { return } if let parent = window.sheetParent { parent.endSheet(window) return } window.close() } }