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

Fix Google account mismatch when opening Meet links

Route Google OAuth through the default browser and add account hints when opening Google URLs so Meet continues with the same signed-in account (or prompts login).

Co-authored-by: Cursor <cursoragent@cursor.com>
huzaifahayat12 1 месяц назад
Родитель
Сommit
889274c388

+ 51 - 24
meetings_app/Auth/GoogleOAuthService.swift

@@ -55,6 +55,35 @@ final class GoogleOAuthService: NSObject {
55
     private let tokenStore = KeychainTokenStore()
55
     private let tokenStore = KeychainTokenStore()
56
     @MainActor private var inAppOAuthWindowController: InAppOAuthWindowController?
56
     @MainActor private var inAppOAuthWindowController: InAppOAuthWindowController?
57
     private override init() {}
57
     private override init() {}
58
+    
59
+    private let lastSignedInEmailDefaultsKey = "google.oauth.lastSignedInEmail"
60
+    
61
+    func lastSignedInEmailHint() -> String? {
62
+        let value = UserDefaults.standard
63
+            .string(forKey: lastSignedInEmailDefaultsKey)?
64
+            .trimmingCharacters(in: .whitespacesAndNewlines)
65
+        guard let value, value.isEmpty == false else { return nil }
66
+        return value.lowercased()
67
+    }
68
+    
69
+    /// Wraps a Google URL so the default browser is nudged to use the same account as the app.
70
+    /// If the browser isn't signed into that account, Google will prompt to sign in.
71
+    func urlForPreferredGoogleAccountIfPossible(_ url: URL) -> URL {
72
+        guard let email = lastSignedInEmailHint() else { return url }
73
+        guard let scheme = url.scheme?.lowercased(), scheme == "https" || scheme == "http" else { return url }
74
+        guard let host = url.host?.lowercased(), host.isEmpty == false else { return url }
75
+        
76
+        // Only attempt for Google properties where account mixups are common.
77
+        let isGoogle = host == "google.com" || host.hasSuffix(".google.com")
78
+        guard isGoogle else { return url }
79
+        
80
+        var components = URLComponents(string: "https://accounts.google.com/AccountChooser")!
81
+        components.queryItems = [
82
+            URLQueryItem(name: "continue", value: url.absoluteString),
83
+            URLQueryItem(name: "Email", value: email)
84
+        ]
85
+        return components.url ?? url
86
+    }
58
 
87
 
59
     func configuredClientId() -> String? {
88
     func configuredClientId() -> String? {
60
         let value = UserDefaults.standard.string(forKey: clientIdDefaultsKey)?.trimmingCharacters(in: .whitespacesAndNewlines)
89
         let value = UserDefaults.standard.string(forKey: clientIdDefaultsKey)?.trimmingCharacters(in: .whitespacesAndNewlines)
@@ -108,7 +137,12 @@ final class GoogleOAuthService: NSObject {
108
             let details = String(data: data, encoding: .utf8) ?? "HTTP \((response as? HTTPURLResponse)?.statusCode ?? -1)"
137
             let details = String(data: data, encoding: .utf8) ?? "HTTP \((response as? HTTPURLResponse)?.statusCode ?? -1)"
109
             throw GoogleOAuthError.tokenExchangeFailed(details)
138
             throw GoogleOAuthError.tokenExchangeFailed(details)
110
         }
139
         }
111
-        return try JSONDecoder().decode(GoogleUserProfile.self, from: data)
140
+        let profile = try JSONDecoder().decode(GoogleUserProfile.self, from: data)
141
+        let cleaned = profile.email?.trimmingCharacters(in: .whitespacesAndNewlines)
142
+        if let cleaned, cleaned.isEmpty == false {
143
+            UserDefaults.standard.set(cleaned.lowercased(), forKey: lastSignedInEmailDefaultsKey)
144
+        }
145
+        return profile
112
     }
146
     }
113
 
147
 
114
     func validAccessToken(presentingWindow: NSWindow?) async throws -> String {
148
     func validAccessToken(presentingWindow: NSWindow?) async throws -> String {
@@ -140,9 +174,13 @@ final class GoogleOAuthService: NSObject {
140
         let loopback = try await OAuthLoopbackServer.start()
174
         let loopback = try await OAuthLoopbackServer.start()
141
         defer { loopback.stop() }
175
         defer { loopback.stop() }
142
         let redirectURI = loopback.redirectURI
176
         let redirectURI = loopback.redirectURI
177
+        
178
+        let lastEmailHint = UserDefaults.standard
179
+            .string(forKey: lastSignedInEmailDefaultsKey)?
180
+            .trimmingCharacters(in: .whitespacesAndNewlines)
143
 
181
 
144
         var components = URLComponents(string: "https://accounts.google.com/o/oauth2/v2/auth")!
182
         var components = URLComponents(string: "https://accounts.google.com/o/oauth2/v2/auth")!
145
-        components.queryItems = [
183
+        var queryItems: [URLQueryItem] = [
146
             URLQueryItem(name: "client_id", value: clientId),
184
             URLQueryItem(name: "client_id", value: clientId),
147
             URLQueryItem(name: "redirect_uri", value: redirectURI),
185
             URLQueryItem(name: "redirect_uri", value: redirectURI),
148
             URLQueryItem(name: "response_type", value: "code"),
186
             URLQueryItem(name: "response_type", value: "code"),
@@ -152,19 +190,21 @@ final class GoogleOAuthService: NSObject {
152
             URLQueryItem(name: "state", value: state),
190
             URLQueryItem(name: "state", value: state),
153
             URLQueryItem(name: "code_challenge", value: codeChallenge),
191
             URLQueryItem(name: "code_challenge", value: codeChallenge),
154
             URLQueryItem(name: "code_challenge_method", value: "S256"),
192
             URLQueryItem(name: "code_challenge_method", value: "S256"),
155
-            URLQueryItem(name: "access_type", value: "offline")
193
+            URLQueryItem(name: "access_type", value: "offline"),
194
+            // Prefer the same Google account when multiple are present, and if none are signed in
195
+            // this forces Google to show sign-in UI.
196
+            URLQueryItem(name: "prompt", value: "select_account")
156
         ]
197
         ]
198
+        if let lastEmailHint, lastEmailHint.isEmpty == false {
199
+            queryItems.append(URLQueryItem(name: "login_hint", value: lastEmailHint))
200
+        }
201
+        components.queryItems = queryItems
157
 
202
 
158
         guard let authURL = components.url else { throw GoogleOAuthError.invalidCallbackURL }
203
         guard let authURL = components.url else { throw GoogleOAuthError.invalidCallbackURL }
159
         let opened = await MainActor.run { [self] in
204
         let opened = await MainActor.run { [self] in
160
-            openAuthURLInApp(authURL, presentingWindow: presentingWindow)
205
+            openAuthURLInDefaultBrowser(authURL)
161
         }
206
         }
162
         guard opened else { throw GoogleOAuthError.unableToOpenBrowser }
207
         guard opened else { throw GoogleOAuthError.unableToOpenBrowser }
163
-        defer {
164
-            Task { @MainActor [weak self] in
165
-                self?.closeInAppOAuthWindow()
166
-            }
167
-        }
168
         let callbackURL = try await loopback.waitForCallback()
208
         let callbackURL = try await loopback.waitForCallback()
169
 
209
 
170
         guard let returnedState = URLComponents(url: callbackURL, resolvingAgainstBaseURL: false)?
210
         guard let returnedState = URLComponents(url: callbackURL, resolvingAgainstBaseURL: false)?
@@ -189,21 +229,8 @@ final class GoogleOAuthService: NSObject {
189
     }
229
     }
190
 
230
 
191
     @MainActor
231
     @MainActor
192
-    private func openAuthURLInApp(_ url: URL, presentingWindow: NSWindow?) -> Bool {
193
-        let controller: InAppOAuthWindowController
194
-        if let existing = inAppOAuthWindowController {
195
-            controller = existing
196
-        } else {
197
-            controller = InAppOAuthWindowController()
198
-            inAppOAuthWindowController = controller
199
-        }
200
-        controller.alignWithPresentingWindow(presentingWindow)
201
-        controller.load(url: url)
202
-        controller.showWindow(nil)
203
-        controller.window?.makeKeyAndOrderFront(nil)
204
-        controller.window?.orderFrontRegardless()
205
-        NSApp.activate(ignoringOtherApps: true)
206
-        return true
232
+    private func openAuthURLInDefaultBrowser(_ url: URL) -> Bool {
233
+        NSWorkspace.shared.open(url)
207
     }
234
     }
208
 
235
 
209
     @MainActor
236
     @MainActor

+ 2 - 1
meetings_app/StatusBar/StatusBarController.swift

@@ -130,7 +130,8 @@ final class StatusBarController: NSObject {
130
     @objc private func meetingClicked(_ sender: NSMenuItem) {
130
     @objc private func meetingClicked(_ sender: NSMenuItem) {
131
         guard let link = sender.representedObject as? String,
131
         guard let link = sender.representedObject as? String,
132
               let url = URL(string: link.trimmingCharacters(in: .whitespacesAndNewlines)) else { return }
132
               let url = URL(string: link.trimmingCharacters(in: .whitespacesAndNewlines)) else { return }
133
-        NSWorkspace.shared.open(url)
133
+        let routed = authService.urlForPreferredGoogleAccountIfPossible(url)
134
+        NSWorkspace.shared.open(routed)
134
     }
135
     }
135
 
136
 
136
     @objc private func quitClicked() {
137
     @objc private func quitClicked() {

+ 3 - 2
meetings_app/ViewController.swift

@@ -1076,7 +1076,8 @@ private extension ViewController {
1076
     }
1076
     }
1077
 
1077
 
1078
     private func openInDefaultBrowser(url: URL) {
1078
     private func openInDefaultBrowser(url: URL) {
1079
-        NSWorkspace.shared.open(url, configuration: NSWorkspace.OpenConfiguration()) { [weak self] _, error in
1079
+        let routed = googleOAuth.urlForPreferredGoogleAccountIfPossible(url)
1080
+        NSWorkspace.shared.open(routed, configuration: NSWorkspace.OpenConfiguration()) { [weak self] _, error in
1080
             if let error {
1081
             if let error {
1081
                 DispatchQueue.main.async {
1082
                 DispatchQueue.main.async {
1082
                     self?.showSimpleAlert(title: "Unable to open browser", message: error.localizedDescription)
1083
                     self?.showSimpleAlert(title: "Unable to open browser", message: error.localizedDescription)
@@ -9534,7 +9535,7 @@ private extension ViewController {
9534
         let title = meeting?.title ?? "Scheduled Meeting"
9535
         let title = meeting?.title ?? "Scheduled Meeting"
9535
         let shouldOpenMeeting = beginMeetingRecordingIfConsented(meetingTitle: title, meetingURL: url)
9536
         let shouldOpenMeeting = beginMeetingRecordingIfConsented(meetingTitle: title, meetingURL: url)
9536
         guard shouldOpenMeeting else { return }
9537
         guard shouldOpenMeeting else { return }
9537
-        NSWorkspace.shared.open(url)
9538
+        openInDefaultBrowser(url: url)
9538
     }
9539
     }
9539
 
9540
 
9540
     private func renderScheduleCards(into stack: NSStackView, meetings: [ScheduledMeeting]) {
9541
     private func renderScheduleCards(into stack: NSStackView, meetings: [ScheduledMeeting]) {