diff --git a/Frameworks/Account/Feedly/OAuthAccountAuthorizationOperation.swift b/Frameworks/Account/Feedly/OAuthAccountAuthorizationOperation.swift index 7c72d34d1..4378bedc5 100644 --- a/Frameworks/Account/Feedly/OAuthAccountAuthorizationOperation.swift +++ b/Frameworks/Account/Feedly/OAuthAccountAuthorizationOperation.swift @@ -71,10 +71,26 @@ public enum OAuthAccountAuthorizationOperationError: LocalizedError { self?.didEndAuthentication(url: url, error: error) } } - self.session = session + session.presentationContextProvider = self - session.start() + guard session.start() else { + + /// Documentation does not say on why `ASWebAuthenticationSession.start` or `canStart` might return false. + /// Perhaps it has something to do with an inter-process communication failure? No browsers installed? No browsers that support web authentication? + struct UnableToStartASWebAuthenticationSessionError: LocalizedError { + let errorDescription: String? = NSLocalizedString("Unable to start a web authentication session with the default web browser.", + comment: "OAuth - error description - unable to authorize because ASWebAuthenticationSession did not start.") + let recoverySuggestion: String? = NSLocalizedString("Check your default web browser in System Preferences or change it to Safari and try again.", + comment: "OAuth - recovery suggestion - ensure browser selected supports web authentication.") + } + + didFinish(UnableToStartASWebAuthenticationSessionError()) + + return + } + + self.session = session } public func cancel() { diff --git a/Mac/Preferences/Accounts/AccountsAddViewController.swift b/Mac/Preferences/Accounts/AccountsAddViewController.swift index 98f2e093a..ef6a2c441 100644 --- a/Mac/Preferences/Accounts/AccountsAddViewController.swift +++ b/Mac/Preferences/Accounts/AccountsAddViewController.swift @@ -106,6 +106,7 @@ extension AccountsAddViewController: AccountsAddTableCellViewDelegate { let accountsAddLocalWindowController = AccountsAddLocalWindowController() accountsAddLocalWindowController.runSheetOnWindow(self.view.window!) accountsAddWindowController = accountsAddLocalWindowController + case .cloudKit: let accountsAddCloudKitWindowController = AccountsAddCloudKitWindowController() accountsAddCloudKitWindowController.runSheetOnWindow(self.view.window!) { response in @@ -115,24 +116,32 @@ extension AccountsAddViewController: AccountsAddTableCellViewDelegate { } } accountsAddWindowController = accountsAddCloudKitWindowController + case .feedbin: let accountsFeedbinWindowController = AccountsFeedbinWindowController() accountsFeedbinWindowController.runSheetOnWindow(self.view.window!) accountsAddWindowController = accountsFeedbinWindowController + case .feedWrangler: let accountsFeedWranglerWindowController = AccountsFeedWranglerWindowController() accountsFeedWranglerWindowController.runSheetOnWindow(self.view.window!) accountsAddWindowController = accountsFeedWranglerWindowController + case .freshRSS: let accountsReaderAPIWindowController = AccountsReaderAPIWindowController() accountsReaderAPIWindowController.accountType = .freshRSS accountsReaderAPIWindowController.runSheetOnWindow(self.view.window!) accountsAddWindowController = accountsReaderAPIWindowController + case .feedly: let addAccount = OAuthAccountAuthorizationOperation(accountType: .feedly) addAccount.delegate = self addAccount.presentationAnchor = self.view.window! + + runAwaitingFeedlyLoginAlertModal(forLifetimeOf: addAccount) + MainThreadOperationQueue.shared.add(addAccount) + case .newsBlur: let accountsNewsBlurWindowController = AccountsNewsBlurWindowController() accountsNewsBlurWindowController.runSheetOnWindow(self.view.window!) @@ -141,6 +150,32 @@ extension AccountsAddViewController: AccountsAddTableCellViewDelegate { } + private func runAwaitingFeedlyLoginAlertModal(forLifetimeOf operation: OAuthAccountAuthorizationOperation) { + let alert = NSAlert() + alert.alertStyle = .informational + alert.messageText = NSLocalizedString("Waiting for access to Feedly", + comment: "Alert title when adding a Feedly account and waiting for authorization from the user.") + + alert.informativeText = NSLocalizedString("Your default web browser will open the Feedly login for you to authorize access.", + comment: "Alert informative text when adding a Feedly account and waiting for authorization from the user.") + + alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "Cancel")) + + let attachedWindow = self.view.window! + + alert.beginSheetModal(for: attachedWindow) { response in + if response == .alertFirstButtonReturn { + operation.cancel() + } + } + + operation.completionBlock = { _ in + guard alert.window.isVisible else { + return + } + attachedWindow.endSheet(alert.window) + } + } } // MARK: OAuthAccountAuthorizationOperationDelegate @@ -148,6 +183,12 @@ extension AccountsAddViewController: AccountsAddTableCellViewDelegate { extension AccountsAddViewController: OAuthAccountAuthorizationOperationDelegate { func oauthAccountAuthorizationOperation(_ operation: OAuthAccountAuthorizationOperation, didCreate account: Account) { + // `OAuthAccountAuthorizationOperation` is using `ASWebAuthenticationSession` which bounces the user + // to their browser on macOS for authorizing NetNewsWire to access the user's Feedly account. + // When this authorization is granted, the browser remains the foreground app which is unfortunate + // because the user probably wants to see the result of authorizing NetNewsWire to act on their behalf. + NSApp.activate(ignoringOtherApps: true) + account.refreshAll { [weak self] result in switch result { case .success: @@ -159,6 +200,10 @@ extension AccountsAddViewController: OAuthAccountAuthorizationOperationDelegate } func oauthAccountAuthorizationOperation(_ operation: OAuthAccountAuthorizationOperation, didFailWith error: Error) { + // `OAuthAccountAuthorizationOperation` is using `ASWebAuthenticationSession` which bounces the user + // to their browser on macOS for authorizing NetNewsWire to access the user's Feedly account. + NSApp.activate(ignoringOtherApps: true) + view.window?.presentError(error) } } diff --git a/Multiplatform/macOS/Preferences/Preference Panes/Accounts/Account Preferences/Add Account/AddAccountModel.swift b/Multiplatform/macOS/Preferences/Preference Panes/Accounts/Account Preferences/Add Account/AddAccountModel.swift index 6354600ba..21074d617 100644 --- a/Multiplatform/macOS/Preferences/Preference Panes/Accounts/Account Preferences/Add Account/AddAccountModel.swift +++ b/Multiplatform/macOS/Preferences/Preference Panes/Accounts/Account Preferences/Add Account/AddAccountModel.swift @@ -277,8 +277,14 @@ extension AddAccountModel { // MARK:- OAuthAccountAuthorizationOperationDelegate extension AddAccountModel: OAuthAccountAuthorizationOperationDelegate { func oauthAccountAuthorizationOperation(_ operation: OAuthAccountAuthorizationOperation, didCreate account: Account) { + accountIsAuthenticating = false accountAdded = true + + // macOS only: `ASWebAuthenticationSession` leaves the browser in the foreground. + // Ensure the app is in the foreground so the user can see their Feedly account load. + NSApplication.shared.activate(ignoringOtherApps: true) + account.refreshAll { [weak self] result in switch result { case .success: @@ -291,6 +297,11 @@ extension AddAccountModel: OAuthAccountAuthorizationOperationDelegate { func oauthAccountAuthorizationOperation(_ operation: OAuthAccountAuthorizationOperation, didFailWith error: Error) { accountIsAuthenticating = false + + // macOS only: `ASWebAuthenticationSession` leaves the browser in the foreground. + // Ensure the app is in the foreground so the user can see the error. + NSApplication.shared.activate(ignoringOtherApps: true) + addAccountError = .other(error: error) } }