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

Redesign paywall to match the classroom landscape experience.

Align package, free-trial, and footer actions by adding manage and restore flows and updating the paywall interaction model.

Made-with: Cursor
huzaifahayat12 1 месяц назад
Родитель
Сommit
ac818bbe83
1 измененных файлов с 330 добавлено и 140 удалено
  1. 330 140
      meetings_app/ViewController.swift

+ 330 - 140
meetings_app/ViewController.swift

@@ -226,6 +226,15 @@ private final class StoreKitCoordinator {
226
 }
226
 }
227
 
227
 
228
 final class ViewController: NSViewController {
228
 final class ViewController: NSViewController {
229
+    private enum PaywallFooterAction {
230
+        case manageSubscription
231
+        case restorePurchase
232
+        case continueWithFreePlan
233
+        case privacyPolicy
234
+        case support
235
+        case termsOfServices
236
+    }
237
+
229
     private struct GoogleProfileDisplay {
238
     private struct GoogleProfileDisplay {
230
         let name: String
239
         let name: String
231
         let email: String
240
         let email: String
@@ -249,11 +258,13 @@ final class ViewController: NSViewController {
249
     private var zoomJoinModeViews: [ZoomJoinMode: NSView] = [:]
258
     private var zoomJoinModeViews: [ZoomJoinMode: NSView] = [:]
250
     private var settingsActionByView = [ObjectIdentifier: SettingsAction]()
259
     private var settingsActionByView = [ObjectIdentifier: SettingsAction]()
251
     private var paywallWindow: NSWindow?
260
     private var paywallWindow: NSWindow?
261
+    private weak var paywallOverlayView: NSView?
252
     private let paywallContentWidth: CGFloat = 520
262
     private let paywallContentWidth: CGFloat = 520
253
     private let launchWindowLeftOffset: CGFloat = 80
263
     private let launchWindowLeftOffset: CGFloat = 80
254
     private var selectedPremiumPlan: PremiumPlan = .monthly
264
     private var selectedPremiumPlan: PremiumPlan = .monthly
255
     private var paywallPlanViews: [PremiumPlan: NSView] = [:]
265
     private var paywallPlanViews: [PremiumPlan: NSView] = [:]
256
     private var premiumPlanByView = [ObjectIdentifier: PremiumPlan]()
266
     private var premiumPlanByView = [ObjectIdentifier: PremiumPlan]()
267
+    private var paywallFooterActionByView = [ObjectIdentifier: PaywallFooterAction]()
257
     private weak var paywallOfferLabel: NSTextField?
268
     private weak var paywallOfferLabel: NSTextField?
258
     private weak var paywallContinueLabel: NSTextField?
269
     private weak var paywallContinueLabel: NSTextField?
259
     private weak var paywallContinueButton: NSView?
270
     private weak var paywallContinueButton: NSView?
@@ -745,6 +756,10 @@ private extension ViewController {
745
     }
756
     }
746
 
757
 
747
     private func openManageSubscriptions() {
758
     private func openManageSubscriptions() {
759
+        if let appStoreURL = URL(string: "macappstore://apps.apple.com/account/subscriptions"),
760
+           NSWorkspace.shared.open(appStoreURL) {
761
+            return
762
+        }
748
         guard let url = URL(string: "https://apps.apple.com/account/subscriptions") else {
763
         guard let url = URL(string: "https://apps.apple.com/account/subscriptions") else {
749
             showSimpleAlert(title: "Unable to Open Subscriptions", message: "The subscriptions URL is invalid.")
764
             showSimpleAlert(title: "Unable to Open Subscriptions", message: "The subscriptions URL is invalid.")
750
             return
765
             return
@@ -752,6 +767,27 @@ private extension ViewController {
752
         openInDefaultBrowser(url: url)
767
         openInDefaultBrowser(url: url)
753
     }
768
     }
754
 
769
 
770
+    private func openRestoreSubscriptionPage() {
771
+        let fallbackURL = "https://support.apple.com/en-us/108096"
772
+        let restoreURL = (Bundle.main.object(forInfoDictionaryKey: "RestoreSubscriptionURL") as? String) ?? fallbackURL
773
+        guard let url = URL(string: restoreURL) else {
774
+            if let fallback = URL(string: fallbackURL) {
775
+                NSWorkspace.shared.open(fallback)
776
+            }
777
+            return
778
+        }
779
+        NSWorkspace.shared.open(url)
780
+    }
781
+
782
+    private func performRestorePurchases() {
783
+        Task { [weak self] in
784
+            guard let self else { return }
785
+            let message = await self.storeKitCoordinator.restorePurchases()
786
+            self.refreshPaywallStoreUI()
787
+            self.showSimpleAlert(title: "Restore Purchases", message: message)
788
+        }
789
+    }
790
+
755
     private func shareAppURL() -> URL? {
791
     private func shareAppURL() -> URL? {
756
         if let configured = Bundle.main.object(forInfoDictionaryKey: "AppShareURL") as? String {
792
         if let configured = Bundle.main.object(forInfoDictionaryKey: "AppShareURL") as? String {
757
             let trimmed = configured.trimmingCharacters(in: .whitespacesAndNewlines)
793
             let trimmed = configured.trimmingCharacters(in: .whitespacesAndNewlines)
@@ -839,6 +875,7 @@ private extension ViewController {
839
         settingsActionByView.removeAll()
875
         settingsActionByView.removeAll()
840
         paywallPlanViews.removeAll()
876
         paywallPlanViews.removeAll()
841
         premiumPlanByView.removeAll()
877
         premiumPlanByView.removeAll()
878
+        paywallFooterActionByView.removeAll()
842
         paywallPriceLabels.removeAll()
879
         paywallPriceLabels.removeAll()
843
         paywallSubtitleLabels.removeAll()
880
         paywallSubtitleLabels.removeAll()
844
         paywallContinueLabel = nil
881
         paywallContinueLabel = nil
@@ -860,12 +897,7 @@ private extension ViewController {
860
     private func handleSettingsAction(_ action: SettingsAction, sourceView: NSView? = nil, clickLocationInSourceView: NSPoint? = nil) {
897
     private func handleSettingsAction(_ action: SettingsAction, sourceView: NSView? = nil, clickLocationInSourceView: NSPoint? = nil) {
861
         switch action {
898
         switch action {
862
         case .restore:
899
         case .restore:
863
-            Task { [weak self] in
864
-                guard let self else { return }
865
-                let message = await self.storeKitCoordinator.restorePurchases()
866
-                self.refreshPaywallStoreUI()
867
-                self.showSimpleAlert(title: "Restore Purchases", message: message)
868
-            }
900
+            performRestorePurchases()
869
         case .rateUs:
901
         case .rateUs:
870
             openRateUsDestination()
902
             openRateUsDestination()
871
         case .support:
903
         case .support:
@@ -1000,10 +1032,22 @@ private extension ViewController {
1000
     }
1032
     }
1001
 
1033
 
1002
     private func showPaywall(upgradeFlow: Bool = false, preferredPlan: PremiumPlan? = nil) {
1034
     private func showPaywall(upgradeFlow: Bool = false, preferredPlan: PremiumPlan? = nil) {
1035
+        if !Thread.isMainThread {
1036
+            DispatchQueue.main.async { [weak self] in
1037
+                self?.showPaywall(upgradeFlow: upgradeFlow, preferredPlan: preferredPlan)
1038
+            }
1039
+            return
1040
+        }
1003
         paywallUpgradeFlowEnabled = upgradeFlow
1041
         paywallUpgradeFlowEnabled = upgradeFlow
1004
         if let preferredPlan {
1042
         if let preferredPlan {
1005
             selectedPremiumPlan = preferredPlan
1043
             selectedPremiumPlan = preferredPlan
1006
         }
1044
         }
1045
+        if let existingOverlay = paywallOverlayView {
1046
+            refreshPaywallStoreUI()
1047
+            existingOverlay.alphaValue = 1
1048
+            view.addSubview(existingOverlay, positioned: .above, relativeTo: nil)
1049
+            return
1050
+        }
1007
         if let existing = paywallWindow {
1051
         if let existing = paywallWindow {
1008
             refreshPaywallStoreUI()
1052
             refreshPaywallStoreUI()
1009
             animatePaywallPresentation(existing)
1053
             animatePaywallPresentation(existing)
@@ -1013,33 +1057,30 @@ private extension ViewController {
1013
         }
1057
         }
1014
 
1058
 
1015
         let content = makePaywallContent()
1059
         let content = makePaywallContent()
1016
-        let controller = NSViewController()
1017
-        controller.view = content
1018
-
1019
-        let panel = NSPanel(
1020
-            contentRect: NSRect(x: 0, y: 0, width: 640, height: 820),
1021
-            styleMask: [.titled, .closable, .fullSizeContentView],
1022
-            backing: .buffered,
1023
-            defer: false
1024
-        )
1025
-        panel.title = "Get Premium"
1026
-        panel.titleVisibility = .hidden
1027
-        panel.titlebarAppearsTransparent = true
1028
-        panel.isFloatingPanel = false
1029
-        panel.level = .normal
1030
-        panel.hidesOnDeactivate = true
1031
-        panel.isReleasedWhenClosed = false
1032
-        panel.delegate = self
1033
-        panel.standardWindowButton(.closeButton)?.isHidden = true
1034
-        panel.standardWindowButton(.miniaturizeButton)?.isHidden = true
1035
-        panel.standardWindowButton(.zoomButton)?.isHidden = true
1036
-        panel.center()
1037
-        panel.contentViewController = controller
1038
-        panel.alphaValue = 0
1039
-        panel.makeKeyAndOrderFront(nil)
1040
-        NSApp.activate(ignoringOtherApps: true)
1041
-        paywallWindow = panel
1042
-        animatePaywallPresentation(panel)
1060
+        let overlay = NSView()
1061
+        overlay.translatesAutoresizingMaskIntoConstraints = false
1062
+        overlay.wantsLayer = true
1063
+        overlay.layer?.backgroundColor = palette.pageBackground.withAlphaComponent(0.98).cgColor
1064
+        overlay.alphaValue = 0
1065
+        overlay.addSubview(content)
1066
+        view.addSubview(overlay, positioned: .above, relativeTo: nil)
1067
+        NSLayoutConstraint.activate([
1068
+            overlay.leadingAnchor.constraint(equalTo: view.leadingAnchor),
1069
+            overlay.trailingAnchor.constraint(equalTo: view.trailingAnchor),
1070
+            overlay.topAnchor.constraint(equalTo: view.topAnchor),
1071
+            overlay.bottomAnchor.constraint(equalTo: view.bottomAnchor),
1072
+
1073
+            content.leadingAnchor.constraint(equalTo: overlay.leadingAnchor),
1074
+            content.trailingAnchor.constraint(equalTo: overlay.trailingAnchor),
1075
+            content.topAnchor.constraint(equalTo: overlay.topAnchor),
1076
+            content.bottomAnchor.constraint(equalTo: overlay.bottomAnchor)
1077
+        ])
1078
+        paywallOverlayView = overlay
1079
+        NSAnimationContext.runAnimationGroup { context in
1080
+            context.duration = 0.20
1081
+            context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
1082
+            overlay.animator().alphaValue = 1
1083
+        }
1043
 
1084
 
1044
         Task { [weak self] in
1085
         Task { [weak self] in
1045
             guard let self else { return }
1086
             guard let self else { return }
@@ -1071,6 +1112,12 @@ private extension ViewController {
1071
     }
1112
     }
1072
 
1113
 
1073
     @objc private func closePaywallClicked(_ sender: Any?) {
1114
     @objc private func closePaywallClicked(_ sender: Any?) {
1115
+        if let overlay = paywallOverlayView {
1116
+            paywallOverlayView = nil
1117
+            paywallUpgradeFlowEnabled = false
1118
+            overlay.removeFromSuperview()
1119
+            return
1120
+        }
1074
         if let win = paywallWindow {
1121
         if let win = paywallWindow {
1075
             win.performClose(nil)
1122
             win.performClose(nil)
1076
             return
1123
             return
@@ -1087,18 +1134,35 @@ private extension ViewController {
1087
 
1134
 
1088
     @objc private func paywallFooterLinkClicked(_ sender: NSClickGestureRecognizer) {
1135
     @objc private func paywallFooterLinkClicked(_ sender: NSClickGestureRecognizer) {
1089
         guard let view = sender.view else { return }
1136
         guard let view = sender.view else { return }
1137
+        let action = paywallFooterActionByView[ObjectIdentifier(view)]
1090
         let text = (view.subviews.first { $0 is NSTextField } as? NSTextField)?.stringValue ?? "Link"
1138
         let text = (view.subviews.first { $0 is NSTextField } as? NSTextField)?.stringValue ?? "Link"
1091
-        let defaultURL = (Bundle.main.object(forInfoDictionaryKey: "AppLaunchPlaceholderURL") as? String) ?? "https://example.com/app-link-coming-soon"
1092
-        let map: [String: String] = [
1093
-            "Privacy Policy": (Bundle.main.object(forInfoDictionaryKey: "PrivacyPolicyURL") as? String) ?? defaultURL,
1094
-            "Support": (Bundle.main.object(forInfoDictionaryKey: "SupportURL") as? String) ?? defaultURL,
1095
-            "Terms of Services": (Bundle.main.object(forInfoDictionaryKey: "TermsOfServiceURL") as? String) ?? defaultURL
1096
-        ]
1097
-        if let urlString = map[text], let url = URL(string: urlString) {
1098
-            openInAppBrowser(with: url, policy: inAppBrowserDefaultPolicy)
1139
+        guard let action else {
1140
+            showSimpleAlert(title: text, message: "\(text) tapped.")
1099
             return
1141
             return
1100
         }
1142
         }
1101
-        showSimpleAlert(title: text, message: "\(text) tapped.")
1143
+        handlePaywallFooterAction(action)
1144
+    }
1145
+
1146
+    private func handlePaywallFooterAction(_ action: PaywallFooterAction) {
1147
+        switch action {
1148
+        case .manageSubscription:
1149
+            openManageSubscriptions()
1150
+        case .restorePurchase:
1151
+            openRestoreSubscriptionPage()
1152
+        case .continueWithFreePlan:
1153
+            closePaywallClicked(nil)
1154
+        case .privacyPolicy:
1155
+            openSettingsLink(infoKey: "PrivacyPolicyURL")
1156
+        case .support:
1157
+            openSettingsLink(infoKey: "SupportURL")
1158
+        case .termsOfServices:
1159
+            openSettingsLink(infoKey: "TermsOfServiceURL")
1160
+        }
1161
+    }
1162
+
1163
+    @objc private func paywallFooterButtonPressed(_ sender: NSButton) {
1164
+        guard let action = paywallFooterActionByView[ObjectIdentifier(sender)] else { return }
1165
+        handlePaywallFooterAction(action)
1102
     }
1166
     }
1103
 
1167
 
1104
     @objc private func paywallPlanClicked(_ sender: NSClickGestureRecognizer) {
1168
     @objc private func paywallPlanClicked(_ sender: NSClickGestureRecognizer) {
@@ -1141,8 +1205,13 @@ private extension ViewController {
1141
             if product.type == .nonConsumable {
1205
             if product.type == .nonConsumable {
1142
                 return "\(pkrPrice) one-time purchase"
1206
                 return "\(pkrPrice) one-time purchase"
1143
             }
1207
             }
1144
-            if let period = product.subscription?.subscriptionPeriod {
1145
-                return "\(pkrPrice)/\(subscriptionUnitText(period.unit))"
1208
+            if let subscription = product.subscription {
1209
+                let billingText = "\(pkrPrice)/\(subscriptionUnitText(subscription.subscriptionPeriod.unit))"
1210
+                if let introOffer = subscription.introductoryOffer,
1211
+                   introOffer.paymentMode == .freeTrial {
1212
+                    return "Free for \(subscriptionPeriodText(introOffer.period)), then \(billingText)"
1213
+                }
1214
+                return billingText
1146
             }
1215
             }
1147
             return pkrPrice
1216
             return pkrPrice
1148
         }
1217
         }
@@ -1150,7 +1219,7 @@ private extension ViewController {
1150
         case .weekly:
1219
         case .weekly:
1151
             return "PKR 1,100.00/week"
1220
             return "PKR 1,100.00/week"
1152
         case .monthly:
1221
         case .monthly:
1153
-            return "Free for 3 Days then PKR 2,500.00/month"
1222
+            return "PKR 2,500.00/month (3 days free trial)"
1154
         case .yearly:
1223
         case .yearly:
1155
             return "PKR 9,900.00/year (about 190.38/week)"
1224
             return "PKR 9,900.00/year (about 190.38/week)"
1156
         case .lifetime:
1225
         case .lifetime:
@@ -1177,6 +1246,22 @@ private extension ViewController {
1177
         }
1246
         }
1178
     }
1247
     }
1179
 
1248
 
1249
+    private func subscriptionPeriodText(_ period: Product.SubscriptionPeriod) -> String {
1250
+        let unit = subscriptionUnitText(period.unit)
1251
+        if period.value == 1 {
1252
+            return "1 \(unit)"
1253
+        }
1254
+        return "\(period.value) \(unit)s"
1255
+    }
1256
+
1257
+    private func freeTrialPackageText(for product: Product) -> String? {
1258
+        guard let introOffer = product.subscription?.introductoryOffer,
1259
+              introOffer.paymentMode == .freeTrial else {
1260
+            return nil
1261
+        }
1262
+        return "\(subscriptionPeriodText(introOffer.period)) free trial"
1263
+    }
1264
+
1180
     private func startStoreKit() {
1265
     private func startStoreKit() {
1181
         storeKitStartupTask?.cancel()
1266
         storeKitStartupTask?.cancel()
1182
         storeKitStartupTask = Task { [weak self] in
1267
         storeKitStartupTask = Task { [weak self] in
@@ -1198,8 +1283,18 @@ private extension ViewController {
1198
         for (plan, label) in paywallSubtitleLabels {
1283
         for (plan, label) in paywallSubtitleLabels {
1199
             let productID = PremiumStoreProduct.productID(for: plan)
1284
             let productID = PremiumStoreProduct.productID(for: plan)
1200
             guard let product = storeKitCoordinator.productsByID[productID],
1285
             guard let product = storeKitCoordinator.productsByID[productID],
1201
-                  let period = product.subscription?.subscriptionPeriod else { continue }
1202
-            label.stringValue = "\(pkrDisplayPrice(product.displayPrice))/\(subscriptionUnitText(period.unit))"
1286
+                  let period = product.subscription?.subscriptionPeriod else {
1287
+                if plan == .monthly {
1288
+                    label.stringValue = "3 days free trial"
1289
+                }
1290
+                continue
1291
+            }
1292
+            let recurringText = "\(pkrDisplayPrice(product.displayPrice))/\(subscriptionUnitText(period.unit))"
1293
+            if let trialText = freeTrialPackageText(for: product) {
1294
+                label.stringValue = "\(recurringText)  •  \(trialText)"
1295
+            } else {
1296
+                label.stringValue = recurringText
1297
+            }
1203
         }
1298
         }
1204
         refreshSidebarPremiumButton()
1299
         refreshSidebarPremiumButton()
1205
         refreshInstantMeetPremiumState()
1300
         refreshInstantMeetPremiumState()
@@ -2568,8 +2663,10 @@ private extension ViewController {
2568
         contentStack.translatesAutoresizingMaskIntoConstraints = false
2663
         contentStack.translatesAutoresizingMaskIntoConstraints = false
2569
         contentStack.orientation = .vertical
2664
         contentStack.orientation = .vertical
2570
         contentStack.spacing = 12
2665
         contentStack.spacing = 12
2571
-        contentStack.alignment = .leading
2666
+        contentStack.distribution = .fill
2667
+        contentStack.alignment = .centerX
2572
         panel.addSubview(contentStack)
2668
         panel.addSubview(contentStack)
2669
+        let paywallLayoutWidth: CGFloat = 980
2573
 
2670
 
2574
         let topRow = NSStackView()
2671
         let topRow = NSStackView()
2575
         topRow.translatesAutoresizingMaskIntoConstraints = false
2672
         topRow.translatesAutoresizingMaskIntoConstraints = false
@@ -2577,73 +2674,109 @@ private extension ViewController {
2577
         topRow.alignment = .centerY
2674
         topRow.alignment = .centerY
2578
         topRow.distribution = .fill
2675
         topRow.distribution = .fill
2579
         topRow.spacing = 10
2676
         topRow.spacing = 10
2580
-        topRow.addArrangedSubview(textLabel("Get Premium", font: NSFont.systemFont(ofSize: 24, weight: .bold), color: palette.textPrimary))
2581
-        let topSpacer = NSView()
2582
-        topSpacer.translatesAutoresizingMaskIntoConstraints = false
2583
-        topRow.addArrangedSubview(topSpacer)
2584
-        let closeButton = HoverButton(title: "✕", target: self, action: #selector(closePaywallClicked(_:)))
2585
-        closeButton.translatesAutoresizingMaskIntoConstraints = false
2586
-        closeButton.isBordered = false
2587
-        closeButton.bezelStyle = .regularSquare
2588
-        closeButton.wantsLayer = true
2589
-        closeButton.layer?.cornerRadius = 14
2590
-        closeButton.layer?.backgroundColor = palette.inputBackground.cgColor
2591
-        closeButton.layer?.borderColor = palette.inputBorder.cgColor
2592
-        closeButton.layer?.borderWidth = 1
2593
-        closeButton.font = typography.iconButton
2594
-        closeButton.contentTintColor = palette.textSecondary
2595
-        closeButton.widthAnchor.constraint(equalToConstant: 28).isActive = true
2596
-        closeButton.heightAnchor.constraint(equalToConstant: 28).isActive = true
2597
-        closeButton.onHoverChanged = { [weak closeButton, weak self] hovering in
2598
-            guard let closeButton, let self else { return }
2599
-            let base = self.palette.inputBackground
2600
-            let hoverBlend = self.darkModeEnabled ? NSColor.white : NSColor.black
2601
-            let hover = base.blended(withFraction: 0.10, of: hoverBlend) ?? base
2602
-            closeButton.layer?.backgroundColor = (hovering ? hover : base).cgColor
2603
-            closeButton.contentTintColor = hovering ? (self.darkModeEnabled ? .white : self.palette.textPrimary) : self.palette.textSecondary
2677
+        topRow.addArrangedSubview(textLabel("Meetings Pro", font: NSFont.systemFont(ofSize: 27, weight: .bold), color: palette.textPrimary))
2678
+        var closeButton: HoverButton?
2679
+        if storeKitCoordinator.hasPremiumAccess {
2680
+            let button = HoverButton(title: "✕", target: self, action: #selector(closePaywallClicked(_:)))
2681
+            button.translatesAutoresizingMaskIntoConstraints = false
2682
+            button.isBordered = false
2683
+            button.bezelStyle = .regularSquare
2684
+            button.wantsLayer = true
2685
+            button.layer?.cornerRadius = 14
2686
+            button.layer?.backgroundColor = palette.inputBackground.cgColor
2687
+            button.layer?.borderColor = palette.inputBorder.cgColor
2688
+            button.layer?.borderWidth = 1
2689
+            button.font = typography.iconButton
2690
+            button.contentTintColor = palette.textSecondary
2691
+            button.widthAnchor.constraint(equalToConstant: 28).isActive = true
2692
+            button.heightAnchor.constraint(equalToConstant: 28).isActive = true
2693
+            button.onHoverChanged = { [weak button, weak self] hovering in
2694
+                guard let button, let self else { return }
2695
+                let base = self.palette.inputBackground
2696
+                let hoverBlend = self.darkModeEnabled ? NSColor.white : NSColor.black
2697
+                let hover = base.blended(withFraction: 0.10, of: hoverBlend) ?? base
2698
+                button.layer?.backgroundColor = (hovering ? hover : base).cgColor
2699
+                button.contentTintColor = hovering ? (self.darkModeEnabled ? .white : self.palette.textPrimary) : self.palette.textSecondary
2700
+            }
2701
+            panel.addSubview(button)
2702
+            closeButton = button
2604
         }
2703
         }
2605
-        topRow.addArrangedSubview(closeButton)
2606
-        topRow.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
2607
         contentStack.addArrangedSubview(topRow)
2704
         contentStack.addArrangedSubview(topRow)
2705
+        topRow.widthAnchor.constraint(equalTo: contentStack.widthAnchor).isActive = true
2706
+
2707
+        let hero = roundedContainer(cornerRadius: 16, color: palette.sectionCard)
2708
+        hero.translatesAutoresizingMaskIntoConstraints = false
2709
+        styleSurface(hero, borderColor: palette.inputBorder, borderWidth: 1, shadow: false)
2710
+        contentStack.addArrangedSubview(hero)
2711
+        hero.widthAnchor.constraint(equalTo: contentStack.widthAnchor).isActive = true
2712
+
2713
+        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))
2714
+        hero.addSubview(heroBadge)
2715
+        let heroTitle = textLabel("Elevate your meetings workflow", font: NSFont.systemFont(ofSize: 22, weight: .bold), color: palette.textPrimary)
2716
+        let heroSubtitle = textLabel("Unlock automation, premium meeting tools, and priority support for every session.", font: NSFont.systemFont(ofSize: 13, weight: .medium), color: palette.textSecondary)
2717
+        heroTitle.maximumNumberOfLines = 2
2718
+        heroSubtitle.maximumNumberOfLines = 3
2719
+        hero.addSubview(heroTitle)
2720
+        hero.addSubview(heroSubtitle)
2721
+        NSLayoutConstraint.activate([
2722
+            heroBadge.leadingAnchor.constraint(equalTo: hero.leadingAnchor, constant: 16),
2723
+            heroBadge.topAnchor.constraint(equalTo: hero.topAnchor, constant: 14),
2724
+            heroTitle.leadingAnchor.constraint(equalTo: hero.leadingAnchor, constant: 16),
2725
+            heroTitle.trailingAnchor.constraint(lessThanOrEqualTo: hero.trailingAnchor, constant: -16),
2726
+            heroTitle.topAnchor.constraint(equalTo: heroBadge.bottomAnchor, constant: 10),
2727
+            heroSubtitle.leadingAnchor.constraint(equalTo: heroTitle.leadingAnchor),
2728
+            heroSubtitle.trailingAnchor.constraint(equalTo: hero.trailingAnchor, constant: -16),
2729
+            heroSubtitle.topAnchor.constraint(equalTo: heroTitle.bottomAnchor, constant: 8),
2730
+            heroSubtitle.bottomAnchor.constraint(equalTo: hero.bottomAnchor, constant: -16)
2731
+        ])
2608
 
2732
 
2609
-        contentStack.addArrangedSubview(textLabel("Upgrade to unlock premium features.", font: NSFont.systemFont(ofSize: 12, weight: .medium), color: palette.textSecondary))
2610
-        let benefits = paywallBenefitsSection()
2611
-        contentStack.addArrangedSubview(benefits)
2612
-        contentStack.setCustomSpacing(18, after: benefits)
2733
+        let benefitsRow = NSStackView()
2734
+        benefitsRow.translatesAutoresizingMaskIntoConstraints = false
2735
+        benefitsRow.orientation = .horizontal
2736
+        benefitsRow.spacing = 8
2737
+        benefitsRow.distribution = .fillEqually
2738
+        benefitsRow.alignment = .centerY
2739
+        benefitsRow.addArrangedSubview(paywallBenefitItem(icon: "📅", text: "Manage meetings"))
2740
+        benefitsRow.addArrangedSubview(paywallBenefitItem(icon: "🧠", text: "Smart scheduling"))
2741
+        benefitsRow.addArrangedSubview(paywallBenefitItem(icon: "⚡", text: "Faster workflow"))
2742
+        benefitsRow.addArrangedSubview(paywallBenefitItem(icon: "🔔", text: "Reminder notifications"))
2743
+        contentStack.addArrangedSubview(benefitsRow)
2744
+        benefitsRow.widthAnchor.constraint(equalTo: contentStack.widthAnchor).isActive = true
2745
+
2746
+        let plansRow = NSStackView()
2747
+        plansRow.translatesAutoresizingMaskIntoConstraints = false
2748
+        plansRow.orientation = .horizontal
2749
+        plansRow.spacing = 10
2750
+        plansRow.distribution = .fillEqually
2751
+        plansRow.alignment = .centerY
2613
 
2752
 
2614
         let weeklyCard = paywallPlanCard(
2753
         let weeklyCard = paywallPlanCard(
2615
             title: "Weekly",
2754
             title: "Weekly",
2616
             price: "PKR 1,100.00",
2755
             price: "PKR 1,100.00",
2617
-            badge: "Basic Deal",
2756
+            badge: "Basic",
2618
             badgeColor: NSColor(calibratedRed: 1.0, green: 0.60, blue: 0.20, alpha: 1),
2757
             badgeColor: NSColor(calibratedRed: 1.0, green: 0.60, blue: 0.20, alpha: 1),
2619
             subtitle: nil,
2758
             subtitle: nil,
2620
             plan: .weekly,
2759
             plan: .weekly,
2621
             strikePrice: nil
2760
             strikePrice: nil
2622
         )
2761
         )
2623
-        contentStack.addArrangedSubview(weeklyCard)
2624
-
2625
         let monthlyCard = paywallPlanCard(
2762
         let monthlyCard = paywallPlanCard(
2626
             title: "Monthly",
2763
             title: "Monthly",
2627
             price: "PKR 2,500.00",
2764
             price: "PKR 2,500.00",
2628
-            badge: "Free Trial",
2765
+            badge: "Popular",
2629
             badgeColor: NSColor(calibratedRed: 0.19, green: 0.82, blue: 0.39, alpha: 1),
2766
             badgeColor: NSColor(calibratedRed: 0.19, green: 0.82, blue: 0.39, alpha: 1),
2630
-            subtitle: "625.00/week",
2767
+            subtitle: "3 days free trial",
2631
             plan: .monthly,
2768
             plan: .monthly,
2632
             strikePrice: nil
2769
             strikePrice: nil
2633
         )
2770
         )
2634
-        contentStack.addArrangedSubview(monthlyCard)
2635
-
2636
         let yearlyCard = paywallPlanCard(
2771
         let yearlyCard = paywallPlanCard(
2637
             title: "Yearly",
2772
             title: "Yearly",
2638
             price: "PKR 9,900.00",
2773
             price: "PKR 9,900.00",
2639
-            badge: "Best Deal",
2774
+            badge: "Best Value",
2640
             badgeColor: NSColor(calibratedRed: 1.0, green: 0.60, blue: 0.20, alpha: 1),
2775
             badgeColor: NSColor(calibratedRed: 1.0, green: 0.60, blue: 0.20, alpha: 1),
2641
             subtitle: "190.38/week",
2776
             subtitle: "190.38/week",
2642
             plan: .yearly,
2777
             plan: .yearly,
2643
             strikePrice: nil
2778
             strikePrice: nil
2644
         )
2779
         )
2645
-        contentStack.addArrangedSubview(yearlyCard)
2646
-
2647
         let lifetimeCard = paywallPlanCard(
2780
         let lifetimeCard = paywallPlanCard(
2648
             title: "Lifetime",
2781
             title: "Lifetime",
2649
             price: "PKR 14,900.00",
2782
             price: "PKR 14,900.00",
@@ -2653,9 +2786,25 @@ private extension ViewController {
2653
             plan: .lifetime,
2786
             plan: .lifetime,
2654
             strikePrice: "PKR 29,800.00"
2787
             strikePrice: "PKR 29,800.00"
2655
         )
2788
         )
2656
-        contentStack.addArrangedSubview(lifetimeCard)
2789
+        plansRow.addArrangedSubview(weeklyCard)
2790
+        plansRow.addArrangedSubview(monthlyCard)
2791
+        plansRow.addArrangedSubview(yearlyCard)
2792
+        plansRow.addArrangedSubview(lifetimeCard)
2793
+        contentStack.addArrangedSubview(plansRow)
2794
+        plansRow.widthAnchor.constraint(equalTo: contentStack.widthAnchor).isActive = true
2657
         updatePaywallPlanSelection()
2795
         updatePaywallPlanSelection()
2658
-        contentStack.setCustomSpacing(20, after: lifetimeCard)
2796
+
2797
+        let trustRow = NSStackView()
2798
+        trustRow.translatesAutoresizingMaskIntoConstraints = false
2799
+        trustRow.orientation = .horizontal
2800
+        trustRow.spacing = 8
2801
+        trustRow.distribution = .fillEqually
2802
+        trustRow.alignment = .centerY
2803
+        trustRow.addArrangedSubview(paywallMetaItem(title: "Cancel anytime", subtitle: "No lock-in"))
2804
+        trustRow.addArrangedSubview(paywallMetaItem(title: "Instant access", subtitle: "Unlock all tools"))
2805
+        trustRow.addArrangedSubview(paywallMetaItem(title: "Secure billing", subtitle: "Handled by Apple"))
2806
+        contentStack.addArrangedSubview(trustRow)
2807
+        trustRow.widthAnchor.constraint(equalTo: contentStack.widthAnchor).isActive = true
2659
 
2808
 
2660
         let offer = textLabel(paywallOfferText(for: selectedPremiumPlan), font: NSFont.systemFont(ofSize: 13, weight: .semibold), color: palette.textPrimary)
2809
         let offer = textLabel(paywallOfferText(for: selectedPremiumPlan), font: NSFont.systemFont(ofSize: 13, weight: .semibold), color: palette.textPrimary)
2661
         offer.alignment = .center
2810
         offer.alignment = .center
@@ -2663,26 +2812,24 @@ private extension ViewController {
2663
         let offerWrap = NSView()
2812
         let offerWrap = NSView()
2664
         offerWrap.translatesAutoresizingMaskIntoConstraints = false
2813
         offerWrap.translatesAutoresizingMaskIntoConstraints = false
2665
         offerWrap.addSubview(offer)
2814
         offerWrap.addSubview(offer)
2815
+        contentStack.addArrangedSubview(offerWrap)
2816
+        offerWrap.widthAnchor.constraint(equalTo: contentStack.widthAnchor).isActive = true
2666
         NSLayoutConstraint.activate([
2817
         NSLayoutConstraint.activate([
2667
-            offerWrap.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth),
2668
             offer.centerXAnchor.constraint(equalTo: offerWrap.centerXAnchor),
2818
             offer.centerXAnchor.constraint(equalTo: offerWrap.centerXAnchor),
2669
-            offer.topAnchor.constraint(equalTo: offerWrap.topAnchor, constant: 6),
2819
+            offer.topAnchor.constraint(equalTo: offerWrap.topAnchor, constant: 4),
2670
             offer.bottomAnchor.constraint(equalTo: offerWrap.bottomAnchor, constant: -2)
2820
             offer.bottomAnchor.constraint(equalTo: offerWrap.bottomAnchor, constant: -2)
2671
         ])
2821
         ])
2672
-        contentStack.addArrangedSubview(offerWrap)
2673
-        contentStack.setCustomSpacing(18, after: offerWrap)
2674
 
2822
 
2675
         let continueButton = HoverButton(title: "", target: self, action: #selector(paywallContinueClicked(_:)))
2823
         let continueButton = HoverButton(title: "", target: self, action: #selector(paywallContinueClicked(_:)))
2676
         continueButton.translatesAutoresizingMaskIntoConstraints = false
2824
         continueButton.translatesAutoresizingMaskIntoConstraints = false
2677
         continueButton.isBordered = false
2825
         continueButton.isBordered = false
2678
         continueButton.bezelStyle = .regularSquare
2826
         continueButton.bezelStyle = .regularSquare
2679
         continueButton.wantsLayer = true
2827
         continueButton.wantsLayer = true
2680
-        continueButton.layer?.cornerRadius = 14
2828
+        continueButton.layer?.cornerRadius = 12
2681
         continueButton.layer?.backgroundColor = palette.primaryBlue.cgColor
2829
         continueButton.layer?.backgroundColor = palette.primaryBlue.cgColor
2682
-        continueButton.heightAnchor.constraint(equalToConstant: 44).isActive = true
2683
-        continueButton.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
2830
+        continueButton.heightAnchor.constraint(equalToConstant: 36).isActive = true
2684
         styleSurface(continueButton, borderColor: palette.primaryBlueBorder, borderWidth: 1, shadow: true)
2831
         styleSurface(continueButton, borderColor: palette.primaryBlueBorder, borderWidth: 1, shadow: true)
2685
-        let continueLabel = textLabel("Continue", font: NSFont.systemFont(ofSize: 16, weight: .bold), color: .white)
2832
+        let continueLabel = textLabel("Continue", font: NSFont.systemFont(ofSize: 14, weight: .bold), color: .white)
2686
         continueButton.addSubview(continueLabel)
2833
         continueButton.addSubview(continueLabel)
2687
         NSLayoutConstraint.activate([
2834
         NSLayoutConstraint.activate([
2688
             continueLabel.centerXAnchor.constraint(equalTo: continueButton.centerXAnchor),
2835
             continueLabel.centerXAnchor.constraint(equalTo: continueButton.centerXAnchor),
@@ -2698,31 +2845,36 @@ private extension ViewController {
2698
         paywallContinueButton = continueButton
2845
         paywallContinueButton = continueButton
2699
         paywallContinueLabel = continueLabel
2846
         paywallContinueLabel = continueLabel
2700
         contentStack.addArrangedSubview(continueButton)
2847
         contentStack.addArrangedSubview(continueButton)
2701
-        contentStack.setCustomSpacing(16, after: continueButton)
2848
+        continueButton.widthAnchor.constraint(equalToConstant: 360).isActive = true
2702
 
2849
 
2703
         let secure = textLabel("Secured by Apple. Cancel anytime.", font: NSFont.systemFont(ofSize: 12, weight: .semibold), color: palette.textSecondary)
2850
         let secure = textLabel("Secured by Apple. Cancel anytime.", font: NSFont.systemFont(ofSize: 12, weight: .semibold), color: palette.textSecondary)
2704
         secure.alignment = .center
2851
         secure.alignment = .center
2705
         let secureWrap = NSView()
2852
         let secureWrap = NSView()
2706
         secureWrap.translatesAutoresizingMaskIntoConstraints = false
2853
         secureWrap.translatesAutoresizingMaskIntoConstraints = false
2707
         secureWrap.addSubview(secure)
2854
         secureWrap.addSubview(secure)
2855
+        contentStack.addArrangedSubview(secureWrap)
2856
+        secureWrap.widthAnchor.constraint(equalTo: contentStack.widthAnchor).isActive = true
2708
         NSLayoutConstraint.activate([
2857
         NSLayoutConstraint.activate([
2709
-            secureWrap.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth),
2710
             secure.centerXAnchor.constraint(equalTo: secureWrap.centerXAnchor),
2858
             secure.centerXAnchor.constraint(equalTo: secureWrap.centerXAnchor),
2711
-            secure.topAnchor.constraint(equalTo: secureWrap.topAnchor, constant: 4),
2712
-            secure.bottomAnchor.constraint(equalTo: secureWrap.bottomAnchor, constant: -8)
2859
+            secure.topAnchor.constraint(equalTo: secureWrap.topAnchor, constant: 2),
2860
+            secure.bottomAnchor.constraint(equalTo: secureWrap.bottomAnchor, constant: -4)
2713
         ])
2861
         ])
2714
-        contentStack.addArrangedSubview(secureWrap)
2715
-        contentStack.setCustomSpacing(16, after: secureWrap)
2716
 
2862
 
2717
         let footer = paywallFooterLinks()
2863
         let footer = paywallFooterLinks()
2718
         contentStack.addArrangedSubview(footer)
2864
         contentStack.addArrangedSubview(footer)
2865
+        footer.widthAnchor.constraint(equalTo: contentStack.widthAnchor).isActive = true
2719
 
2866
 
2720
-        NSLayoutConstraint.activate([
2721
-            contentStack.leadingAnchor.constraint(equalTo: panel.leadingAnchor, constant: 18),
2722
-            contentStack.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -18),
2723
-            contentStack.topAnchor.constraint(equalTo: panel.topAnchor, constant: 16),
2724
-            contentStack.bottomAnchor.constraint(lessThanOrEqualTo: panel.bottomAnchor, constant: -12)
2725
-        ])
2867
+        var panelConstraints: [NSLayoutConstraint] = [
2868
+            contentStack.centerXAnchor.constraint(equalTo: panel.centerXAnchor),
2869
+            contentStack.widthAnchor.constraint(equalToConstant: paywallLayoutWidth),
2870
+            contentStack.topAnchor.constraint(equalTo: panel.topAnchor, constant: 80),
2871
+            contentStack.bottomAnchor.constraint(equalTo: panel.bottomAnchor, constant: -12)
2872
+        ]
2873
+        if let closeButton {
2874
+            panelConstraints.append(closeButton.topAnchor.constraint(equalTo: panel.topAnchor, constant: 30))
2875
+            panelConstraints.append(closeButton.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -30))
2876
+        }
2877
+        NSLayoutConstraint.activate(panelConstraints)
2726
 
2878
 
2727
         refreshPaywallStoreUI()
2879
         refreshPaywallStoreUI()
2728
         return panel
2880
         return panel
@@ -2743,8 +2895,8 @@ private extension ViewController {
2743
         wrapper.bezelStyle = .regularSquare
2895
         wrapper.bezelStyle = .regularSquare
2744
         wrapper.wantsLayer = true
2896
         wrapper.wantsLayer = true
2745
         wrapper.layer?.backgroundColor = NSColor.clear.cgColor
2897
         wrapper.layer?.backgroundColor = NSColor.clear.cgColor
2746
-        wrapper.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
2747
-        wrapper.heightAnchor.constraint(equalToConstant: 94).isActive = true
2898
+        wrapper.widthAnchor.constraint(greaterThanOrEqualToConstant: 160).isActive = true
2899
+        wrapper.heightAnchor.constraint(equalToConstant: 162).isActive = true
2748
         wrapper.tag = plan.rawValue
2900
         wrapper.tag = plan.rawValue
2749
 
2901
 
2750
         let card = HoverTrackingView()
2902
         let card = HoverTrackingView()
@@ -2752,7 +2904,7 @@ private extension ViewController {
2752
         card.wantsLayer = true
2904
         card.wantsLayer = true
2753
         card.layer?.cornerRadius = 16
2905
         card.layer?.cornerRadius = 16
2754
         card.layer?.backgroundColor = palette.sectionCard.cgColor
2906
         card.layer?.backgroundColor = palette.sectionCard.cgColor
2755
-        card.heightAnchor.constraint(equalToConstant: 82).isActive = true
2907
+        card.heightAnchor.constraint(equalToConstant: 150).isActive = true
2756
         wrapper.addSubview(card)
2908
         wrapper.addSubview(card)
2757
         NSLayoutConstraint.activate([
2909
         NSLayoutConstraint.activate([
2758
             card.leadingAnchor.constraint(equalTo: wrapper.leadingAnchor),
2910
             card.leadingAnchor.constraint(equalTo: wrapper.leadingAnchor),
@@ -2788,14 +2940,15 @@ private extension ViewController {
2788
         paywallPriceLabels[plan] = priceLabel
2940
         paywallPriceLabels[plan] = priceLabel
2789
 
2941
 
2790
         NSLayoutConstraint.activate([
2942
         NSLayoutConstraint.activate([
2791
-            badgeWrap.centerXAnchor.constraint(equalTo: card.centerXAnchor),
2943
+            badgeWrap.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 12),
2792
             badgeWrap.centerYAnchor.constraint(equalTo: card.topAnchor),
2944
             badgeWrap.centerYAnchor.constraint(equalTo: card.topAnchor),
2793
 
2945
 
2794
-            titleLabel.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 16),
2946
+            titleLabel.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 12),
2795
             titleLabel.topAnchor.constraint(equalTo: card.topAnchor, constant: 34),
2947
             titleLabel.topAnchor.constraint(equalTo: card.topAnchor, constant: 34),
2796
 
2948
 
2797
-            priceLabel.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -16),
2798
-            priceLabel.topAnchor.constraint(equalTo: card.topAnchor, constant: 32)
2949
+            priceLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
2950
+            priceLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 10),
2951
+            priceLabel.trailingAnchor.constraint(lessThanOrEqualTo: card.trailingAnchor, constant: -12)
2799
         ])
2952
         ])
2800
 
2953
 
2801
         if let subtitle {
2954
         if let subtitle {
@@ -2803,8 +2956,9 @@ private extension ViewController {
2803
             card.addSubview(sub)
2956
             card.addSubview(sub)
2804
             paywallSubtitleLabels[plan] = sub
2957
             paywallSubtitleLabels[plan] = sub
2805
             NSLayoutConstraint.activate([
2958
             NSLayoutConstraint.activate([
2806
-                sub.trailingAnchor.constraint(equalTo: priceLabel.trailingAnchor),
2807
-                sub.topAnchor.constraint(equalTo: priceLabel.bottomAnchor, constant: 0)
2959
+                sub.leadingAnchor.constraint(equalTo: priceLabel.leadingAnchor),
2960
+                sub.topAnchor.constraint(equalTo: priceLabel.bottomAnchor, constant: 2),
2961
+                sub.trailingAnchor.constraint(lessThanOrEqualTo: card.trailingAnchor, constant: -12)
2808
             ])
2962
             ])
2809
         }
2963
         }
2810
 
2964
 
@@ -2812,8 +2966,9 @@ private extension ViewController {
2812
             let strike = textLabel(strikePrice, font: NSFont.systemFont(ofSize: 12, weight: .medium), color: NSColor.systemRed)
2966
             let strike = textLabel(strikePrice, font: NSFont.systemFont(ofSize: 12, weight: .medium), color: NSColor.systemRed)
2813
             card.addSubview(strike)
2967
             card.addSubview(strike)
2814
             NSLayoutConstraint.activate([
2968
             NSLayoutConstraint.activate([
2815
-                strike.trailingAnchor.constraint(equalTo: priceLabel.trailingAnchor),
2816
-                strike.topAnchor.constraint(equalTo: priceLabel.bottomAnchor, constant: 4)
2969
+                strike.leadingAnchor.constraint(equalTo: priceLabel.leadingAnchor),
2970
+                strike.topAnchor.constraint(equalTo: priceLabel.bottomAnchor, constant: 4),
2971
+                strike.trailingAnchor.constraint(lessThanOrEqualTo: card.trailingAnchor, constant: -12)
2817
             ])
2972
             ])
2818
         }
2973
         }
2819
 
2974
 
@@ -2830,8 +2985,8 @@ private extension ViewController {
2830
     func paywallFooterLinks() -> NSView {
2985
     func paywallFooterLinks() -> NSView {
2831
         let wrap = NSView()
2986
         let wrap = NSView()
2832
         wrap.translatesAutoresizingMaskIntoConstraints = false
2987
         wrap.translatesAutoresizingMaskIntoConstraints = false
2833
-        wrap.heightAnchor.constraint(equalToConstant: 34).isActive = true
2834
-        wrap.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
2988
+        wrap.heightAnchor.constraint(equalToConstant: 40).isActive = true
2989
+        paywallFooterActionByView.removeAll()
2835
 
2990
 
2836
         let row = NSStackView()
2991
         let row = NSStackView()
2837
         row.translatesAutoresizingMaskIntoConstraints = false
2992
         row.translatesAutoresizingMaskIntoConstraints = false
@@ -2841,9 +2996,15 @@ private extension ViewController {
2841
         row.spacing = 0
2996
         row.spacing = 0
2842
         wrap.addSubview(row)
2997
         wrap.addSubview(row)
2843
 
2998
 
2844
-        row.addArrangedSubview(footerLink("Privacy Policy"))
2845
-        row.addArrangedSubview(footerLink("Support"))
2846
-        row.addArrangedSubview(footerLink("Terms of Services"))
2999
+        if storeKitCoordinator.hasPremiumAccess {
3000
+            row.addArrangedSubview(footerLink("Manage Subscription", action: .manageSubscription))
3001
+            row.addArrangedSubview(footerLink("Restore Purchase", action: .restorePurchase))
3002
+        } else {
3003
+            row.addArrangedSubview(footerLink("Continue with free plan", action: .continueWithFreePlan))
3004
+        }
3005
+        row.addArrangedSubview(footerLink("Privacy Policy", action: .privacyPolicy))
3006
+        row.addArrangedSubview(footerLink("Support", action: .support))
3007
+        row.addArrangedSubview(footerLink("Terms of Services", action: .termsOfServices))
2847
 
3008
 
2848
         NSLayoutConstraint.activate([
3009
         NSLayoutConstraint.activate([
2849
             row.leadingAnchor.constraint(equalTo: wrap.leadingAnchor),
3010
             row.leadingAnchor.constraint(equalTo: wrap.leadingAnchor),
@@ -2855,25 +3016,54 @@ private extension ViewController {
2855
         return wrap
3016
         return wrap
2856
     }
3017
     }
2857
 
3018
 
2858
-    func footerLink(_ title: String) -> NSView {
2859
-        let container = HoverTrackingView()
2860
-        container.translatesAutoresizingMaskIntoConstraints = false
2861
-        let label = textLabel(title, font: NSFont.systemFont(ofSize: 12, weight: .semibold), color: palette.textSecondary)
2862
-        label.alignment = .center
2863
-        container.addSubview(label)
3019
+    private func footerLink(_ title: String, action: PaywallFooterAction) -> NSView {
3020
+        let button = HoverButton(title: title, target: self, action: #selector(paywallFooterButtonPressed(_:)))
3021
+        button.translatesAutoresizingMaskIntoConstraints = false
3022
+        button.isBordered = false
3023
+        button.bezelStyle = .regularSquare
3024
+        button.focusRingType = .none
3025
+        button.font = NSFont.systemFont(ofSize: 12, weight: .semibold)
3026
+        button.alignment = .center
3027
+        button.setButtonType(.momentaryChange)
3028
+        button.contentTintColor = palette.textSecondary
3029
+        paywallFooterActionByView[ObjectIdentifier(button)] = action
3030
+        button.onHoverChanged = { [weak self, weak button] hovering in
3031
+            guard let self, let button else { return }
3032
+            button.contentTintColor = hovering ? (self.darkModeEnabled ? .white : self.palette.textPrimary) : self.palette.textSecondary
3033
+        }
3034
+        button.onHoverChanged?(false)
3035
+        return button
3036
+    }
2864
 
3037
 
3038
+    func paywallMetaItem(title: String, subtitle: String) -> NSView {
3039
+        let card = HoverTrackingView()
3040
+        card.translatesAutoresizingMaskIntoConstraints = false
3041
+        card.wantsLayer = true
3042
+        card.layer?.cornerRadius = 10
3043
+        card.layer?.backgroundColor = palette.inputBackground.cgColor
3044
+        card.heightAnchor.constraint(equalToConstant: 44).isActive = true
3045
+        styleSurface(card, borderColor: palette.inputBorder, borderWidth: 1, shadow: false)
3046
+
3047
+        let titleLabel = textLabel(title, font: NSFont.systemFont(ofSize: 11, weight: .semibold), color: palette.textPrimary)
3048
+        let subtitleLabel = textLabel(subtitle, font: NSFont.systemFont(ofSize: 10, weight: .medium), color: palette.textSecondary)
3049
+        card.addSubview(titleLabel)
3050
+        card.addSubview(subtitleLabel)
2865
         NSLayoutConstraint.activate([
3051
         NSLayoutConstraint.activate([
2866
-            label.centerXAnchor.constraint(equalTo: container.centerXAnchor),
2867
-            label.centerYAnchor.constraint(equalTo: container.centerYAnchor)
3052
+            titleLabel.centerXAnchor.constraint(equalTo: card.centerXAnchor),
3053
+            titleLabel.topAnchor.constraint(equalTo: card.topAnchor, constant: 7),
3054
+            subtitleLabel.centerXAnchor.constraint(equalTo: card.centerXAnchor),
3055
+            subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 1),
3056
+            subtitleLabel.bottomAnchor.constraint(lessThanOrEqualTo: card.bottomAnchor, constant: -6)
2868
         ])
3057
         ])
2869
 
3058
 
2870
-        let click = NSClickGestureRecognizer(target: self, action: #selector(paywallFooterLinkClicked(_:)))
2871
-        container.addGestureRecognizer(click)
2872
-        container.onHoverChanged = { hovering in
2873
-            label.textColor = hovering ? (self.darkModeEnabled ? .white : self.palette.textPrimary) : self.palette.textSecondary
3059
+        let base = palette.inputBackground
3060
+        let hoverBlend = darkModeEnabled ? NSColor.white : NSColor.black
3061
+        let hover = base.blended(withFraction: 0.10, of: hoverBlend) ?? base
3062
+        card.onHoverChanged = { [weak card] hovering in
3063
+            card?.layer?.backgroundColor = (hovering ? hover : base).cgColor
2874
         }
3064
         }
2875
-        container.onHoverChanged?(false)
2876
-        return container
3065
+        card.onHoverChanged?(false)
3066
+        return card
2877
     }
3067
     }
2878
 
3068
 
2879
     func paywallBenefitsSection() -> NSView {
3069
     func paywallBenefitsSection() -> NSView {