Explorar o código

Merge branch 'paywall-landscape-redesign'

huzaifahayat12 hai 3 meses
pai
achega
118119fa56
Modificáronse 2 ficheiros con 425 adicións e 192 borrados
  1. 72 46
      meetings_app/StoreKit.storekit
  2. 353 146
      meetings_app/ViewController.swift

+ 72 - 46
meetings_app/StoreKit.storekit

@@ -2,51 +2,6 @@
   "identifier" : "7B5DA685-94A9-4A9B-86EA-F7D90A0D5249",
   "nonRenewingSubscriptions" : [],
   "products" : [
-    {
-      "displayPrice" : "1100.00",
-      "familyShareable" : false,
-      "internalID" : "F16C0A1F-5B83-41AB-A9F2-69157A11A11A",
-      "localizations" : [
-        {
-          "description" : "Unlock premium features with weekly access.",
-          "displayName" : "Premium Weekly",
-          "locale" : "en_US"
-        }
-      ],
-      "productID" : "com.mqldev.meetingsapp.premium.weekly",
-      "referenceName" : "Premium Weekly",
-      "type" : "NonConsumable"
-    },
-    {
-      "displayPrice" : "2500.00",
-      "familyShareable" : false,
-      "internalID" : "B2B57D59-AE2B-4953-BF03-5D4AFECAC6C1",
-      "localizations" : [
-        {
-          "description" : "Unlock premium features with monthly access.",
-          "displayName" : "Premium Monthly",
-          "locale" : "en_US"
-        }
-      ],
-      "productID" : "com.mqldev.meetingsapp.premium.monthly",
-      "referenceName" : "Premium Monthly",
-      "type" : "NonConsumable"
-    },
-    {
-      "displayPrice" : "9900.00",
-      "familyShareable" : false,
-      "internalID" : "C5694F51-47D8-4AFD-9D33-95A888527BB5",
-      "localizations" : [
-        {
-          "description" : "Unlock premium features with yearly access.",
-          "displayName" : "Premium Yearly",
-          "locale" : "en_US"
-        }
-      ],
-      "productID" : "com.mqldev.meetingsapp.premium.yearly",
-      "referenceName" : "Premium Yearly",
-      "type" : "NonConsumable"
-    },
     {
       "displayPrice" : "14900.00",
       "familyShareable" : false,
@@ -74,7 +29,78 @@
     "_storefront" : "PAK",
     "_timeRate" : 0
   },
-  "subscriptionGroups" : [],
+  "subscriptionGroups" : [
+    {
+      "id" : "A2D0B1D6",
+      "localizations" : [],
+      "name" : "Premium",
+      "subscriptions" : [
+        {
+          "adHocOffers" : [],
+          "codeOffers" : [],
+          "displayPrice" : "1100.00",
+          "familyShareable" : false,
+          "groupNumber" : 3,
+          "internalID" : "F16C0A1F-5B83-41AB-A9F2-69157A11A11A",
+          "introductoryOffer" : null,
+          "localizations" : [
+            {
+              "description" : "Unlock premium features with weekly access.",
+              "displayName" : "Premium Weekly",
+              "locale" : "en_US"
+            }
+          ],
+          "productID" : "com.mqldev.meetingsapp.premium.weekly",
+          "recurringSubscriptionPeriod" : "P1W",
+          "referenceName" : "Premium Weekly",
+          "subscriptionGroupID" : "A2D0B1D6",
+          "type" : "RecurringSubscription"
+        },
+        {
+          "adHocOffers" : [],
+          "codeOffers" : [],
+          "displayPrice" : "2500.00",
+          "familyShareable" : false,
+          "groupNumber" : 2,
+          "internalID" : "B2B57D59-AE2B-4953-BF03-5D4AFECAC6C1",
+          "introductoryOffer" : null,
+          "localizations" : [
+            {
+              "description" : "Unlock premium features with monthly access.",
+              "displayName" : "Premium Monthly",
+              "locale" : "en_US"
+            }
+          ],
+          "productID" : "com.mqldev.meetingsapp.premium.monthly",
+          "recurringSubscriptionPeriod" : "P1M",
+          "referenceName" : "Premium Monthly",
+          "subscriptionGroupID" : "A2D0B1D6",
+          "type" : "RecurringSubscription"
+        },
+        {
+          "adHocOffers" : [],
+          "codeOffers" : [],
+          "displayPrice" : "9900.00",
+          "familyShareable" : false,
+          "groupNumber" : 1,
+          "internalID" : "C5694F51-47D8-4AFD-9D33-95A888527BB5",
+          "introductoryOffer" : null,
+          "localizations" : [
+            {
+              "description" : "Unlock premium features with yearly access.",
+              "displayName" : "Premium Yearly",
+              "locale" : "en_US"
+            }
+          ],
+          "productID" : "com.mqldev.meetingsapp.premium.yearly",
+          "recurringSubscriptionPeriod" : "P1Y",
+          "referenceName" : "Premium Yearly",
+          "subscriptionGroupID" : "A2D0B1D6",
+          "type" : "RecurringSubscription"
+        }
+      ]
+    }
+  ],
   "version" : {
     "major" : 3,
     "minor" : 0

+ 353 - 146
meetings_app/ViewController.swift

@@ -226,6 +226,15 @@ private final class StoreKitCoordinator {
 }
 
 final class ViewController: NSViewController {
+    private enum PaywallFooterAction {
+        case manageSubscription
+        case restorePurchase
+        case continueWithFreePlan
+        case privacyPolicy
+        case support
+        case termsOfServices
+    }
+
     private struct GoogleProfileDisplay {
         let name: String
         let email: String
@@ -249,11 +258,13 @@ final class ViewController: NSViewController {
     private var zoomJoinModeViews: [ZoomJoinMode: NSView] = [:]
     private var settingsActionByView = [ObjectIdentifier: SettingsAction]()
     private var paywallWindow: NSWindow?
+    private weak var paywallOverlayView: NSView?
     private let paywallContentWidth: CGFloat = 520
     private let launchWindowLeftOffset: CGFloat = 80
     private var selectedPremiumPlan: PremiumPlan = .monthly
     private var paywallPlanViews: [PremiumPlan: NSView] = [:]
     private var premiumPlanByView = [ObjectIdentifier: PremiumPlan]()
+    private var paywallFooterActionByView = [ObjectIdentifier: PaywallFooterAction]()
     private weak var paywallOfferLabel: NSTextField?
     private weak var paywallContinueLabel: NSTextField?
     private weak var paywallContinueButton: NSView?
@@ -539,8 +550,10 @@ private extension ViewController {
     }
 
     @objc private func premiumButtonClicked(_ sender: NSClickGestureRecognizer) {
-        if storeKitCoordinator.hasPremiumAccess {
+        if storeKitCoordinator.hasLifetimeAccess {
             openManageSubscriptions()
+        } else if storeKitCoordinator.hasPremiumAccess {
+            showPaywall(upgradeFlow: true, preferredPlan: .lifetime)
         } else {
             showPaywall()
         }
@@ -745,6 +758,10 @@ private extension ViewController {
     }
 
     private func openManageSubscriptions() {
+        if let appStoreURL = URL(string: "macappstore://apps.apple.com/account/subscriptions"),
+           NSWorkspace.shared.open(appStoreURL) {
+            return
+        }
         guard let url = URL(string: "https://apps.apple.com/account/subscriptions") else {
             showSimpleAlert(title: "Unable to Open Subscriptions", message: "The subscriptions URL is invalid.")
             return
@@ -752,6 +769,27 @@ private extension ViewController {
         openInDefaultBrowser(url: url)
     }
 
+    private func openRestoreSubscriptionPage() {
+        let fallbackURL = "https://support.apple.com/en-us/108096"
+        let restoreURL = (Bundle.main.object(forInfoDictionaryKey: "RestoreSubscriptionURL") as? String) ?? fallbackURL
+        guard let url = URL(string: restoreURL) else {
+            if let fallback = URL(string: fallbackURL) {
+                NSWorkspace.shared.open(fallback)
+            }
+            return
+        }
+        NSWorkspace.shared.open(url)
+    }
+
+    private func performRestorePurchases() {
+        Task { [weak self] in
+            guard let self else { return }
+            let message = await self.storeKitCoordinator.restorePurchases()
+            self.refreshPaywallStoreUI()
+            self.showSimpleAlert(title: "Restore Purchases", message: message)
+        }
+    }
+
     private func shareAppURL() -> URL? {
         if let configured = Bundle.main.object(forInfoDictionaryKey: "AppShareURL") as? String {
             let trimmed = configured.trimmingCharacters(in: .whitespacesAndNewlines)
@@ -839,6 +877,7 @@ private extension ViewController {
         settingsActionByView.removeAll()
         paywallPlanViews.removeAll()
         premiumPlanByView.removeAll()
+        paywallFooterActionByView.removeAll()
         paywallPriceLabels.removeAll()
         paywallSubtitleLabels.removeAll()
         paywallContinueLabel = nil
@@ -860,12 +899,7 @@ private extension ViewController {
     private func handleSettingsAction(_ action: SettingsAction, sourceView: NSView? = nil, clickLocationInSourceView: NSPoint? = nil) {
         switch action {
         case .restore:
-            Task { [weak self] in
-                guard let self else { return }
-                let message = await self.storeKitCoordinator.restorePurchases()
-                self.refreshPaywallStoreUI()
-                self.showSimpleAlert(title: "Restore Purchases", message: message)
-            }
+            performRestorePurchases()
         case .rateUs:
             openRateUsDestination()
         case .support:
@@ -990,20 +1024,37 @@ private extension ViewController {
         DispatchQueue.main.asyncAfter(deadline: .now() + 2.0, execute: hideWorkItem)
     }
 
-    private func confirmPremiumUpgrade() -> Bool {
+    private func confirmPremiumUpgrade(for targetPlan: PremiumPlan) -> Bool {
         let alert = NSAlert()
-        alert.messageText = "Already Premium"
-        alert.informativeText = "You are already premium. Do you want to continue with this purchase?"
+        if targetPlan == .lifetime, storeKitCoordinator.hasPremiumAccess, !storeKitCoordinator.hasLifetimeAccess {
+            alert.messageText = "Switching to Lifetime"
+            alert.informativeText = "You already have an active subscription. If you buy Lifetime, cancel your current subscription in App Store Subscriptions to avoid future renewal charges."
+        } else {
+            alert.messageText = "Already Premium"
+            alert.informativeText = "You are already premium. Do you want to continue with this purchase?"
+        }
         alert.addButton(withTitle: "Continue")
         alert.addButton(withTitle: "Cancel")
         return alert.runModal() == .alertFirstButtonReturn
     }
 
     private func showPaywall(upgradeFlow: Bool = false, preferredPlan: PremiumPlan? = nil) {
+        if !Thread.isMainThread {
+            DispatchQueue.main.async { [weak self] in
+                self?.showPaywall(upgradeFlow: upgradeFlow, preferredPlan: preferredPlan)
+            }
+            return
+        }
         paywallUpgradeFlowEnabled = upgradeFlow
         if let preferredPlan {
             selectedPremiumPlan = preferredPlan
         }
+        if let existingOverlay = paywallOverlayView {
+            refreshPaywallStoreUI()
+            existingOverlay.alphaValue = 1
+            view.addSubview(existingOverlay, positioned: .above, relativeTo: nil)
+            return
+        }
         if let existing = paywallWindow {
             refreshPaywallStoreUI()
             animatePaywallPresentation(existing)
@@ -1013,33 +1064,30 @@ private extension ViewController {
         }
 
         let content = makePaywallContent()
-        let controller = NSViewController()
-        controller.view = content
-
-        let panel = NSPanel(
-            contentRect: NSRect(x: 0, y: 0, width: 640, height: 820),
-            styleMask: [.titled, .closable, .fullSizeContentView],
-            backing: .buffered,
-            defer: false
-        )
-        panel.title = "Get Premium"
-        panel.titleVisibility = .hidden
-        panel.titlebarAppearsTransparent = true
-        panel.isFloatingPanel = false
-        panel.level = .normal
-        panel.hidesOnDeactivate = true
-        panel.isReleasedWhenClosed = false
-        panel.delegate = self
-        panel.standardWindowButton(.closeButton)?.isHidden = true
-        panel.standardWindowButton(.miniaturizeButton)?.isHidden = true
-        panel.standardWindowButton(.zoomButton)?.isHidden = true
-        panel.center()
-        panel.contentViewController = controller
-        panel.alphaValue = 0
-        panel.makeKeyAndOrderFront(nil)
-        NSApp.activate(ignoringOtherApps: true)
-        paywallWindow = panel
-        animatePaywallPresentation(panel)
+        let overlay = NSView()
+        overlay.translatesAutoresizingMaskIntoConstraints = false
+        overlay.wantsLayer = true
+        overlay.layer?.backgroundColor = palette.pageBackground.withAlphaComponent(0.98).cgColor
+        overlay.alphaValue = 0
+        overlay.addSubview(content)
+        view.addSubview(overlay, positioned: .above, relativeTo: nil)
+        NSLayoutConstraint.activate([
+            overlay.leadingAnchor.constraint(equalTo: view.leadingAnchor),
+            overlay.trailingAnchor.constraint(equalTo: view.trailingAnchor),
+            overlay.topAnchor.constraint(equalTo: view.topAnchor),
+            overlay.bottomAnchor.constraint(equalTo: view.bottomAnchor),
+
+            content.leadingAnchor.constraint(equalTo: overlay.leadingAnchor),
+            content.trailingAnchor.constraint(equalTo: overlay.trailingAnchor),
+            content.topAnchor.constraint(equalTo: overlay.topAnchor),
+            content.bottomAnchor.constraint(equalTo: overlay.bottomAnchor)
+        ])
+        paywallOverlayView = overlay
+        NSAnimationContext.runAnimationGroup { context in
+            context.duration = 0.20
+            context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
+            overlay.animator().alphaValue = 1
+        }
 
         Task { [weak self] in
             guard let self else { return }
@@ -1071,6 +1119,12 @@ private extension ViewController {
     }
 
     @objc private func closePaywallClicked(_ sender: Any?) {
+        if let overlay = paywallOverlayView {
+            paywallOverlayView = nil
+            paywallUpgradeFlowEnabled = false
+            overlay.removeFromSuperview()
+            return
+        }
         if let win = paywallWindow {
             win.performClose(nil)
             return
@@ -1085,20 +1139,47 @@ private extension ViewController {
         }
     }
 
+    private func dismissPaywallIfPresented() {
+        if !Thread.isMainThread {
+            DispatchQueue.main.async { [weak self] in
+                self?.dismissPaywallIfPresented()
+            }
+            return
+        }
+        closePaywallClicked(nil)
+    }
+
     @objc private func paywallFooterLinkClicked(_ sender: NSClickGestureRecognizer) {
         guard let view = sender.view else { return }
+        let action = paywallFooterActionByView[ObjectIdentifier(view)]
         let text = (view.subviews.first { $0 is NSTextField } as? NSTextField)?.stringValue ?? "Link"
-        let defaultURL = (Bundle.main.object(forInfoDictionaryKey: "AppLaunchPlaceholderURL") as? String) ?? "https://example.com/app-link-coming-soon"
-        let map: [String: String] = [
-            "Privacy Policy": (Bundle.main.object(forInfoDictionaryKey: "PrivacyPolicyURL") as? String) ?? defaultURL,
-            "Support": (Bundle.main.object(forInfoDictionaryKey: "SupportURL") as? String) ?? defaultURL,
-            "Terms of Services": (Bundle.main.object(forInfoDictionaryKey: "TermsOfServiceURL") as? String) ?? defaultURL
-        ]
-        if let urlString = map[text], let url = URL(string: urlString) {
-            openInAppBrowser(with: url, policy: inAppBrowserDefaultPolicy)
+        guard let action else {
+            showSimpleAlert(title: text, message: "\(text) tapped.")
             return
         }
-        showSimpleAlert(title: text, message: "\(text) tapped.")
+        handlePaywallFooterAction(action)
+    }
+
+    private func handlePaywallFooterAction(_ action: PaywallFooterAction) {
+        switch action {
+        case .manageSubscription:
+            openManageSubscriptions()
+        case .restorePurchase:
+            openRestoreSubscriptionPage()
+        case .continueWithFreePlan:
+            closePaywallClicked(nil)
+        case .privacyPolicy:
+            openSettingsLink(infoKey: "PrivacyPolicyURL")
+        case .support:
+            openSettingsLink(infoKey: "SupportURL")
+        case .termsOfServices:
+            openSettingsLink(infoKey: "TermsOfServiceURL")
+        }
+    }
+
+    @objc private func paywallFooterButtonPressed(_ sender: NSButton) {
+        guard let action = paywallFooterActionByView[ObjectIdentifier(sender)] else { return }
+        handlePaywallFooterAction(action)
     }
 
     @objc private func paywallPlanClicked(_ sender: NSClickGestureRecognizer) {
@@ -1141,8 +1222,13 @@ private extension ViewController {
             if product.type == .nonConsumable {
                 return "\(pkrPrice) one-time purchase"
             }
-            if let period = product.subscription?.subscriptionPeriod {
-                return "\(pkrPrice)/\(subscriptionUnitText(period.unit))"
+            if let subscription = product.subscription {
+                let billingText = "\(pkrPrice)/\(subscriptionUnitText(subscription.subscriptionPeriod.unit))"
+                if let introOffer = subscription.introductoryOffer,
+                   introOffer.paymentMode == .freeTrial {
+                    return "Free for \(subscriptionPeriodText(introOffer.period)), then \(billingText)"
+                }
+                return billingText
             }
             return pkrPrice
         }
@@ -1150,7 +1236,7 @@ private extension ViewController {
         case .weekly:
             return "PKR 1,100.00/week"
         case .monthly:
-            return "Free for 3 Days then PKR 2,500.00/month"
+            return "PKR 2,500.00/month (3 days free trial)"
         case .yearly:
             return "PKR 9,900.00/year (about 190.38/week)"
         case .lifetime:
@@ -1177,6 +1263,22 @@ private extension ViewController {
         }
     }
 
+    private func subscriptionPeriodText(_ period: Product.SubscriptionPeriod) -> String {
+        let unit = subscriptionUnitText(period.unit)
+        if period.value == 1 {
+            return "1 \(unit)"
+        }
+        return "\(period.value) \(unit)s"
+    }
+
+    private func freeTrialPackageText(for product: Product) -> String? {
+        guard let introOffer = product.subscription?.introductoryOffer,
+              introOffer.paymentMode == .freeTrial else {
+            return nil
+        }
+        return "\(subscriptionPeriodText(introOffer.period)) free trial"
+    }
+
     private func startStoreKit() {
         storeKitStartupTask?.cancel()
         storeKitStartupTask = Task { [weak self] in
@@ -1198,8 +1300,17 @@ private extension ViewController {
         for (plan, label) in paywallSubtitleLabels {
             let productID = PremiumStoreProduct.productID(for: plan)
             guard let product = storeKitCoordinator.productsByID[productID],
-                  let period = product.subscription?.subscriptionPeriod else { continue }
-            label.stringValue = "\(pkrDisplayPrice(product.displayPrice))/\(subscriptionUnitText(period.unit))"
+                  let period = product.subscription?.subscriptionPeriod else {
+                // Show neutral fallback text when subscription metadata isn't available.
+                label.stringValue = "Billed via App Store"
+                continue
+            }
+            let recurringText = "\(pkrDisplayPrice(product.displayPrice))/\(subscriptionUnitText(period.unit))"
+            if let trialText = freeTrialPackageText(for: product) {
+                label.stringValue = "\(recurringText)  •  \(trialText)"
+            } else {
+                label.stringValue = recurringText
+            }
         }
         refreshSidebarPremiumButton()
         refreshInstantMeetPremiumState()
@@ -1236,6 +1347,7 @@ private extension ViewController {
         }
 
         if !hadPremiumAccess && hasPremiumAccess {
+            dismissPaywallIfPresented()
             if selectedSidebarPage != .joinMeetings {
                 Task { [weak self] in
                     await self?.loadSchedule()
@@ -1419,7 +1531,7 @@ private extension ViewController {
             }
             return
         }
-        if paywallUpgradeFlowEnabled && storeKitCoordinator.hasPremiumAccess && !confirmPremiumUpgrade() {
+        if paywallUpgradeFlowEnabled && storeKitCoordinator.hasPremiumAccess && !confirmPremiumUpgrade(for: selectedPremiumPlan) {
             return
         }
         paywallPurchaseTask?.cancel()
@@ -1437,7 +1549,7 @@ private extension ViewController {
                     await self?.loadSchedule()
                 }
                 self.showSimpleAlert(title: "Purchase Complete", message: "Premium has been unlocked successfully.")
-                self.paywallWindow?.performClose(nil)
+                self.dismissPaywallIfPresented()
                 self.scheduleRatingPromptAfterPremiumUpgrade()
             case .cancelled:
                 break
@@ -2568,8 +2680,10 @@ private extension ViewController {
         contentStack.translatesAutoresizingMaskIntoConstraints = false
         contentStack.orientation = .vertical
         contentStack.spacing = 12
-        contentStack.alignment = .leading
+        contentStack.distribution = .fill
+        contentStack.alignment = .centerX
         panel.addSubview(contentStack)
+        let paywallLayoutWidth: CGFloat = 980
 
         let topRow = NSStackView()
         topRow.translatesAutoresizingMaskIntoConstraints = false
@@ -2577,73 +2691,109 @@ private extension ViewController {
         topRow.alignment = .centerY
         topRow.distribution = .fill
         topRow.spacing = 10
-        topRow.addArrangedSubview(textLabel("Get Premium", font: NSFont.systemFont(ofSize: 24, weight: .bold), color: palette.textPrimary))
-        let topSpacer = NSView()
-        topSpacer.translatesAutoresizingMaskIntoConstraints = false
-        topRow.addArrangedSubview(topSpacer)
-        let closeButton = HoverButton(title: "✕", target: self, action: #selector(closePaywallClicked(_:)))
-        closeButton.translatesAutoresizingMaskIntoConstraints = false
-        closeButton.isBordered = false
-        closeButton.bezelStyle = .regularSquare
-        closeButton.wantsLayer = true
-        closeButton.layer?.cornerRadius = 14
-        closeButton.layer?.backgroundColor = palette.inputBackground.cgColor
-        closeButton.layer?.borderColor = palette.inputBorder.cgColor
-        closeButton.layer?.borderWidth = 1
-        closeButton.font = typography.iconButton
-        closeButton.contentTintColor = palette.textSecondary
-        closeButton.widthAnchor.constraint(equalToConstant: 28).isActive = true
-        closeButton.heightAnchor.constraint(equalToConstant: 28).isActive = true
-        closeButton.onHoverChanged = { [weak closeButton, weak self] hovering in
-            guard let closeButton, let self else { return }
-            let base = self.palette.inputBackground
-            let hoverBlend = self.darkModeEnabled ? NSColor.white : NSColor.black
-            let hover = base.blended(withFraction: 0.10, of: hoverBlend) ?? base
-            closeButton.layer?.backgroundColor = (hovering ? hover : base).cgColor
-            closeButton.contentTintColor = hovering ? (self.darkModeEnabled ? .white : self.palette.textPrimary) : self.palette.textSecondary
+        topRow.addArrangedSubview(textLabel("Meetings Pro", font: NSFont.systemFont(ofSize: 27, weight: .bold), color: palette.textPrimary))
+        var closeButton: HoverButton?
+        if storeKitCoordinator.hasPremiumAccess {
+            let button = HoverButton(title: "✕", target: self, action: #selector(closePaywallClicked(_:)))
+            button.translatesAutoresizingMaskIntoConstraints = false
+            button.isBordered = false
+            button.bezelStyle = .regularSquare
+            button.wantsLayer = true
+            button.layer?.cornerRadius = 14
+            button.layer?.backgroundColor = palette.inputBackground.cgColor
+            button.layer?.borderColor = palette.inputBorder.cgColor
+            button.layer?.borderWidth = 1
+            button.font = typography.iconButton
+            button.contentTintColor = palette.textSecondary
+            button.widthAnchor.constraint(equalToConstant: 28).isActive = true
+            button.heightAnchor.constraint(equalToConstant: 28).isActive = true
+            button.onHoverChanged = { [weak button, weak self] hovering in
+                guard let button, let self else { return }
+                let base = self.palette.inputBackground
+                let hoverBlend = self.darkModeEnabled ? NSColor.white : NSColor.black
+                let hover = base.blended(withFraction: 0.10, of: hoverBlend) ?? base
+                button.layer?.backgroundColor = (hovering ? hover : base).cgColor
+                button.contentTintColor = hovering ? (self.darkModeEnabled ? .white : self.palette.textPrimary) : self.palette.textSecondary
+            }
+            panel.addSubview(button)
+            closeButton = button
         }
-        topRow.addArrangedSubview(closeButton)
-        topRow.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
         contentStack.addArrangedSubview(topRow)
+        topRow.widthAnchor.constraint(equalTo: contentStack.widthAnchor).isActive = true
+
+        let hero = roundedContainer(cornerRadius: 16, color: palette.sectionCard)
+        hero.translatesAutoresizingMaskIntoConstraints = false
+        styleSurface(hero, borderColor: palette.inputBorder, borderWidth: 1, shadow: false)
+        contentStack.addArrangedSubview(hero)
+        hero.widthAnchor.constraint(equalTo: contentStack.widthAnchor).isActive = true
+
+        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))
+        hero.addSubview(heroBadge)
+        let heroTitle = textLabel("Elevate your meetings workflow", font: NSFont.systemFont(ofSize: 22, weight: .bold), color: palette.textPrimary)
+        let heroSubtitle = textLabel("Unlock automation, premium meeting tools, and priority support for every session.", font: NSFont.systemFont(ofSize: 13, weight: .medium), color: palette.textSecondary)
+        heroTitle.maximumNumberOfLines = 2
+        heroSubtitle.maximumNumberOfLines = 3
+        hero.addSubview(heroTitle)
+        hero.addSubview(heroSubtitle)
+        NSLayoutConstraint.activate([
+            heroBadge.leadingAnchor.constraint(equalTo: hero.leadingAnchor, constant: 16),
+            heroBadge.topAnchor.constraint(equalTo: hero.topAnchor, constant: 14),
+            heroTitle.leadingAnchor.constraint(equalTo: hero.leadingAnchor, constant: 16),
+            heroTitle.trailingAnchor.constraint(lessThanOrEqualTo: hero.trailingAnchor, constant: -16),
+            heroTitle.topAnchor.constraint(equalTo: heroBadge.bottomAnchor, constant: 10),
+            heroSubtitle.leadingAnchor.constraint(equalTo: heroTitle.leadingAnchor),
+            heroSubtitle.trailingAnchor.constraint(equalTo: hero.trailingAnchor, constant: -16),
+            heroSubtitle.topAnchor.constraint(equalTo: heroTitle.bottomAnchor, constant: 8),
+            heroSubtitle.bottomAnchor.constraint(equalTo: hero.bottomAnchor, constant: -16)
+        ])
 
-        contentStack.addArrangedSubview(textLabel("Upgrade to unlock premium features.", font: NSFont.systemFont(ofSize: 12, weight: .medium), color: palette.textSecondary))
-        let benefits = paywallBenefitsSection()
-        contentStack.addArrangedSubview(benefits)
-        contentStack.setCustomSpacing(18, after: benefits)
+        let benefitsRow = NSStackView()
+        benefitsRow.translatesAutoresizingMaskIntoConstraints = false
+        benefitsRow.orientation = .horizontal
+        benefitsRow.spacing = 8
+        benefitsRow.distribution = .fillEqually
+        benefitsRow.alignment = .centerY
+        benefitsRow.addArrangedSubview(paywallBenefitItem(icon: "📅", text: "Manage meetings"))
+        benefitsRow.addArrangedSubview(paywallBenefitItem(icon: "🧠", text: "Smart scheduling"))
+        benefitsRow.addArrangedSubview(paywallBenefitItem(icon: "⚡", text: "Faster workflow"))
+        benefitsRow.addArrangedSubview(paywallBenefitItem(icon: "🔔", text: "Reminder notifications"))
+        contentStack.addArrangedSubview(benefitsRow)
+        benefitsRow.widthAnchor.constraint(equalTo: contentStack.widthAnchor).isActive = true
+
+        let plansRow = NSStackView()
+        plansRow.translatesAutoresizingMaskIntoConstraints = false
+        plansRow.orientation = .horizontal
+        plansRow.spacing = 10
+        plansRow.distribution = .fillEqually
+        plansRow.alignment = .centerY
 
         let weeklyCard = paywallPlanCard(
             title: "Weekly",
             price: "PKR 1,100.00",
-            badge: "Basic Deal",
+            badge: "Basic",
             badgeColor: NSColor(calibratedRed: 1.0, green: 0.60, blue: 0.20, alpha: 1),
             subtitle: nil,
             plan: .weekly,
             strikePrice: nil
         )
-        contentStack.addArrangedSubview(weeklyCard)
-
         let monthlyCard = paywallPlanCard(
             title: "Monthly",
             price: "PKR 2,500.00",
-            badge: "Free Trial",
+            badge: "Popular",
             badgeColor: NSColor(calibratedRed: 0.19, green: 0.82, blue: 0.39, alpha: 1),
-            subtitle: "625.00/week",
+            subtitle: "3 days free trial",
             plan: .monthly,
             strikePrice: nil
         )
-        contentStack.addArrangedSubview(monthlyCard)
-
         let yearlyCard = paywallPlanCard(
             title: "Yearly",
             price: "PKR 9,900.00",
-            badge: "Best Deal",
+            badge: "Best Value",
             badgeColor: NSColor(calibratedRed: 1.0, green: 0.60, blue: 0.20, alpha: 1),
             subtitle: "190.38/week",
             plan: .yearly,
             strikePrice: nil
         )
-        contentStack.addArrangedSubview(yearlyCard)
-
         let lifetimeCard = paywallPlanCard(
             title: "Lifetime",
             price: "PKR 14,900.00",
@@ -2653,9 +2803,25 @@ private extension ViewController {
             plan: .lifetime,
             strikePrice: "PKR 29,800.00"
         )
-        contentStack.addArrangedSubview(lifetimeCard)
+        plansRow.addArrangedSubview(weeklyCard)
+        plansRow.addArrangedSubview(monthlyCard)
+        plansRow.addArrangedSubview(yearlyCard)
+        plansRow.addArrangedSubview(lifetimeCard)
+        contentStack.addArrangedSubview(plansRow)
+        plansRow.widthAnchor.constraint(equalTo: contentStack.widthAnchor).isActive = true
         updatePaywallPlanSelection()
-        contentStack.setCustomSpacing(20, after: lifetimeCard)
+
+        let trustRow = NSStackView()
+        trustRow.translatesAutoresizingMaskIntoConstraints = false
+        trustRow.orientation = .horizontal
+        trustRow.spacing = 8
+        trustRow.distribution = .fillEqually
+        trustRow.alignment = .centerY
+        trustRow.addArrangedSubview(paywallMetaItem(title: "Cancel anytime", subtitle: "No lock-in"))
+        trustRow.addArrangedSubview(paywallMetaItem(title: "Instant access", subtitle: "Unlock all tools"))
+        trustRow.addArrangedSubview(paywallMetaItem(title: "Secure billing", subtitle: "Handled by Apple"))
+        contentStack.addArrangedSubview(trustRow)
+        trustRow.widthAnchor.constraint(equalTo: contentStack.widthAnchor).isActive = true
 
         let offer = textLabel(paywallOfferText(for: selectedPremiumPlan), font: NSFont.systemFont(ofSize: 13, weight: .semibold), color: palette.textPrimary)
         offer.alignment = .center
@@ -2663,26 +2829,24 @@ private extension ViewController {
         let offerWrap = NSView()
         offerWrap.translatesAutoresizingMaskIntoConstraints = false
         offerWrap.addSubview(offer)
+        contentStack.addArrangedSubview(offerWrap)
+        offerWrap.widthAnchor.constraint(equalTo: contentStack.widthAnchor).isActive = true
         NSLayoutConstraint.activate([
-            offerWrap.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth),
             offer.centerXAnchor.constraint(equalTo: offerWrap.centerXAnchor),
-            offer.topAnchor.constraint(equalTo: offerWrap.topAnchor, constant: 6),
+            offer.topAnchor.constraint(equalTo: offerWrap.topAnchor, constant: 4),
             offer.bottomAnchor.constraint(equalTo: offerWrap.bottomAnchor, constant: -2)
         ])
-        contentStack.addArrangedSubview(offerWrap)
-        contentStack.setCustomSpacing(18, after: offerWrap)
 
         let continueButton = HoverButton(title: "", target: self, action: #selector(paywallContinueClicked(_:)))
         continueButton.translatesAutoresizingMaskIntoConstraints = false
         continueButton.isBordered = false
         continueButton.bezelStyle = .regularSquare
         continueButton.wantsLayer = true
-        continueButton.layer?.cornerRadius = 14
+        continueButton.layer?.cornerRadius = 12
         continueButton.layer?.backgroundColor = palette.primaryBlue.cgColor
-        continueButton.heightAnchor.constraint(equalToConstant: 44).isActive = true
-        continueButton.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
+        continueButton.heightAnchor.constraint(equalToConstant: 36).isActive = true
         styleSurface(continueButton, borderColor: palette.primaryBlueBorder, borderWidth: 1, shadow: true)
-        let continueLabel = textLabel("Continue", font: NSFont.systemFont(ofSize: 16, weight: .bold), color: .white)
+        let continueLabel = textLabel("Continue", font: NSFont.systemFont(ofSize: 14, weight: .bold), color: .white)
         continueButton.addSubview(continueLabel)
         NSLayoutConstraint.activate([
             continueLabel.centerXAnchor.constraint(equalTo: continueButton.centerXAnchor),
@@ -2698,31 +2862,36 @@ private extension ViewController {
         paywallContinueButton = continueButton
         paywallContinueLabel = continueLabel
         contentStack.addArrangedSubview(continueButton)
-        contentStack.setCustomSpacing(16, after: continueButton)
+        continueButton.widthAnchor.constraint(equalToConstant: 360).isActive = true
 
         let secure = textLabel("Secured by Apple. Cancel anytime.", font: NSFont.systemFont(ofSize: 12, weight: .semibold), color: palette.textSecondary)
         secure.alignment = .center
         let secureWrap = NSView()
         secureWrap.translatesAutoresizingMaskIntoConstraints = false
         secureWrap.addSubview(secure)
+        contentStack.addArrangedSubview(secureWrap)
+        secureWrap.widthAnchor.constraint(equalTo: contentStack.widthAnchor).isActive = true
         NSLayoutConstraint.activate([
-            secureWrap.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth),
             secure.centerXAnchor.constraint(equalTo: secureWrap.centerXAnchor),
-            secure.topAnchor.constraint(equalTo: secureWrap.topAnchor, constant: 4),
-            secure.bottomAnchor.constraint(equalTo: secureWrap.bottomAnchor, constant: -8)
+            secure.topAnchor.constraint(equalTo: secureWrap.topAnchor, constant: 2),
+            secure.bottomAnchor.constraint(equalTo: secureWrap.bottomAnchor, constant: -4)
         ])
-        contentStack.addArrangedSubview(secureWrap)
-        contentStack.setCustomSpacing(16, after: secureWrap)
 
         let footer = paywallFooterLinks()
         contentStack.addArrangedSubview(footer)
+        footer.widthAnchor.constraint(equalTo: contentStack.widthAnchor).isActive = true
 
-        NSLayoutConstraint.activate([
-            contentStack.leadingAnchor.constraint(equalTo: panel.leadingAnchor, constant: 18),
-            contentStack.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -18),
-            contentStack.topAnchor.constraint(equalTo: panel.topAnchor, constant: 16),
-            contentStack.bottomAnchor.constraint(lessThanOrEqualTo: panel.bottomAnchor, constant: -12)
-        ])
+        var panelConstraints: [NSLayoutConstraint] = [
+            contentStack.centerXAnchor.constraint(equalTo: panel.centerXAnchor),
+            contentStack.widthAnchor.constraint(equalToConstant: paywallLayoutWidth),
+            contentStack.topAnchor.constraint(equalTo: panel.topAnchor, constant: 80),
+            contentStack.bottomAnchor.constraint(equalTo: panel.bottomAnchor, constant: -12)
+        ]
+        if let closeButton {
+            panelConstraints.append(closeButton.topAnchor.constraint(equalTo: panel.topAnchor, constant: 30))
+            panelConstraints.append(closeButton.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -30))
+        }
+        NSLayoutConstraint.activate(panelConstraints)
 
         refreshPaywallStoreUI()
         return panel
@@ -2743,8 +2912,8 @@ private extension ViewController {
         wrapper.bezelStyle = .regularSquare
         wrapper.wantsLayer = true
         wrapper.layer?.backgroundColor = NSColor.clear.cgColor
-        wrapper.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
-        wrapper.heightAnchor.constraint(equalToConstant: 94).isActive = true
+        wrapper.widthAnchor.constraint(greaterThanOrEqualToConstant: 160).isActive = true
+        wrapper.heightAnchor.constraint(equalToConstant: 162).isActive = true
         wrapper.tag = plan.rawValue
 
         let card = HoverTrackingView()
@@ -2752,7 +2921,7 @@ private extension ViewController {
         card.wantsLayer = true
         card.layer?.cornerRadius = 16
         card.layer?.backgroundColor = palette.sectionCard.cgColor
-        card.heightAnchor.constraint(equalToConstant: 82).isActive = true
+        card.heightAnchor.constraint(equalToConstant: 150).isActive = true
         wrapper.addSubview(card)
         NSLayoutConstraint.activate([
             card.leadingAnchor.constraint(equalTo: wrapper.leadingAnchor),
@@ -2788,14 +2957,15 @@ private extension ViewController {
         paywallPriceLabels[plan] = priceLabel
 
         NSLayoutConstraint.activate([
-            badgeWrap.centerXAnchor.constraint(equalTo: card.centerXAnchor),
+            badgeWrap.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 12),
             badgeWrap.centerYAnchor.constraint(equalTo: card.topAnchor),
 
-            titleLabel.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 16),
+            titleLabel.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 12),
             titleLabel.topAnchor.constraint(equalTo: card.topAnchor, constant: 34),
 
-            priceLabel.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -16),
-            priceLabel.topAnchor.constraint(equalTo: card.topAnchor, constant: 32)
+            priceLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
+            priceLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 10),
+            priceLabel.trailingAnchor.constraint(lessThanOrEqualTo: card.trailingAnchor, constant: -12)
         ])
 
         if let subtitle {
@@ -2803,8 +2973,9 @@ private extension ViewController {
             card.addSubview(sub)
             paywallSubtitleLabels[plan] = sub
             NSLayoutConstraint.activate([
-                sub.trailingAnchor.constraint(equalTo: priceLabel.trailingAnchor),
-                sub.topAnchor.constraint(equalTo: priceLabel.bottomAnchor, constant: 0)
+                sub.leadingAnchor.constraint(equalTo: priceLabel.leadingAnchor),
+                sub.topAnchor.constraint(equalTo: priceLabel.bottomAnchor, constant: 2),
+                sub.trailingAnchor.constraint(lessThanOrEqualTo: card.trailingAnchor, constant: -12)
             ])
         }
 
@@ -2812,8 +2983,9 @@ private extension ViewController {
             let strike = textLabel(strikePrice, font: NSFont.systemFont(ofSize: 12, weight: .medium), color: NSColor.systemRed)
             card.addSubview(strike)
             NSLayoutConstraint.activate([
-                strike.trailingAnchor.constraint(equalTo: priceLabel.trailingAnchor),
-                strike.topAnchor.constraint(equalTo: priceLabel.bottomAnchor, constant: 4)
+                strike.leadingAnchor.constraint(equalTo: priceLabel.leadingAnchor),
+                strike.topAnchor.constraint(equalTo: priceLabel.bottomAnchor, constant: 4),
+                strike.trailingAnchor.constraint(lessThanOrEqualTo: card.trailingAnchor, constant: -12)
             ])
         }
 
@@ -2830,8 +3002,8 @@ private extension ViewController {
     func paywallFooterLinks() -> NSView {
         let wrap = NSView()
         wrap.translatesAutoresizingMaskIntoConstraints = false
-        wrap.heightAnchor.constraint(equalToConstant: 34).isActive = true
-        wrap.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
+        wrap.heightAnchor.constraint(equalToConstant: 40).isActive = true
+        paywallFooterActionByView.removeAll()
 
         let row = NSStackView()
         row.translatesAutoresizingMaskIntoConstraints = false
@@ -2841,9 +3013,15 @@ private extension ViewController {
         row.spacing = 0
         wrap.addSubview(row)
 
-        row.addArrangedSubview(footerLink("Privacy Policy"))
-        row.addArrangedSubview(footerLink("Support"))
-        row.addArrangedSubview(footerLink("Terms of Services"))
+        if storeKitCoordinator.hasPremiumAccess {
+            row.addArrangedSubview(footerLink("Manage Subscription", action: .manageSubscription))
+            row.addArrangedSubview(footerLink("Restore Purchase", action: .restorePurchase))
+        } else {
+            row.addArrangedSubview(footerLink("Continue with free plan", action: .continueWithFreePlan))
+        }
+        row.addArrangedSubview(footerLink("Privacy Policy", action: .privacyPolicy))
+        row.addArrangedSubview(footerLink("Support", action: .support))
+        row.addArrangedSubview(footerLink("Terms of Services", action: .termsOfServices))
 
         NSLayoutConstraint.activate([
             row.leadingAnchor.constraint(equalTo: wrap.leadingAnchor),
@@ -2855,25 +3033,54 @@ private extension ViewController {
         return wrap
     }
 
-    func footerLink(_ title: String) -> NSView {
-        let container = HoverTrackingView()
-        container.translatesAutoresizingMaskIntoConstraints = false
-        let label = textLabel(title, font: NSFont.systemFont(ofSize: 12, weight: .semibold), color: palette.textSecondary)
-        label.alignment = .center
-        container.addSubview(label)
+    private func footerLink(_ title: String, action: PaywallFooterAction) -> NSView {
+        let button = HoverButton(title: title, target: self, action: #selector(paywallFooterButtonPressed(_:)))
+        button.translatesAutoresizingMaskIntoConstraints = false
+        button.isBordered = false
+        button.bezelStyle = .regularSquare
+        button.focusRingType = .none
+        button.font = NSFont.systemFont(ofSize: 12, weight: .semibold)
+        button.alignment = .center
+        button.setButtonType(.momentaryChange)
+        button.contentTintColor = palette.textSecondary
+        paywallFooterActionByView[ObjectIdentifier(button)] = action
+        button.onHoverChanged = { [weak self, weak button] hovering in
+            guard let self, let button else { return }
+            button.contentTintColor = hovering ? (self.darkModeEnabled ? .white : self.palette.textPrimary) : self.palette.textSecondary
+        }
+        button.onHoverChanged?(false)
+        return button
+    }
+
+    func paywallMetaItem(title: String, subtitle: String) -> NSView {
+        let card = HoverTrackingView()
+        card.translatesAutoresizingMaskIntoConstraints = false
+        card.wantsLayer = true
+        card.layer?.cornerRadius = 10
+        card.layer?.backgroundColor = palette.inputBackground.cgColor
+        card.heightAnchor.constraint(equalToConstant: 44).isActive = true
+        styleSurface(card, borderColor: palette.inputBorder, borderWidth: 1, shadow: false)
 
+        let titleLabel = textLabel(title, font: NSFont.systemFont(ofSize: 11, weight: .semibold), color: palette.textPrimary)
+        let subtitleLabel = textLabel(subtitle, font: NSFont.systemFont(ofSize: 10, weight: .medium), color: palette.textSecondary)
+        card.addSubview(titleLabel)
+        card.addSubview(subtitleLabel)
         NSLayoutConstraint.activate([
-            label.centerXAnchor.constraint(equalTo: container.centerXAnchor),
-            label.centerYAnchor.constraint(equalTo: container.centerYAnchor)
+            titleLabel.centerXAnchor.constraint(equalTo: card.centerXAnchor),
+            titleLabel.topAnchor.constraint(equalTo: card.topAnchor, constant: 7),
+            subtitleLabel.centerXAnchor.constraint(equalTo: card.centerXAnchor),
+            subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 1),
+            subtitleLabel.bottomAnchor.constraint(lessThanOrEqualTo: card.bottomAnchor, constant: -6)
         ])
 
-        let click = NSClickGestureRecognizer(target: self, action: #selector(paywallFooterLinkClicked(_:)))
-        container.addGestureRecognizer(click)
-        container.onHoverChanged = { hovering in
-            label.textColor = hovering ? (self.darkModeEnabled ? .white : self.palette.textPrimary) : self.palette.textSecondary
+        let base = palette.inputBackground
+        let hoverBlend = darkModeEnabled ? NSColor.white : NSColor.black
+        let hover = base.blended(withFraction: 0.10, of: hoverBlend) ?? base
+        card.onHoverChanged = { [weak card] hovering in
+            card?.layer?.backgroundColor = (hovering ? hover : base).cgColor
         }
-        container.onHoverChanged?(false)
-        return container
+        card.onHoverChanged?(false)
+        return card
     }
 
     func paywallBenefitsSection() -> NSView {