Parcourir la Source

Redesign paywall as a centered in-app premium overlay.

Replace the modal panel with a full-app overlay, refine layout hierarchy, and polish plan cards/CTA spacing so the paywall feels professional while preserving existing purchase behavior.

Made-with: Cursor
huzaifahayat12 il y a 7 heures
Parent
commit
d04f843968
1 fichiers modifiés avec 140 ajouts et 83 suppressions
  1. 140 83
      classroom_app/ViewController.swift

+ 140 - 83
classroom_app/ViewController.swift

@@ -250,6 +250,7 @@ final class ViewController: NSViewController {
250
     private var zoomJoinModeViews: [ZoomJoinMode: NSView] = [:]
250
     private var zoomJoinModeViews: [ZoomJoinMode: NSView] = [:]
251
     private var settingsActionByView = [ObjectIdentifier: SettingsAction]()
251
     private var settingsActionByView = [ObjectIdentifier: SettingsAction]()
252
     private var paywallWindow: NSWindow?
252
     private var paywallWindow: NSWindow?
253
+    private weak var paywallOverlayView: NSView?
253
     private let paywallContentWidth: CGFloat = 520
254
     private let paywallContentWidth: CGFloat = 520
254
     private let launchWindowLeftOffset: CGFloat = 80
255
     private let launchWindowLeftOffset: CGFloat = 80
255
     private var selectedPremiumPlan: PremiumPlan = .monthly
256
     private var selectedPremiumPlan: PremiumPlan = .monthly
@@ -414,7 +415,9 @@ final class ViewController: NSViewController {
414
         palette = Palette(isDarkMode: darkModeEnabled)
415
         palette = Palette(isDarkMode: darkModeEnabled)
415
         storeKitCoordinator.onEntitlementsChanged = { [weak self] hasPremiumAccess in
416
         storeKitCoordinator.onEntitlementsChanged = { [weak self] hasPremiumAccess in
416
             guard let self else { return }
417
             guard let self else { return }
417
-            self.handlePremiumAccessChanged(hasPremiumAccess)
418
+            DispatchQueue.main.async {
419
+                self.handlePremiumAccessChanged(hasPremiumAccess)
420
+            }
418
         }
421
         }
419
         migrateLegacyRatingStateIfNeeded()
422
         migrateLegacyRatingStateIfNeeded()
420
         beginUsageTrackingSessionIfNeeded()
423
         beginUsageTrackingSessionIfNeeded()
@@ -1021,10 +1024,22 @@ private extension ViewController {
1021
     }
1024
     }
1022
 
1025
 
1023
     private func showPaywall(upgradeFlow: Bool = false, preferredPlan: PremiumPlan? = nil) {
1026
     private func showPaywall(upgradeFlow: Bool = false, preferredPlan: PremiumPlan? = nil) {
1027
+        if !Thread.isMainThread {
1028
+            DispatchQueue.main.async { [weak self] in
1029
+                self?.showPaywall(upgradeFlow: upgradeFlow, preferredPlan: preferredPlan)
1030
+            }
1031
+            return
1032
+        }
1024
         paywallUpgradeFlowEnabled = upgradeFlow
1033
         paywallUpgradeFlowEnabled = upgradeFlow
1025
         if let preferredPlan {
1034
         if let preferredPlan {
1026
             selectedPremiumPlan = preferredPlan
1035
             selectedPremiumPlan = preferredPlan
1027
         }
1036
         }
1037
+        if let existingOverlay = paywallOverlayView {
1038
+            refreshPaywallStoreUI()
1039
+            existingOverlay.alphaValue = 1
1040
+            view.addSubview(existingOverlay, positioned: .above, relativeTo: nil)
1041
+            return
1042
+        }
1028
         if let existing = paywallWindow {
1043
         if let existing = paywallWindow {
1029
             refreshPaywallStoreUI()
1044
             refreshPaywallStoreUI()
1030
             animatePaywallPresentation(existing)
1045
             animatePaywallPresentation(existing)
@@ -1034,33 +1049,30 @@ private extension ViewController {
1034
         }
1049
         }
1035
 
1050
 
1036
         let content = makePaywallContent()
1051
         let content = makePaywallContent()
1037
-        let controller = NSViewController()
1038
-        controller.view = content
1039
-
1040
-        let panel = NSPanel(
1041
-            contentRect: NSRect(x: 0, y: 0, width: 640, height: 820),
1042
-            styleMask: [.titled, .closable, .fullSizeContentView],
1043
-            backing: .buffered,
1044
-            defer: false
1045
-        )
1046
-        panel.title = "Get Premium"
1047
-        panel.titleVisibility = .hidden
1048
-        panel.titlebarAppearsTransparent = true
1049
-        panel.isFloatingPanel = false
1050
-        panel.level = .normal
1051
-        panel.hidesOnDeactivate = true
1052
-        panel.isReleasedWhenClosed = false
1053
-        panel.delegate = self
1054
-        panel.standardWindowButton(.closeButton)?.isHidden = true
1055
-        panel.standardWindowButton(.miniaturizeButton)?.isHidden = true
1056
-        panel.standardWindowButton(.zoomButton)?.isHidden = true
1057
-        panel.center()
1058
-        panel.contentViewController = controller
1059
-        panel.alphaValue = 0
1060
-        panel.makeKeyAndOrderFront(nil)
1061
-        NSApp.activate(ignoringOtherApps: true)
1062
-        paywallWindow = panel
1063
-        animatePaywallPresentation(panel)
1052
+        let overlay = NSView()
1053
+        overlay.translatesAutoresizingMaskIntoConstraints = false
1054
+        overlay.wantsLayer = true
1055
+        overlay.layer?.backgroundColor = palette.pageBackground.withAlphaComponent(0.98).cgColor
1056
+        overlay.alphaValue = 0
1057
+        overlay.addSubview(content)
1058
+        view.addSubview(overlay, positioned: .above, relativeTo: nil)
1059
+        NSLayoutConstraint.activate([
1060
+            overlay.leadingAnchor.constraint(equalTo: view.leadingAnchor),
1061
+            overlay.trailingAnchor.constraint(equalTo: view.trailingAnchor),
1062
+            overlay.topAnchor.constraint(equalTo: view.topAnchor),
1063
+            overlay.bottomAnchor.constraint(equalTo: view.bottomAnchor),
1064
+
1065
+            content.leadingAnchor.constraint(equalTo: overlay.leadingAnchor),
1066
+            content.trailingAnchor.constraint(equalTo: overlay.trailingAnchor),
1067
+            content.topAnchor.constraint(equalTo: overlay.topAnchor),
1068
+            content.bottomAnchor.constraint(equalTo: overlay.bottomAnchor)
1069
+        ])
1070
+        paywallOverlayView = overlay
1071
+        NSAnimationContext.runAnimationGroup { context in
1072
+            context.duration = 0.20
1073
+            context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
1074
+            overlay.animator().alphaValue = 1
1075
+        }
1064
 
1076
 
1065
         Task { [weak self] in
1077
         Task { [weak self] in
1066
             guard let self else { return }
1078
             guard let self else { return }
@@ -1092,15 +1104,19 @@ private extension ViewController {
1092
     }
1104
     }
1093
 
1105
 
1094
     @objc private func closePaywallClicked(_ sender: Any?) {
1106
     @objc private func closePaywallClicked(_ sender: Any?) {
1095
-        if let win = paywallWindow {
1096
-            win.performClose(nil)
1097
-            return
1098
-        }
1099
-        if let gesture = sender as? NSGestureRecognizer, let win = gesture.view?.window {
1100
-            win.performClose(nil)
1107
+        if let overlay = paywallOverlayView {
1108
+            paywallOverlayView = nil
1109
+            paywallUpgradeFlowEnabled = false
1110
+            NSAnimationContext.runAnimationGroup({ context in
1111
+                context.duration = 0.16
1112
+                context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
1113
+                overlay.animator().alphaValue = 0
1114
+            }, completionHandler: {
1115
+                overlay.removeFromSuperview()
1116
+            })
1101
             return
1117
             return
1102
         }
1118
         }
1103
-        if let view = sender as? NSView, let win = view.window {
1119
+        if let win = paywallWindow {
1104
             win.performClose(nil)
1120
             win.performClose(nil)
1105
             return
1121
             return
1106
         }
1122
         }
@@ -1459,7 +1475,7 @@ private extension ViewController {
1459
                     await self?.loadSchedule()
1475
                     await self?.loadSchedule()
1460
                 }
1476
                 }
1461
                 self.showSimpleAlert(title: "Purchase Complete", message: "Premium has been unlocked successfully.")
1477
                 self.showSimpleAlert(title: "Purchase Complete", message: "Premium has been unlocked successfully.")
1462
-                self.paywallWindow?.performClose(nil)
1478
+                self.closePaywallClicked(nil)
1463
                 self.scheduleRatingPromptAfterPremiumUpgrade()
1479
                 self.scheduleRatingPromptAfterPremiumUpgrade()
1464
             case .cancelled:
1480
             case .cancelled:
1465
                 break
1481
                 break
@@ -2621,9 +2637,10 @@ private extension ViewController {
2621
         let contentStack = NSStackView()
2637
         let contentStack = NSStackView()
2622
         contentStack.translatesAutoresizingMaskIntoConstraints = false
2638
         contentStack.translatesAutoresizingMaskIntoConstraints = false
2623
         contentStack.orientation = .vertical
2639
         contentStack.orientation = .vertical
2624
-        contentStack.spacing = 12
2625
-        contentStack.alignment = .leading
2640
+        contentStack.spacing = 14
2641
+        contentStack.alignment = .centerX
2626
         panel.addSubview(contentStack)
2642
         panel.addSubview(contentStack)
2643
+        let paywallLayoutWidth: CGFloat = 980
2627
 
2644
 
2628
         let topRow = NSStackView()
2645
         let topRow = NSStackView()
2629
         topRow.translatesAutoresizingMaskIntoConstraints = false
2646
         topRow.translatesAutoresizingMaskIntoConstraints = false
@@ -2631,7 +2648,7 @@ private extension ViewController {
2631
         topRow.alignment = .centerY
2648
         topRow.alignment = .centerY
2632
         topRow.distribution = .fill
2649
         topRow.distribution = .fill
2633
         topRow.spacing = 10
2650
         topRow.spacing = 10
2634
-        topRow.addArrangedSubview(textLabel("Get Premium", font: NSFont.systemFont(ofSize: 24, weight: .bold), color: palette.textPrimary))
2651
+        topRow.addArrangedSubview(textLabel("Classroom Pro", font: NSFont.systemFont(ofSize: 27, weight: .bold), color: palette.textPrimary))
2635
         let topSpacer = NSView()
2652
         let topSpacer = NSView()
2636
         topSpacer.translatesAutoresizingMaskIntoConstraints = false
2653
         topSpacer.translatesAutoresizingMaskIntoConstraints = false
2637
         topRow.addArrangedSubview(topSpacer)
2654
         topRow.addArrangedSubview(topSpacer)
@@ -2657,47 +2674,83 @@ private extension ViewController {
2657
             closeButton.contentTintColor = hovering ? (self.darkModeEnabled ? .white : self.palette.textPrimary) : self.palette.textSecondary
2674
             closeButton.contentTintColor = hovering ? (self.darkModeEnabled ? .white : self.palette.textPrimary) : self.palette.textSecondary
2658
         }
2675
         }
2659
         topRow.addArrangedSubview(closeButton)
2676
         topRow.addArrangedSubview(closeButton)
2660
-        topRow.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
2661
         contentStack.addArrangedSubview(topRow)
2677
         contentStack.addArrangedSubview(topRow)
2678
+        topRow.widthAnchor.constraint(equalTo: contentStack.widthAnchor).isActive = true
2679
+
2680
+        let hero = roundedContainer(cornerRadius: 16, color: palette.sectionCard)
2681
+        hero.translatesAutoresizingMaskIntoConstraints = false
2682
+        styleSurface(hero, borderColor: palette.inputBorder, borderWidth: 1, shadow: false)
2683
+        contentStack.addArrangedSubview(hero)
2684
+        hero.widthAnchor.constraint(equalTo: contentStack.widthAnchor).isActive = true
2685
+
2686
+        let heroBadge = textLabel("PREMIUM EXPERIENCE", font: NSFont.systemFont(ofSize: 10, weight: .bold), color: NSColor(calibratedRed: 0.42, green: 0.93, blue: 0.70, alpha: 1))
2687
+        hero.addSubview(heroBadge)
2688
+
2689
+        let heroTitle = textLabel("Elevate your classroom experience", font: NSFont.systemFont(ofSize: 22, weight: .bold), color: palette.textPrimary)
2690
+        let heroSubtitle = textLabel("Unlock intelligent automation, deep analytics, and premium support for modern educators.", font: NSFont.systemFont(ofSize: 13, weight: .medium), color: palette.textSecondary)
2691
+        heroTitle.maximumNumberOfLines = 2
2692
+        heroSubtitle.maximumNumberOfLines = 3
2693
+        hero.addSubview(heroTitle)
2694
+        hero.addSubview(heroSubtitle)
2695
+        NSLayoutConstraint.activate([
2696
+            heroBadge.leadingAnchor.constraint(equalTo: hero.leadingAnchor, constant: 16),
2697
+            heroBadge.topAnchor.constraint(equalTo: hero.topAnchor, constant: 14),
2698
+            heroTitle.leadingAnchor.constraint(equalTo: hero.leadingAnchor, constant: 16),
2699
+            heroTitle.trailingAnchor.constraint(lessThanOrEqualTo: hero.trailingAnchor, constant: -16),
2700
+            heroTitle.topAnchor.constraint(equalTo: heroBadge.bottomAnchor, constant: 10),
2701
+            heroSubtitle.leadingAnchor.constraint(equalTo: heroTitle.leadingAnchor),
2702
+            heroSubtitle.trailingAnchor.constraint(equalTo: hero.trailingAnchor, constant: -16),
2703
+            heroSubtitle.topAnchor.constraint(equalTo: heroTitle.bottomAnchor, constant: 8),
2704
+            heroSubtitle.bottomAnchor.constraint(equalTo: hero.bottomAnchor, constant: -16)
2705
+        ])
2662
 
2706
 
2663
-        contentStack.addArrangedSubview(textLabel("Upgrade to unlock premium features.", font: NSFont.systemFont(ofSize: 12, weight: .medium), color: palette.textSecondary))
2664
-        let benefits = paywallBenefitsSection()
2665
-        contentStack.addArrangedSubview(benefits)
2666
-        contentStack.setCustomSpacing(18, after: benefits)
2707
+        let benefitsRow = NSStackView()
2708
+        benefitsRow.translatesAutoresizingMaskIntoConstraints = false
2709
+        benefitsRow.orientation = .horizontal
2710
+        benefitsRow.spacing = 8
2711
+        benefitsRow.distribution = .fillEqually
2712
+        benefitsRow.alignment = .centerY
2713
+        benefitsRow.addArrangedSubview(paywallBenefitItem(icon: "📅", text: "Manage classes"))
2714
+        benefitsRow.addArrangedSubview(paywallBenefitItem(icon: "🧠", text: "Smart to-dos"))
2715
+        benefitsRow.addArrangedSubview(paywallBenefitItem(icon: "⚡", text: "Faster workflow"))
2716
+        benefitsRow.addArrangedSubview(paywallBenefitItem(icon: "🛟", text: "Priority support"))
2717
+        contentStack.addArrangedSubview(benefitsRow)
2718
+        benefitsRow.widthAnchor.constraint(equalTo: contentStack.widthAnchor).isActive = true
2719
+
2720
+        let plansRow = NSStackView()
2721
+        plansRow.translatesAutoresizingMaskIntoConstraints = false
2722
+        plansRow.orientation = .horizontal
2723
+        plansRow.spacing = 10
2724
+        plansRow.distribution = .fillEqually
2725
+        plansRow.alignment = .centerY
2667
 
2726
 
2668
         let weeklyCard = paywallPlanCard(
2727
         let weeklyCard = paywallPlanCard(
2669
             title: "Weekly",
2728
             title: "Weekly",
2670
             price: "PKR 1,100.00",
2729
             price: "PKR 1,100.00",
2671
-            badge: "Basic Deal",
2730
+            badge: "Basic",
2672
             badgeColor: NSColor(calibratedRed: 1.0, green: 0.60, blue: 0.20, alpha: 1),
2731
             badgeColor: NSColor(calibratedRed: 1.0, green: 0.60, blue: 0.20, alpha: 1),
2673
             subtitle: nil,
2732
             subtitle: nil,
2674
             plan: .weekly,
2733
             plan: .weekly,
2675
             strikePrice: nil
2734
             strikePrice: nil
2676
         )
2735
         )
2677
-        contentStack.addArrangedSubview(weeklyCard)
2678
-
2679
         let monthlyCard = paywallPlanCard(
2736
         let monthlyCard = paywallPlanCard(
2680
             title: "Monthly",
2737
             title: "Monthly",
2681
             price: "PKR 2,500.00",
2738
             price: "PKR 2,500.00",
2682
-            badge: "Free Trial",
2739
+            badge: "Popular",
2683
             badgeColor: NSColor(calibratedRed: 0.19, green: 0.82, blue: 0.39, alpha: 1),
2740
             badgeColor: NSColor(calibratedRed: 0.19, green: 0.82, blue: 0.39, alpha: 1),
2684
             subtitle: "625.00/week",
2741
             subtitle: "625.00/week",
2685
             plan: .monthly,
2742
             plan: .monthly,
2686
             strikePrice: nil
2743
             strikePrice: nil
2687
         )
2744
         )
2688
-        contentStack.addArrangedSubview(monthlyCard)
2689
-
2690
         let yearlyCard = paywallPlanCard(
2745
         let yearlyCard = paywallPlanCard(
2691
             title: "Yearly",
2746
             title: "Yearly",
2692
             price: "PKR 9,900.00",
2747
             price: "PKR 9,900.00",
2693
-            badge: "Best Deal",
2748
+            badge: "Best Value",
2694
             badgeColor: NSColor(calibratedRed: 1.0, green: 0.60, blue: 0.20, alpha: 1),
2749
             badgeColor: NSColor(calibratedRed: 1.0, green: 0.60, blue: 0.20, alpha: 1),
2695
             subtitle: "190.38/week",
2750
             subtitle: "190.38/week",
2696
             plan: .yearly,
2751
             plan: .yearly,
2697
             strikePrice: nil
2752
             strikePrice: nil
2698
         )
2753
         )
2699
-        contentStack.addArrangedSubview(yearlyCard)
2700
-
2701
         let lifetimeCard = paywallPlanCard(
2754
         let lifetimeCard = paywallPlanCard(
2702
             title: "Lifetime",
2755
             title: "Lifetime",
2703
             price: "PKR 14,900.00",
2756
             price: "PKR 14,900.00",
@@ -2707,9 +2760,13 @@ private extension ViewController {
2707
             plan: .lifetime,
2760
             plan: .lifetime,
2708
             strikePrice: "PKR 29,800.00"
2761
             strikePrice: "PKR 29,800.00"
2709
         )
2762
         )
2710
-        contentStack.addArrangedSubview(lifetimeCard)
2763
+        plansRow.addArrangedSubview(weeklyCard)
2764
+        plansRow.addArrangedSubview(monthlyCard)
2765
+        plansRow.addArrangedSubview(yearlyCard)
2766
+        plansRow.addArrangedSubview(lifetimeCard)
2767
+        contentStack.addArrangedSubview(plansRow)
2768
+        plansRow.widthAnchor.constraint(equalTo: contentStack.widthAnchor).isActive = true
2711
         updatePaywallPlanSelection()
2769
         updatePaywallPlanSelection()
2712
-        contentStack.setCustomSpacing(20, after: lifetimeCard)
2713
 
2770
 
2714
         let offer = textLabel(paywallOfferText(for: selectedPremiumPlan), font: NSFont.systemFont(ofSize: 13, weight: .semibold), color: palette.textPrimary)
2771
         let offer = textLabel(paywallOfferText(for: selectedPremiumPlan), font: NSFont.systemFont(ofSize: 13, weight: .semibold), color: palette.textPrimary)
2715
         offer.alignment = .center
2772
         offer.alignment = .center
@@ -2717,14 +2774,13 @@ private extension ViewController {
2717
         let offerWrap = NSView()
2774
         let offerWrap = NSView()
2718
         offerWrap.translatesAutoresizingMaskIntoConstraints = false
2775
         offerWrap.translatesAutoresizingMaskIntoConstraints = false
2719
         offerWrap.addSubview(offer)
2776
         offerWrap.addSubview(offer)
2777
+        contentStack.addArrangedSubview(offerWrap)
2778
+        offerWrap.widthAnchor.constraint(equalTo: contentStack.widthAnchor).isActive = true
2720
         NSLayoutConstraint.activate([
2779
         NSLayoutConstraint.activate([
2721
-            offerWrap.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth),
2722
             offer.centerXAnchor.constraint(equalTo: offerWrap.centerXAnchor),
2780
             offer.centerXAnchor.constraint(equalTo: offerWrap.centerXAnchor),
2723
-            offer.topAnchor.constraint(equalTo: offerWrap.topAnchor, constant: 6),
2781
+            offer.topAnchor.constraint(equalTo: offerWrap.topAnchor, constant: 4),
2724
             offer.bottomAnchor.constraint(equalTo: offerWrap.bottomAnchor, constant: -2)
2782
             offer.bottomAnchor.constraint(equalTo: offerWrap.bottomAnchor, constant: -2)
2725
         ])
2783
         ])
2726
-        contentStack.addArrangedSubview(offerWrap)
2727
-        contentStack.setCustomSpacing(18, after: offerWrap)
2728
 
2784
 
2729
         let continueButton = HoverButton(title: "", target: self, action: #selector(paywallContinueClicked(_:)))
2785
         let continueButton = HoverButton(title: "", target: self, action: #selector(paywallContinueClicked(_:)))
2730
         continueButton.translatesAutoresizingMaskIntoConstraints = false
2786
         continueButton.translatesAutoresizingMaskIntoConstraints = false
@@ -2734,7 +2790,6 @@ private extension ViewController {
2734
         continueButton.layer?.cornerRadius = 14
2790
         continueButton.layer?.cornerRadius = 14
2735
         continueButton.layer?.backgroundColor = palette.primaryBlue.cgColor
2791
         continueButton.layer?.backgroundColor = palette.primaryBlue.cgColor
2736
         continueButton.heightAnchor.constraint(equalToConstant: 44).isActive = true
2792
         continueButton.heightAnchor.constraint(equalToConstant: 44).isActive = true
2737
-        continueButton.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
2738
         styleSurface(continueButton, borderColor: palette.primaryBlueBorder, borderWidth: 1, shadow: true)
2793
         styleSurface(continueButton, borderColor: palette.primaryBlueBorder, borderWidth: 1, shadow: true)
2739
         let continueLabel = textLabel("Continue", font: NSFont.systemFont(ofSize: 16, weight: .bold), color: .white)
2794
         let continueLabel = textLabel("Continue", font: NSFont.systemFont(ofSize: 16, weight: .bold), color: .white)
2740
         continueButton.addSubview(continueLabel)
2795
         continueButton.addSubview(continueLabel)
@@ -2752,30 +2807,30 @@ private extension ViewController {
2752
         paywallContinueButton = continueButton
2807
         paywallContinueButton = continueButton
2753
         paywallContinueLabel = continueLabel
2808
         paywallContinueLabel = continueLabel
2754
         contentStack.addArrangedSubview(continueButton)
2809
         contentStack.addArrangedSubview(continueButton)
2755
-        contentStack.setCustomSpacing(16, after: continueButton)
2810
+        continueButton.widthAnchor.constraint(equalTo: contentStack.widthAnchor).isActive = true
2756
 
2811
 
2757
         let secure = textLabel("Secured by Apple. Cancel anytime.", font: NSFont.systemFont(ofSize: 12, weight: .semibold), color: palette.textSecondary)
2812
         let secure = textLabel("Secured by Apple. Cancel anytime.", font: NSFont.systemFont(ofSize: 12, weight: .semibold), color: palette.textSecondary)
2758
         secure.alignment = .center
2813
         secure.alignment = .center
2759
         let secureWrap = NSView()
2814
         let secureWrap = NSView()
2760
         secureWrap.translatesAutoresizingMaskIntoConstraints = false
2815
         secureWrap.translatesAutoresizingMaskIntoConstraints = false
2761
         secureWrap.addSubview(secure)
2816
         secureWrap.addSubview(secure)
2817
+        contentStack.addArrangedSubview(secureWrap)
2818
+        secureWrap.widthAnchor.constraint(equalTo: contentStack.widthAnchor).isActive = true
2762
         NSLayoutConstraint.activate([
2819
         NSLayoutConstraint.activate([
2763
-            secureWrap.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth),
2764
             secure.centerXAnchor.constraint(equalTo: secureWrap.centerXAnchor),
2820
             secure.centerXAnchor.constraint(equalTo: secureWrap.centerXAnchor),
2765
-            secure.topAnchor.constraint(equalTo: secureWrap.topAnchor, constant: 4),
2766
-            secure.bottomAnchor.constraint(equalTo: secureWrap.bottomAnchor, constant: -8)
2821
+            secure.topAnchor.constraint(equalTo: secureWrap.topAnchor, constant: 2),
2822
+            secure.bottomAnchor.constraint(equalTo: secureWrap.bottomAnchor, constant: -4)
2767
         ])
2823
         ])
2768
-        contentStack.addArrangedSubview(secureWrap)
2769
-        contentStack.setCustomSpacing(16, after: secureWrap)
2770
 
2824
 
2771
         let footer = paywallFooterLinks()
2825
         let footer = paywallFooterLinks()
2772
         contentStack.addArrangedSubview(footer)
2826
         contentStack.addArrangedSubview(footer)
2827
+        footer.widthAnchor.constraint(equalTo: contentStack.widthAnchor).isActive = true
2773
 
2828
 
2774
         NSLayoutConstraint.activate([
2829
         NSLayoutConstraint.activate([
2775
-            contentStack.leadingAnchor.constraint(equalTo: panel.leadingAnchor, constant: 18),
2776
-            contentStack.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -18),
2777
-            contentStack.topAnchor.constraint(equalTo: panel.topAnchor, constant: 16),
2778
-            contentStack.bottomAnchor.constraint(lessThanOrEqualTo: panel.bottomAnchor, constant: -12)
2830
+            contentStack.centerXAnchor.constraint(equalTo: panel.centerXAnchor),
2831
+            contentStack.widthAnchor.constraint(equalToConstant: paywallLayoutWidth),
2832
+            contentStack.topAnchor.constraint(equalTo: panel.topAnchor, constant: 20),
2833
+            contentStack.bottomAnchor.constraint(lessThanOrEqualTo: panel.bottomAnchor, constant: -16)
2779
         ])
2834
         ])
2780
 
2835
 
2781
         refreshPaywallStoreUI()
2836
         refreshPaywallStoreUI()
@@ -2797,8 +2852,8 @@ private extension ViewController {
2797
         wrapper.bezelStyle = .regularSquare
2852
         wrapper.bezelStyle = .regularSquare
2798
         wrapper.wantsLayer = true
2853
         wrapper.wantsLayer = true
2799
         wrapper.layer?.backgroundColor = NSColor.clear.cgColor
2854
         wrapper.layer?.backgroundColor = NSColor.clear.cgColor
2800
-        wrapper.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
2801
-        wrapper.heightAnchor.constraint(equalToConstant: 94).isActive = true
2855
+        wrapper.widthAnchor.constraint(greaterThanOrEqualToConstant: 160).isActive = true
2856
+        wrapper.heightAnchor.constraint(equalToConstant: 162).isActive = true
2802
         wrapper.tag = plan.rawValue
2857
         wrapper.tag = plan.rawValue
2803
 
2858
 
2804
         let card = HoverTrackingView()
2859
         let card = HoverTrackingView()
@@ -2806,7 +2861,7 @@ private extension ViewController {
2806
         card.wantsLayer = true
2861
         card.wantsLayer = true
2807
         card.layer?.cornerRadius = 16
2862
         card.layer?.cornerRadius = 16
2808
         card.layer?.backgroundColor = palette.sectionCard.cgColor
2863
         card.layer?.backgroundColor = palette.sectionCard.cgColor
2809
-        card.heightAnchor.constraint(equalToConstant: 82).isActive = true
2864
+        card.heightAnchor.constraint(equalToConstant: 150).isActive = true
2810
         wrapper.addSubview(card)
2865
         wrapper.addSubview(card)
2811
         NSLayoutConstraint.activate([
2866
         NSLayoutConstraint.activate([
2812
             card.leadingAnchor.constraint(equalTo: wrapper.leadingAnchor),
2867
             card.leadingAnchor.constraint(equalTo: wrapper.leadingAnchor),
@@ -2842,14 +2897,15 @@ private extension ViewController {
2842
         paywallPriceLabels[plan] = priceLabel
2897
         paywallPriceLabels[plan] = priceLabel
2843
 
2898
 
2844
         NSLayoutConstraint.activate([
2899
         NSLayoutConstraint.activate([
2845
-            badgeWrap.centerXAnchor.constraint(equalTo: card.centerXAnchor),
2900
+            badgeWrap.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 12),
2846
             badgeWrap.centerYAnchor.constraint(equalTo: card.topAnchor),
2901
             badgeWrap.centerYAnchor.constraint(equalTo: card.topAnchor),
2847
 
2902
 
2848
-            titleLabel.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 16),
2903
+            titleLabel.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 12),
2849
             titleLabel.topAnchor.constraint(equalTo: card.topAnchor, constant: 34),
2904
             titleLabel.topAnchor.constraint(equalTo: card.topAnchor, constant: 34),
2850
 
2905
 
2851
-            priceLabel.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -16),
2852
-            priceLabel.topAnchor.constraint(equalTo: card.topAnchor, constant: 32)
2906
+            priceLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
2907
+            priceLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 10),
2908
+            priceLabel.trailingAnchor.constraint(lessThanOrEqualTo: card.trailingAnchor, constant: -12)
2853
         ])
2909
         ])
2854
 
2910
 
2855
         if let subtitle {
2911
         if let subtitle {
@@ -2857,8 +2913,9 @@ private extension ViewController {
2857
             card.addSubview(sub)
2913
             card.addSubview(sub)
2858
             paywallSubtitleLabels[plan] = sub
2914
             paywallSubtitleLabels[plan] = sub
2859
             NSLayoutConstraint.activate([
2915
             NSLayoutConstraint.activate([
2860
-                sub.trailingAnchor.constraint(equalTo: priceLabel.trailingAnchor),
2861
-                sub.topAnchor.constraint(equalTo: priceLabel.bottomAnchor, constant: 0)
2916
+                sub.leadingAnchor.constraint(equalTo: priceLabel.leadingAnchor),
2917
+                sub.topAnchor.constraint(equalTo: priceLabel.bottomAnchor, constant: 2),
2918
+                sub.trailingAnchor.constraint(lessThanOrEqualTo: card.trailingAnchor, constant: -12)
2862
             ])
2919
             ])
2863
         }
2920
         }
2864
 
2921
 
@@ -2866,8 +2923,9 @@ private extension ViewController {
2866
             let strike = textLabel(strikePrice, font: NSFont.systemFont(ofSize: 12, weight: .medium), color: NSColor.systemRed)
2923
             let strike = textLabel(strikePrice, font: NSFont.systemFont(ofSize: 12, weight: .medium), color: NSColor.systemRed)
2867
             card.addSubview(strike)
2924
             card.addSubview(strike)
2868
             NSLayoutConstraint.activate([
2925
             NSLayoutConstraint.activate([
2869
-                strike.trailingAnchor.constraint(equalTo: priceLabel.trailingAnchor),
2870
-                strike.topAnchor.constraint(equalTo: priceLabel.bottomAnchor, constant: 4)
2926
+                strike.leadingAnchor.constraint(equalTo: priceLabel.leadingAnchor),
2927
+                strike.topAnchor.constraint(equalTo: priceLabel.bottomAnchor, constant: 4),
2928
+                strike.trailingAnchor.constraint(lessThanOrEqualTo: card.trailingAnchor, constant: -12)
2871
             ])
2929
             ])
2872
         }
2930
         }
2873
 
2931
 
@@ -2885,7 +2943,6 @@ private extension ViewController {
2885
         let wrap = NSView()
2943
         let wrap = NSView()
2886
         wrap.translatesAutoresizingMaskIntoConstraints = false
2944
         wrap.translatesAutoresizingMaskIntoConstraints = false
2887
         wrap.heightAnchor.constraint(equalToConstant: 34).isActive = true
2945
         wrap.heightAnchor.constraint(equalToConstant: 34).isActive = true
2888
-        wrap.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
2889
 
2946
 
2890
         let row = NSStackView()
2947
         let row = NSStackView()
2891
         row.translatesAutoresizingMaskIntoConstraints = false
2948
         row.translatesAutoresizingMaskIntoConstraints = false
@@ -2967,7 +3024,7 @@ private extension ViewController {
2967
         card.wantsLayer = true
3024
         card.wantsLayer = true
2968
         card.layer?.cornerRadius = 10
3025
         card.layer?.cornerRadius = 10
2969
         card.layer?.backgroundColor = palette.inputBackground.cgColor
3026
         card.layer?.backgroundColor = palette.inputBackground.cgColor
2970
-        card.heightAnchor.constraint(equalToConstant: 36).isActive = true
3027
+        card.heightAnchor.constraint(equalToConstant: 44).isActive = true
2971
         styleSurface(card, borderColor: palette.inputBorder, borderWidth: 1, shadow: false)
3028
         styleSurface(card, borderColor: palette.inputBorder, borderWidth: 1, shadow: false)
2972
 
3029
 
2973
         let iconWrap = roundedContainer(cornerRadius: 8, color: palette.inputBackground)
3030
         let iconWrap = roundedContainer(cornerRadius: 8, color: palette.inputBackground)