Преглед на файлове

Show paywall in-app with desktop layout.

Replace the separate paywall window with an in-app overlay and add responsive horizontal/vertical desktop/mobile arrangement.

Co-authored-by: Cursor <cursoragent@cursor.com>
huzaifahayat12 преди 2 месеца
родител
ревизия
a20d7ab26c
променени са 1 файла, в които са добавени 182 реда и са изтрити 43 реда
  1. 182 43
      zoom_app/ViewController.swift

+ 182 - 43
zoom_app/ViewController.swift

@@ -256,7 +256,12 @@ class ViewController: NSViewController {
     private weak var topBarAuthButton: NSButton?
     private var topBarAuthButtonWidthConstraint: NSLayoutConstraint?
     private var profileMenuPopover: NSPopover?
-    private var paywallWindow: NSWindow?
+    private weak var paywallOverlayView: NSView?
+    private weak var paywallPanelView: NSView?
+    private weak var paywallBodyStack: NSStackView?
+    private var paywallIsDesktopLayout = false
+    private var paywallPanelWidthConstraint: NSLayoutConstraint?
+    private var paywallPanelHeightConstraint: NSLayoutConstraint?
     private var joinMeetingWindow: NSWindow?
     private var scheduleMeetingWindow: NSWindow?
     private weak var scheduleTopicField: NSTextField?
@@ -602,6 +607,10 @@ class ViewController: NSViewController {
     override func viewDidLayout() {
         super.viewDidLayout()
         alignNativeTrafficLights()
+        if paywallOverlayView != nil {
+            updatePaywallPanelSize()
+            updatePaywallResponsiveLayout()
+        }
     }
 
     private func setupUI() {
@@ -3521,49 +3530,164 @@ class ViewController: NSViewController {
     // MARK: - Paywall (ported from meetings_app)
 
     private func showPaywall() {
-        if let existing = paywallWindow {
+        if let existing = paywallOverlayView {
             refreshPaywallStoreUI()
-            existing.makeKeyAndOrderFront(nil)
+            if let superview = existing.superview {
+                superview.addSubview(existing)
+            }
+            updatePaywallResponsiveLayout()
             NSApp.activate(ignoringOtherApps: true)
             return
         }
 
+        let overlay = NSView()
+        overlay.translatesAutoresizingMaskIntoConstraints = false
+        overlay.wantsLayer = true
+        overlay.alphaValue = 0
+
+        // Full-app dim + blur background.
+        let blur = NSVisualEffectView()
+        blur.translatesAutoresizingMaskIntoConstraints = false
+        blur.material = palette.isDarkMode ? .hudWindow : .popover
+        blur.blendingMode = .withinWindow
+        blur.state = .active
+        overlay.addSubview(blur)
+
+        let dim = NSView()
+        dim.translatesAutoresizingMaskIntoConstraints = false
+        dim.wantsLayer = true
+        dim.layer?.backgroundColor = palette.isDarkMode
+            ? NSColor.black.withAlphaComponent(0.50).cgColor
+            : NSColor.white.withAlphaComponent(0.22).cgColor
+        overlay.addSubview(dim)
+
+        NSLayoutConstraint.activate([
+            blur.leadingAnchor.constraint(equalTo: overlay.leadingAnchor),
+            blur.trailingAnchor.constraint(equalTo: overlay.trailingAnchor),
+            blur.topAnchor.constraint(equalTo: overlay.topAnchor),
+            blur.bottomAnchor.constraint(equalTo: overlay.bottomAnchor),
+            dim.leadingAnchor.constraint(equalTo: overlay.leadingAnchor),
+            dim.trailingAnchor.constraint(equalTo: overlay.trailingAnchor),
+            dim.topAnchor.constraint(equalTo: overlay.topAnchor),
+            dim.bottomAnchor.constraint(equalTo: overlay.bottomAnchor)
+        ])
+
+        // Clicking outside the paywall dismisses it.
+        let outsideButton = NSButton(title: "", target: self, action: #selector(closePaywallClicked(_:)))
+        outsideButton.translatesAutoresizingMaskIntoConstraints = false
+        outsideButton.isBordered = false
+        outsideButton.bezelStyle = .regularSquare
+        outsideButton.contentTintColor = .clear
+        outsideButton.wantsLayer = true
+        outsideButton.layer?.backgroundColor = NSColor.clear.cgColor
+        overlay.addSubview(outsideButton)
+
         let content = makePaywallContent()
-        let controller = NSViewController()
-        controller.view = content
+        paywallPanelView = content
+        overlay.addSubview(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.hidesOnDeactivate = true
-        panel.isReleasedWhenClosed = false
-        panel.standardWindowButton(.closeButton)?.isHidden = true
-        panel.standardWindowButton(.miniaturizeButton)?.isHidden = true
-        panel.standardWindowButton(.zoomButton)?.isHidden = true
-        panel.center()
-        panel.contentViewController = controller
-        panel.makeKeyAndOrderFront(nil)
-        NSApp.activate(ignoringOtherApps: true)
-        paywallWindow = panel
+        view.addSubview(overlay)
+        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),
+            outsideButton.leadingAnchor.constraint(equalTo: overlay.leadingAnchor),
+            outsideButton.trailingAnchor.constraint(equalTo: overlay.trailingAnchor),
+            outsideButton.topAnchor.constraint(equalTo: overlay.topAnchor),
+            outsideButton.bottomAnchor.constraint(equalTo: overlay.bottomAnchor),
+            content.centerXAnchor.constraint(equalTo: overlay.centerXAnchor),
+            content.centerYAnchor.constraint(equalTo: overlay.centerYAnchor)
+        ])
+
+        // Choose an initial panel size for the current window size.
+        let safeInsetX: CGFloat = 80
+        let safeInsetY: CGFloat = 110
+        let maxWidth = max(640, view.bounds.width - safeInsetX)
+        let maxHeight = max(560, view.bounds.height - safeInsetY)
+        let panelWidth = min(1040, maxWidth)
+        let panelHeight = min(860, maxHeight)
+
+        paywallPanelWidthConstraint = content.widthAnchor.constraint(equalToConstant: panelWidth)
+        paywallPanelHeightConstraint = content.heightAnchor.constraint(equalToConstant: panelHeight)
+        paywallPanelWidthConstraint?.isActive = true
+        paywallPanelHeightConstraint?.isActive = true
+
+        paywallOverlayView = overlay
+
+        NSAnimationContext.runAnimationGroup { context in
+            context.duration = 0.18
+            overlay.animator().alphaValue = 1
+        }
 
         Task { [weak self] in
             guard let self else { return }
             await self.storeKitCoordinator.refreshProducts()
             await MainActor.run {
                 self.refreshPaywallStoreUI()
+                self.updatePaywallResponsiveLayout()
             }
         }
     }
 
     @objc private func closePaywallClicked(_ sender: Any?) {
-        paywallWindow?.performClose(nil)
-        paywallWindow = nil
+        hidePaywall()
+    }
+
+    private func hidePaywall() {
+        guard let overlay = paywallOverlayView else { return }
+
+        NSAnimationContext.runAnimationGroup { context in
+            context.duration = 0.16
+            overlay.animator().alphaValue = 0
+        } completionHandler: { [weak self] in
+            overlay.removeFromSuperview()
+            self?.paywallPanelWidthConstraint = nil
+            self?.paywallPanelHeightConstraint = nil
+            self?.paywallPanelView = nil
+            self?.paywallBodyStack = nil
+            self?.paywallIsDesktopLayout = false
+        }
+    }
+
+    private func updatePaywallPanelSize() {
+        guard paywallPanelWidthConstraint != nil, paywallPanelHeightConstraint != nil else { return }
+
+        let safeInsetX: CGFloat = 80
+        let safeInsetY: CGFloat = 110
+        let maxWidth = max(640, view.bounds.width - safeInsetX)
+        let maxHeight = max(560, view.bounds.height - safeInsetY)
+
+        let panelWidth = min(1040, maxWidth)
+        let panelHeight = min(860, maxHeight)
+
+        if abs(paywallPanelWidthConstraint?.constant ?? 0 - panelWidth) > 1 {
+            paywallPanelWidthConstraint?.constant = panelWidth
+        }
+        if abs(paywallPanelHeightConstraint?.constant ?? 0 - panelHeight) > 1 {
+            paywallPanelHeightConstraint?.constant = panelHeight
+        }
+    }
+
+    private func updatePaywallResponsiveLayout() {
+        guard let bodyStack = paywallBodyStack, let panel = paywallPanelView else { return }
+        let width = panel.bounds.width
+        // Two-column desktop layout needs room for the plan column (520+) + a slimmer benefits column.
+        let shouldUseDesktop = width >= 900
+        guard shouldUseDesktop != paywallIsDesktopLayout else { return }
+        paywallIsDesktopLayout = shouldUseDesktop
+
+        if shouldUseDesktop {
+            bodyStack.orientation = .horizontal
+            bodyStack.alignment = .top
+            bodyStack.spacing = 26
+        } else {
+            bodyStack.orientation = .vertical
+            bodyStack.alignment = .leading
+            bodyStack.spacing = 18
+        }
+
+        bodyStack.layoutSubtreeIfNeeded()
     }
 
     private func makePaywallContent() -> NSView {
@@ -3619,9 +3743,25 @@ class ViewController: NSViewController {
         contentStack.addArrangedSubview(topRow)
 
         contentStack.addArrangedSubview(textLabel("Upgrade to unlock premium features.", font: NSFont.systemFont(ofSize: 12, weight: .medium), color: secondaryText))
+        let bodyStack = NSStackView()
+        bodyStack.translatesAutoresizingMaskIntoConstraints = false
+        bodyStack.orientation = .vertical
+        bodyStack.spacing = 18
+        bodyStack.alignment = .leading
+        bodyStack.distribution = .fill
+        contentStack.addArrangedSubview(bodyStack)
+        paywallBodyStack = bodyStack
+
         let benefits = paywallBenefitsSection()
-        contentStack.addArrangedSubview(benefits)
-        contentStack.setCustomSpacing(18, after: benefits)
+        bodyStack.addArrangedSubview(benefits)
+
+        let planColumn = NSStackView()
+        planColumn.translatesAutoresizingMaskIntoConstraints = false
+        planColumn.orientation = .vertical
+        planColumn.spacing = 12
+        planColumn.alignment = .leading
+        planColumn.distribution = .fill
+        bodyStack.addArrangedSubview(planColumn)
 
         let weeklyCard = paywallPlanCard(
             title: "Weekly",
@@ -3632,7 +3772,7 @@ class ViewController: NSViewController {
             plan: .weekly,
             strikePrice: nil
         )
-        contentStack.addArrangedSubview(weeklyCard)
+        planColumn.addArrangedSubview(weeklyCard)
 
         let monthlyCard = paywallPlanCard(
             title: "Monthly",
@@ -3643,7 +3783,7 @@ class ViewController: NSViewController {
             plan: .monthly,
             strikePrice: nil
         )
-        contentStack.addArrangedSubview(monthlyCard)
+        planColumn.addArrangedSubview(monthlyCard)
 
         let yearlyCard = paywallPlanCard(
             title: "Yearly",
@@ -3654,7 +3794,7 @@ class ViewController: NSViewController {
             plan: .yearly,
             strikePrice: nil
         )
-        contentStack.addArrangedSubview(yearlyCard)
+        planColumn.addArrangedSubview(yearlyCard)
 
         let lifetimeCard = paywallPlanCard(
             title: "Lifetime",
@@ -3665,9 +3805,9 @@ class ViewController: NSViewController {
             plan: .lifetime,
             strikePrice: nil
         )
-        contentStack.addArrangedSubview(lifetimeCard)
+        planColumn.addArrangedSubview(lifetimeCard)
         updatePaywallPlanSelection()
-        contentStack.setCustomSpacing(20, after: lifetimeCard)
+        planColumn.setCustomSpacing(20, after: lifetimeCard)
 
         let offer = textLabel(paywallOfferText(for: selectedPremiumPlan), font: NSFont.systemFont(ofSize: 13, weight: .semibold), color: primaryText)
         offer.alignment = .center
@@ -3681,8 +3821,8 @@ class ViewController: NSViewController {
             offer.topAnchor.constraint(equalTo: offerWrap.topAnchor, constant: 6),
             offer.bottomAnchor.constraint(equalTo: offerWrap.bottomAnchor, constant: -2)
         ])
-        contentStack.addArrangedSubview(offerWrap)
-        contentStack.setCustomSpacing(18, after: offerWrap)
+        planColumn.addArrangedSubview(offerWrap)
+        planColumn.setCustomSpacing(18, after: offerWrap)
 
         let continueButton = HoverButton(title: "", target: self, action: #selector(paywallContinueClicked(_:)))
         continueButton.translatesAutoresizingMaskIntoConstraints = false
@@ -3703,8 +3843,8 @@ class ViewController: NSViewController {
         ])
         paywallContinueButton = continueButton
         paywallContinueLabel = continueLabel
-        contentStack.addArrangedSubview(continueButton)
-        contentStack.setCustomSpacing(16, after: continueButton)
+        planColumn.addArrangedSubview(continueButton)
+        planColumn.setCustomSpacing(16, after: continueButton)
 
         let secure = textLabel("Secured by Apple. Cancel anytime.", font: NSFont.systemFont(ofSize: 12, weight: .semibold), color: secondaryText)
         secure.alignment = .center
@@ -3717,8 +3857,8 @@ class ViewController: NSViewController {
             secure.topAnchor.constraint(equalTo: secureWrap.topAnchor, constant: 4),
             secure.bottomAnchor.constraint(equalTo: secureWrap.bottomAnchor, constant: -8)
         ])
-        contentStack.addArrangedSubview(secureWrap)
-        contentStack.setCustomSpacing(8, after: secureWrap)
+        planColumn.addArrangedSubview(secureWrap)
+        planColumn.setCustomSpacing(8, after: secureWrap)
 
         let footerButtons = NSStackView()
         footerButtons.translatesAutoresizingMaskIntoConstraints = false
@@ -3743,7 +3883,7 @@ class ViewController: NSViewController {
         footerButtons.addArrangedSubview(privacyButton)
         footerButtons.addArrangedSubview(supportButton)
         footerButtons.addArrangedSubview(termsButton)
-        contentStack.addArrangedSubview(footerButtons)
+        planColumn.addArrangedSubview(footerButtons)
 
         NSLayoutConstraint.activate([
             contentStack.leadingAnchor.constraint(equalTo: panel.leadingAnchor, constant: 18),
@@ -3762,7 +3902,7 @@ class ViewController: NSViewController {
         stack.orientation = .vertical
         stack.spacing = 8
         stack.alignment = .leading
-        stack.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
+        stack.widthAnchor.constraint(greaterThanOrEqualToConstant: 320).isActive = true
 
         let rowOne = NSStackView()
         rowOne.translatesAutoresizingMaskIntoConstraints = false
@@ -4036,8 +4176,7 @@ class ViewController: NSViewController {
                         self.schedulePremiumRatingPromptIfNeeded()
                     }
                     self.showSimpleAlert(title: "Purchase Complete", message: "Premium has been unlocked successfully.")
-                    self.paywallWindow?.performClose(nil)
-                    self.paywallWindow = nil
+                    self.hidePaywall()
                     if self.storeKitCoordinator.hasPremiumAccess {
                         DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) { [weak self] in
                             self?.presentRatingPrompt(source: "premium")