From e2f70dfbbdc14a4592abd943840fa47ef8db743b Mon Sep 17 00:00:00 2001 From: Borut Tomazin Date: Thu, 28 Sep 2023 21:34:29 +0200 Subject: [PATCH 1/3] Feature/LinkedIn (#11) --- .../xcschemes/PovioKitAuth-Package.xcscheme | 14 +++ Package.swift | 14 ++- README.md | 5 +- Resources/LinkedIn/README.md | 32 ++++++ Sources/.DS_Store | Bin 6148 -> 6148 bytes Sources/LinkedIn/API/EndpointEncodable.swift | 23 ++++ .../LinkedIn/API/LinkedInAPI+Endpoints.swift | 38 +++++++ Sources/LinkedIn/API/LinkedInAPI+Models.swift | 61 +++++++++++ Sources/LinkedIn/API/LinkedInAPI.swift | 79 ++++++++++++++ .../LinkedInAuthenticator+Models.swift | 47 ++++++++ Sources/LinkedIn/LinkedInAuthenticator.swift | 68 ++++++++++++ Sources/LinkedIn/WebView/LinkedInSheet.swift | 40 +++++++ .../LinkedIn/WebView/LinkedInWebView.swift | 102 ++++++++++++++++++ 13 files changed, 519 insertions(+), 4 deletions(-) create mode 100644 Resources/LinkedIn/README.md create mode 100644 Sources/LinkedIn/API/EndpointEncodable.swift create mode 100644 Sources/LinkedIn/API/LinkedInAPI+Endpoints.swift create mode 100644 Sources/LinkedIn/API/LinkedInAPI+Models.swift create mode 100644 Sources/LinkedIn/API/LinkedInAPI.swift create mode 100644 Sources/LinkedIn/LinkedInAuthenticator+Models.swift create mode 100644 Sources/LinkedIn/LinkedInAuthenticator.swift create mode 100644 Sources/LinkedIn/WebView/LinkedInSheet.swift create mode 100644 Sources/LinkedIn/WebView/LinkedInWebView.swift diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/PovioKitAuth-Package.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/PovioKitAuth-Package.xcscheme index 10139c2..c920580 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/PovioKitAuth-Package.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/PovioKitAuth-Package.xcscheme @@ -62,6 +62,20 @@ ReferencedContainer = "container:"> + + + + B)qu~2NHo}wrV0|Nsi1A_nqLk>e~Qh9MfQcix-W=5vvjDjF3HU@Ww zd>~8*Nl%twlwsvy=iug;Y{?iV%fZRP880AFU2SS=p`&18U{B`mu~2NHo}wrd0|Nsi1A_oVQh9MfQcivnkiTPM;qu7_A}pLN42}$? z3?&R1lLeS%SlQXxx!EThGKa}>0ztfhM0K^Pk+F_~k(pVojzYDik%5kanT7e}4NS%e rots}U%d;#NU}4(K&cV+Cw0AQj%Xj9<{34bdK$}2TGi?qK*}@C}y1E~H diff --git a/Sources/LinkedIn/API/EndpointEncodable.swift b/Sources/LinkedIn/API/EndpointEncodable.swift new file mode 100644 index 0000000..02b0599 --- /dev/null +++ b/Sources/LinkedIn/API/EndpointEncodable.swift @@ -0,0 +1,23 @@ +// +// EndpointEncodable.swift +// PovioKitAuth +// +// Created by Borut Tomazin on 04/09/2023. +// Copyright © 2023 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 new file mode 100644 index 0000000..5d111f7 --- /dev/null +++ b/Sources/LinkedIn/API/LinkedInAPI+Endpoints.swift @@ -0,0 +1,38 @@ +// +// LinkedInAPI+Endpoints.swift +// PovioKitAuth +// +// Created by Borut Tomazin on 04/09/2023. +// Copyright © 2023 Povio Inc. All rights reserved. +// + +import Foundation +import PovioKitNetworking + +extension LinkedInAPI { + enum Endpoints: EndpointEncodable { + case accessToken + case profile + case email + + var path: Path { + switch self { + case .accessToken: + return "accessToken" + case .profile: + return "me" + case .email: + return "emailAddress?q=members&projection=(elements*(handle~))" + } + } + + var url: String { + switch self { + case .accessToken: + return "https://www.linkedin.com/oauth/v2/\(path)" + case .profile, .email: + return "https://api.linkedin.com/v2/\(path)" + } + } + } +} diff --git a/Sources/LinkedIn/API/LinkedInAPI+Models.swift b/Sources/LinkedIn/API/LinkedInAPI+Models.swift new file mode 100644 index 0000000..6d7da0a --- /dev/null +++ b/Sources/LinkedIn/API/LinkedInAPI+Models.swift @@ -0,0 +1,61 @@ +// +// LinkedInAPI+Models.swift +// PovioKitAuth +// +// Created by Borut Tomazin on 04/09/2023. +// Copyright © 2023 Povio Inc. All rights reserved. +// + +import Foundation + +public extension LinkedInAPI { + struct LinkedInAuthRequest: Encodable { + let grantType: String = "authorization_code" + let code: String + let redirectUri: String + let clientId: String + let clientSecret: String + + public init(code: String, redirectUri: String, clientId: String, clientSecret: String) { + self.code = code + self.redirectUri = redirectUri + self.clientId = clientId + self.clientSecret = clientSecret + } + } + + struct LinkedInAuthResponse: Decodable { + public let accessToken: String + public let expiresIn: Date + } + + struct LinkedInProfileRequest: Encodable { + let token: String + + public init(token: String) { + self.token = token + } + } + + struct LinkedInProfileResponse: Decodable { + public let id: String + public let localizedFirstName: String + public let localizedLastName: String + } + + struct LinkedInEmailResponse: Decodable { + public let elements: [LinkedInEmailHandleResponse] + } + + struct LinkedInEmailHandleResponse: Decodable { + public let handle: LinkedInEmailValueResponse + + enum CodingKeys: String, CodingKey { + case handle = "handle~" + } + } + + struct LinkedInEmailValueResponse: Decodable { + public let emailAddress: String + } +} diff --git a/Sources/LinkedIn/API/LinkedInAPI.swift b/Sources/LinkedIn/API/LinkedInAPI.swift new file mode 100644 index 0000000..0c21651 --- /dev/null +++ b/Sources/LinkedIn/API/LinkedInAPI.swift @@ -0,0 +1,79 @@ +// +// LinkedInAPI.swift +// PovioKitAuth +// +// Created by Borut Tomazin on 04/09/2023. +// Copyright © 2023 Povio Inc. All rights reserved. +// + +import Foundation +import PovioKitNetworking + +public final class LinkedInAPI { + private let client: AlamofireNetworkClient + + public init(client: AlamofireNetworkClient = .init()) { + self.client = client + } +} + +public extension LinkedInAPI { + func login(with request: LinkedInAuthRequest) async throws -> LinkedInAuthResponse { + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + decoder.dateDecodingStrategy = .custom { decoder in + let container = try decoder.singleValueContainer() + let secondsRemaining = try container.decode(Int.self) + return Date().addingTimeInterval(TimeInterval(secondsRemaining)) + } + + let encoder = JSONEncoder() + encoder.keyEncodingStrategy = .convertToSnakeCase + + return try await client + .request( + method: .post, + endpoint: Endpoints.accessToken, + encode: request, + parameterEncoder: .urlEncoder(encoder: encoder) + ) + .validate() + .decode(LinkedInAuthResponse.self, decoder: decoder) + .asAsync + } + + func loadProfile(with request: LinkedInProfileRequest) async throws -> LinkedInProfileResponse { + 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 + } + + 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 + } +} + +// MARK: - Error +public extension LinkedInAPI { + enum Error: Swift.Error { + case missingParameters + } +} diff --git a/Sources/LinkedIn/LinkedInAuthenticator+Models.swift b/Sources/LinkedIn/LinkedInAuthenticator+Models.swift new file mode 100644 index 0000000..4383ea7 --- /dev/null +++ b/Sources/LinkedIn/LinkedInAuthenticator+Models.swift @@ -0,0 +1,47 @@ +// +// GoogleAuthenticator+Models.swift +// PovioKitAuth +// +// Created by Borut Tomazin on 30/01/2023. +// Copyright © 2023 Povio Inc. All rights reserved. +// + +import Foundation + +public extension LinkedInAuthenticator { + struct Configuration { + let clientId: String + let clientSecret: String + let permissions: String + let redirectUrl: URL + let authEndpoint: URL = "https://www.linkedin.com/oauth/v2/authorization" + let authCancel: URL = "https://www.linkedin.com/oauth/v2/login-cancel" + + public init(clientId: String, clientSecret: String, permissions: String, redirectUrl: URL) { + self.clientId = clientId + self.clientSecret = clientSecret + self.permissions = permissions + self.redirectUrl = redirectUrl + } + + func authorizationUrl(state: String) -> URL? { + guard var urlComponents = URLComponents(url: authEndpoint, resolvingAgainstBaseURL: false) else { return nil } + urlComponents.queryItems = [ + .init(name: "response_type", value: "code"), + .init(name: "client_id", value: clientId), + .init(name: "redirect_uri", value: redirectUrl.absoluteString), + .init(name: "state", value: state), + .init(name: "scope", value: permissions) + ] + return urlComponents.url + } + } + + struct Response { + public let userId: String + public let token: String + public let name: String + public let email: String + public let expiresAt: Date + } +} diff --git a/Sources/LinkedIn/LinkedInAuthenticator.swift b/Sources/LinkedIn/LinkedInAuthenticator.swift new file mode 100644 index 0000000..cfb8ca7 --- /dev/null +++ b/Sources/LinkedIn/LinkedInAuthenticator.swift @@ -0,0 +1,68 @@ +// +// LinkedInAuthenticator.swift +// PovioKitAuth +// +// Created by Borut Tomazin on 04/09/2023. +// Copyright © 2023 Povio Inc. All rights reserved. +// + +import UIKit +import PovioKitAuthCore + +public final class LinkedInAuthenticator { + private let storage: UserDefaults + private let storageIsAuthenticatedKey = "signIn.isAuthenticated" + private let linkedInAPI: LinkedInAPI + + public init(storage: UserDefaults? = nil, + linkedInAPI: LinkedInAPI = .init()) { + self.storage = storage ?? .init(suiteName: "povioKit.auth.linkedIn") ?? .standard + self.linkedInAPI = linkedInAPI + } +} + +// MARK: - Public Methods +extension LinkedInAuthenticator: Authenticator { + /// SignIn user. + /// + /// Will return promise with the `Response` object on success or with `Error` on error. + public func signIn(authCode: String, configuration: Configuration) async throws -> Response { + let authRequest: LinkedInAPI.LinkedInAuthRequest = .init( + code: authCode, + redirectUri: configuration.redirectUrl.absoluteString, + clientId: configuration.clientId, + clientSecret: configuration.clientSecret + ) + let authResponse = try await linkedInAPI.login(with: authRequest) + let profileResponse = try await linkedInAPI.loadProfile(with: .init(token: authResponse.accessToken)) + let emailResponse = try await linkedInAPI.loadEmail(with: .init(token: authResponse.accessToken)) + + storage.set(true, forKey: storageIsAuthenticatedKey) + + let name = "\(profileResponse.localizedFirstName) \(profileResponse.localizedLastName)" + return Response( + userId: profileResponse.id, + token: authResponse.accessToken, + name: name, + email: emailResponse.emailAddress, + expiresAt: authResponse.expiresIn + ) + } + + /// Clears the signIn footprint and logs out the user immediatelly. + public func signOut() { + storage.removeObject(forKey: storageIsAuthenticatedKey) + } + + /// Returns the current authentication state. + public var isAuthenticated: Authenticated { + storage.bool(forKey: storageIsAuthenticatedKey) + } + + /// Boolean if given `url` should be handled. + /// + /// Call this from UIApplicationDelegate’s `application:openURL:options:` method. + public func canOpenUrl(_ url: URL, application: UIApplication, options: [UIApplication.OpenURLOptionsKey : Any]) -> Bool { + true + } +} diff --git a/Sources/LinkedIn/WebView/LinkedInSheet.swift b/Sources/LinkedIn/WebView/LinkedInSheet.swift new file mode 100644 index 0000000..aaf96fd --- /dev/null +++ b/Sources/LinkedIn/WebView/LinkedInSheet.swift @@ -0,0 +1,40 @@ +// +// File.swift +// +// +// Created by Borut Tomazin on 04/09/2023. +// + +import SwiftUI + +@available(iOS 15.0, *) +struct LinkedInSheet: ViewModifier { + typealias SuccessHandler = LinkedInWebView.SuccessHandler // (Bool) -> Void + typealias ErrorHandler = LinkedInWebView.ErrorHandler // (Error) -> Void + let config: LinkedInAuthenticator.Configuration + let isPresented: Binding + let onSuccess: SuccessHandler? + let onError: ErrorHandler? + + func body(content: Content) -> some View { + content + .sheet(isPresented: isPresented) { + LinkedInWebView(with: config) { data in + onSuccess?(data) + } onFailure: { + onError?() + } + } + } +} + +@available(iOS 15.0, *) +extension View { + /// ViewModifier to present `LinkedInWebView` in sheet + func linkedInSheet(with config: LinkedInAuthenticator.Configuration, + isPresented: Binding, + onSuccess: LinkedInSheet.SuccessHandler? = nil, + onError: LinkedInSheet.ErrorHandler? = nil) -> some View { + modifier(LinkedInSheet(config: config, isPresented: isPresented, onSuccess: onSuccess, onError: onError)) + } +} diff --git a/Sources/LinkedIn/WebView/LinkedInWebView.swift b/Sources/LinkedIn/WebView/LinkedInWebView.swift new file mode 100644 index 0000000..d15b4ab --- /dev/null +++ b/Sources/LinkedIn/WebView/LinkedInWebView.swift @@ -0,0 +1,102 @@ +// +// LinkedInWebView.swift +// PovioKitAuth +// +// Created by Borut Tomazin on 04/09/2023. +// Copyright © 2023 Povio Inc. All rights reserved. +// + +import PovioKitCore +import SwiftUI +import WebKit + +@available(iOS 15.0, *) +public struct LinkedInWebView: UIViewRepresentable { + @Environment(\.dismiss) var dismiss + // create a random string based on the time interval (it will be in the number form) - Needed for state. + public typealias SuccessHandler = ((code: String, state: String)) -> Void + public typealias ErrorHandler = () -> Void + private let requestState: String = "\(Int(Date().timeIntervalSince1970))" + private let webView: WKWebView + private let configuration: LinkedInAuthenticator.Configuration + public let onSuccess: SuccessHandler? + public let onFailure: ErrorHandler? + + public init(with configuration: LinkedInAuthenticator.Configuration, + onSuccess: @escaping SuccessHandler, + onFailure: @escaping ErrorHandler) { + self.configuration = configuration + let config = WKWebViewConfiguration() + config.websiteDataStore = WKWebsiteDataStore.default() + webView = WKWebView(frame: .zero, configuration: config) + self.onSuccess = onSuccess + self.onFailure = onFailure + + webView.navigationDelegate = makeCoordinator() + } + + public func makeUIView(context: Context) -> some UIView { + webView + } + + 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 + } + webView.navigationDelegate = context.coordinator + webView.load(.init(url: authURL)) + } + + public func makeCoordinator() -> Coordinator { + Coordinator(self, requestState: requestState) + } +} + +@available(iOS 15.0, *) +public extension LinkedInWebView { + class Coordinator: NSObject, WKNavigationDelegate { + private let parent: LinkedInWebView + private let requestState: String + + public init(_ parent: LinkedInWebView, requestState: String) { + self.parent = parent + self.requestState = requestState + } + + public func webView(_ webView: WKWebView, + decidePolicyFor navigationAction: WKNavigationAction, + decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { + if let url = navigationAction.request.url, + url.absoluteString.hasPrefix(parent.configuration.authCancel.absoluteString) { + decisionHandler(.cancel) + parent.dismiss() + return + } + + guard let url = webView.url, url.host == parent.configuration.redirectUrl.host else { + decisionHandler(.allow) + return + } + + // extract the authorization code + let components = URLComponents(string: url.absoluteString) + guard let state = components?.queryItems?.first(where: { $0.name == "state" }), + let code = components?.queryItems?.first(where: { $0.name == "code" }) else { + decisionHandler(.allow) + return + } + guard requestState == state.value ?? "" else { + parent.onFailure?() + decisionHandler(.allow) + parent.dismiss() + return + } + parent.onSuccess?((code.value ?? "", parent.requestState)) + decisionHandler(.allow) + parent.dismiss() + } + } +} From f34d118665da8d127f7faa631243421316a6586c Mon Sep 17 00:00:00 2001 From: Borut Tomazin Date: Fri, 29 Sep 2023 07:28:06 +0200 Subject: [PATCH 2/3] Chore/Apple (#12) --- Sources/Apple/AppleAuthenticator+Models.swift | 5 +++-- Sources/Apple/AppleAuthenticator.swift | 21 +++++++++++++++++-- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/Sources/Apple/AppleAuthenticator+Models.swift b/Sources/Apple/AppleAuthenticator+Models.swift index e6a77e0..fc528c5 100644 --- a/Sources/Apple/AppleAuthenticator+Models.swift +++ b/Sources/Apple/AppleAuthenticator+Models.swift @@ -17,9 +17,10 @@ public extension AppleAuthenticator { struct Response { public let userId: String public let token: String + public let authCode: String public let name: String? - public let email: Email? - public let expiresAt: Date? + public let email: Email + public let expiresAt: Date } } diff --git a/Sources/Apple/AppleAuthenticator.swift b/Sources/Apple/AppleAuthenticator.swift index ba09f92..d5228c1 100644 --- a/Sources/Apple/AppleAuthenticator.swift +++ b/Sources/Apple/AppleAuthenticator.swift @@ -90,7 +90,9 @@ extension AppleAuthenticator: ASAuthorizationControllerDelegate { public func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) { switch authorization.credential { case let credential as ASAuthorizationAppleIDCredential: - guard let identityToken = credential.identityToken, + guard let authCodeData = credential.authorizationCode, + let authCode = String(data: authCodeData, encoding: .utf8), + let identityToken = credential.identityToken, let identityTokenString = String(data: identityToken, encoding: .utf8) else { rejectSignIn(with: .invalidIdentityToken) return @@ -108,11 +110,24 @@ extension AppleAuthenticator: ASAuthorizationControllerDelegate { return .init(address: $0, isPrivate: isEmailPrivate, isVerified: isEmailVerified) } + // do not continue if `email` is missing + guard let email else { + rejectSignIn(with: .missingEmail) + return + } + + // do not continue if `expiresAt` is missing + guard let expiresAt = jwt?.expiresAt else { + rejectSignIn(with: .missingExpiration) + return + } + let response = Response(userId: credential.user, token: identityTokenString, + authCode: authCode, name: credential.displayName, email: email, - expiresAt: jwt?.expiresAt) + expiresAt: expiresAt) processingPromise?.resolve(with: response) case _: @@ -139,6 +154,8 @@ public extension AppleAuthenticator { case invalidIdentityToken case unhandledAuthorization case credentialsRevoked + case missingExpiration + case missingEmail } } From 234b9f05ae7cb2260c3e429eed51ed234b5c570d Mon Sep 17 00:00:00 2001 From: Borut Tomazin Date: Wed, 18 Oct 2023 15:40:27 +0200 Subject: [PATCH 3/3] Patch/LinkedIn (#13) --- .../LinkedInAuthenticator+Models.swift | 56 +++++++++++++++++-- Sources/LinkedIn/WebView/LinkedInSheet.swift | 18 +++--- .../LinkedIn/WebView/LinkedInWebView.swift | 27 +++------ 3 files changed, 69 insertions(+), 32 deletions(-) diff --git a/Sources/LinkedIn/LinkedInAuthenticator+Models.swift b/Sources/LinkedIn/LinkedInAuthenticator+Models.swift index 4383ea7..956d1ab 100644 --- a/Sources/LinkedIn/LinkedInAuthenticator+Models.swift +++ b/Sources/LinkedIn/LinkedInAuthenticator+Models.swift @@ -14,25 +14,71 @@ public extension LinkedInAuthenticator { let clientSecret: String let permissions: String let redirectUrl: URL - let authEndpoint: URL = "https://www.linkedin.com/oauth/v2/authorization" - let authCancel: URL = "https://www.linkedin.com/oauth/v2/login-cancel" + var authEndpoint: URL = "https://www.linkedin.com/oauth/v2/authorization" + var authCancel: URL = "https://www.linkedin.com/oauth/v2/login-cancel" + var audience: String? + public var codeVerifier: String? + var codeChallenge: String? + var codeChallengeMethod: String? - public init(clientId: String, clientSecret: String, permissions: String, redirectUrl: URL) { + public init( + clientId: String, + clientSecret: String, + permissions: String, + redirectUrl: URL + ) { self.clientId = clientId self.clientSecret = clientSecret self.permissions = permissions self.redirectUrl = redirectUrl } - func authorizationUrl(state: String) -> URL? { + public init( + clientId: String, + clientSecret: String, + permissions: String, + redirectUrl: URL, + authEndpoint: URL, + authCancel: URL, + audience: String?, + codeVerifier: String?, + codeChallenge: String?, + codeChallengeMethod: String? + ) { + self.clientId = clientId + self.clientSecret = clientSecret + self.permissions = permissions + self.redirectUrl = redirectUrl + self.authEndpoint = authEndpoint + self.authCancel = authCancel + self.audience = audience + self.codeVerifier = codeVerifier + self.codeChallenge = codeChallenge + self.codeChallengeMethod = codeChallengeMethod + } + + public func authorizationUrl(state: String) -> URL? { guard var urlComponents = URLComponents(url: authEndpoint, resolvingAgainstBaseURL: false) else { return nil } - urlComponents.queryItems = [ + var queryItems: [URLQueryItem] = [ .init(name: "response_type", value: "code"), + .init(name: "connection", value: "linkedin"), .init(name: "client_id", value: clientId), .init(name: "redirect_uri", value: redirectUrl.absoluteString), .init(name: "state", value: state), .init(name: "scope", value: permissions) ] + + if let audience { + queryItems.append(.init(name: "audience", value: audience)) + } + if let codeChallenge { + queryItems.append(.init(name: "code_challenge", value: codeChallenge)) + } + if let codeChallengeMethod { + queryItems.append(.init(name: "code_challenge_method", value: codeChallengeMethod)) + } + + urlComponents.queryItems = queryItems return urlComponents.url } } diff --git a/Sources/LinkedIn/WebView/LinkedInSheet.swift b/Sources/LinkedIn/WebView/LinkedInSheet.swift index aaf96fd..92d5a0d 100644 --- a/Sources/LinkedIn/WebView/LinkedInSheet.swift +++ b/Sources/LinkedIn/WebView/LinkedInSheet.swift @@ -8,15 +8,15 @@ import SwiftUI @available(iOS 15.0, *) -struct LinkedInSheet: ViewModifier { - typealias SuccessHandler = LinkedInWebView.SuccessHandler // (Bool) -> Void - typealias ErrorHandler = LinkedInWebView.ErrorHandler // (Error) -> Void - let config: LinkedInAuthenticator.Configuration - let isPresented: Binding - let onSuccess: SuccessHandler? - let onError: ErrorHandler? +public struct LinkedInSheet: ViewModifier { + public typealias SuccessHandler = LinkedInWebView.SuccessHandler // (Bool) -> Void + public typealias ErrorHandler = LinkedInWebView.ErrorHandler // (Error) -> Void + public let config: LinkedInAuthenticator.Configuration + public let isPresented: Binding + public let onSuccess: SuccessHandler? + public let onError: ErrorHandler? - func body(content: Content) -> some View { + public func body(content: Content) -> some View { content .sheet(isPresented: isPresented) { LinkedInWebView(with: config) { data in @@ -29,7 +29,7 @@ struct LinkedInSheet: ViewModifier { } @available(iOS 15.0, *) -extension View { +public extension View { /// ViewModifier to present `LinkedInWebView` in sheet func linkedInSheet(with config: LinkedInAuthenticator.Configuration, isPresented: Binding, diff --git a/Sources/LinkedIn/WebView/LinkedInWebView.swift b/Sources/LinkedIn/WebView/LinkedInWebView.swift index d15b4ab..fe4396f 100644 --- a/Sources/LinkedIn/WebView/LinkedInWebView.swift +++ b/Sources/LinkedIn/WebView/LinkedInWebView.swift @@ -31,8 +31,6 @@ public struct LinkedInWebView: UIViewRepresentable { webView = WKWebView(frame: .zero, configuration: config) self.onSuccess = onSuccess self.onFailure = onFailure - - webView.navigationDelegate = makeCoordinator() } public func makeUIView(context: Context) -> some UIView { @@ -69,6 +67,7 @@ public extension LinkedInWebView { public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { + // if authCancel endpoint was called, dismiss the view if let url = navigationAction.request.url, url.absoluteString.hasPrefix(parent.configuration.authCancel.absoluteString) { decisionHandler(.cancel) @@ -76,26 +75,18 @@ public extension LinkedInWebView { return } - guard let url = webView.url, url.host == parent.configuration.redirectUrl.host else { - decisionHandler(.allow) - return - } - - // extract the authorization code - let components = URLComponents(string: url.absoluteString) - guard let state = components?.queryItems?.first(where: { $0.name == "state" }), - let code = components?.queryItems?.first(where: { $0.name == "code" }) else { + // extract the authorization code from the redirect url + guard let url = webView.url, + url.host == parent.configuration.redirectUrl.host, + let components = URLComponents(string: url.absoluteString), + let state = components.queryItems?.first(where: { $0.name == "state" })?.value, + requestState == state, + let code = components.queryItems?.first(where: { $0.name == "code" }) else { decisionHandler(.allow) return } - guard requestState == state.value ?? "" else { - parent.onFailure?() - decisionHandler(.allow) - parent.dismiss() - return - } parent.onSuccess?((code.value ?? "", parent.requestState)) - decisionHandler(.allow) + decisionHandler(.cancel) parent.dismiss() } }