Improve error reporting for onboarding, use async/await

This commit is contained in:
Shadowfacts 2022-03-29 11:58:11 -04:00
parent f31c909517
commit 12bcf52764
2 changed files with 143 additions and 66 deletions

View File

@ -67,29 +67,42 @@ class MastodonController: ObservableObject {
return client.run(request, completion: completion) return client.run(request, completion: completion)
} }
func registerApp(completion: @escaping (_ clientID: String, _ clientSecret: String) -> Void) { /// - Returns: A tuple of client ID and client secret.
guard client.clientID == nil, func registerApp() async throws -> (String, String) {
client.clientSecret == nil else { if let clientID = client.clientID,
let clientSecret = client.clientSecret {
completion(client.clientID!, client.clientSecret!) return (clientID, clientSecret)
return } else {
} let app: RegisteredApplication = try await withCheckedThrowingContinuation({ continuation in
client.registerApp(name: "Tusker", redirectURI: "tusker://oauth", scopes: [.read, .write, .follow]) { response in client.registerApp(name: "Tusker", redirectURI: "tusker://oauth", scopes: [.read, .write, .follow]) { response in
guard case let .success(app, _) = response else { fatalError() } switch response {
case .failure(let error):
continuation.resume(throwing: error)
case .success(let app, _):
continuation.resume(returning: app)
}
}
})
self.client.clientID = app.clientID self.client.clientID = app.clientID
self.client.clientSecret = app.clientSecret self.client.clientSecret = app.clientSecret
completion(app.clientID, app.clientSecret) return (app.clientID, app.clientSecret)
} }
} }
func authorize(authorizationCode: String, completion: @escaping (_ accessToken: String) -> Void) { /// - Returns: The access token
func authorize(authorizationCode: String) async throws -> String {
return try await withCheckedThrowingContinuation({ continuation in
client.getAccessToken(authorizationCode: authorizationCode, redirectURI: "tusker://oauth") { response in client.getAccessToken(authorizationCode: authorizationCode, redirectURI: "tusker://oauth") { response in
guard case let .success(settings, _) = response else { fatalError() } switch response {
case .failure(let error):
continuation.resume(throwing: error)
case .success(let settings, _):
self.client.accessToken = settings.accessToken self.client.accessToken = settings.accessToken
completion(settings.accessToken) continuation.resume(returning: settings.accessToken)
} }
} }
})
}
func getOwnAccount(completion: ((Result<Account, Client.Error>) -> Void)? = nil) { func getOwnAccount(completion: ((Result<Account, Client.Error>) -> Void)? = nil) {
if account != nil { if account != nil {
@ -120,6 +133,18 @@ class MastodonController: ObservableObject {
} }
} }
func getOwnAccount() async throws -> Account {
if let account = account {
return account
} else {
return try await withCheckedThrowingContinuation({ continuation in
self.getOwnAccount { result in
continuation.resume(with: result)
}
})
}
}
func getOwnInstance(completion: ((Instance) -> Void)? = nil) { func getOwnInstance(completion: ((Instance) -> Void)? = nil) {
getOwnInstanceInternal(retryAttempt: 0, completion: completion) getOwnInstanceInternal(retryAttempt: 0, completion: completion)
} }

View File

@ -8,8 +8,10 @@
import UIKit import UIKit
import AuthenticationServices import AuthenticationServices
import Pachyderm
protocol OnboardingViewControllerDelegate { protocol OnboardingViewControllerDelegate {
@MainActor
func didFinishOnboarding(account: LocalData.UserAccountInfo) func didFinishOnboarding(account: LocalData.UserAccountInfo)
} }
@ -41,13 +43,43 @@ class OnboardingViewController: UINavigationController {
instanceSelector.delegate = self instanceSelector.delegate = self
} }
} @MainActor
private func tryLoginTo(instanceURL: URL) async throws {
extension OnboardingViewController: InstanceSelectorTableViewControllerDelegate {
func didSelectInstance(url instanceURL: URL) {
let mastodonController = MastodonController(instanceURL: instanceURL) let mastodonController = MastodonController(instanceURL: instanceURL)
mastodonController.registerApp { (clientID, clientSecret) in let clientID: String
let clientSecret: String
do {
(clientID, clientSecret) = try await mastodonController.registerApp()
} catch {
throw Error.registeringApp(error)
}
let authCode = try await getAuthorizationCode(instanceURL: instanceURL, clientID: clientID)
let accessToken: String
do {
accessToken = try await mastodonController.authorize(authorizationCode: authCode)
} catch {
throw Error.gettingAccessToken(error)
}
// construct a temporary UserAccountInfo instance for the MastodonController to use to fetch its own account
let tempAccountInfo = LocalData.UserAccountInfo(id: "temp", instanceURL: instanceURL, clientID: clientID, clientSecret: clientSecret, username: nil, accessToken: accessToken)
mastodonController.accountInfo = tempAccountInfo
let ownAccount: Account
do {
ownAccount = try await mastodonController.getOwnAccount()
} catch {
throw Error.gettingOwnAccount(error)
}
let accountInfo = LocalData.shared.addAccount(instanceURL: instanceURL, clientID: clientID, clientSecret: clientSecret, username: ownAccount.username, accessToken: accessToken)
mastodonController.accountInfo = accountInfo
self.onboardingDelegate?.didFinishOnboarding(account: accountInfo)
}
@MainActor
private func getAuthorizationCode(instanceURL: URL, clientID: String) async throws -> String {
var components = URLComponents(url: instanceURL, resolvingAgainstBaseURL: false)! var components = URLComponents(url: instanceURL, resolvingAgainstBaseURL: false)!
components.path = "/oauth/authorize" components.path = "/oauth/authorize"
components.queryItems = [ components.queryItems = [
@ -58,42 +90,62 @@ extension OnboardingViewController: InstanceSelectorTableViewControllerDelegate
] ]
let authorizeURL = components.url! let authorizeURL = components.url!
self.authenticationSession = ASWebAuthenticationSession(url: authorizeURL, callbackURLScheme: "tusker") { url, error in return try await withCheckedThrowingContinuation({ continuation in
guard error == nil, self.authenticationSession = ASWebAuthenticationSession(url: authorizeURL, callbackURLScheme: "tusker", completionHandler: { url, error in
let url = url, if let error = error {
continuation.resume(throwing: Error.authenticationSessionError(error))
} else if let url = url,
let components = URLComponents(url: url, resolvingAgainstBaseURL: true), let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
let item = components.queryItems?.first(where: { $0.name == "code" }), let item = components.queryItems?.first(where: { $0.name == "code" }),
let authCode = item.value else { return } let code = item.value {
continuation.resume(returning: code)
mastodonController.authorize(authorizationCode: authCode) { (accessToken) in } else {
// construct a temporary UserAccountInfo instance for the MastodonController to use to fetch it's own account continuation.resume(throwing: Error.noAuthorizationCode)
let tempAccountInfo = LocalData.UserAccountInfo(id: "temp", instanceURL: instanceURL, clientID: clientID, clientSecret: clientSecret, username: nil, accessToken: accessToken)
mastodonController.accountInfo = tempAccountInfo
mastodonController.getOwnAccount { (result) in
DispatchQueue.main.async {
switch result {
case let .failure(error):
let alert = UIAlertController(title: "Unable to Verify Credentials", message: "Your account could not be fetched at this time: \(error.localizedDescription)", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Ok", style: .default, handler: nil))
self.present(alert, animated: true)
case let .success(account):
// this needs to happen on the main thread because it publishes a new value for the ObservableObject
let accountInfo = LocalData.shared.addAccount(instanceURL: instanceURL, clientID: clientID, clientSecret: clientSecret, username: account.username, accessToken: accessToken)
mastodonController.accountInfo = accountInfo
self.onboardingDelegate?.didFinishOnboarding(account: accountInfo)
} }
} })
}
}
}
DispatchQueue.main.async {
// Prefer ephemeral sessions to make it easier to sign into multiple accounts on the same instance. // Prefer ephemeral sessions to make it easier to sign into multiple accounts on the same instance.
self.authenticationSession!.prefersEphemeralWebBrowserSession = true self.authenticationSession!.prefersEphemeralWebBrowserSession = true
self.authenticationSession!.presentationContextProvider = self self.authenticationSession!.presentationContextProvider = self
self.authenticationSession!.start() self.authenticationSession!.start()
})
}
}
extension OnboardingViewController {
enum Error: Swift.Error {
case registeringApp(Swift.Error)
case authenticationSessionError(Swift.Error)
case noAuthorizationCode
case gettingAccessToken(Swift.Error)
case gettingOwnAccount(Swift.Error)
var localizedDescription: String {
switch self {
case .registeringApp(let error):
return "Couldn't register app: \(error)"
case .authenticationSessionError(let error):
return error.localizedDescription
case .noAuthorizationCode:
return "No authorization code"
case .gettingAccessToken(let error):
return "Couldn't get access token: \(error)"
case .gettingOwnAccount(let error):
return "Couldn't fetch account: \(error)"
}
}
}
}
extension OnboardingViewController: InstanceSelectorTableViewControllerDelegate {
func didSelectInstance(url instanceURL: URL) {
Task {
do {
try await self.tryLoginTo(instanceURL: instanceURL)
} catch let error as Error {
let alert = UIAlertController(title: "Error Logging In", message: error.localizedDescription, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Ok", style: .default))
self.present(alert, animated: true)
} }
} }
} }