Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/Rewrite to async #24

Merged
merged 7 commits into from
May 15, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 6 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,19 +54,13 @@ let manager = SocialAuthenticationManager(authenticators: [AppleAuthenticator(),

// signIn user with Apple
let appleAuthenticator = manager.authenticator(for: AppleAuthenticator.self)
appleAuthenticator?
let result = try await appleAuthenticator?
.signIn(from: <view-controller-instance>, with: .random(length: 32))
.finally {
// handle result
}

// signIn user with Facebook
let facebookAuthenticator = manager.authenticator(for: FacebookAuthenticator.self)
facebookAuthenticator?
let result = try await facebookAuthenticator?
.signIn(from: <view-controller-instance>, with: [.email])
.finally {
// handle result
}

// return currently authenticated authenticator
let authenticated: Authenticator? = manager.authenticator
Expand Down Expand Up @@ -123,19 +117,19 @@ You can easily add new authenticator that is not built-in with PovioKitAuth pack

```swift
final class SnapchatAuthenticator: Authenticator {
public func signIn(from presentingViewController: UIViewController) -> Promise<Response> {
Promise { seal in
public func signIn(from presentingViewController: UIViewController) async throws -> Response {
try await withCheckedThrowingContinuation { continuation in
SCSDKLoginClient.login(from: presentingViewController) { [weak self] success, error in
guard success, error == nil else {
seal.reject(with: error)
continuation.resume(throwing: error)
return
}

let query = "{me{displayName, bitmoji{avatar}}}"
let variables = ["page": "bitmoji"]
SCSDKLoginClient.fetchUserData(withQuery: query, variables: variables) { resources in
...
seal.resolve(with: response)
continuation.resume(returning: response)
}
}
}
Expand Down
17 changes: 4 additions & 13 deletions Resources/Apple/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,29 +20,20 @@ Please read [official documentation](https://developer.apple.com/sign-in-with-ap
let authenticator = AppleAuthenticator() // conforms to `AppleAuthProvidable` protocol

// signIn user
authenticator
let result = try await authenticator
.signIn(from: <view-controller-instance>)
.finally {
// handle result
}

// signIn user with nonce
authenticator
let result = try await authenticator
.signIn(from: <view-controller-instance>, with: .random(length: 32))
.finally {
// handle result
}

// get authentication status
let status = authenticator.isAuthenticated

// check authentication status
// we should check this when we need to explicitly query authenticator to check if authenticated
authenticator
.checkAuthentication
.finally {
// check result
}
let isAuthenticated = try await authenticator
.checkAuthentication()

// signOut user
authenticator.signOut() // all provider data regarding the use auth is cleared at this point
Expand Down
10 changes: 2 additions & 8 deletions Resources/Facebook/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,12 @@ Please read [official documentation](https://developers.facebook.com/docs/facebo
let authenticator = FacebookAuthenticator()

// signIn user with default permissions
authenticator
let result = try await authenticator
.signIn(from: <view-controller-instance>)
.finally {
// handle result
}

// signIn user with custom permissions
authenticator
let result = try await authenticator
.signIn(from: <view-controller-instance>, with: [<array-of-custom-permissions>])
.finally {
// handle result
}

// get authentication status
let state = authenticator.isAuthenticated
Expand Down
5 changes: 1 addition & 4 deletions Resources/Google/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,8 @@ Please read [official documentation](https://developers.google.com/identity/sign
let authenticator = GoogleAuthenticator()

// signIn user
authenticator
let result = try await authenticator
.signIn(from: <view-controller-instance>)
.finally {
// handle result
}

// get authentication status
let state = authenticator.isAuthenticated
Expand Down
73 changes: 33 additions & 40 deletions Sources/Apple/AppleAuthenticator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,14 @@
import AuthenticationServices
import Foundation
import PovioKitAuthCore
import PovioKitPromise

public final class AppleAuthenticator: NSObject {
private let storage: UserDefaults
private let storageUserIdKey = "signIn.userId"
private let storageAuthenticatedKey = "authenticated"
private let provider: ASAuthorizationAppleIDProvider
private var processingPromise: Promise<Response>?
private var continuation: CheckedContinuation<Response, Swift.Error>?

public init(storage: UserDefaults? = nil) {
self.provider = .init()
self.storage = storage ?? .init(suiteName: "povioKit.auth.apple") ?? .standard
Expand All @@ -34,49 +33,41 @@ public final class AppleAuthenticator: NSObject {
extension AppleAuthenticator: Authenticator {
/// SignIn user
///
/// Will return promise with the `Response` object on success or with `Error` on error.
public func signIn(from presentingViewController: UIViewController) -> Promise<Response> {
let promise = Promise<Response>()
processingPromise = promise
appleSignIn(on: presentingViewController, with: nil)
return promise
/// Will asynchronously return the `Response` object on success or `Error` on error.
public func signIn(from presentingViewController: UIViewController) async throws -> Response {
try await appleSignIn(on: presentingViewController, with: nil)
}

/// SignIn user with `nonce` value
///
/// Nonce is usually needed when doing auth with an external auth provider (e.g. firebase).
/// Will return promise with the `Response` object on success or with `Error` on error.
public func signIn(from presentingViewController: UIViewController, with nonce: Nonce) -> Promise<Response> {
let promise = Promise<Response>()
processingPromise = promise
appleSignIn(on: presentingViewController, with: nonce)
return promise
/// Will asynchronously return the `Response` object on success or `Error` on error.
public func signIn(from presentingViewController: UIViewController, with nonce: Nonce) async throws -> Response {
try await appleSignIn(on: presentingViewController, with: nonce)
}

/// Clears the signIn footprint and logs out the user immediatelly.
public func signOut() {
storage.removeObject(forKey: storageUserIdKey)
rejectSignIn(with: .cancelled)
storage.setValue(false, forKey: storageAuthenticatedKey)
continuation = nil
}

/// Returns the current authentication state.
public var isAuthenticated: Authenticated {
storage.string(forKey: storageUserIdKey) != nil && storage.bool(forKey: storageAuthenticatedKey)
}

/// Checks the current auth state and returns the boolean value as promise.
public var checkAuthentication: Promise<Authenticated> {
/// Checks the current auth state and returns the boolean value asynchronously.
public func checkAuthentication() async -> Authenticated {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps this function should be throwable and the error handled at the call site.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And you can leave it as a getter as well. :) Though a function is fine by me as well.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove this function completely. We have a mechanism in terms of notification about the revocation.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed.

guard let userId = storage.string(forKey: storageUserIdKey) else {
return .value(false)
}

return Promise { seal in
ASAuthorizationAppleIDProvider().getCredentialState(forUserID: userId) { credentialsState, _ in
seal.resolve(with: credentialsState == .authorized)
}
return false
}

let credentialState = try? await ASAuthorizationAppleIDProvider().credentialState(forUserID: userId)
return credentialState == .authorized
}

/// Boolean if given `url` should be handled.
///
/// Call this from UIApplicationDelegate’s `application:openURL:options:` method.
Expand Down Expand Up @@ -137,7 +128,7 @@ extension AppleAuthenticator: ASAuthorizationControllerDelegate {
email: email,
expiresAt: expiresAt)

processingPromise?.resolve(with: response)
continuation?.resume(with: .success(response))
case _:
rejectSignIn(with: .unhandledAuthorization)
}
Expand All @@ -155,27 +146,29 @@ extension AppleAuthenticator: ASAuthorizationControllerDelegate {

// MARK: - Private Methods
private extension AppleAuthenticator {
func appleSignIn(on presentingViewController: UIViewController, with nonce: Nonce?) {
func appleSignIn(on presentingViewController: UIViewController, with nonce: Nonce?) async throws -> Response {
let request = provider.createRequest()
request.requestedScopes = [.fullName, .email]

switch nonce {
case .random(let length):
guard length > 0 else {
rejectSignIn(with: .invalidNonceLength)
return
throw Error.invalidNonceLength
}
request.nonce = generateNonceString(length: length).sha256
case .custom(let value):
request.nonce = value
case .none:
break
}

let controller = ASAuthorizationController(authorizationRequests: [request])
controller.delegate = self
controller.presentationContextProvider = presentingViewController
controller.performRequests()

return try await withCheckedThrowingContinuation { continuation in
let controller = ASAuthorizationController(authorizationRequests: [request])
controller.delegate = self
controller.presentationContextProvider = presentingViewController
self.continuation = continuation
controller.performRequests()
}
}

func setupCredentialsRevokeListener() {
Expand All @@ -187,8 +180,8 @@ private extension AppleAuthenticator {

func rejectSignIn(with error: Error) {
storage.setValue(false, forKey: storageAuthenticatedKey)
processingPromise?.reject(with: error)
processingPromise = nil
continuation?.resume(throwing: error)
continuation = nil
}
}

Expand Down
73 changes: 35 additions & 38 deletions Sources/Facebook/FacebookAuthenticator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import Foundation
import FacebookLogin
import PovioKitCore
import PovioKitAuthCore
import PovioKitPromise

public final class FacebookAuthenticator {
private let provider: LoginManager
Expand All @@ -25,15 +24,14 @@ extension FacebookAuthenticator: Authenticator {
/// SignIn user.
///
/// The `permissions` to use when doing a sign in.
/// Will return promise with the `Response` object on success or with `Error` on error.
/// Will asynchronously return the `Response` object on success or `Error` on error.
public func signIn(
from presentingViewController: UIViewController,
with permissions: [Permission] = [.email, .publicProfile]) -> Promise<Response>
with permissions: [Permission] = [.email, .publicProfile]) async throws -> Response
{
let permissions: [String] = permissions.map { $0.name }

return signIn(with: permissions, on: presentingViewController)
.flatMap(with: fetchUserDetails)
let token = try await signIn(with: permissions, on: presentingViewController)
return try await fetchUserDetails(with: token)
}

/// Clears the signIn footprint and logs out the user immediatelly.
Expand Down Expand Up @@ -69,38 +67,37 @@ public extension FacebookAuthenticator {

// MARK: - Private Methods
private extension FacebookAuthenticator {
func signIn(with permissions: [String], on presentingViewController: UIViewController) -> Promise<AccessToken> {
Promise { seal in
provider
.logIn(permissions: permissions, from: presentingViewController) { result, error in
switch (result, error) {
case (let result?, nil):
if result.isCancelled {
seal.reject(with: Error.cancelled)
} else if let token = result.token {
seal.resolve(with: token)
} else {
seal.reject(with: Error.invalidIdentityToken)
}
case (nil, let error?):
seal.reject(with: Error.system(error))
case _:
seal.reject(with: Error.system(NSError(domain: "com.povio.facebook.error", code: -1, userInfo: nil)))
func signIn(with permissions: [String], on presentingViewController: UIViewController) async throws -> AccessToken {
try await withCheckedThrowingContinuation { continuation in
provider.logIn(permissions: permissions, from: presentingViewController) { result, error in
switch (result, error) {
case (let result?, nil):
if result.isCancelled {
continuation.resume(throwing: Error.cancelled)
} else if let token = result.token {
continuation.resume(returning: token)
} else {
continuation.resume(throwing: Error.invalidIdentityToken)
}
case (nil, let error?):
continuation.resume(throwing: Error.system(error))
default:
continuation.resume(throwing: Error.system(NSError(domain: "com.povio.facebook.error", code: -1, userInfo: nil)))
}
}
}
}
func fetchUserDetails(with token: AccessToken) -> Promise<Response> {
let request = GraphRequest(
graphPath: "me",
parameters: ["fields": "id, email, first_name, last_name"],
tokenString: token.tokenString,
httpMethod: nil,
flags: .doNotInvalidateTokenOnError
)

return Promise { seal in

func fetchUserDetails(with token: AccessToken) async throws -> Response {
try await withCheckedThrowingContinuation { continuation in
let request = GraphRequest(
graphPath: "me",
parameters: ["fields": "id, email, first_name, last_name"],
tokenString: token.tokenString,
httpMethod: nil,
flags: .doNotInvalidateTokenOnError
)

request.start { _, result, error in
switch result {
case .some(let response):
Expand All @@ -110,20 +107,20 @@ private extension FacebookAuthenticator {
do {
let data = try JSONSerialization.data(withJSONObject: response, options: [])
let object = try data.decode(GraphResponse.self, with: decoder)

let authResponse = Response(
userId: object.id,
token: token.tokenString,
name: object.displayName,
email: object.email,
expiresAt: token.expirationDate
)
seal.resolve(with: authResponse)
continuation.resume(returning: authResponse)
} catch {
seal.reject(with: Error.userDataDecode)
continuation.resume(throwing: Error.userDataDecode)
}
case .none:
seal.reject(with: Error.missingUserData)
continuation.resume(throwing: Error.missingUserData)
}
}
}
Expand Down
Loading
Loading