|
@@ -749,21 +749,8 @@ class ViewController: NSViewController {
|
|
|
ensureUsageSessionStartDate()
|
|
ensureUsageSessionStartDate()
|
|
|
scheduleUsageBasedRatingPromptIfNeeded()
|
|
scheduleUsageBasedRatingPromptIfNeeded()
|
|
|
recordedMeetings = MeetingRecordingManager.shared.recordings
|
|
recordedMeetings = MeetingRecordingManager.shared.recordings
|
|
|
- zoomOAuth.embeddedOAuthSession = { [weak self] startURL, callbackPrefix, timeout in
|
|
|
|
|
- guard let self else { throw ZoomOAuthError.unableToOpenBrowser }
|
|
|
|
|
- do {
|
|
|
|
|
- return try await self.runEmbeddedOAuthSession(
|
|
|
|
|
- startURL: startURL,
|
|
|
|
|
- callbackURLPrefix: callbackPrefix,
|
|
|
|
|
- timeoutSeconds: timeout
|
|
|
|
|
- )
|
|
|
|
|
- } catch EmbeddedOAuthFlowError.userCancelled {
|
|
|
|
|
- throw ZoomOAuthError.authenticationCancelled
|
|
|
|
|
- } catch EmbeddedOAuthFlowError.timedOut {
|
|
|
|
|
- throw ZoomOAuthError.authenticationTimedOut
|
|
|
|
|
- } catch EmbeddedOAuthFlowError.webViewUnavailable {
|
|
|
|
|
- throw ZoomOAuthError.unableToOpenBrowser
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ zoomOAuth.onInteractiveSignInCompleted = { [weak self] in
|
|
|
|
|
+ self?.primeZoomEmbeddedWebSession()
|
|
|
}
|
|
}
|
|
|
NotificationCenter.default.addObserver(
|
|
NotificationCenter.default.addObserver(
|
|
|
forName: MeetingRecordingManager.recordingsDidChangeNotification,
|
|
forName: MeetingRecordingManager.recordingsDidChangeNotification,
|
|
@@ -974,6 +961,9 @@ class ViewController: NSViewController {
|
|
|
applyAuthenticationUIState(triggerInitialRefresh: refreshMeetings)
|
|
applyAuthenticationUIState(triggerInitialRefresh: refreshMeetings)
|
|
|
updateHomeSidebarHighlight()
|
|
updateHomeSidebarHighlight()
|
|
|
updateSelectedHomeSectionUI()
|
|
updateSelectedHomeSectionUI()
|
|
|
|
|
+ if isUserLoggedIn() {
|
|
|
|
|
+ primeZoomEmbeddedWebSession()
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private func isUserLoggedIn() -> Bool {
|
|
private func isUserLoggedIn() -> Bool {
|
|
@@ -1050,7 +1040,6 @@ class ViewController: NSViewController {
|
|
|
UserDefaults.standard.set(profileTimeZone.identifier, forKey: Self.zoomAccountTimeZoneDefaultsKey)
|
|
UserDefaults.standard.set(profileTimeZone.identifier, forKey: Self.zoomAccountTimeZoneDefaultsKey)
|
|
|
}
|
|
}
|
|
|
self.completeLoginAndRestart(profile: profile)
|
|
self.completeLoginAndRestart(profile: profile)
|
|
|
- self.primeZoomEmbeddedWebSession()
|
|
|
|
|
}
|
|
}
|
|
|
} catch {
|
|
} catch {
|
|
|
await MainActor.run {
|
|
await MainActor.run {
|
|
@@ -2368,8 +2357,17 @@ class ViewController: NSViewController {
|
|
|
joinZoomMeetingFromLink(url.absoluteString, displayName: meeting.title)
|
|
joinZoomMeetingFromLink(url.absoluteString, displayName: meeting.title)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /// True for zoom.us / app.zoom.us pages (not API hosts). When signed in, these load in the shared embedded browser so OAuth cookies apply.
|
|
|
|
|
+ private func isZoomWebsiteURL(_ url: URL) -> Bool {
|
|
|
|
|
+ guard let host = url.host?.lowercased(), host.contains("zoom.") else { return false }
|
|
|
|
|
+ if host.hasPrefix("api.") { return false }
|
|
|
|
|
+ let path = url.path.lowercased()
|
|
|
|
|
+ if path.hasPrefix("/oauth") { return false }
|
|
|
|
|
+ return true
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/// Opens Zoom meeting join/start links in the right-panel `WKWebView` (same store as embedded Zoom OAuth).
|
|
/// Opens Zoom meeting join/start links in the right-panel `WKWebView` (same store as embedded Zoom OAuth).
|
|
|
- /// Other URLs open in the system default browser.
|
|
|
|
|
|
|
+ /// Other zoom.us pages open in that web view when signed in; everything else uses the system browser.
|
|
|
/// For Zoom meeting join/start links, asks for recording consent first, persists transcription languages from that sheet,
|
|
/// For Zoom meeting join/start links, asks for recording consent first, persists transcription languages from that sheet,
|
|
|
/// then optionally begins local audio recording (when the user allows and recording is not already active).
|
|
/// then optionally begins local audio recording (when the user allows and recording is not already active).
|
|
|
@MainActor
|
|
@MainActor
|
|
@@ -2389,8 +2387,10 @@ class ViewController: NSViewController {
|
|
|
shouldRecordLocally = false
|
|
shouldRecordLocally = false
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ let useEmbeddedBrowser = embeddedBrowserWebView != nil
|
|
|
|
|
+ && (isZoomMeetingJoinOrStartURL(targetURL) || (isUserLoggedIn() && isZoomWebsiteURL(targetURL)))
|
|
|
let opened: Bool
|
|
let opened: Bool
|
|
|
- if isZoomMeetingJoinOrStartURL(targetURL), embeddedBrowserWebView != nil {
|
|
|
|
|
|
|
+ if useEmbeddedBrowser {
|
|
|
presentURLInEmbeddedRightPanel(targetURL)
|
|
presentURLInEmbeddedRightPanel(targetURL)
|
|
|
opened = true
|
|
opened = true
|
|
|
} else {
|
|
} else {
|
|
@@ -3354,6 +3354,7 @@ class ViewController: NSViewController {
|
|
|
let trimmedName = profile?.name?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
let trimmedName = profile?.name?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
|
zoomSignedInDisplayName = trimmedName.isEmpty ? nil : trimmedName
|
|
zoomSignedInDisplayName = trimmedName.isEmpty ? nil : trimmedName
|
|
|
showHomeView(profile: profile)
|
|
showHomeView(profile: profile)
|
|
|
|
|
+ primeZoomEmbeddedWebSession()
|
|
|
if let window = view.window {
|
|
if let window = view.window {
|
|
|
window.makeKeyAndOrderFront(nil)
|
|
window.makeKeyAndOrderFront(nil)
|
|
|
}
|
|
}
|
|
@@ -10644,10 +10645,14 @@ final class ZoomOAuthService: NSObject {
|
|
|
static let shared = ZoomOAuthService()
|
|
static let shared = ZoomOAuthService()
|
|
|
private var inAppOAuthWindowController: InAppOAuthWindowController?
|
|
private var inAppOAuthWindowController: InAppOAuthWindowController?
|
|
|
|
|
|
|
|
- /// When set (by `ViewController`), Zoom OAuth runs in the right-panel web view so cookies match embedded meetings.
|
|
|
|
|
|
|
+ /// When set, Zoom OAuth runs in this session instead of a separate window (unused; sign-in uses `InAppOAuthWindowController`).
|
|
|
@MainActor
|
|
@MainActor
|
|
|
var embeddedOAuthSession: (@MainActor (URL, String, Double) async throws -> URL)?
|
|
var embeddedOAuthSession: (@MainActor (URL, String, Double) async throws -> URL)?
|
|
|
|
|
|
|
|
|
|
+ /// Called after a successful interactive OAuth flow (separate sign-in window).
|
|
|
|
|
+ @MainActor
|
|
|
|
|
+ var onInteractiveSignInCompleted: (() -> Void)?
|
|
|
|
|
+
|
|
|
private let tokenStore = ZoomOAuthTokenStore()
|
|
private let tokenStore = ZoomOAuthTokenStore()
|
|
|
private let clientIdDefaultsKey = "zoom.oauth.clientId"
|
|
private let clientIdDefaultsKey = "zoom.oauth.clientId"
|
|
|
private let clientSecretDefaultsKey = "zoom.oauth.clientSecret"
|
|
private let clientSecretDefaultsKey = "zoom.oauth.clientSecret"
|
|
@@ -10803,7 +10808,9 @@ final class ZoomOAuthService: NSObject {
|
|
|
guard let code = queryItems?.first(where: { $0.name == "code" })?.value, code.isEmpty == false else {
|
|
guard let code = queryItems?.first(where: { $0.name == "code" })?.value, code.isEmpty == false else {
|
|
|
throw ZoomOAuthError.missingAuthorizationCode
|
|
throw ZoomOAuthError.missingAuthorizationCode
|
|
|
}
|
|
}
|
|
|
- return try await exchangeCodeForTokens(code: code, redirectURI: redirectURI, clientId: clientId, clientSecret: clientSecret)
|
|
|
|
|
|
|
+ let tokens = try await exchangeCodeForTokens(code: code, redirectURI: redirectURI, clientId: clientId, clientSecret: clientSecret)
|
|
|
|
|
+ await MainActor.run { self.onInteractiveSignInCompleted?() }
|
|
|
|
|
+ return tokens
|
|
|
} catch GoogleOAuthError.authenticationTimedOut {
|
|
} catch GoogleOAuthError.authenticationTimedOut {
|
|
|
throw ZoomOAuthError.authenticationTimedOut
|
|
throw ZoomOAuthError.authenticationTimedOut
|
|
|
} catch GoogleOAuthError.authenticationCancelled {
|
|
} catch GoogleOAuthError.authenticationCancelled {
|