diff --git a/.gitignore b/.gitignore index 3b29812..5922fda 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ DerivedData/ .swiftpm/config/registries.json .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata .netrc +Package.resolved diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/PovioKitAuth-Package.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/PovioKitAuth-Package.xcscheme index c920580..d279f7c 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/PovioKitAuth-Package.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/PovioKitAuth-Package.xcscheme @@ -1,6 +1,6 @@ + + + + diff --git a/Demo/PovioKitAuthDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Demo/PovioKitAuthDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/Demo/PovioKitAuthDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Package.resolved b/Demo/PovioKitAuthDemo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved similarity index 90% rename from Package.resolved rename to Demo/PovioKitAuthDemo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 0fe7538..60af7e5 100644 --- a/Package.resolved +++ b/Demo/PovioKitAuthDemo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -23,8 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/facebook/facebook-ios-sdk", "state" : { - "revision" : "09eb5b0cb74127a360e0bc74a2d75ec1fd351e48", - "version" : "17.0.0" + "revision" : "9b89497b97ef043991862c9a8c5bb7fffaf31988", + "version" : "17.0.1" } }, { @@ -59,8 +59,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/poviolabs/PovioKit", "state" : { - "revision" : "95371aebca733fa67be58c0239e12a97d1fcf163", - "version" : "3.3.1" + "revision" : "96fa97cffa12ff644f10008133da062bec463ff8", + "version" : "4.0.0" } } ], diff --git a/Demo/PovioKitAuthDemo/Assets.xcassets/AccentColor.colorset/Contents.json b/Demo/PovioKitAuthDemo/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/Demo/PovioKitAuthDemo/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Demo/PovioKitAuthDemo/Assets.xcassets/AppIcon.appiconset/Contents.json b/Demo/PovioKitAuthDemo/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..532cd72 --- /dev/null +++ b/Demo/PovioKitAuthDemo/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,63 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Demo/PovioKitAuthDemo/Assets.xcassets/Contents.json b/Demo/PovioKitAuthDemo/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Demo/PovioKitAuthDemo/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Demo/PovioKitAuthDemo/DemoApp.swift b/Demo/PovioKitAuthDemo/DemoApp.swift new file mode 100644 index 0000000..d880eda --- /dev/null +++ b/Demo/PovioKitAuthDemo/DemoApp.swift @@ -0,0 +1,18 @@ +// +// DemoApp.swift +// PovioKitAuthDemo +// +// Created by Borut Tomazin on 10/05/2024. +// Copyright © 2024 Povio Inc. All rights reserved. +// + +import SwiftUI + +@main +struct DemoApp: App { + var body: some Scene { + WindowGroup { + SignInView(viewModel: .init()) + } + } +} diff --git a/Demo/PovioKitAuthDemo/PovioKitAuthDemo.entitlements b/Demo/PovioKitAuthDemo/PovioKitAuthDemo.entitlements new file mode 100644 index 0000000..4210463 --- /dev/null +++ b/Demo/PovioKitAuthDemo/PovioKitAuthDemo.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.developer.applesignin + + Default + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-only + + + diff --git a/Demo/PovioKitAuthDemo/Preview Content/Preview Assets.xcassets/Contents.json b/Demo/PovioKitAuthDemo/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Demo/PovioKitAuthDemo/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Demo/PovioKitAuthDemo/SignInView.swift b/Demo/PovioKitAuthDemo/SignInView.swift new file mode 100644 index 0000000..33fc2f3 --- /dev/null +++ b/Demo/PovioKitAuthDemo/SignInView.swift @@ -0,0 +1,60 @@ +// +// SignInView.swift +// PovioKitAuthDemo +// +// Created by Borut Tomazin on 10/05/2024. +// Copyright © 2024 Povio Inc. All rights reserved. +// + +import SwiftUI + +struct SignInView: View { + let viewModel: SignInViewModel + @State private var showErrorAlert: Bool = false + @State private var showSuccessAlert: Bool = false + + var body: some View { + VStack(spacing: 20) { + Button { + viewModel.signInWithApple() + } label: { + Text("SignIn with Apple") + } + Button { + viewModel.signInWithGoogle() + } label: { + Text("SignIn with Google") + } + Button { + viewModel.signInWithFacebook() + } label: { + Text("SignIn with Facebook") + } + Button { + viewModel.signInWithLinkedIn() + } label: { + Text("SignIn with LinkedIn") + } + } + .onChange(of: viewModel.error) { oldValue, newValue in + showErrorAlert = true + } + .onChange(of: viewModel.success) { oldValue, newValue in + showSuccessAlert = true + } + .alert("Error", isPresented: $showErrorAlert) { + Button("Ok", action: {}) + } message: { + Text(viewModel.error) + } + .alert("Success", isPresented: $showSuccessAlert) { + Button("Ok", action: {}) + } message: { + Text(viewModel.success) + } + } +} + +#Preview { + SignInView(viewModel: .init()) +} diff --git a/Demo/PovioKitAuthDemo/SignInViewModel.swift b/Demo/PovioKitAuthDemo/SignInViewModel.swift new file mode 100644 index 0000000..b431c56 --- /dev/null +++ b/Demo/PovioKitAuthDemo/SignInViewModel.swift @@ -0,0 +1,105 @@ +// +// SignInViewModel.swift +// PovioKitAuthDemo +// +// Created by Borut Tomazin on 10/05/2024. +// Copyright © 2024 Povio Inc. All rights reserved. +// + +import UIKit +import SwiftUI +import PovioKitCore +import PovioKitAuthCore +import PovioKitAuthApple +import PovioKitAuthGoogle +import PovioKitAuthFacebook +import PovioKitAuthLinkedIn +import Observation + +@Observable +final class SignInViewModel { + private let socialAuthManager: SocialAuthenticationManager + + var error: String = "" + var success: String = "" + + init() { + socialAuthManager = SocialAuthenticationManager(authenticators: [ + AppleAuthenticator(), + GoogleAuthenticator(), + FacebookAuthenticator(), + LinkedInAuthenticator() + ]) + } +} + +extension SignInViewModel { + func signInWithApple() { + guard let auth = socialAuthManager.authenticator(for: AppleAuthenticator.self) else { return } + + Task { @MainActor in + do { + _ = try await auth.signIn(from: UIViewController()) + self.success = "SignIn with Apple succeeded." + } catch AppleAuthenticator.Error.cancelled { + Logger.debug("Apple Auth cancelled.") + } catch { + Logger.error(error.localizedDescription) + self.error = error.localizedDescription + } + } + } + + // TODO: provide `GoogleService-Info.plist` configuration file + func signInWithGoogle() { + guard let auth = socialAuthManager.authenticator(for: GoogleAuthenticator.self) else { return } + + Task { @MainActor in + do { + _ = try await auth.signIn(from: UIViewController()) + self.success = "SignIn with Google succeeded." + } catch GoogleAuthenticator.Error.cancelled { + Logger.debug("Apple Auth cancelled.") + } catch { + Logger.error(error.localizedDescription) + self.error = error.localizedDescription + } + } + } + + // TODO: provide `FacebookAppID` to the `Info.plist` + func signInWithFacebook() { + guard let auth = socialAuthManager.authenticator(for: FacebookAuthenticator.self) else { return } + + Task { @MainActor in + do { + _ = try await auth.signIn(from: UIViewController()) + self.success = "SignIn with Facebook succeeded." + } catch FacebookAuthenticator.Error.cancelled { + Logger.debug("Apple Auth cancelled.") + } catch { + Logger.error(error.localizedDescription) + self.error = error.localizedDescription + } + } + } + + // TODO: provide configuration details + func signInWithLinkedIn() { + guard let auth = socialAuthManager.authenticator(for: LinkedInAuthenticator.self) else { return } + + Task { @MainActor in + do { + _ = try await auth.signIn(authCode: "...", configuration: .init( + clientId: "...", + clientSecret: "...", + permissions: "email profile openid offline_access", + redirectUrl: "...")) + self.success = "SignIn with LinkedIn succeeded." + } catch { + Logger.error(error.localizedDescription) + self.error = error.localizedDescription + } + } + } +} diff --git a/MIGRATING.md b/MIGRATING.md index fdb835e..790fb0f 100644 --- a/MIGRATING.md +++ b/MIGRATING.md @@ -1,4 +1,8 @@ ## Migration Guides +### Migration from versions < 2.0.0 +* [Apple] Response field `name` is now optional. +* [All] Migrated all authenticators from promises to async/await. If you insist working with promises, do not update. + ### Migration from versions < 1.4.0 * [Google] Response field `token` was renamed to `accessToken`. You'll need to handle it on the call site. Additionally, `idToken` field was added in the response. diff --git a/Package.swift b/Package.swift index 1475912..5411593 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.8 +// swift-tools-version: 5.7 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -6,8 +6,7 @@ import PackageDescription let package = Package( name: "PovioKitAuth", platforms: [ - .iOS(.v13), - .macOS(.v13) + .iOS(.v13) ], products: [ .library(name: "PovioKitAuthCore", targets: ["PovioKitAuthCore"]), @@ -17,16 +16,13 @@ let package = Package( .library(name: "PovioKitAuthLinkedIn", targets: ["PovioKitAuthLinkedIn"]) ], dependencies: [ - .package(url: "https://github.com/poviolabs/PovioKit", .upToNextMajor(from: "3.0.0")), .package(url: "https://github.com/google/GoogleSignIn-iOS", .upToNextMajor(from: "7.0.0")), .package(url: "https://github.com/facebook/facebook-ios-sdk", .upToNextMajor(from: "17.0.0")), ], targets: [ .target( name: "PovioKitAuthCore", - dependencies: [ - .product(name: "PovioKitPromise", package: "PovioKit"), - ], + dependencies: [], path: "Sources/Core", resources: [.copy("../../Resources/PrivacyInfo.xcprivacy")] ), @@ -51,7 +47,6 @@ let package = Package( name: "PovioKitAuthFacebook", dependencies: [ "PovioKitAuthCore", - .product(name: "PovioKitCore", package: "PovioKit"), .product(name: "FacebookLogin", package: "facebook-ios-sdk") ], path: "Sources/Facebook", @@ -60,8 +55,7 @@ let package = Package( .target( name: "PovioKitAuthLinkedIn", dependencies: [ - "PovioKitAuthCore", - .product(name: "PovioKitNetworking", package: "PovioKit") + "PovioKitAuthCore" ], path: "Sources/LinkedIn", resources: [.copy("../../Resources/PrivacyInfo.xcprivacy")] diff --git a/README.md b/README.md index 2009216..4ac93f9 100644 --- a/README.md +++ b/README.md @@ -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: , 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: , with: [.email]) - .finally { - // handle result - } // return currently authenticated authenticator let authenticated: Authenticator? = manager.authenticator @@ -123,11 +117,11 @@ 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 { - 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 } @@ -135,7 +129,7 @@ final class SnapchatAuthenticator: Authenticator { let variables = ["page": "bitmoji"] SCSDKLoginClient.fetchUserData(withQuery: query, variables: variables) { resources in ... - seal.resolve(with: response) + continuation.resume(returning: response) } } } diff --git a/Resources/Apple/README.md b/Resources/Apple/README.md index 74e9dde..c701d5a 100644 --- a/Resources/Apple/README.md +++ b/Resources/Apple/README.md @@ -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: ) - .finally { - // handle result - } // signIn user with nonce -authenticator +let result = try await authenticator .signIn(from: , 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 ``` diff --git a/Resources/Facebook/README.md b/Resources/Facebook/README.md index 3335fdf..3cb3062 100644 --- a/Resources/Facebook/README.md +++ b/Resources/Facebook/README.md @@ -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: ) - .finally { - // handle result - } // signIn user with custom permissions -authenticator +let result = try await authenticator .signIn(from: , with: []) - .finally { - // handle result - } // get authentication status let state = authenticator.isAuthenticated diff --git a/Resources/Google/README.md b/Resources/Google/README.md index 725d2d6..5fa50ee 100644 --- a/Resources/Google/README.md +++ b/Resources/Google/README.md @@ -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: ) - .finally { - // handle result - } // get authentication status let state = authenticator.isAuthenticated diff --git a/Sources/Apple/AppleAuthenticator+Models.swift b/Sources/Apple/AppleAuthenticator+Models.swift index 49dbd8f..935eb49 100644 --- a/Sources/Apple/AppleAuthenticator+Models.swift +++ b/Sources/Apple/AppleAuthenticator+Models.swift @@ -22,11 +22,16 @@ public extension AppleAuthenticator { public let nameComponents: PersonNameComponents? public let email: Email public let expiresAt: Date - - public var name: String { - [nameComponents?.givenName, nameComponents?.familyName] - .compactMap { $0 } - .joined(separator: " ") + + /// User full name represented by `givenName` and `familyName` + public var name: String? { + guard let givenName = nameComponents?.givenName else { + return nameComponents?.familyName + } + guard let familyName = nameComponents?.familyName else { + return givenName + } + return "\(givenName) \(familyName)" } } diff --git a/Sources/Apple/AppleAuthenticator.swift b/Sources/Apple/AppleAuthenticator.swift index a881de2..37901de 100644 --- a/Sources/Apple/AppleAuthenticator.swift +++ b/Sources/Apple/AppleAuthenticator.swift @@ -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? - + private var continuation: CheckedContinuation? + public init(storage: UserDefaults? = nil) { self.provider = .init() self.storage = storage ?? .init(suiteName: "povioKit.auth.apple") ?? .standard @@ -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 { - let promise = Promise() - 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 { - let promise = Promise() - 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 { - 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. @@ -137,7 +118,7 @@ extension AppleAuthenticator: ASAuthorizationControllerDelegate { email: email, expiresAt: expiresAt) - processingPromise?.resolve(with: response) + continuation?.resume(with: .success(response)) case _: rejectSignIn(with: .unhandledAuthorization) } @@ -155,15 +136,14 @@ 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): @@ -171,11 +151,14 @@ private extension AppleAuthenticator { 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() { @@ -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 } } diff --git a/Sources/Facebook/FacebookAuthenticator.swift b/Sources/Facebook/FacebookAuthenticator.swift index bc115b2..90bd25a 100644 --- a/Sources/Facebook/FacebookAuthenticator.swift +++ b/Sources/Facebook/FacebookAuthenticator.swift @@ -8,9 +8,7 @@ import Foundation import FacebookLogin -import PovioKitCore import PovioKitAuthCore -import PovioKitPromise public final class FacebookAuthenticator { private let provider: LoginManager @@ -25,15 +23,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 + 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. @@ -69,38 +66,37 @@ public extension FacebookAuthenticator { // MARK: - Private Methods private extension FacebookAuthenticator { - func signIn(with permissions: [String], on presentingViewController: UIViewController) -> Promise { - 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 { - 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): @@ -109,8 +105,8 @@ private extension FacebookAuthenticator { do { let data = try JSONSerialization.data(withJSONObject: response, options: []) - let object = try data.decode(GraphResponse.self, with: decoder) - + let object = try decoder.decode(GraphResponse.self, from: data) + let authResponse = Response( userId: object.id, token: token.tokenString, @@ -118,12 +114,12 @@ private extension FacebookAuthenticator { 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) } } } diff --git a/Sources/Google/GoogleAuthenticator.swift b/Sources/Google/GoogleAuthenticator.swift index 467c553..6e7f88d 100644 --- a/Sources/Google/GoogleAuthenticator.swift +++ b/Sources/Google/GoogleAuthenticator.swift @@ -9,7 +9,6 @@ import Foundation import GoogleSignIn import PovioKitAuthCore -import PovioKitPromise public final class GoogleAuthenticator { private let provider: GIDSignIn @@ -23,45 +22,15 @@ public final class GoogleAuthenticator { extension GoogleAuthenticator: Authenticator { /// SignIn user. /// - /// 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, hint: String? = .none, - additionalScopes: [String]? = .none) -> Promise { + additionalScopes: [String]? = .none) async throws -> Response { guard !provider.hasPreviousSignIn() else { - // restore user - return Promise { seal in - provider.restorePreviousSignIn { result, error in - switch (result, error) { - case (let user?, _): - seal.resolve(with: user.authResponse) - case (_, let actualError?): - seal.reject(with: Error.system(actualError)) - case (.none, .none): - seal.reject(with: Error.unhandledAuthorization) - } - } - } + return try await restorePreviousSignIn() } - // sign in - return Promise { seal in - provider - .signIn(withPresenting: presentingViewController, hint: hint, additionalScopes: additionalScopes) { result, error in - switch (result, error) { - case (let signInResult?, _): - seal.resolve(with: signInResult.user.authResponse) - case (_, let actualError?): - let errorCode = (actualError as NSError).code - if errorCode == GIDSignInError.Code.canceled.rawValue { - seal.reject(with: Error.cancelled) - } else { - seal.reject(with: Error.system(actualError)) - } - case (.none, .none): - seal.reject(with: Error.unhandledAuthorization) - } - } - } + return try await signInUser(from: presentingViewController, hint: hint, additionalScopes: additionalScopes) } /// Clears the signIn footprint and logs out the user immediatelly. @@ -82,6 +51,44 @@ extension GoogleAuthenticator: Authenticator { } } +// MARK: - Private Methods +private extension GoogleAuthenticator { + func restorePreviousSignIn() async throws -> Response { + try await withCheckedThrowingContinuation { continuation in + provider.restorePreviousSignIn { user, error in + if let user = user { + continuation.resume(returning: user.authResponse) + } else if let error = error { + continuation.resume(throwing: Error.system(error)) + } else { + continuation.resume(throwing: Error.unhandledAuthorization) + } + } + } + } + + func signInUser(from presentingViewController: UIViewController, hint: String?, additionalScopes: [String]?) async throws -> Response { + try await withCheckedThrowingContinuation { continuation in + provider + .signIn(withPresenting: presentingViewController, hint: hint, additionalScopes: additionalScopes) { result, error in + switch (result, error) { + case (let signInResult?, _): + continuation.resume(returning: signInResult.user.authResponse) + case (_, let actualError?): + let errorCode = (actualError as NSError).code + if errorCode == GIDSignInError.Code.canceled.rawValue { + continuation.resume(throwing: Error.cancelled) + } else { + continuation.resume(throwing: Error.system(actualError)) + } + case (.none, .none): + continuation.resume(throwing: Error.unhandledAuthorization) + } + } + } + } +} + // MARK: - Error public extension GoogleAuthenticator { enum Error: Swift.Error { diff --git a/Sources/LinkedIn/API/EndpointEncodable.swift b/Sources/LinkedIn/API/EndpointEncodable.swift deleted file mode 100644 index b49f054..0000000 --- a/Sources/LinkedIn/API/EndpointEncodable.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// EndpointEncodable.swift -// PovioKitAuth -// -// Created by Borut Tomazin on 04/09/2023. -// Copyright © 2024 Povio Inc. All rights reserved. -// - -import Foundation -import PovioKitNetworking - -protocol EndpointEncodable: URLConvertible { - typealias Path = String - - var path: Path { get } - var url: String { get } -} - -extension EndpointEncodable { - func asURL() throws -> URL { - .init(stringLiteral: url) - } -} diff --git a/Sources/LinkedIn/API/LinkedInAPI+Endpoints.swift b/Sources/LinkedIn/API/LinkedInAPI+Endpoints.swift index 81f6198..ed4c977 100644 --- a/Sources/LinkedIn/API/LinkedInAPI+Endpoints.swift +++ b/Sources/LinkedIn/API/LinkedInAPI+Endpoints.swift @@ -7,15 +7,14 @@ // import Foundation -import PovioKitNetworking extension LinkedInAPI { - enum Endpoints: EndpointEncodable { + enum Endpoints { case accessToken case profile case email - var path: Path { + var path: String { switch self { case .accessToken: return "accessToken" diff --git a/Sources/LinkedIn/API/LinkedInAPI.swift b/Sources/LinkedIn/API/LinkedInAPI.swift index b2bd4b8..ab24be1 100644 --- a/Sources/LinkedIn/API/LinkedInAPI.swift +++ b/Sources/LinkedIn/API/LinkedInAPI.swift @@ -7,18 +7,36 @@ // import Foundation -import PovioKitNetworking -public final class LinkedInAPI { - private let client: AlamofireNetworkClient +public struct LinkedInAPI { + private let client: HttpClient = .init() - public init(client: AlamofireNetworkClient = .init()) { - self.client = client - } + public init() {} } public extension LinkedInAPI { func login(with request: LinkedInAuthRequest) async throws -> LinkedInAuthResponse { + let encoder = JSONEncoder() + encoder.keyEncodingStrategy = .convertToSnakeCase + let jsonData = try encoder.encode(request) + let jsonObject = try JSONSerialization.jsonObject(with: jsonData, options: []) + guard let jsonDict = jsonObject as? [String: Any] else { + throw Error.invalidRequest + } + + guard var components = URLComponents(string: Endpoints.accessToken.url) else { + throw Error.invalidUrl + } + + let queryItems: [URLQueryItem] = jsonDict.compactMap { key, value in + (value as? String).map { .init(name: key, value: $0) } + } + + components.queryItems = queryItems + guard let url = components.url else { + throw Error.invalidUrl + } + let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase decoder.dateDecodingStrategy = .custom { decoder in @@ -27,47 +45,50 @@ public extension LinkedInAPI { return Date().addingTimeInterval(TimeInterval(secondsRemaining)) } - let encoder = JSONEncoder() - encoder.keyEncodingStrategy = .convertToSnakeCase + let response = try await client.request( + method: "POST", + url: url, + headers: ["Content-Type": "application/x-www-form-urlencoded"], + decodeTo: LinkedInAuthResponse.self, + with: decoder + ) - return try await client - .request( - method: .post, - endpoint: Endpoints.accessToken, - encode: request, - parameterEncoder: .urlEncoder(encoder: encoder) - ) - .validate() - .decode(LinkedInAuthResponse.self, decoder: decoder) - .asAsync + return response } func loadProfile(with request: LinkedInProfileRequest) async throws -> LinkedInProfileResponse { + guard let url = URL(string: Endpoints.profile.url) else { throw Error.invalidUrl } + let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase decoder.dateDecodingStrategy = .iso8601 - return try await client - .request( - method: .get, - endpoint: Endpoints.profile, - headers: ["Authorization": "Bearer \(request.token)"] - ) - .validate() - .decode(LinkedInProfileResponse.self, decoder: decoder) - .asAsync + let response = try await client.request( + method: "GET", + url: url, + headers: ["Authorization": "Bearer \(request.token)"], + decodeTo: LinkedInProfileResponse.self, + with: decoder + ) + + return response } func loadEmail(with request: LinkedInProfileRequest) async throws -> LinkedInEmailValueResponse { - return try await client - .request( - method: .get, - endpoint: Endpoints.email, - headers: ["Authorization": "Bearer \(request.token)"]) - .validate() - .decode(LinkedInEmailResponse.self) - .compactMap { $0.elements.first?.handle } - .asAsync + guard let url = URL(string: Endpoints.email.url) else { throw Error.invalidUrl } + + let response = try await client.request( + method: "GET", + url: url, + headers: ["Authorization": "Bearer \(request.token)"], + decodeTo: LinkedInEmailResponse.self + ) + + guard let emailObject = response.elements.first?.handle else { + throw Error.invalidResponse + } + + return emailObject } } @@ -75,5 +96,8 @@ public extension LinkedInAPI { public extension LinkedInAPI { enum Error: Swift.Error { case missingParameters + case invalidUrl + case invalidRequest + case invalidResponse } } diff --git a/Sources/LinkedIn/Core/HttpClient.swift b/Sources/LinkedIn/Core/HttpClient.swift new file mode 100644 index 0000000..a5de27d --- /dev/null +++ b/Sources/LinkedIn/Core/HttpClient.swift @@ -0,0 +1,32 @@ +// +// HttpClient.swift +// PovioKitAuth +// +// Created by Borut Tomazin on 22/05/2024. +// Copyright © 2024 Povio Inc. All rights reserved. +// + +import Foundation + +struct HttpClient { + func request( + method: String, + url: URL, + headers: [String: String]?, + decodeTo decode: D.Type, + with decoder: JSONDecoder = .init() + ) async throws -> D { + var urlRequest = URLRequest(url: url) + urlRequest.httpMethod = method + urlRequest.allHTTPHeaderFields = headers + + let (data, response) = try await URLSession.shared.data(for: urlRequest) + + if let httpResponse = response as? HTTPURLResponse, + !(200...299).contains(httpResponse.statusCode) { + throw NSError(domain: "HTTP Error", code: httpResponse.statusCode, userInfo: nil) + } + + return try decoder.decode(decode, from: data) + } +} diff --git a/Sources/LinkedIn/Core/URL+PovioKitAuth.swift b/Sources/LinkedIn/Core/URL+PovioKitAuth.swift new file mode 100644 index 0000000..5160797 --- /dev/null +++ b/Sources/LinkedIn/Core/URL+PovioKitAuth.swift @@ -0,0 +1,18 @@ +// +// URL+PovioKitAuth.swift +// PovioKitAuth +// +// Created by Borut Tomazin on 22/05/2024. +// Copyright © 2024 Povio Inc. All rights reserved. +// + +import Foundation + +extension URL: ExpressibleByStringLiteral { + public init(stringLiteral value: String) { + guard let url = URL(string: value) else { + fatalError("Invalid URL string!") + } + self = url + } +} diff --git a/Sources/LinkedIn/LinkedInAuthenticator.swift b/Sources/LinkedIn/LinkedInAuthenticator.swift index 79155d3..bc91c4d 100644 --- a/Sources/LinkedIn/LinkedInAuthenticator.swift +++ b/Sources/LinkedIn/LinkedInAuthenticator.swift @@ -25,7 +25,7 @@ public final class LinkedInAuthenticator { extension LinkedInAuthenticator: Authenticator { /// SignIn user. /// - /// 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(authCode: String, configuration: Configuration) async throws -> Response { let authRequest: LinkedInAPI.LinkedInAuthRequest = .init( code: authCode, diff --git a/Sources/LinkedIn/WebView/LinkedInWebView.swift b/Sources/LinkedIn/WebView/LinkedInWebView.swift index 6f1ba22..bb26b86 100644 --- a/Sources/LinkedIn/WebView/LinkedInWebView.swift +++ b/Sources/LinkedIn/WebView/LinkedInWebView.swift @@ -6,7 +6,6 @@ // Copyright © 2024 Povio Inc. All rights reserved. // -import PovioKitCore import SwiftUI import WebKit @@ -40,7 +39,6 @@ public struct LinkedInWebView: UIViewRepresentable { public func updateUIView(_ uiView: UIViewType, context: Context) { guard let webView = uiView as? WKWebView else { return } guard let authURL = configuration.authorizationUrl(state: requestState) else { - Logger.error("Failed to geet auth url!") dismiss() return }