Skip to content

Commit

Permalink
Feature/Rewrite to async (#24)
Browse files Browse the repository at this point in the history
  • Loading branch information
yllfejziu authored May 15, 2024
1 parent 8915aca commit 0b5e7b3
Show file tree
Hide file tree
Showing 8 changed files with 116 additions and 158 deletions.
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
18 changes: 2 additions & 16 deletions Resources/Apple/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,30 +20,16 @@ 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
}

// signOut user
authenticator.signOut() // all provider data regarding the use auth is cleared at this point
```
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
71 changes: 27 additions & 44 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,31 @@ 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> {
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)
}
}
}


/// Boolean if given `url` should be handled.
///
/// Call this from UIApplicationDelegate’s `application:openURL:options:` method.
Expand Down Expand Up @@ -137,7 +118,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 +136,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 +170,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

0 comments on commit 0b5e7b3

Please sign in to comment.