Просмотр исходного кода

Redesign paywall UI and make it full-screen.

Update the in-app paywall layout to match the new card/grid design and remove the blur backdrop while keeping the existing palette and StoreKit data.

Co-authored-by: Cursor <cursoragent@cursor.com>
huzaifahayat12 2 месяцев назад
Родитель
Сommit
8b2eb7b30c
1 измененных файлов с 294 добавлено и 71 удалено
  1. 294 71
      zoom_app/ViewController.swift

+ 294 - 71
zoom_app/ViewController.swift

@@ -259,6 +259,8 @@ class ViewController: NSViewController {
     private weak var paywallOverlayView: NSView?
     private weak var paywallOverlayView: NSView?
     private weak var paywallPanelView: NSView?
     private weak var paywallPanelView: NSView?
     private weak var paywallBodyStack: NSStackView?
     private weak var paywallBodyStack: NSStackView?
+    private weak var paywallPlansRowWide: NSStackView?
+    private weak var paywallPlansGridNarrow: NSStackView?
     private var paywallIsDesktopLayout = false
     private var paywallIsDesktopLayout = false
     private var paywallPanelWidthConstraint: NSLayoutConstraint?
     private var paywallPanelWidthConstraint: NSLayoutConstraint?
     private var paywallPanelHeightConstraint: NSLayoutConstraint?
     private var paywallPanelHeightConstraint: NSLayoutConstraint?
@@ -608,7 +610,6 @@ class ViewController: NSViewController {
         super.viewDidLayout()
         super.viewDidLayout()
         alignNativeTrafficLights()
         alignNativeTrafficLights()
         if paywallOverlayView != nil {
         if paywallOverlayView != nil {
-            updatePaywallPanelSize()
             updatePaywallResponsiveLayout()
             updatePaywallResponsiveLayout()
         }
         }
     }
     }
@@ -3544,34 +3545,16 @@ class ViewController: NSViewController {
         overlay.translatesAutoresizingMaskIntoConstraints = false
         overlay.translatesAutoresizingMaskIntoConstraints = false
         overlay.wantsLayer = true
         overlay.wantsLayer = true
         overlay.alphaValue = 0
         overlay.alphaValue = 0
-
-        // Full-app dim + blur background.
-        let blur = NSVisualEffectView()
-        blur.translatesAutoresizingMaskIntoConstraints = false
-        blur.material = palette.isDarkMode ? .hudWindow : .popover
-        blur.blendingMode = .withinWindow
-        blur.state = .active
-        overlay.addSubview(blur)
+        overlay.layer?.backgroundColor = NSColor.clear.cgColor
 
 
         let dim = NSView()
         let dim = NSView()
         dim.translatesAutoresizingMaskIntoConstraints = false
         dim.translatesAutoresizingMaskIntoConstraints = false
         dim.wantsLayer = true
         dim.wantsLayer = true
         dim.layer?.backgroundColor = palette.isDarkMode
         dim.layer?.backgroundColor = palette.isDarkMode
-            ? NSColor.black.withAlphaComponent(0.50).cgColor
-            : NSColor.white.withAlphaComponent(0.22).cgColor
+            ? NSColor.black.withAlphaComponent(0.62).cgColor
+            : NSColor.black.withAlphaComponent(0.28).cgColor
         overlay.addSubview(dim)
         overlay.addSubview(dim)
 
 
-        NSLayoutConstraint.activate([
-            blur.leadingAnchor.constraint(equalTo: overlay.leadingAnchor),
-            blur.trailingAnchor.constraint(equalTo: overlay.trailingAnchor),
-            blur.topAnchor.constraint(equalTo: overlay.topAnchor),
-            blur.bottomAnchor.constraint(equalTo: overlay.bottomAnchor),
-            dim.leadingAnchor.constraint(equalTo: overlay.leadingAnchor),
-            dim.trailingAnchor.constraint(equalTo: overlay.trailingAnchor),
-            dim.topAnchor.constraint(equalTo: overlay.topAnchor),
-            dim.bottomAnchor.constraint(equalTo: overlay.bottomAnchor)
-        ])
-
         // Clicking outside the paywall dismisses it.
         // Clicking outside the paywall dismisses it.
         let outsideButton = NSButton(title: "", target: self, action: #selector(closePaywallClicked(_:)))
         let outsideButton = NSButton(title: "", target: self, action: #selector(closePaywallClicked(_:)))
         outsideButton.translatesAutoresizingMaskIntoConstraints = false
         outsideButton.translatesAutoresizingMaskIntoConstraints = false
@@ -3583,7 +3566,6 @@ class ViewController: NSViewController {
         overlay.addSubview(outsideButton)
         overlay.addSubview(outsideButton)
 
 
         let content = makePaywallContent()
         let content = makePaywallContent()
-        paywallPanelView = content
         overlay.addSubview(content)
         overlay.addSubview(content)
 
 
         view.addSubview(overlay)
         view.addSubview(overlay)
@@ -3592,26 +3574,21 @@ class ViewController: NSViewController {
             overlay.trailingAnchor.constraint(equalTo: view.trailingAnchor),
             overlay.trailingAnchor.constraint(equalTo: view.trailingAnchor),
             overlay.topAnchor.constraint(equalTo: view.topAnchor),
             overlay.topAnchor.constraint(equalTo: view.topAnchor),
             overlay.bottomAnchor.constraint(equalTo: view.bottomAnchor),
             overlay.bottomAnchor.constraint(equalTo: view.bottomAnchor),
+            dim.leadingAnchor.constraint(equalTo: overlay.leadingAnchor),
+            dim.trailingAnchor.constraint(equalTo: overlay.trailingAnchor),
+            dim.topAnchor.constraint(equalTo: overlay.topAnchor),
+            dim.bottomAnchor.constraint(equalTo: overlay.bottomAnchor),
             outsideButton.leadingAnchor.constraint(equalTo: overlay.leadingAnchor),
             outsideButton.leadingAnchor.constraint(equalTo: overlay.leadingAnchor),
             outsideButton.trailingAnchor.constraint(equalTo: overlay.trailingAnchor),
             outsideButton.trailingAnchor.constraint(equalTo: overlay.trailingAnchor),
             outsideButton.topAnchor.constraint(equalTo: overlay.topAnchor),
             outsideButton.topAnchor.constraint(equalTo: overlay.topAnchor),
             outsideButton.bottomAnchor.constraint(equalTo: overlay.bottomAnchor),
             outsideButton.bottomAnchor.constraint(equalTo: overlay.bottomAnchor),
-            content.centerXAnchor.constraint(equalTo: overlay.centerXAnchor),
-            content.centerYAnchor.constraint(equalTo: overlay.centerYAnchor)
+            content.leadingAnchor.constraint(equalTo: overlay.leadingAnchor),
+            content.trailingAnchor.constraint(equalTo: overlay.trailingAnchor),
+            content.topAnchor.constraint(equalTo: overlay.topAnchor),
+            content.bottomAnchor.constraint(equalTo: overlay.bottomAnchor)
         ])
         ])
-
-        // Choose an initial panel size for the current window size.
-        let safeInsetX: CGFloat = 80
-        let safeInsetY: CGFloat = 110
-        let maxWidth = max(640, view.bounds.width - safeInsetX)
-        let maxHeight = max(560, view.bounds.height - safeInsetY)
-        let panelWidth = min(1040, maxWidth)
-        let panelHeight = min(860, maxHeight)
-
-        paywallPanelWidthConstraint = content.widthAnchor.constraint(equalToConstant: panelWidth)
-        paywallPanelHeightConstraint = content.heightAnchor.constraint(equalToConstant: panelHeight)
-        paywallPanelWidthConstraint?.isActive = true
-        paywallPanelHeightConstraint?.isActive = true
+        paywallPanelWidthConstraint = nil
+        paywallPanelHeightConstraint = nil
 
 
         paywallOverlayView = overlay
         paywallOverlayView = overlay
 
 
@@ -3687,6 +3664,10 @@ class ViewController: NSViewController {
             bodyStack.spacing = 18
             bodyStack.spacing = 18
         }
         }
 
 
+        let useWidePlans = width >= 880
+        paywallPlansRowWide?.isHidden = useWidePlans == false
+        paywallPlansGridNarrow?.isHidden = useWidePlans
+
         bodyStack.layoutSubtreeIfNeeded()
         bodyStack.layoutSubtreeIfNeeded()
     }
     }
 
 
@@ -3700,16 +3681,39 @@ class ViewController: NSViewController {
         paywallContinueLabel = nil
         paywallContinueLabel = nil
         paywallContinueButton = nil
         paywallContinueButton = nil
         paywallContinueEnabled = true
         paywallContinueEnabled = true
+        paywallPlansRowWide = nil
+        paywallPlansGridNarrow = nil
+
+        let root = NSView()
+        root.translatesAutoresizingMaskIntoConstraints = false
+        root.wantsLayer = true
+        root.layer?.backgroundColor = NSColor.clear.cgColor
 
 
         let panel = NSView()
         let panel = NSView()
         panel.translatesAutoresizingMaskIntoConstraints = false
         panel.translatesAutoresizingMaskIntoConstraints = false
         panel.wantsLayer = true
         panel.wantsLayer = true
-        panel.layer?.backgroundColor = appBackground.cgColor
+        panel.layer?.backgroundColor = palette.sectionCard.cgColor
+        panel.layer?.cornerRadius = 0
+        styleSurface(panel, borderColor: NSColor.clear, borderWidth: 0, shadow: false)
+        root.addSubview(panel)
+
+        // Full-screen: fill the entire app content area.
+        let insetX: CGFloat = 0
+        let insetY: CGFloat = 0
+        NSLayoutConstraint.activate([
+            panel.leadingAnchor.constraint(equalTo: root.leadingAnchor, constant: insetX),
+            panel.trailingAnchor.constraint(equalTo: root.trailingAnchor, constant: -insetX),
+            panel.topAnchor.constraint(equalTo: root.topAnchor, constant: insetY),
+            panel.bottomAnchor.constraint(equalTo: root.bottomAnchor, constant: -insetY)
+        ])
+
+        // Responsive logic should measure the panel size (not the full-screen root).
+        self.paywallPanelView = panel
 
 
         let contentStack = NSStackView()
         let contentStack = NSStackView()
         contentStack.translatesAutoresizingMaskIntoConstraints = false
         contentStack.translatesAutoresizingMaskIntoConstraints = false
         contentStack.orientation = .vertical
         contentStack.orientation = .vertical
-        contentStack.spacing = 12
+        contentStack.spacing = 14
         contentStack.alignment = .leading
         contentStack.alignment = .leading
         panel.addSubview(contentStack)
         panel.addSubview(contentStack)
 
 
@@ -3742,19 +3746,25 @@ class ViewController: NSViewController {
         topRow.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
         topRow.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
         contentStack.addArrangedSubview(topRow)
         contentStack.addArrangedSubview(topRow)
 
 
-        contentStack.addArrangedSubview(textLabel("Upgrade to unlock premium features.", font: NSFont.systemFont(ofSize: 12, weight: .medium), color: secondaryText))
+        let hero = paywallHeroCard(
+            eyebrow: "PREMIUM EXPERIENCE",
+            title: "Elevate your classroom experience",
+            subtitle: "Unlock intelligent automation, deep analytics, and premium support for modern educators."
+        )
+        contentStack.addArrangedSubview(hero)
+
+        let features = paywallFeaturesRow()
+        contentStack.addArrangedSubview(features)
+
         let bodyStack = NSStackView()
         let bodyStack = NSStackView()
         bodyStack.translatesAutoresizingMaskIntoConstraints = false
         bodyStack.translatesAutoresizingMaskIntoConstraints = false
         bodyStack.orientation = .vertical
         bodyStack.orientation = .vertical
-        bodyStack.spacing = 18
+        bodyStack.spacing = 16
         bodyStack.alignment = .leading
         bodyStack.alignment = .leading
         bodyStack.distribution = .fill
         bodyStack.distribution = .fill
         contentStack.addArrangedSubview(bodyStack)
         contentStack.addArrangedSubview(bodyStack)
         paywallBodyStack = bodyStack
         paywallBodyStack = bodyStack
 
 
-        let benefits = paywallBenefitsSection()
-        bodyStack.addArrangedSubview(benefits)
-
         let planColumn = NSStackView()
         let planColumn = NSStackView()
         planColumn.translatesAutoresizingMaskIntoConstraints = false
         planColumn.translatesAutoresizingMaskIntoConstraints = false
         planColumn.orientation = .vertical
         planColumn.orientation = .vertical
@@ -3772,8 +3782,6 @@ class ViewController: NSViewController {
             plan: .weekly,
             plan: .weekly,
             strikePrice: nil
             strikePrice: nil
         )
         )
-        planColumn.addArrangedSubview(weeklyCard)
-
         let monthlyCard = paywallPlanCard(
         let monthlyCard = paywallPlanCard(
             title: "Monthly",
             title: "Monthly",
             price: paywallDisplayPrice(for: .monthly),
             price: paywallDisplayPrice(for: .monthly),
@@ -3783,8 +3791,6 @@ class ViewController: NSViewController {
             plan: .monthly,
             plan: .monthly,
             strikePrice: nil
             strikePrice: nil
         )
         )
-        planColumn.addArrangedSubview(monthlyCard)
-
         let yearlyCard = paywallPlanCard(
         let yearlyCard = paywallPlanCard(
             title: "Yearly",
             title: "Yearly",
             price: paywallDisplayPrice(for: .yearly),
             price: paywallDisplayPrice(for: .yearly),
@@ -3794,8 +3800,6 @@ class ViewController: NSViewController {
             plan: .yearly,
             plan: .yearly,
             strikePrice: nil
             strikePrice: nil
         )
         )
-        planColumn.addArrangedSubview(yearlyCard)
-
         let lifetimeCard = paywallPlanCard(
         let lifetimeCard = paywallPlanCard(
             title: "Lifetime",
             title: "Lifetime",
             price: paywallDisplayPrice(for: .lifetime),
             price: paywallDisplayPrice(for: .lifetime),
@@ -3805,9 +3809,85 @@ class ViewController: NSViewController {
             plan: .lifetime,
             plan: .lifetime,
             strikePrice: nil
             strikePrice: nil
         )
         )
-        planColumn.addArrangedSubview(lifetimeCard)
+
+        let plansRowWide = NSStackView()
+        plansRowWide.translatesAutoresizingMaskIntoConstraints = false
+        plansRowWide.orientation = .horizontal
+        plansRowWide.spacing = 12
+        plansRowWide.alignment = .centerY
+        plansRowWide.distribution = .fillEqually
+        plansRowWide.addArrangedSubview(weeklyCard)
+        plansRowWide.addArrangedSubview(monthlyCard)
+        plansRowWide.addArrangedSubview(yearlyCard)
+        plansRowWide.addArrangedSubview(lifetimeCard)
+        paywallPlansRowWide = plansRowWide
+        planColumn.addArrangedSubview(plansRowWide)
+
+        let plansGridNarrow = NSStackView()
+        plansGridNarrow.translatesAutoresizingMaskIntoConstraints = false
+        plansGridNarrow.orientation = .vertical
+        plansGridNarrow.spacing = 12
+        plansGridNarrow.alignment = .leading
+        plansGridNarrow.distribution = .fillEqually
+
+        let plansRow1 = NSStackView()
+        plansRow1.translatesAutoresizingMaskIntoConstraints = false
+        plansRow1.orientation = .horizontal
+        plansRow1.spacing = 12
+        plansRow1.alignment = .centerY
+        plansRow1.distribution = .fillEqually
+        plansRow1.addArrangedSubview(paywallPlanCard(
+            title: "Weekly",
+            price: paywallDisplayPrice(for: .weekly),
+            badge: "Basic Deal",
+            badgeColor: NSColor(calibratedRed: 1.0, green: 0.60, blue: 0.20, alpha: 1),
+            subtitle: "Billed weekly",
+            plan: .weekly,
+            strikePrice: nil
+        ))
+        plansRow1.addArrangedSubview(paywallPlanCard(
+            title: "Monthly",
+            price: paywallDisplayPrice(for: .monthly),
+            badge: "Free Trial",
+            badgeColor: NSColor(calibratedRed: 0.19, green: 0.82, blue: 0.39, alpha: 1),
+            subtitle: "7-day free trial, then monthly",
+            plan: .monthly,
+            strikePrice: nil
+        ))
+
+        let plansRow2 = NSStackView()
+        plansRow2.translatesAutoresizingMaskIntoConstraints = false
+        plansRow2.orientation = .horizontal
+        plansRow2.spacing = 12
+        plansRow2.alignment = .centerY
+        plansRow2.distribution = .fillEqually
+        plansRow2.addArrangedSubview(paywallPlanCard(
+            title: "Yearly",
+            price: paywallDisplayPrice(for: .yearly),
+            badge: "Best Deal",
+            badgeColor: NSColor(calibratedRed: 1.0, green: 0.60, blue: 0.20, alpha: 1),
+            subtitle: "Billed yearly",
+            plan: .yearly,
+            strikePrice: nil
+        ))
+        plansRow2.addArrangedSubview(paywallPlanCard(
+            title: "Lifetime",
+            price: paywallDisplayPrice(for: .lifetime),
+            badge: "Save 50%",
+            badgeColor: NSColor(calibratedRed: 1.0, green: 0.60, blue: 0.20, alpha: 1),
+            subtitle: "One-time purchase",
+            plan: .lifetime,
+            strikePrice: nil
+        ))
+
+        plansGridNarrow.addArrangedSubview(plansRow1)
+        plansGridNarrow.addArrangedSubview(plansRow2)
+        plansGridNarrow.isHidden = true
+        paywallPlansGridNarrow = plansGridNarrow
+        planColumn.addArrangedSubview(plansGridNarrow)
+
         updatePaywallPlanSelection()
         updatePaywallPlanSelection()
-        planColumn.setCustomSpacing(20, after: lifetimeCard)
+        planColumn.setCustomSpacing(16, after: plansGridNarrow)
 
 
         let offer = textLabel(paywallOfferText(for: selectedPremiumPlan), font: NSFont.systemFont(ofSize: 13, weight: .semibold), color: primaryText)
         let offer = textLabel(paywallOfferText(for: selectedPremiumPlan), font: NSFont.systemFont(ofSize: 13, weight: .semibold), color: primaryText)
         offer.alignment = .center
         offer.alignment = .center
@@ -3822,7 +3902,11 @@ class ViewController: NSViewController {
             offer.bottomAnchor.constraint(equalTo: offerWrap.bottomAnchor, constant: -2)
             offer.bottomAnchor.constraint(equalTo: offerWrap.bottomAnchor, constant: -2)
         ])
         ])
         planColumn.addArrangedSubview(offerWrap)
         planColumn.addArrangedSubview(offerWrap)
-        planColumn.setCustomSpacing(18, after: offerWrap)
+        planColumn.setCustomSpacing(14, after: offerWrap)
+
+        let trustRow = paywallTrustRow()
+        planColumn.addArrangedSubview(trustRow)
+        planColumn.setCustomSpacing(14, after: trustRow)
 
 
         let continueButton = HoverButton(title: "", target: self, action: #selector(paywallContinueClicked(_:)))
         let continueButton = HoverButton(title: "", target: self, action: #selector(paywallContinueClicked(_:)))
         continueButton.translatesAutoresizingMaskIntoConstraints = false
         continueButton.translatesAutoresizingMaskIntoConstraints = false
@@ -3830,11 +3914,11 @@ class ViewController: NSViewController {
         continueButton.bezelStyle = .regularSquare
         continueButton.bezelStyle = .regularSquare
         continueButton.wantsLayer = true
         continueButton.wantsLayer = true
         continueButton.layer?.cornerRadius = 14
         continueButton.layer?.cornerRadius = 14
-        continueButton.normalColor = accentBlue
-        continueButton.hoverColor = accentBlue.withAlphaComponent(0.92)
+        continueButton.normalColor = NSColor(calibratedRed: 0.0, green: 0.70, blue: 0.42, alpha: 1)
+        continueButton.hoverColor = NSColor(calibratedRed: 0.0, green: 0.70, blue: 0.42, alpha: 0.92)
         continueButton.heightAnchor.constraint(equalToConstant: 44).isActive = true
         continueButton.heightAnchor.constraint(equalToConstant: 44).isActive = true
         continueButton.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
         continueButton.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
-        styleSurface(continueButton, borderColor: accentBlue.withAlphaComponent(0.85), borderWidth: 1, shadow: true)
+        styleSurface(continueButton, borderColor: NSColor.black.withAlphaComponent(0.12), borderWidth: 1, shadow: true)
         let continueLabel = textLabel("Continue", font: NSFont.systemFont(ofSize: 16, weight: .bold), color: .white)
         let continueLabel = textLabel("Continue", font: NSFont.systemFont(ofSize: 16, weight: .bold), color: .white)
         continueButton.addSubview(continueLabel)
         continueButton.addSubview(continueLabel)
         NSLayoutConstraint.activate([
         NSLayoutConstraint.activate([
@@ -3864,10 +3948,30 @@ class ViewController: NSViewController {
         footerButtons.translatesAutoresizingMaskIntoConstraints = false
         footerButtons.translatesAutoresizingMaskIntoConstraints = false
         footerButtons.orientation = .horizontal
         footerButtons.orientation = .horizontal
         footerButtons.alignment = .centerY
         footerButtons.alignment = .centerY
-        footerButtons.distribution = .fillEqually
+        footerButtons.distribution = .fill
         footerButtons.spacing = 8
         footerButtons.spacing = 8
         footerButtons.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
         footerButtons.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
 
 
+        let leftFooter = NSStackView()
+        leftFooter.translatesAutoresizingMaskIntoConstraints = false
+        leftFooter.orientation = .horizontal
+        leftFooter.alignment = .centerY
+        leftFooter.spacing = 14
+
+        let manageButton = paywallFooterButton(
+            title: "Manage Subscription",
+            action: #selector(paywallManageSubscriptionClicked(_:))
+        )
+        let restoreButton = paywallFooterButton(
+            title: "Restore Purchase",
+            action: #selector(paywallRestorePurchaseClicked(_:))
+        )
+        leftFooter.addArrangedSubview(manageButton)
+        leftFooter.addArrangedSubview(restoreButton)
+
+        let footerSpacer = NSView()
+        footerSpacer.translatesAutoresizingMaskIntoConstraints = false
+
         let privacyButton = paywallFooterButton(
         let privacyButton = paywallFooterButton(
             title: "Privacy Policy",
             title: "Privacy Policy",
             action: #selector(paywallPrivacyPolicyClicked(_:))
             action: #selector(paywallPrivacyPolicyClicked(_:))
@@ -3880,6 +3984,8 @@ class ViewController: NSViewController {
             title: "Terms of Services",
             title: "Terms of Services",
             action: #selector(paywallTermsOfServicesClicked(_:))
             action: #selector(paywallTermsOfServicesClicked(_:))
         )
         )
+        footerButtons.addArrangedSubview(leftFooter)
+        footerButtons.addArrangedSubview(footerSpacer)
         footerButtons.addArrangedSubview(privacyButton)
         footerButtons.addArrangedSubview(privacyButton)
         footerButtons.addArrangedSubview(supportButton)
         footerButtons.addArrangedSubview(supportButton)
         footerButtons.addArrangedSubview(termsButton)
         footerButtons.addArrangedSubview(termsButton)
@@ -3889,11 +3995,12 @@ class ViewController: NSViewController {
             contentStack.leadingAnchor.constraint(equalTo: panel.leadingAnchor, constant: 18),
             contentStack.leadingAnchor.constraint(equalTo: panel.leadingAnchor, constant: 18),
             contentStack.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -18),
             contentStack.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -18),
             contentStack.topAnchor.constraint(equalTo: panel.topAnchor, constant: 16),
             contentStack.topAnchor.constraint(equalTo: panel.topAnchor, constant: 16),
-            contentStack.bottomAnchor.constraint(lessThanOrEqualTo: panel.bottomAnchor, constant: -12)
+            contentStack.bottomAnchor.constraint(lessThanOrEqualTo: panel.bottomAnchor, constant: -16)
         ])
         ])
 
 
         refreshPaywallStoreUI()
         refreshPaywallStoreUI()
-        return panel
+        updatePaywallResponsiveLayout()
+        return root
     }
     }
 
 
     private func paywallBenefitsSection() -> NSView {
     private func paywallBenefitsSection() -> NSView {
@@ -3927,6 +4034,103 @@ class ViewController: NSViewController {
         return stack
         return stack
     }
     }
 
 
+    private func paywallHeroCard(eyebrow: String, title: String, subtitle: String) -> NSView {
+        let card = NSView()
+        card.translatesAutoresizingMaskIntoConstraints = false
+        card.wantsLayer = true
+        card.layer?.cornerRadius = 14
+        card.layer?.backgroundColor = palette.secondaryCardBackground.cgColor
+        styleSurface(card, borderColor: palette.inputBorder, borderWidth: 1, shadow: false)
+
+        let eyebrowLabel = textLabel(eyebrow, font: NSFont.systemFont(ofSize: 10, weight: .bold), color: NSColor(calibratedRed: 0.19, green: 0.82, blue: 0.39, alpha: 1))
+        eyebrowLabel.alignment = .left
+
+        let titleLabel = textLabel(title, font: NSFont.systemFont(ofSize: 16, weight: .bold), color: primaryText)
+        titleLabel.alignment = .left
+
+        let subtitleLabel = textLabel(subtitle, font: NSFont.systemFont(ofSize: 12, weight: .medium), color: secondaryText)
+        subtitleLabel.maximumNumberOfLines = 2
+        subtitleLabel.lineBreakMode = .byTruncatingTail
+        subtitleLabel.alignment = .left
+
+        let stack = NSStackView()
+        stack.translatesAutoresizingMaskIntoConstraints = false
+        stack.orientation = .vertical
+        stack.alignment = .leading
+        stack.spacing = 6
+        stack.addArrangedSubview(eyebrowLabel)
+        stack.addArrangedSubview(titleLabel)
+        stack.addArrangedSubview(subtitleLabel)
+        card.addSubview(stack)
+
+        NSLayoutConstraint.activate([
+            stack.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 16),
+            stack.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -16),
+            stack.topAnchor.constraint(equalTo: card.topAnchor, constant: 12),
+            stack.bottomAnchor.constraint(equalTo: card.bottomAnchor, constant: -12)
+        ])
+
+        return card
+    }
+
+    private func paywallFeaturesRow() -> NSView {
+        let row = NSStackView()
+        row.translatesAutoresizingMaskIntoConstraints = false
+        row.orientation = .horizontal
+        row.spacing = 10
+        row.alignment = .centerY
+        row.distribution = .fillEqually
+
+        row.addArrangedSubview(paywallBenefitItem(icon: "📅", text: "Manage classes"))
+        row.addArrangedSubview(paywallBenefitItem(icon: "💬", text: "Smart to-dos"))
+        row.addArrangedSubview(paywallBenefitItem(icon: "⚡", text: "Faster workflow"))
+        row.addArrangedSubview(paywallBenefitItem(icon: "🔔", text: "Reminder notifications"))
+        return row
+    }
+
+    private func paywallTrustRow() -> NSView {
+        let row = NSStackView()
+        row.translatesAutoresizingMaskIntoConstraints = false
+        row.orientation = .horizontal
+        row.spacing = 10
+        row.alignment = .centerY
+        row.distribution = .fillEqually
+
+        row.addArrangedSubview(paywallTrustItem(title: "Cancel anytime", subtitle: "No lock-in"))
+        row.addArrangedSubview(paywallTrustItem(title: "Instant access", subtitle: "Unlock all tools"))
+        row.addArrangedSubview(paywallTrustItem(title: "Secure billing", subtitle: "Handled by Apple"))
+        return row
+    }
+
+    private func paywallTrustItem(title: String, subtitle: String) -> NSView {
+        let card = NSView()
+        card.translatesAutoresizingMaskIntoConstraints = false
+        card.wantsLayer = true
+        card.layer?.cornerRadius = 12
+        card.layer?.backgroundColor = palette.inputBackground.cgColor
+        styleSurface(card, borderColor: palette.inputBorder, borderWidth: 1, shadow: false)
+
+        let titleLabel = textLabel(title, font: NSFont.systemFont(ofSize: 11, weight: .semibold), color: primaryText)
+        let subtitleLabel = textLabel(subtitle, font: NSFont.systemFont(ofSize: 10, weight: .medium), color: secondaryText)
+
+        let stack = NSStackView()
+        stack.translatesAutoresizingMaskIntoConstraints = false
+        stack.orientation = .vertical
+        stack.alignment = .centerX
+        stack.spacing = 2
+        stack.addArrangedSubview(titleLabel)
+        stack.addArrangedSubview(subtitleLabel)
+        card.addSubview(stack)
+
+        NSLayoutConstraint.activate([
+            card.heightAnchor.constraint(equalToConstant: 44),
+            stack.centerXAnchor.constraint(equalTo: card.centerXAnchor),
+            stack.centerYAnchor.constraint(equalTo: card.centerYAnchor)
+        ])
+
+        return card
+    }
+
     private func paywallBenefitItem(icon: String, text: String) -> NSView {
     private func paywallBenefitItem(icon: String, text: String) -> NSView {
         let card = NSView()
         let card = NSView()
         card.translatesAutoresizingMaskIntoConstraints = false
         card.translatesAutoresizingMaskIntoConstraints = false
@@ -3979,8 +4183,7 @@ class ViewController: NSViewController {
         wrapper.bezelStyle = .regularSquare
         wrapper.bezelStyle = .regularSquare
         wrapper.wantsLayer = true
         wrapper.wantsLayer = true
         wrapper.layer?.backgroundColor = NSColor.clear.cgColor
         wrapper.layer?.backgroundColor = NSColor.clear.cgColor
-        wrapper.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
-        wrapper.heightAnchor.constraint(equalToConstant: 94).isActive = true
+        wrapper.heightAnchor.constraint(equalToConstant: 122).isActive = true
         wrapper.tag = PremiumPlan.allCases.firstIndex(of: plan) ?? 0
         wrapper.tag = PremiumPlan.allCases.firstIndex(of: plan) ?? 0
 
 
         let card = NSView()
         let card = NSView()
@@ -3988,7 +4191,6 @@ class ViewController: NSViewController {
         card.wantsLayer = true
         card.wantsLayer = true
         card.layer?.cornerRadius = 16
         card.layer?.cornerRadius = 16
         card.layer?.backgroundColor = palette.sectionCard.cgColor
         card.layer?.backgroundColor = palette.sectionCard.cgColor
-        card.heightAnchor.constraint(equalToConstant: 82).isActive = true
         wrapper.addSubview(card)
         wrapper.addSubview(card)
         NSLayoutConstraint.activate([
         NSLayoutConstraint.activate([
             card.leadingAnchor.constraint(equalTo: wrapper.leadingAnchor),
             card.leadingAnchor.constraint(equalTo: wrapper.leadingAnchor),
@@ -4019,19 +4221,19 @@ class ViewController: NSViewController {
 
 
         let titleLabel = textLabel(title, font: NSFont.systemFont(ofSize: 15, weight: .bold), color: accentBlue)
         let titleLabel = textLabel(title, font: NSFont.systemFont(ofSize: 15, weight: .bold), color: accentBlue)
         card.addSubview(titleLabel)
         card.addSubview(titleLabel)
-        let priceLabel = textLabel(price, font: NSFont.systemFont(ofSize: 12, weight: .bold), color: primaryText)
+        let priceLabel = textLabel(price, font: NSFont.systemFont(ofSize: 13, weight: .bold), color: primaryText)
         card.addSubview(priceLabel)
         card.addSubview(priceLabel)
         paywallPlanPriceLabels[plan] = priceLabel
         paywallPlanPriceLabels[plan] = priceLabel
 
 
         NSLayoutConstraint.activate([
         NSLayoutConstraint.activate([
-            badgeWrap.centerXAnchor.constraint(equalTo: card.centerXAnchor),
+            badgeWrap.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 14),
             badgeWrap.centerYAnchor.constraint(equalTo: card.topAnchor),
             badgeWrap.centerYAnchor.constraint(equalTo: card.topAnchor),
 
 
             titleLabel.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 16),
             titleLabel.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 16),
-            titleLabel.topAnchor.constraint(equalTo: card.topAnchor, constant: 34),
+            titleLabel.topAnchor.constraint(equalTo: card.topAnchor, constant: 22),
 
 
-            priceLabel.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -16),
-            priceLabel.topAnchor.constraint(equalTo: card.topAnchor, constant: 32)
+            priceLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
+            priceLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8)
         ])
         ])
 
 
         if let subtitle {
         if let subtitle {
@@ -4039,8 +4241,9 @@ class ViewController: NSViewController {
             card.addSubview(sub)
             card.addSubview(sub)
             paywallPlanSubtitleLabels[plan] = sub
             paywallPlanSubtitleLabels[plan] = sub
             NSLayoutConstraint.activate([
             NSLayoutConstraint.activate([
-                sub.trailingAnchor.constraint(equalTo: priceLabel.trailingAnchor),
-                sub.topAnchor.constraint(equalTo: priceLabel.bottomAnchor, constant: 0)
+                sub.leadingAnchor.constraint(equalTo: priceLabel.leadingAnchor),
+                sub.topAnchor.constraint(equalTo: priceLabel.bottomAnchor, constant: 2),
+                sub.trailingAnchor.constraint(lessThanOrEqualTo: card.trailingAnchor, constant: -12)
             ])
             ])
         }
         }
 
 
@@ -4152,6 +4355,22 @@ class ViewController: NSViewController {
         )
         )
     }
     }
 
 
+    @objc private func paywallManageSubscriptionClicked(_ sender: Any?) {
+        openManageSubscriptions()
+    }
+
+    @objc private func paywallRestorePurchaseClicked(_ sender: Any?) {
+        Task { [weak self] in
+            guard let self else { return }
+            let message = await self.storeKitCoordinator.restorePurchases()
+            await MainActor.run {
+                self.refreshPaywallStoreUI()
+                self.updatePremiumButtons()
+                self.showSimpleAlert(title: "Restore Purchase", message: message)
+            }
+        }
+    }
+
     private func startSelectedPlanPurchase() {
     private func startSelectedPlanPurchase() {
         guard paywallContinueEnabled else { return }
         guard paywallContinueEnabled else { return }
         paywallPurchaseTask?.cancel()
         paywallPurchaseTask?.cancel()
@@ -4215,7 +4434,11 @@ class ViewController: NSViewController {
             paywallContinueButton?.alphaValue = 0.75
             paywallContinueButton?.alphaValue = 0.75
         } else {
         } else {
             paywallContinueEnabled = true
             paywallContinueEnabled = true
-            paywallContinueLabel?.stringValue = "Continue"
+            if selectedPremiumPlan == .monthly {
+                paywallContinueLabel?.stringValue = "Start with free trial"
+            } else {
+                paywallContinueLabel?.stringValue = "Continue"
+            }
             paywallContinueButton?.alphaValue = 1.0
             paywallContinueButton?.alphaValue = 1.0
         }
         }
     }
     }