forked from shadowfacts/Tusker
Improve error reporting for onboarding, use async/await
This commit is contained in:
parent
f31c909517
commit
12bcf52764
|
@ -67,28 +67,41 @@ 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
|
switch response {
|
||||||
guard case let .success(app, _) = response else { fatalError() }
|
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
|
||||||
client.getAccessToken(authorizationCode: authorizationCode, redirectURI: "tusker://oauth") { response in
|
func authorize(authorizationCode: String) async throws -> String {
|
||||||
guard case let .success(settings, _) = response else { fatalError() }
|
return try await withCheckedThrowingContinuation({ continuation in
|
||||||
self.client.accessToken = settings.accessToken
|
client.getAccessToken(authorizationCode: authorizationCode, redirectURI: "tusker://oauth") { response in
|
||||||
completion(settings.accessToken)
|
switch response {
|
||||||
}
|
case .failure(let error):
|
||||||
|
continuation.resume(throwing: error)
|
||||||
|
case .success(let settings, _):
|
||||||
|
self.client.accessToken = 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) {
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,59 +43,109 @@ class OnboardingViewController: UINavigationController {
|
||||||
instanceSelector.delegate = self
|
instanceSelector.delegate = self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
private func tryLoginTo(instanceURL: URL) async throws {
|
||||||
|
let mastodonController = MastodonController(instanceURL: instanceURL)
|
||||||
|
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)!
|
||||||
|
components.path = "/oauth/authorize"
|
||||||
|
components.queryItems = [
|
||||||
|
URLQueryItem(name: "client_id", value: clientID),
|
||||||
|
URLQueryItem(name: "response_type", value: "code"),
|
||||||
|
URLQueryItem(name: "scope", value: "read write follow"),
|
||||||
|
URLQueryItem(name: "redirect_uri", value: "tusker://oauth")
|
||||||
|
]
|
||||||
|
let authorizeURL = components.url!
|
||||||
|
|
||||||
|
return try await withCheckedThrowingContinuation({ continuation in
|
||||||
|
self.authenticationSession = ASWebAuthenticationSession(url: authorizeURL, callbackURLScheme: "tusker", completionHandler: { url, error in
|
||||||
|
if let error = error {
|
||||||
|
continuation.resume(throwing: Error.authenticationSessionError(error))
|
||||||
|
} else if let url = url,
|
||||||
|
let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
|
||||||
|
let item = components.queryItems?.first(where: { $0.name == "code" }),
|
||||||
|
let code = item.value {
|
||||||
|
continuation.resume(returning: code)
|
||||||
|
} else {
|
||||||
|
continuation.resume(throwing: Error.noAuthorizationCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// Prefer ephemeral sessions to make it easier to sign into multiple accounts on the same instance.
|
||||||
|
self.authenticationSession!.prefersEphemeralWebBrowserSession = true
|
||||||
|
self.authenticationSession!.presentationContextProvider = self
|
||||||
|
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 {
|
extension OnboardingViewController: InstanceSelectorTableViewControllerDelegate {
|
||||||
func didSelectInstance(url instanceURL: URL) {
|
func didSelectInstance(url instanceURL: URL) {
|
||||||
let mastodonController = MastodonController(instanceURL: instanceURL)
|
Task {
|
||||||
mastodonController.registerApp { (clientID, clientSecret) in
|
do {
|
||||||
|
try await self.tryLoginTo(instanceURL: instanceURL)
|
||||||
var components = URLComponents(url: instanceURL, resolvingAgainstBaseURL: false)!
|
} catch let error as Error {
|
||||||
components.path = "/oauth/authorize"
|
let alert = UIAlertController(title: "Error Logging In", message: error.localizedDescription, preferredStyle: .alert)
|
||||||
components.queryItems = [
|
alert.addAction(UIAlertAction(title: "Ok", style: .default))
|
||||||
URLQueryItem(name: "client_id", value: clientID),
|
self.present(alert, animated: true)
|
||||||
URLQueryItem(name: "response_type", value: "code"),
|
|
||||||
URLQueryItem(name: "scope", value: "read write follow"),
|
|
||||||
URLQueryItem(name: "redirect_uri", value: "tusker://oauth")
|
|
||||||
]
|
|
||||||
let authorizeURL = components.url!
|
|
||||||
|
|
||||||
self.authenticationSession = ASWebAuthenticationSession(url: authorizeURL, callbackURLScheme: "tusker") { url, error in
|
|
||||||
guard error == nil,
|
|
||||||
let url = url,
|
|
||||||
let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
|
|
||||||
let item = components.queryItems?.first(where: { $0.name == "code" }),
|
|
||||||
let authCode = item.value else { return }
|
|
||||||
|
|
||||||
mastodonController.authorize(authorizationCode: authCode) { (accessToken) in
|
|
||||||
// construct a temporary UserAccountInfo instance for the MastodonController to use to fetch it's own account
|
|
||||||
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.
|
|
||||||
self.authenticationSession!.prefersEphemeralWebBrowserSession = true
|
|
||||||
self.authenticationSession!.presentationContextProvider = self
|
|
||||||
self.authenticationSession!.start()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue