Selaa lähdekoodia

Update web URL handling and keep OAuth sign-in window.

Open non-login links in the system default browser while preserving the previous in-app OAuth sign-in flow.

Co-authored-by: Cursor <cursoragent@cursor.com>
huzaifahayat12 2 kuukautta sitten
vanhempi
sitoutus
7b595b0c7a
1 muutettua tiedostoa jossa 375 lisäystä ja 39 poistoa
  1. 375 39
      zoom_app/ViewController.swift

+ 375 - 39
zoom_app/ViewController.swift

@@ -16,6 +16,32 @@ private enum AppWindowMetrics {
     static let defaultContentHeight: CGFloat = 690
 }
 
+/// OAuth entry points stay in the system browser so enterprise SSO extensions and password managers behave as users expect.
+private enum InAppWebBrowserPolicy {
+    static func shouldOpenInSystemBrowser(_ url: URL) -> Bool {
+        guard let host = url.host?.lowercased() else { return false }
+        let path = url.path.lowercased()
+        let zoomHost = host == "zoom.us" || host.hasSuffix(".zoom.us") || host == "zoom.com" || host.hasSuffix(".zoom.com")
+        if zoomHost, path.contains("/oauth/authorize") { return true }
+        if host == "accounts.google.com" {
+            return path.contains("/o/oauth2/") || path.contains("/signin/oauth")
+        }
+        return false
+    }
+}
+
+/// Errors from the main-window embedded OAuth web session (right panel).
+private enum EmbeddedOAuthFlowError: Error {
+    case webViewUnavailable
+    case userCancelled
+    case timedOut
+}
+
+@MainActor
+protocol EmbeddedOAuthPresenting: AnyObject {
+    func runEmbeddedOAuthSession(startURL: URL, callbackURLPrefix: String, timeoutSeconds: Double) async throws -> URL
+}
+
 class ViewController: NSViewController {
     enum HomeSection: String {
         case home = "Home"
@@ -248,6 +274,18 @@ class ViewController: NSViewController {
     private weak var meetingsPageEmptyLabel: NSTextField?
     private weak var meetingsPageLoadMoreButton: NSButton?
     private weak var meetingsPageScrollView: NSScrollView?
+    private weak var embeddedBrowserWebView: WKWebView?
+    private weak var embeddedBrowserPanel: NSView?
+    private weak var homeBodyRowView: NSView?
+    private weak var homeMainContentColumnView: NSView?
+    private weak var embeddedBrowserBackButton: NSButton?
+    private weak var embeddedBrowserForwardButton: NSButton?
+    private var embeddedBrowserPanelWidthConstraint: NSLayoutConstraint?
+    private var embeddedBrowserMainColumnCollapsedWidthConstraint: NSLayoutConstraint?
+    private var oauthSessionContinuation: CheckedContinuation<URL, Error>?
+    private var oauthSessionCallbackPrefix: String?
+    /// Gap between main column and right web panel (must match Auto Layout constant).
+    private let homeEmbeddedBrowserColumnGap: CGFloat = 10
     private weak var homePremiumLockOverlay: NSButton?
     private weak var meetingsPremiumLockOverlay: NSButton?
     private weak var schedulerPremiumLockOverlay: NSButton?
@@ -562,6 +600,9 @@ class ViewController: NSViewController {
             updatePaywallPanelSize()
             updatePaywallResponsiveLayout()
         }
+        if (embeddedBrowserPanelWidthConstraint?.constant ?? 0) > 0.5 {
+            updateEmbeddedRightPanelWidthForOpenState()
+        }
     }
 
     private func setupUI() {
@@ -1519,8 +1560,14 @@ class ViewController: NSViewController {
         return url
     }
 
+    /// Opens non-sign-in web content in the system default browser.
+    @MainActor
+    private func openWebURLPreferringInApp(_ url: URL) {
+        _ = openURLInDefaultBrowser(url)
+    }
+
     private func openZoomMeetingInDefaultBrowser(_ url: URL) {
-        let opened = NSWorkspace.shared.open(url)
+        let opened = openURLInDefaultBrowser(url)
         if opened {
             joinMeetingWindow?.performClose(nil)
         } else {
@@ -1528,6 +1575,129 @@ class ViewController: NSViewController {
         }
     }
 
+    @discardableResult
+    private func openURLInDefaultBrowser(_ url: URL) -> Bool {
+        NSWorkspace.shared.open(url)
+    }
+
+    @MainActor
+    private func presentURLInEmbeddedRightPanel(_ url: URL) {
+        guard let webView = embeddedBrowserWebView else {
+            NSWorkspace.shared.open(url)
+            return
+        }
+        updateEmbeddedRightPanelWidthForOpenState()
+        webView.load(URLRequest(url: url))
+        refreshEmbeddedBrowserNavButtons()
+        view.window?.contentView?.layoutSubtreeIfNeeded()
+    }
+
+    /// Makes the embedded web panel fill the entire right-side content area (to match the reference behavior).
+    @MainActor
+    private func updateEmbeddedRightPanelWidthForOpenState() {
+        guard let widthConstraint = embeddedBrowserPanelWidthConstraint else { return }
+        view.layoutSubtreeIfNeeded()
+        view.window?.contentView?.layoutSubtreeIfNeeded()
+        var rowW = homeBodyRowView?.bounds.width ?? 0
+        if rowW < 1 {
+            rowW = view.bounds.width
+        }
+        // Fill the entire body row so the embedded web experience matches a dedicated right-side panel.
+        widthConstraint.constant = max(320, floor(rowW))
+
+        // Collapse the left main content column while web is visible.
+        homeMainContentColumnView?.isHidden = true
+        embeddedBrowserMainColumnCollapsedWidthConstraint?.isActive = true
+    }
+
+    @MainActor
+    private func refreshEmbeddedBrowserNavButtons() {
+        embeddedBrowserBackButton?.isEnabled = embeddedBrowserWebView?.canGoBack == true
+        embeddedBrowserForwardButton?.isEnabled = embeddedBrowserWebView?.canGoForward == true
+    }
+
+    @objc private func embeddedBrowserBackTapped() {
+        embeddedBrowserWebView?.goBack()
+    }
+
+    @objc private func embeddedBrowserForwardTapped() {
+        embeddedBrowserWebView?.goForward()
+    }
+
+    @objc private func embeddedBrowserReloadTapped() {
+        embeddedBrowserWebView?.reload()
+    }
+
+    @objc private func embeddedBrowserClosePanelTapped() {
+        closeEmbeddedBrowserPanel()
+    }
+
+    @MainActor
+    private func closeEmbeddedBrowserPanel() {
+        if let cont = oauthSessionContinuation {
+            oauthSessionContinuation = nil
+            oauthSessionCallbackPrefix = nil
+            cont.resume(throwing: EmbeddedOAuthFlowError.userCancelled)
+        }
+        embeddedBrowserWebView?.stopLoading()
+        embeddedBrowserPanelWidthConstraint?.constant = 0
+        embeddedBrowserMainColumnCollapsedWidthConstraint?.isActive = false
+        homeMainContentColumnView?.isHidden = false
+        refreshEmbeddedBrowserNavButtons()
+        view.window?.contentView?.layoutSubtreeIfNeeded()
+    }
+
+    func runEmbeddedOAuthSession(startURL: URL, callbackURLPrefix: String, timeoutSeconds: Double) async throws -> URL {
+        guard let webView = embeddedBrowserWebView else {
+            throw EmbeddedOAuthFlowError.webViewUnavailable
+        }
+        do {
+            return try await withThrowingTaskGroup(of: URL.self) { group in
+                group.addTask { @MainActor in
+                    try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<URL, Error>) in
+                        self.oauthSessionContinuation = continuation
+                        self.oauthSessionCallbackPrefix = callbackURLPrefix
+                        self.updateEmbeddedRightPanelWidthForOpenState()
+                        webView.load(URLRequest(url: startURL))
+                        self.refreshEmbeddedBrowserNavButtons()
+                    }
+                }
+                group.addTask {
+                    try await Task.sleep(nanoseconds: UInt64(timeoutSeconds * 1_000_000_000))
+                    await MainActor.run {
+                        if let cont = self.oauthSessionContinuation {
+                            self.oauthSessionContinuation = nil
+                            self.oauthSessionCallbackPrefix = nil
+                            webView.stopLoading()
+                            cont.resume(throwing: EmbeddedOAuthFlowError.timedOut)
+                        }
+                    }
+                    throw EmbeddedOAuthFlowError.timedOut
+                }
+                let url = try await group.next()!
+                group.cancelAll()
+                await MainActor.run {
+                    self.oauthSessionContinuation = nil
+                    self.oauthSessionCallbackPrefix = nil
+                    self.closeEmbeddedBrowserPanel()
+                    webView.load(URLRequest(url: URL(string: "about:blank")!))
+                }
+                return url
+            }
+        } catch {
+            await MainActor.run {
+                self.oauthSessionContinuation = nil
+                self.oauthSessionCallbackPrefix = nil
+                webView.stopLoading()
+                self.embeddedBrowserPanelWidthConstraint?.constant = 0
+                self.refreshEmbeddedBrowserNavButtons()
+                webView.load(URLRequest(url: URL(string: "about:blank")!))
+                self.view.window?.contentView?.layoutSubtreeIfNeeded()
+            }
+            throw error
+        }
+    }
+
     private func resetJoinMeetingPanelReferences() {
         joinMeetingWindow = nil
         joinURLField = nil
@@ -1663,7 +1833,7 @@ class ViewController: NSViewController {
 
     private func openManageSubscriptions() {
         if let url = URL(string: "https://apps.apple.com/account/subscriptions") {
-            NSWorkspace.shared.open(url)
+            openWebURLPreferringInApp(url)
         }
     }
 
@@ -1966,7 +2136,7 @@ class ViewController: NSViewController {
     @objc private func meetingSearchResultTapped(_ sender: NSButton) {
         guard let meeting = globalSearchResults[safe: sender.tag] else { return }
         if let url = meeting.webURL {
-            NSWorkspace.shared.open(url)
+            openWebURLPreferringInApp(url)
         } else {
             let pasteboard = NSPasteboard.general
             pasteboard.clearContents()
@@ -5054,11 +5224,85 @@ class ViewController: NSViewController {
         let schedulerEmptyLabel = makeLabel("No meetings scheduled for this date.", size: 15, color: secondaryText, weight: .regular, centered: true)
         schedulerEmptyLabel.isHidden = true
 
-        let contentColumn = NSView()
-        contentColumn.translatesAutoresizingMaskIntoConstraints = false
+        let bodyRow = NSView()
+        bodyRow.translatesAutoresizingMaskIntoConstraints = false
+
+        let mainContentColumn = NSView()
+        mainContentColumn.translatesAutoresizingMaskIntoConstraints = false
+        mainContentColumn.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
+
+        let rightWebPanel = NSView()
+        rightWebPanel.translatesAutoresizingMaskIntoConstraints = false
+        rightWebPanel.wantsLayer = true
+        rightWebPanel.layer?.cornerRadius = 16
+        rightWebPanel.layer?.masksToBounds = true
+        rightWebPanel.layer?.backgroundColor = secondaryCardBackground.withAlphaComponent(0.94).cgColor
+        rightWebPanel.layer?.borderWidth = 1
+        rightWebPanel.layer?.borderColor = palette.inputBorder.cgColor
+
+        let webToolbar = NSView()
+        webToolbar.translatesAutoresizingMaskIntoConstraints = false
+        webToolbar.wantsLayer = true
+        webToolbar.layer?.backgroundColor = chromeUnifiedBackground.withAlphaComponent(0.55).cgColor
+
+        func prepareEmbeddedToolButton(_ button: NSButton, symbol: String) {
+            button.translatesAutoresizingMaskIntoConstraints = false
+            button.bezelStyle = .toolbar
+            if let image = NSImage(systemSymbolName: symbol, accessibilityDescription: nil) {
+                button.image = image
+            }
+        }
+
+        let browserBack = NSButton()
+        let browserForward = NSButton()
+        let browserReload = NSButton()
+        let browserClose = NSButton()
+        prepareEmbeddedToolButton(browserBack, symbol: "chevron.left")
+        prepareEmbeddedToolButton(browserForward, symbol: "chevron.right")
+        prepareEmbeddedToolButton(browserReload, symbol: "arrow.clockwise")
+        prepareEmbeddedToolButton(browserClose, symbol: "xmark.circle.fill")
+        browserClose.toolTip = "Close web panel"
+
+        browserBack.target = self
+        browserBack.action = #selector(embeddedBrowserBackTapped)
+        browserForward.target = self
+        browserForward.action = #selector(embeddedBrowserForwardTapped)
+        browserReload.target = self
+        browserReload.action = #selector(embeddedBrowserReloadTapped)
+        browserClose.target = self
+        browserClose.action = #selector(embeddedBrowserClosePanelTapped)
+
+        let toolbarSpring = NSView()
+        toolbarSpring.translatesAutoresizingMaskIntoConstraints = false
+        toolbarSpring.setContentHuggingPriority(.defaultLow, for: .horizontal)
+
+        let webToolBarStack = NSStackView(views: [browserBack, browserForward, browserReload, toolbarSpring, browserClose])
+        webToolBarStack.orientation = .horizontal
+        webToolBarStack.spacing = 6
+        webToolBarStack.alignment = .centerY
+        webToolBarStack.translatesAutoresizingMaskIntoConstraints = false
+
+        let embeddedBrowserConfiguration = WKWebViewConfiguration()
+        embeddedBrowserConfiguration.preferences.javaScriptCanOpenWindowsAutomatically = true
+        let embeddedWK = WKWebView(frame: .zero, configuration: embeddedBrowserConfiguration)
+        embeddedWK.translatesAutoresizingMaskIntoConstraints = false
+        embeddedWK.setContentCompressionResistancePriority(.init(1), for: .horizontal)
+        embeddedWK.setContentHuggingPriority(.init(1), for: .horizontal)
+        embeddedWK.navigationDelegate = self
+        embeddedWK.uiDelegate = self
+
         content.addSubview(topBar)
         content.addSubview(topBarDivider)
-        content.addSubview(contentColumn)
+        content.addSubview(bodyRow)
+        bodyRow.addSubview(mainContentColumn)
+        bodyRow.addSubview(rightWebPanel)
+        rightWebPanel.addSubview(webToolbar)
+        webToolbar.addSubview(webToolBarStack)
+        rightWebPanel.addSubview(embeddedWK)
+
+        let rightPanelWidthConstraint = rightWebPanel.widthAnchor.constraint(equalToConstant: 0)
+        let mainColumnCollapsedWidthConstraint = mainContentColumn.widthAnchor.constraint(equalToConstant: 0)
+        mainColumnCollapsedWidthConstraint.isActive = false
 
         [brandStack, searchRow, rightTopBarCluster, searchPill, searchField, searchIcon, searchHintLabel].forEach {
             $0.translatesAutoresizingMaskIntoConstraints = false
@@ -5074,7 +5318,7 @@ class ViewController: NSViewController {
         }
         [timeTitle, dateTitle, actions, panel, panelHeader, meetingsStatus, meetingsDayNav, noMeeting, meetingsScrollView, refreshMeetingsButton, placeholder, settingsView, meetingsPageView, schedulerRoot].forEach {
             $0.translatesAutoresizingMaskIntoConstraints = false
-            contentColumn.addSubview($0)
+            mainContentColumn.addSubview($0)
         }
         [schedulerSplit, schedulerCalendarPanel, schedulerMeetingsPanel, schedulerCalendarTopRow, schedulerConnectLabel, schedulerPlusButton, schedulerMonthRow, schedulerWeekdaysRow, schedulerCalendarGrid, schedulerRightTopRow, schedulerSelectedDateLabel, schedulerTodayButton, schedulerMeetingsHeader, schedulerMeetingsScroll, schedulerEmptyLabel, schedulerMeetingsDocument, schedulerMeetingsStack].forEach {
             $0.translatesAutoresizingMaskIntoConstraints = false
@@ -5113,10 +5357,34 @@ class ViewController: NSViewController {
             topBarDivider.trailingAnchor.constraint(equalTo: content.trailingAnchor),
             topBarDivider.heightAnchor.constraint(equalToConstant: 1),
 
-            contentColumn.topAnchor.constraint(equalTo: topBarDivider.bottomAnchor, constant: 14),
-            contentColumn.bottomAnchor.constraint(equalTo: content.bottomAnchor, constant: -10),
-            contentColumn.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 8),
-            contentColumn.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -8),
+            bodyRow.topAnchor.constraint(equalTo: topBarDivider.bottomAnchor, constant: 14),
+            bodyRow.bottomAnchor.constraint(equalTo: content.bottomAnchor, constant: -10),
+            bodyRow.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 8),
+            bodyRow.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -8),
+
+            mainContentColumn.leadingAnchor.constraint(equalTo: bodyRow.leadingAnchor),
+            mainContentColumn.topAnchor.constraint(equalTo: bodyRow.topAnchor),
+            mainContentColumn.bottomAnchor.constraint(equalTo: bodyRow.bottomAnchor),
+            mainContentColumn.trailingAnchor.constraint(equalTo: rightWebPanel.leadingAnchor, constant: -homeEmbeddedBrowserColumnGap),
+
+            rightWebPanel.topAnchor.constraint(equalTo: bodyRow.topAnchor),
+            rightWebPanel.bottomAnchor.constraint(equalTo: bodyRow.bottomAnchor),
+            rightWebPanel.trailingAnchor.constraint(equalTo: bodyRow.trailingAnchor),
+            rightPanelWidthConstraint,
+
+            webToolbar.leadingAnchor.constraint(equalTo: rightWebPanel.leadingAnchor),
+            webToolbar.trailingAnchor.constraint(equalTo: rightWebPanel.trailingAnchor),
+            webToolbar.topAnchor.constraint(equalTo: rightWebPanel.topAnchor),
+            webToolbar.heightAnchor.constraint(equalToConstant: 40),
+
+            webToolBarStack.leadingAnchor.constraint(equalTo: webToolbar.leadingAnchor, constant: 8),
+            webToolBarStack.trailingAnchor.constraint(equalTo: webToolbar.trailingAnchor, constant: -8),
+            webToolBarStack.centerYAnchor.constraint(equalTo: webToolbar.centerYAnchor),
+
+            embeddedWK.topAnchor.constraint(equalTo: webToolbar.bottomAnchor),
+            embeddedWK.leadingAnchor.constraint(equalTo: rightWebPanel.leadingAnchor),
+            embeddedWK.trailingAnchor.constraint(equalTo: rightWebPanel.trailingAnchor),
+            embeddedWK.bottomAnchor.constraint(equalTo: rightWebPanel.bottomAnchor),
 
             searchRowCenterX,
             searchRow.centerYAnchor.constraint(equalTo: topBar.centerYAnchor),
@@ -5141,24 +5409,24 @@ class ViewController: NSViewController {
             searchField.centerYAnchor.constraint(equalTo: searchPill.centerYAnchor),
             authButton.heightAnchor.constraint(equalToConstant: 30),
 
-            timeTitle.topAnchor.constraint(equalTo: contentColumn.topAnchor, constant: 20),
-            timeTitle.centerXAnchor.constraint(equalTo: contentColumn.centerXAnchor),
+            timeTitle.topAnchor.constraint(equalTo: mainContentColumn.topAnchor, constant: 20),
+            timeTitle.centerXAnchor.constraint(equalTo: mainContentColumn.centerXAnchor),
             dateTitle.topAnchor.constraint(equalTo: timeTitle.bottomAnchor, constant: 6),
-            dateTitle.centerXAnchor.constraint(equalTo: contentColumn.centerXAnchor),
+            dateTitle.centerXAnchor.constraint(equalTo: mainContentColumn.centerXAnchor),
 
             actions.topAnchor.constraint(equalTo: dateTitle.bottomAnchor, constant: 28),
-            actions.centerXAnchor.constraint(equalTo: contentColumn.centerXAnchor),
-            actions.leadingAnchor.constraint(greaterThanOrEqualTo: contentColumn.leadingAnchor, constant: 12),
-            actions.trailingAnchor.constraint(lessThanOrEqualTo: contentColumn.trailingAnchor, constant: -12),
+            actions.centerXAnchor.constraint(equalTo: mainContentColumn.centerXAnchor),
+            actions.leadingAnchor.constraint(greaterThanOrEqualTo: mainContentColumn.leadingAnchor, constant: 12),
+            actions.trailingAnchor.constraint(lessThanOrEqualTo: mainContentColumn.trailingAnchor, constant: -12),
             actions.heightAnchor.constraint(equalToConstant: 100),
 
             panel.topAnchor.constraint(equalTo: actions.bottomAnchor, constant: 18),
-            panel.centerXAnchor.constraint(equalTo: contentColumn.centerXAnchor),
+            panel.centerXAnchor.constraint(equalTo: mainContentColumn.centerXAnchor),
             panel.widthAnchor.constraint(equalToConstant: 610),
-            panel.leadingAnchor.constraint(greaterThanOrEqualTo: contentColumn.leadingAnchor, constant: 6),
-            panel.trailingAnchor.constraint(lessThanOrEqualTo: contentColumn.trailingAnchor, constant: -6),
+            panel.leadingAnchor.constraint(greaterThanOrEqualTo: mainContentColumn.leadingAnchor, constant: 6),
+            panel.trailingAnchor.constraint(lessThanOrEqualTo: mainContentColumn.trailingAnchor, constant: -6),
             panel.heightAnchor.constraint(greaterThanOrEqualToConstant: 280),
-            panel.bottomAnchor.constraint(equalTo: contentColumn.bottomAnchor, constant: -14),
+            panel.bottomAnchor.constraint(equalTo: mainContentColumn.bottomAnchor, constant: -14),
 
             panelHeader.topAnchor.constraint(equalTo: panel.topAnchor, constant: 20),
             panelHeader.leadingAnchor.constraint(equalTo: panel.leadingAnchor, constant: 16),
@@ -5186,23 +5454,23 @@ class ViewController: NSViewController {
             refreshMeetingsButton.bottomAnchor.constraint(equalTo: panel.bottomAnchor, constant: -12),
             refreshMeetingsButton.heightAnchor.constraint(equalToConstant: 40),
 
-            placeholder.centerXAnchor.constraint(equalTo: contentColumn.centerXAnchor),
-            placeholder.centerYAnchor.constraint(equalTo: contentColumn.centerYAnchor),
+            placeholder.centerXAnchor.constraint(equalTo: mainContentColumn.centerXAnchor),
+            placeholder.centerYAnchor.constraint(equalTo: mainContentColumn.centerYAnchor),
 
-            settingsView.topAnchor.constraint(equalTo: contentColumn.topAnchor),
-            settingsView.leadingAnchor.constraint(equalTo: contentColumn.leadingAnchor),
-            settingsView.trailingAnchor.constraint(equalTo: contentColumn.trailingAnchor),
-            settingsView.bottomAnchor.constraint(equalTo: contentColumn.bottomAnchor),
+            settingsView.topAnchor.constraint(equalTo: mainContentColumn.topAnchor),
+            settingsView.leadingAnchor.constraint(equalTo: mainContentColumn.leadingAnchor),
+            settingsView.trailingAnchor.constraint(equalTo: mainContentColumn.trailingAnchor),
+            settingsView.bottomAnchor.constraint(equalTo: mainContentColumn.bottomAnchor),
 
-            meetingsPageView.topAnchor.constraint(equalTo: contentColumn.topAnchor),
-            meetingsPageView.leadingAnchor.constraint(equalTo: contentColumn.leadingAnchor),
-            meetingsPageView.trailingAnchor.constraint(equalTo: contentColumn.trailingAnchor),
-            meetingsPageView.bottomAnchor.constraint(equalTo: contentColumn.bottomAnchor),
+            meetingsPageView.topAnchor.constraint(equalTo: mainContentColumn.topAnchor),
+            meetingsPageView.leadingAnchor.constraint(equalTo: mainContentColumn.leadingAnchor),
+            meetingsPageView.trailingAnchor.constraint(equalTo: mainContentColumn.trailingAnchor),
+            meetingsPageView.bottomAnchor.constraint(equalTo: mainContentColumn.bottomAnchor),
 
-            schedulerRoot.topAnchor.constraint(equalTo: contentColumn.topAnchor, constant: 8),
-            schedulerRoot.leadingAnchor.constraint(equalTo: contentColumn.leadingAnchor, constant: 8),
-            schedulerRoot.trailingAnchor.constraint(equalTo: contentColumn.trailingAnchor, constant: -8),
-            schedulerRoot.bottomAnchor.constraint(equalTo: contentColumn.bottomAnchor, constant: -8),
+            schedulerRoot.topAnchor.constraint(equalTo: mainContentColumn.topAnchor, constant: 8),
+            schedulerRoot.leadingAnchor.constraint(equalTo: mainContentColumn.leadingAnchor, constant: 8),
+            schedulerRoot.trailingAnchor.constraint(equalTo: mainContentColumn.trailingAnchor, constant: -8),
+            schedulerRoot.bottomAnchor.constraint(equalTo: mainContentColumn.bottomAnchor, constant: -8),
 
             schedulerSplit.topAnchor.constraint(equalTo: schedulerRoot.topAnchor),
             schedulerSplit.leadingAnchor.constraint(equalTo: schedulerRoot.leadingAnchor),
@@ -5353,6 +5621,16 @@ class ViewController: NSViewController {
         applyAuthenticationUIState(triggerInitialRefresh: false)
         updatePremiumLockedSections()
 
+        embeddedBrowserWebView = embeddedWK
+        embeddedBrowserPanel = rightWebPanel
+        homeBodyRowView = bodyRow
+        homeMainContentColumnView = mainContentColumn
+        embeddedBrowserPanelWidthConstraint = rightPanelWidthConstraint
+        embeddedBrowserMainColumnCollapsedWidthConstraint = mainColumnCollapsedWidthConstraint
+        embeddedBrowserBackButton = browserBack
+        embeddedBrowserForwardButton = browserForward
+        refreshEmbeddedBrowserNavButtons()
+
         return root
     }
 
@@ -5889,6 +6167,7 @@ class ViewController: NSViewController {
 
     @objc private func homeSidebarItemTapped(_ sender: NSButton) {
         guard let item = sender.identifier?.rawValue else { return }
+        closeEmbeddedBrowserPanel()
         selectedHomeSidebarItem = item
         updateHomeSidebarHighlight()
         updateSelectedHomeSectionUI()
@@ -5903,6 +6182,7 @@ class ViewController: NSViewController {
     }
 
     func menuBarShowSection(_ section: HomeSection) {
+        closeEmbeddedBrowserPanel()
         selectedHomeSidebarItem = section.rawValue
         updateHomeSidebarHighlight()
         updateSelectedHomeSectionUI()
@@ -5913,10 +6193,11 @@ class ViewController: NSViewController {
             showSimpleAlert(title: "Sign in required", message: "Please sign in with Zoom to start a new meeting.")
             return
         }
-        guard let url = URL(string: "https://zoom.us/start/videomeeting"), NSWorkspace.shared.open(url) else {
+        guard let url = URL(string: "https://zoom.us/start/videomeeting") else {
             showSimpleAlert(title: "Unable to open", message: "Could not start a new Zoom meeting right now.")
             return
         }
+        openWebURLPreferringInApp(url)
     }
 
     func menuBarTriggerJoinMeeting() {
@@ -6215,7 +6496,14 @@ class ViewController: NSViewController {
         let hoverBorder = palette.isDarkMode
             ? NSColor.white.withAlphaComponent(0.85)
             : NSColor.black.withAlphaComponent(0.22)
-        let card = MeetingCardView(url: meeting.webURL, hoverBackground: hoverBackground, hoverBorder: hoverBorder)
+        let card = MeetingCardView(
+            url: meeting.webURL,
+            hoverBackground: hoverBackground,
+            hoverBorder: hoverBorder,
+            onTapURL: { [weak self] url in
+                self?.openWebURLPreferringInApp(url)
+            }
+        )
         card.wantsLayer = true
         card.layer?.backgroundColor = meetingCardBackground.cgColor
         card.layer?.cornerRadius = 13
@@ -6300,6 +6588,45 @@ class ViewController: NSViewController {
     }
 }
 
+extension ViewController: EmbeddedOAuthPresenting {}
+
+extension ViewController: WKNavigationDelegate, WKUIDelegate {
+    func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
+        guard webView === embeddedBrowserWebView else {
+            decisionHandler(.allow)
+            return
+        }
+        if let prefix = oauthSessionCallbackPrefix,
+           let targetURL = navigationAction.request.url,
+           targetURL.absoluteString.hasPrefix(prefix) {
+            oauthSessionContinuation?.resume(returning: targetURL)
+            oauthSessionContinuation = nil
+            oauthSessionCallbackPrefix = nil
+            decisionHandler(.cancel)
+            return
+        }
+        decisionHandler(.allow)
+    }
+
+    func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
+        guard webView === embeddedBrowserWebView else { return }
+        refreshEmbeddedBrowserNavButtons()
+    }
+
+    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
+        guard webView === embeddedBrowserWebView else { return }
+        refreshEmbeddedBrowserNavButtons()
+    }
+
+    func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
+        guard webView === embeddedBrowserWebView else { return nil }
+        if navigationAction.targetFrame == nil {
+            webView.load(navigationAction.request)
+        }
+        return nil
+    }
+}
+
 extension ViewController: NSWindowDelegate {
     func windowWillClose(_ notification: Notification) {
         guard let window = notification.object as? NSWindow else { return }
@@ -6505,16 +6832,18 @@ private final class MeetingCardView: NSView {
     private let url: URL?
     private let hoverBackground: NSColor
     private let hoverBorder: NSColor
+    private let onTapURL: ((URL) -> Void)?
     private var tracking: NSTrackingArea?
     private var isHovering = false
     private var normalBackgroundColor: CGColor?
     private var normalBorderColor: CGColor?
     private var normalBorderWidth: CGFloat?
 
-    init(url: URL?, hoverBackground: NSColor, hoverBorder: NSColor) {
+    init(url: URL?, hoverBackground: NSColor, hoverBorder: NSColor, onTapURL: ((URL) -> Void)? = nil) {
         self.url = url
         self.hoverBackground = hoverBackground
         self.hoverBorder = hoverBorder
+        self.onTapURL = onTapURL
         super.init(frame: .zero)
     }
 
@@ -6522,6 +6851,7 @@ private final class MeetingCardView: NSView {
         self.url = nil
         self.hoverBackground = NSColor.white.withAlphaComponent(0.075)
         self.hoverBorder = NSColor.white.withAlphaComponent(0.85)
+        self.onTapURL = nil
         super.init(coder: coder)
     }
 
@@ -6558,7 +6888,11 @@ private final class MeetingCardView: NSView {
     override func mouseUp(with event: NSEvent) {
         super.mouseUp(with: event)
         guard let url else { return }
-        NSWorkspace.shared.open(url)
+        if let onTapURL {
+            onTapURL(url)
+        } else {
+            NSWorkspace.shared.open(url)
+        }
     }
 
     private func applyHoverState() {
@@ -7038,6 +7372,8 @@ final class KeychainTokenStore {
     }
 }
 
+// MARK: - OAuth window (previous sign-in behavior)
+
 @MainActor
 private final class OAuthWebViewContainerView: NSView {
     private let webView: WKWebView