diff --git a/Pachyderm/Client.swift b/Pachyderm/Client.swift index 21668cb0..fa4d02e1 100644 --- a/Pachyderm/Client.swift +++ b/Pachyderm/Client.swift @@ -101,6 +101,19 @@ public class Client { return task } + public func run(_ request: Request) async throws -> (Result, Pagination?) { + return try await withCheckedThrowingContinuation { continuation in + run(request) { response in + switch response { + case let .failure(error): + continuation.resume(throwing: error) + case let .success(result, pagination): + continuation.resume(returning: (result, pagination)) + } + } + } + } + func createURLRequest(request: Request) -> URLRequest? { guard var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: true) else { return nil } components.path = request.path @@ -117,24 +130,21 @@ public class Client { } // MARK: - Authorization - public func registerApp(name: String, redirectURI: String, scopes: [Scope], website: URL? = nil, completion: @escaping Callback) { + public func registerApp(name: String, redirectURI: String, scopes: [Scope], website: URL? = nil) async throws -> RegisteredApplication { let request = Request(method: .post, path: "/api/v1/apps", body: ParametersBody([ "client_name" => name, "redirect_uris" => redirectURI, "scopes" => scopes.scopeString, "website" => website?.absoluteString ])) - run(request) { result in - defer { completion(result) } - guard case let .success(application, _) = result else { return } - - self.appID = application.id - self.clientID = application.clientID - self.clientSecret = application.clientSecret - } + let (application, _) = try await run(request) + self.appID = application.id + self.clientID = application.clientID + self.clientSecret = application.clientSecret + return application } - public func getAccessToken(authorizationCode: String, redirectURI: String, completion: @escaping Callback) { + public func getAccessToken(authorizationCode: String, redirectURI: String) async throws -> LoginSettings { let request = Request(method: .post, path: "/oauth/token", body: ParametersBody([ "client_id" => clientID, "client_secret" => clientSecret, @@ -142,12 +152,9 @@ public class Client { "code" => authorizationCode, "redirect_uri" => redirectURI ])) - run(request) { result in - defer { completion(result) } - guard case let .success(loginSettings, _) = result else { return } - - self.accessToken = loginSettings.accessToken - } + let (settings, _) = try await run(request) + self.accessToken = settings.accessToken + return settings } // MARK: - Self diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 487bf316..3c9d1290 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -2342,6 +2342,7 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Pachyderm/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; "IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -2373,6 +2374,7 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Pachyderm/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; "IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -2560,7 +2562,7 @@ CURRENT_PROJECT_VERSION = 19; DEVELOPMENT_TEAM = V4WK9KR9U2; INFOPLIST_FILE = Tusker/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.1; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; "IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -2593,7 +2595,7 @@ CURRENT_PROJECT_VERSION = 19; DEVELOPMENT_TEAM = V4WK9KR9U2; INFOPLIST_FILE = Tusker/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.1; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; "IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", diff --git a/Tusker.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Tusker.xcworkspace/xcshareddata/swiftpm/Package.resolved index 7bb7b47d..abb274a3 100644 --- a/Tusker.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Tusker.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -2,7 +2,7 @@ "object": { "pins": [ { - "package": "PLCrashReporter", + "package": "plcrashreporter", "repositoryURL": "https://github.com/microsoft/plcrashreporter", "state": { "branch": null, diff --git a/Tusker/Controllers/MastodonController.swift b/Tusker/Controllers/MastodonController.swift index b132f3d0..362e430b 100644 --- a/Tusker/Controllers/MastodonController.swift +++ b/Tusker/Controllers/MastodonController.swift @@ -65,28 +65,22 @@ class MastodonController: ObservableObject { return client.run(request, completion: completion) } - func registerApp(completion: @escaping (_ clientID: String, _ clientSecret: String) -> Void) { - guard client.clientID == nil, - client.clientSecret == nil else { - - completion(client.clientID!, client.clientSecret!) - return - } - - client.registerApp(name: "Tusker", redirectURI: "tusker://oauth", scopes: [.read, .write, .follow]) { response in - guard case let .success(app, _) = response else { fatalError() } - self.client.clientID = app.clientID - self.client.clientSecret = app.clientSecret - completion(app.clientID, app.clientSecret) - } + @discardableResult + func run(_ request: Request) async throws -> (Result, Pagination?) { + return try await client.run(request) } - func authorize(authorizationCode: String, completion: @escaping (_ accessToken: String) -> Void) { - client.getAccessToken(authorizationCode: authorizationCode, redirectURI: "tusker://oauth") { response in - guard case let .success(settings, _) = response else { fatalError() } - self.client.accessToken = settings.accessToken - completion(settings.accessToken) - } + func registerApp() async -> (String, String) { + guard client.clientID == nil, + client.clientSecret == nil else { + return (client.clientID!, client.clientSecret!) + } + let app = try! await client.registerApp(name: "Tusker", redirectURI: "tusker://oauth", scopes: [.read, .write, .follow]) + return (app.clientID, app.clientSecret) + } + + func authorize(authorizationCode: String) async -> String { + return try! await client.getAccessToken(authorizationCode: authorizationCode, redirectURI: "tusker://oauth").accessToken } func getOwnAccount(completion: ((Result) -> Void)? = nil) { @@ -118,6 +112,12 @@ class MastodonController: ObservableObject { } } + func getOwnAccount() async throws -> Account { + return try await withCheckedThrowingContinuation { continuation in + getOwnAccount(completion: continuation.resume) + } + } + func getOwnInstance(completion: ((Instance) -> Void)? = nil) { getOwnInstanceInternal(retryAttempt: 0, completion: completion) } diff --git a/Tusker/Screens/Onboarding/OnboardingViewController.swift b/Tusker/Screens/Onboarding/OnboardingViewController.swift index e660fba6..85b6a2bf 100644 --- a/Tusker/Screens/Onboarding/OnboardingViewController.swift +++ b/Tusker/Screens/Onboarding/OnboardingViewController.swift @@ -8,6 +8,7 @@ import UIKit import AuthenticationServices +import Pachyderm protocol OnboardingViewControllerDelegate { func didFinishOnboarding(account: LocalData.UserAccountInfo) @@ -40,54 +41,20 @@ class OnboardingViewController: UINavigationController { instanceSelector.delegate = self } - -} - -extension OnboardingViewController: InstanceSelectorTableViewControllerDelegate { - func didSelectInstance(url instanceURL: URL) { - let mastodonController = MastodonController(instanceURL: instanceURL) - mastodonController.registerApp { (clientID, clientSecret) in - - 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! - - 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) - } - } + + private func doAuthenticationSession(url: URL) async throws -> URL { + return try await withCheckedThrowingContinuation { continuation in + self.authenticationSession = ASWebAuthenticationSession(url: url, callbackURLScheme: "tusker") { url, error in + defer { + DispatchQueue.main.async { + self.authenticationSession = nil } } + if let url = url { + continuation.resume(returning: url) + } else { + continuation.resume(throwing: error!) + } } DispatchQueue.main.async { // Prefer ephemeral sessions to make it easier to sign into multiple accounts on the same instance. @@ -97,6 +64,53 @@ extension OnboardingViewController: InstanceSelectorTableViewControllerDelegate } } } + +} + +extension OnboardingViewController: InstanceSelectorTableViewControllerDelegate { + func didSelectInstance(url instanceURL: URL) { + async { + let mastodonController = MastodonController(instanceURL: instanceURL) + let (clientID, clientSecret) = await mastodonController.registerApp() + + 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") + ] + + guard let url = try? await self.doAuthenticationSession(url: components.url!), + let components = URLComponents(url: url, resolvingAgainstBaseURL: true), + let item = components.queryItems?.first(where: { $0.name == "code" }), + let authCode = item.value else { + return + } + + let accessToken = await mastodonController.authorize(authorizationCode: authCode) + + // 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 account: Account + do { + account = try await mastodonController.getOwnAccount() + } catch { + let alert = UIAlertController(title: "Unable to Verify Credentials", message: "Your account fcould not be fetcheda this time: \(error.localizedDescription)", preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "Ok", style: .default, handler: nil)) + self.present(alert, animated: true) + return + } + + let accountInfo = LocalData.shared.addAccount(instanceURL: instanceURL, clientID: clientID, clientSecret: clientSecret, username: account.username, accessToken: accessToken) + mastodonController.accountInfo = accountInfo + + self.onboardingDelegate?.didFinishOnboarding(account: accountInfo) + } + } } extension OnboardingViewController: ASWebAuthenticationPresentationContextProviding {