diff --git a/Demo App/Twift_SwiftUI/Twift_SwiftUIApp.swift b/Demo App/Twift_SwiftUI/Twift_SwiftUIApp.swift index bce5a4b..20447a6 100644 --- a/Demo App/Twift_SwiftUI/Twift_SwiftUIApp.swift +++ b/Demo App/Twift_SwiftUI/Twift_SwiftUIApp.swift @@ -45,7 +45,7 @@ struct Twift_SwiftUIApp: App { footer: Text("Use this authentication method for most cases. This test app enables all user scopes by default.") ) { AsyncButton { - let (user, _) = await Twift.Authentication().authenticateUser(clientId: CLIENT_ID, + let user = try? await Twift.Authentication().authenticateUser(clientId: CLIENT_ID, redirectUri: URL(string: TWITTER_CALLBACK_URL)!, scope: Set(OAuth2Scope.allCases)) diff --git a/README.md b/README.md index 80333d5..ca74878 100644 --- a/README.md +++ b/README.md @@ -29,19 +29,21 @@ You can authenticate users with `Twift.Authentication().authenticateUser()`: ```swift var client: Twift? -let (oauthUser, error) = await Twift.Authentication().authenticateUser( - clientId: TWITTER_CLIENT_ID, - redirectUri: URL(string: TWITTER_CALLBACK_URL)!, - scope: Set(OAuth2Scope.allCases) -) - -if let oauthUser = oauthUser { +do { + let oauthUser = try await Twift.Authentication().authenticateUser( + clientId: TWITTER_CLIENT_ID, + redirectUri: URL(string: TWITTER_CALLBACK_URL)!, + scope: Set(OAuth2Scope.allCases) + ) + client = Twift(.oauth2UserAuth(oauthUser)) // It's recommended that you store your user auth tokens via Keychain or another secure storage method. // OAuth2User can be encoded to a data object for storage and later retrieval. let encoded = try? JSONEncoder().encode(oauthUser)) saveUserAuthExample(encoded) // Saves the data to Keychain, for example +} catch { + print(error.localizedDescription) } ``` diff --git a/Sources/Twift+Authentication.swift b/Sources/Twift+Authentication.swift index 318e6a9..40a58db 100644 --- a/Sources/Twift+Authentication.swift +++ b/Sources/Twift+Authentication.swift @@ -144,11 +144,33 @@ extension Twift.Authentication { /// - scope: The user access scopes for your authentication. For automatic token refreshing, ensure that `offlineAccess` is included in the scope. /// - presentationContextProvider: Optional presentation context provider. When not provided, this function will handle the presentation context itself. /// - Returns: A tuple containing the authenticated user access tokens or any encoutered error. + @_disfavoredOverload + @available(*, deprecated, message: "Use throwing 'authenticateUser' function instead") public func authenticateUser(clientId: String, redirectUri: URL, scope: Set, presentationContextProvider: ASWebAuthenticationPresentationContextProviding? = nil ) async -> (OAuth2User?, Error?) { + do { + let oauthUser: OAuth2User = try await authenticateUser(clientId: clientId, redirectUri: redirectUri, scope: scope, presentationContextProvider: presentationContextProvider) + return (oauthUser, nil) + } catch { + return (nil, error) + } + } + + /// Authenticates the user using Twitter's OAuth 2.0 PKCE flow. + /// - Parameters: + /// - clientId: The client ID for your Twitter API app + /// - redirectUri: The URI to redirect users to after completing authentication. + /// - scope: The user access scopes for your authentication. For automatic token refreshing, ensure that `offlineAccess` is included in the scope. + /// - presentationContextProvider: Optional presentation context provider. When not provided, this function will handle the presentation context itself. + /// - Returns: The authenticated user access tokens. + public func authenticateUser(clientId: String, + redirectUri: URL, + scope: Set, + presentationContextProvider: ASWebAuthenticationPresentationContextProviding? = nil + ) async throws -> OAuth2User { let state = UUID().uuidString let authUrlQueryItems: [URLQueryItem] = [ @@ -167,39 +189,36 @@ extension Twift.Authentication { authUrl.path = "/i/oauth2/authorize" authUrl.queryItems = authUrlQueryItems - let (returnedUrl, error): (URL?, Error?) = await withCheckedContinuation { continuation in + let returnedUrl: URL = try await withCheckedThrowingContinuation { continuation in guard let authUrl = authUrl.url else { - return continuation.resume(returning: (nil, TwiftError.UnknownError(nil))) + return continuation.resume(throwing: TwiftError.UnknownError(nil)) } let authSession = ASWebAuthenticationSession(url: authUrl, callbackURLScheme: redirectUri.scheme) { (url, error) in - return continuation.resume(returning: (url, error)) + if let error = error { + return continuation.resume(throwing: error) + } + if let url = url { + return continuation.resume(returning: url) + } + return continuation.resume(throwing: TwiftError.UnknownError("There was a problem authenticating the user: no URL was returned from the first authentication step.")) } authSession.presentationContextProvider = presentationContextProvider ?? self authSession.start() } - if let error = error { - print(error.localizedDescription) - return (nil, error) - } - - guard let returnedUrl = returnedUrl else { - return (nil, TwiftError.UnknownError("There was a problem authenticating the user: no URL was returned from the first authentication step.")) - } - let returnedUrlComponents = URLComponents(string: returnedUrl.absoluteString) let returnedState = returnedUrlComponents?.queryItems?.first(where: { $0.name == "state" })?.value guard let returnedState = returnedState, returnedState == state else { - return (nil, TwiftError.UnknownError("There was a problem authenticating the user: the state values for the first authentication step are not equal.")) + throw TwiftError.UnknownError("There was a problem authenticating the user: the state values for the first authentication step are not equal.") } let returnedCode = returnedUrlComponents?.queryItems?.first(where: { $0.name == "code" })?.value guard let returnedCode = returnedCode else { - return (nil, TwiftError.UnknownError("There was a problem authenticating the user: no request token was found in the returned URL.")) + throw TwiftError.UnknownError("There was a problem authenticating the user: no request token was found in the returned URL.") } var codeRequest = URLRequest(url: URL(string: "https://api.twitter.com/2/oauth2/token")!) @@ -223,12 +242,12 @@ extension Twift.Authentication { var oauth2User = try JSONDecoder().decode(OAuth2User.self, from: data) oauth2User.clientId = clientId - return (oauth2User, nil) + return oauth2User } catch { print(error.localizedDescription) } - return (nil, TwiftError.UnknownError("Unable to fetch and decode the OAuth 2.0 user context.")) + throw TwiftError.UnknownError("Unable to fetch and decode the OAuth 2.0 user context.") } }