Преглед изворни кода

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 пре 7 часа
родитељ
комит
d04f843968
1 измењених фајлова са 140 додато и 83 уклоњено
  1. 140 83
      classroom_app/ViewController.swift

+ 140 - 83
classroom_app/ViewController.swift

@@ -250,6 +250,7 @@ final class ViewController: NSViewController {
250 250
     private var zoomJoinModeViews: [ZoomJoinMode: NSView] = [:]
251 251
     private var settingsActionByView = [ObjectIdentifier: SettingsAction]()
252 252
     private var paywallWindow: NSWindow?
253
+    private weak var paywallOverlayView: NSView?
253 254
     private let paywallContentWidth: CGFloat = 520
254 255
     private let launchWindowLeftOffset: CGFloat = 80
255 256
     private var selectedPremiumPlan: PremiumPlan = .monthly
@@ -414,7 +415,9 @@ final class ViewController: NSViewController {
414 415
         palette = Palette(isDarkMode: darkModeEnabled)
415 416
         storeKitCoordinator.onEntitlementsChanged = { [weak self] hasPremiumAccess in
416 417
             guard let self else { return }
417
-            self.handlePremiumAccessChanged(hasPremiumAccess)
418
+            DispatchQueue.main.async {
419
+                self.handlePremiumAccessChanged(hasPremiumAccess)
420
+            }
418 421
         }
419 422
         migrateLegacyRatingStateIfNeeded()
420 423
         beginUsageTrackingSessionIfNeeded()
@@ -1021,10 +1024,22 @@ private extension ViewController {
1021 1024
     }
1022 1025
 
1023 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 1033
         paywallUpgradeFlowEnabled = upgradeFlow
1025 1034
         if let preferredPlan {
1026 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 1043
         if let existing = paywallWindow {
1029 1044
             refreshPaywallStoreUI()
1030 1045
             animatePaywallPresentation(existing)
@@ -1034,33 +1049,30 @@ private extension ViewController {
1034 1049
         }
1035 1050
 
1036 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 1077
         Task { [weak self] in
1066 1078
             guard let self else { return }
@@ -1092,15 +1104,19 @@ private extension ViewController {
1092 1104
     }
1093 1105
 
1094 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 1117
             return
1102 1118
         }
1103
-        if let view = sender as? NSView, let win = view.window {
1119
+        if let win = paywallWindow {
1104 1120
             win.performClose(nil)
1105 1121
             return
1106 1122
         }
@@ -1459,7 +1475,7 @@ private extension ViewController {
1459 1475
                     await self?.loadSchedule()
1460 1476
                 }
1461 1477
                 self.showSimpleAlert(title: "Purchase Complete", message: "Premium has been unlocked successfully.")
1462
-                self.paywallWindow?.performClose(nil)
1478
+                self.closePaywallClicked(nil)
1463 1479
                 self.scheduleRatingPromptAfterPremiumUpgrade()
1464 1480
             case .cancelled:
1465 1481
                 break
@@ -2621,9 +2637,10 @@ private extension ViewController {
2621 2637
         let contentStack = NSStackView()
2622 2638
         contentStack.translatesAutoresizingMaskIntoConstraints = false
2623 2639
         contentStack.orientation = .vertical
2624
-        contentStack.spacing = 12
2625
-        contentStack.alignment = .leading
2640
+        contentStack.spacing = 14
2641
+        contentStack.alignment = .centerX
2626 2642
         panel.addSubview(contentStack)
2643
+        let paywallLayoutWidth: CGFloat = 980
2627 2644
 
2628 2645
         let topRow = NSStackView()
2629 2646
         topRow.translatesAutoresizingMaskIntoConstraints = false
@@ -2631,7 +2648,7 @@ private extension ViewController {
2631 2648
         topRow.alignment = .centerY
2632 2649
         topRow.distribution = .fill
2633 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 2652
         let topSpacer = NSView()
2636 2653
         topSpacer.translatesAutoresizingMaskIntoConstraints = false
2637 2654
         topRow.addArrangedSubview(topSpacer)
@@ -2657,47 +2674,83 @@ private extension ViewController {
2657 2674
             closeButton.contentTintColor = hovering ? (self.darkModeEnabled ? .white : self.palette.textPrimary) : self.palette.textSecondary
2658 2675
         }
2659 2676
         topRow.addArrangedSubview(closeButton)
2660
-        topRow.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
2661 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 2727
         let weeklyCard = paywallPlanCard(
2669 2728
             title: "Weekly",
2670 2729
             price: "PKR 1,100.00",
2671
-            badge: "Basic Deal",
2730
+            badge: "Basic",
2672 2731
             badgeColor: NSColor(calibratedRed: 1.0, green: 0.60, blue: 0.20, alpha: 1),
2673 2732
             subtitle: nil,
2674 2733
             plan: .weekly,
2675 2734
             strikePrice: nil
2676 2735
         )
2677
-        contentStack.addArrangedSubview(weeklyCard)
2678
-
2679 2736
         let monthlyCard = paywallPlanCard(
2680 2737
             title: "Monthly",
2681 2738
             price: "PKR 2,500.00",
2682
-            badge: "Free Trial",
2739
+            badge: "Popular",
2683 2740
             badgeColor: NSColor(calibratedRed: 0.19, green: 0.82, blue: 0.39, alpha: 1),
2684 2741
             subtitle: "625.00/week",
2685 2742
             plan: .monthly,
2686 2743
             strikePrice: nil
2687 2744
         )
2688
-        contentStack.addArrangedSubview(monthlyCard)
2689
-
2690 2745
         let yearlyCard = paywallPlanCard(
2691 2746
             title: "Yearly",
2692 2747
             price: "PKR 9,900.00",
2693
-            badge: "Best Deal",
2748
+            badge: "Best Value",
2694 2749
             badgeColor: NSColor(calibratedRed: 1.0, green: 0.60, blue: 0.20, alpha: 1),
2695 2750
             subtitle: "190.38/week",
2696 2751
             plan: .yearly,
2697 2752
             strikePrice: nil
2698 2753
         )
2699
-        contentStack.addArrangedSubview(yearlyCard)
2700
-
2701 2754
         let lifetimeCard = paywallPlanCard(
2702 2755
             title: "Lifetime",
2703 2756
             price: "PKR 14,900.00",
@@ -2707,9 +2760,13 @@ private extension ViewController {
2707 2760
             plan: .lifetime,
2708 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 2769
         updatePaywallPlanSelection()
2712
-        contentStack.setCustomSpacing(20, after: lifetimeCard)
2713 2770
 
2714 2771
         let offer = textLabel(paywallOfferText(for: selectedPremiumPlan), font: NSFont.systemFont(ofSize: 13, weight: .semibold), color: palette.textPrimary)
2715 2772
         offer.alignment = .center
@@ -2717,14 +2774,13 @@ private extension ViewController {
2717 2774
         let offerWrap = NSView()
2718 2775
         offerWrap.translatesAutoresizingMaskIntoConstraints = false
2719 2776
         offerWrap.addSubview(offer)
2777
+        contentStack.addArrangedSubview(offerWrap)
2778
+        offerWrap.widthAnchor.constraint(equalTo: contentStack.widthAnchor).isActive = true
2720 2779
         NSLayoutConstraint.activate([
2721
-            offerWrap.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth),
2722 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 2782
             offer.bottomAnchor.constraint(equalTo: offerWrap.bottomAnchor, constant: -2)
2725 2783
         ])
2726
-        contentStack.addArrangedSubview(offerWrap)
2727
-        contentStack.setCustomSpacing(18, after: offerWrap)
2728 2784
 
2729 2785
         let continueButton = HoverButton(title: "", target: self, action: #selector(paywallContinueClicked(_:)))
2730 2786
         continueButton.translatesAutoresizingMaskIntoConstraints = false
@@ -2734,7 +2790,6 @@ private extension ViewController {
2734 2790
         continueButton.layer?.cornerRadius = 14
2735 2791
         continueButton.layer?.backgroundColor = palette.primaryBlue.cgColor
2736 2792
         continueButton.heightAnchor.constraint(equalToConstant: 44).isActive = true
2737
-        continueButton.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
2738 2793
         styleSurface(continueButton, borderColor: palette.primaryBlueBorder, borderWidth: 1, shadow: true)
2739 2794
         let continueLabel = textLabel("Continue", font: NSFont.systemFont(ofSize: 16, weight: .bold), color: .white)
2740 2795
         continueButton.addSubview(continueLabel)
@@ -2752,30 +2807,30 @@ private extension ViewController {
2752 2807
         paywallContinueButton = continueButton
2753 2808
         paywallContinueLabel = continueLabel
2754 2809
         contentStack.addArrangedSubview(continueButton)
2755
-        contentStack.setCustomSpacing(16, after: continueButton)
2810
+        continueButton.widthAnchor.constraint(equalTo: contentStack.widthAnchor).isActive = true
2756 2811
 
2757 2812
         let secure = textLabel("Secured by Apple. Cancel anytime.", font: NSFont.systemFont(ofSize: 12, weight: .semibold), color: palette.textSecondary)
2758 2813
         secure.alignment = .center
2759 2814
         let secureWrap = NSView()
2760 2815
         secureWrap.translatesAutoresizingMaskIntoConstraints = false
2761 2816
         secureWrap.addSubview(secure)
2817
+        contentStack.addArrangedSubview(secureWrap)
2818
+        secureWrap.widthAnchor.constraint(equalTo: contentStack.widthAnchor).isActive = true
2762 2819
         NSLayoutConstraint.activate([
2763
-            secureWrap.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth),
2764 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 2825
         let footer = paywallFooterLinks()
2772 2826
         contentStack.addArrangedSubview(footer)
2827
+        footer.widthAnchor.constraint(equalTo: contentStack.widthAnchor).isActive = true
2773 2828
 
2774 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 2836
         refreshPaywallStoreUI()
@@ -2797,8 +2852,8 @@ private extension ViewController {
2797 2852
         wrapper.bezelStyle = .regularSquare
2798 2853
         wrapper.wantsLayer = true
2799 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 2857
         wrapper.tag = plan.rawValue
2803 2858
 
2804 2859
         let card = HoverTrackingView()
@@ -2806,7 +2861,7 @@ private extension ViewController {
2806 2861
         card.wantsLayer = true
2807 2862
         card.layer?.cornerRadius = 16
2808 2863
         card.layer?.backgroundColor = palette.sectionCard.cgColor
2809
-        card.heightAnchor.constraint(equalToConstant: 82).isActive = true
2864
+        card.heightAnchor.constraint(equalToConstant: 150).isActive = true
2810 2865
         wrapper.addSubview(card)
2811 2866
         NSLayoutConstraint.activate([
2812 2867
             card.leadingAnchor.constraint(equalTo: wrapper.leadingAnchor),
@@ -2842,14 +2897,15 @@ private extension ViewController {
2842 2897
         paywallPriceLabels[plan] = priceLabel
2843 2898
 
2844 2899
         NSLayoutConstraint.activate([
2845
-            badgeWrap.centerXAnchor.constraint(equalTo: card.centerXAnchor),
2900
+            badgeWrap.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 12),
2846 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 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 2911
         if let subtitle {
@@ -2857,8 +2913,9 @@ private extension ViewController {
2857 2913
             card.addSubview(sub)
2858 2914
             paywallSubtitleLabels[plan] = sub
2859 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 2923
             let strike = textLabel(strikePrice, font: NSFont.systemFont(ofSize: 12, weight: .medium), color: NSColor.systemRed)
2867 2924
             card.addSubview(strike)
2868 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 2943
         let wrap = NSView()
2886 2944
         wrap.translatesAutoresizingMaskIntoConstraints = false
2887 2945
         wrap.heightAnchor.constraint(equalToConstant: 34).isActive = true
2888
-        wrap.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
2889 2946
 
2890 2947
         let row = NSStackView()
2891 2948
         row.translatesAutoresizingMaskIntoConstraints = false
@@ -2967,7 +3024,7 @@ private extension ViewController {
2967 3024
         card.wantsLayer = true
2968 3025
         card.layer?.cornerRadius = 10
2969 3026
         card.layer?.backgroundColor = palette.inputBackground.cgColor
2970
-        card.heightAnchor.constraint(equalToConstant: 36).isActive = true
3027
+        card.heightAnchor.constraint(equalToConstant: 44).isActive = true
2971 3028
         styleSurface(card, borderColor: palette.inputBorder, borderWidth: 1, shadow: false)
2972 3029
 
2973 3030
         let iconWrap = roundedContainer(cornerRadius: 8, color: palette.inputBackground)