Skip to content

Commit

Permalink
handle oauth deep link when associated domain app links are used
Browse files Browse the repository at this point in the history
  • Loading branch information
mikepitre committed Jan 8, 2025
1 parent 3e98ee2 commit f417348
Show file tree
Hide file tree
Showing 14 changed files with 140 additions and 112 deletions.
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ let package = Package(
.package(url: "https://github.com/apple/swift-algorithms", .upToNextMajor(from: "1.2.0")),
.package(url: "https://github.com/auth0/SimpleKeychain", .upToNextMajor(from: "1.0.0")),
.package(url: "https://github.com/kean/Get", .upToNextMajor(from: "2.1.6")),
// .package(url: "https://github.com/hmlongco/Factory", branch: "swift6"),
.package(url: "https://github.com/hmlongco/Factory", .upToNextMajor(from: "2.3.1")),
.package(url: "https://github.com/kean/Nuke", .upToNextMajor(from: "12.1.6")),
.package(url: "https://github.com/marmelroy/PhoneNumberKit", .upToNextMajor(from: "3.7.4"))
Expand All @@ -32,6 +31,7 @@ let package = Package(
"SimpleKeychain",
"Get",
"Factory",
.product(name: "Nuke", package: "Nuke"),
.product(name: "NukeUI", package: "Nuke"),
"PhoneNumberKit"
],
Expand Down
13 changes: 13 additions & 0 deletions Sources/API/Models/ExternalAuthResult.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// ExternalAuthResult.swift
// Clerk
//
// Created by Mike Pitre on 1/6/25.
//

import Foundation

public struct ExternalAuthResult {
public var signIn: SignIn?
public var signUp: SignUp?
}
24 changes: 14 additions & 10 deletions Sources/API/Models/SignIn.swift
Original file line number Diff line number Diff line change
Expand Up @@ -435,20 +435,24 @@ public struct SignIn: Codable, Sendable, Equatable, Hashable {

let signIn = try await Client.get()?.signIn

guard signIn?.needsTransferToSignUp == true else {
return ExternalAuthResult(signIn: signIn)
}

let botProtectionIsEnabled = Clerk.shared.environment?.displayConfig.botProtectionIsEnabled == true

if botProtectionIsEnabled {
if signIn?.needsTransferToSignUp == true {

let botProtectionIsEnabled = Clerk.shared.environment?.displayConfig.botProtectionIsEnabled == true

if botProtectionIsEnabled {

return ExternalAuthResult(signIn: signIn)
return ExternalAuthResult(signIn: signIn)

} else {

let signUp = try await SignUp.create(strategy: .transfer)
return ExternalAuthResult(signUp: signUp)

}

} else {

let signUp = try await SignUp.create(strategy: .transfer)
return ExternalAuthResult(signUp: signUp)
return ExternalAuthResult(signIn: signIn)

}
}
Expand Down
8 changes: 4 additions & 4 deletions Sources/API/Models/SignUp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -393,12 +393,12 @@ public struct SignUp: Codable, Sendable, Equatable, Hashable {

let signUp = try await Client.get()?.signUp

guard signUp?.needsTransferToSignIn == true else {
if signUp?.needsTransferToSignIn == true {
let signIn = try await SignIn.create(strategy: .transfer)
return ExternalAuthResult(signIn: signIn)
} else {
return ExternalAuthResult(signUp: signUp)
}

let signIn = try await SignIn.create(strategy: .transfer)
return ExternalAuthResult(signIn: signIn)
}
}

Expand Down
12 changes: 2 additions & 10 deletions Sources/API/Utils/DeepLinkHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,21 +32,13 @@ extension Clerk {
)?.value else {
return
}

guard let url = URL(string: finalRedirectUrl) else {
return
}

do {
if try await SignIn.handleOAuthCallbackUrl(url) != nil {
WebAuthentication.currentSession?.cancel()
return
}

if try await SignUp.handleOAuthCallbackUrl(url) != nil {
WebAuthentication.currentSession?.cancel()
return
}
WebAuthentication.finishWithDeeplinkUrl(url: url)
} catch {
dump(error)
}
Expand Down
20 changes: 0 additions & 20 deletions Sources/API/Utils/ExternalAuthUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,24 +24,4 @@ enum ExternalAuthUtils {
return nonceQueryItem.value
}

#if canImport(AuthenticationServices) && !os(watchOS)
/// Presents the native sign in with apple sheet to get an ASAuthorizationAppleIDCredential
@MainActor
static func getAppleIdCredential() async throws -> ASAuthorizationAppleIDCredential {
let authManager = SignInWithAppleManager()
let authorization = try await authManager.start()

guard let appleIdCredential = authorization.credential as? ASAuthorizationAppleIDCredential else {
throw ClerkClientError(message: "Unable to get your Apple ID credential.")
}

return appleIdCredential
}
#endif

}

public struct ExternalAuthResult {
public var signIn: SignIn?
public var signUp: SignUp?
}
22 changes: 18 additions & 4 deletions Sources/API/Utils/SignInWithAppleManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import Foundation
import AuthenticationServices

final class SignInWithAppleManager: NSObject {
final public class SignInWithAppleManager: NSObject {

private var continuation: CheckedContinuation<ASAuthorization,Error>?

Expand Down Expand Up @@ -42,15 +42,29 @@ final class SignInWithAppleManager: NSObject {
authorizationController.performRequests()
}
}

/// Presents the native sign in with apple sheet to get an ASAuthorizationAppleIDCredential
@MainActor
static public func getAppleIdCredential() async throws -> ASAuthorizationAppleIDCredential {
let authManager = SignInWithAppleManager()
let authorization = try await authManager.start()

guard let appleIdCredential = authorization.credential as? ASAuthorizationAppleIDCredential else {
throw ClerkClientError(message: "Unable to get your Apple ID credential.")
}

return appleIdCredential
}

}

extension SignInWithAppleManager: ASAuthorizationControllerDelegate {

func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
public func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
continuation?.resume(returning: authorization)
}

func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: any Error) {
public func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: any Error) {
continuation?.resume(throwing: error)
}

Expand All @@ -59,7 +73,7 @@ extension SignInWithAppleManager: ASAuthorizationControllerDelegate {
extension SignInWithAppleManager: ASAuthorizationControllerPresentationContextProviding {

@MainActor
func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
public func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
ASPresentationAnchor()
}

Expand Down
35 changes: 0 additions & 35 deletions Sources/API/Utils/Task+Ext.swift

This file was deleted.

32 changes: 20 additions & 12 deletions Sources/API/Utils/WebAuthentication.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ final class WebAuthentication: NSObject {
let url: URL
let prefersEphemeralWebBrowserSession: Bool

static var currentSession: ASWebAuthenticationSession?
private static var currentSession: ASWebAuthenticationSession?
private static var continuation: CheckedContinuation<URL, any Error>?

init(
url: URL,
Expand All @@ -29,23 +30,26 @@ final class WebAuthentication: NSObject {
return components
}

private static func completionHandler(_ url: URL?, error: Error?) -> Void {
Self.currentSession = nil

if let url {
Self.continuation?.resume(returning: url)
} else if let error {
Self.continuation?.resume(throwing: error)
} else {
Self.continuation?.resume(throwing: ClerkClientError(message: "Missing callback URL"))
}
}

func start() async throws -> URL {
try await withCheckedThrowingContinuation { continuation in
Self.continuation = continuation

Self.currentSession = ASWebAuthenticationSession(
url: url,
callbackURLScheme: Clerk.shared.redirectConfig.callbackUrlScheme,
completionHandler: { url, error in
Self.currentSession = nil

if let url {
continuation.resume(returning: url)
} else if let error {
continuation.resume(throwing: error)
} else {
continuation.resume(throwing: ClerkClientError(message: "Missing callback URL"))
}
}
completionHandler: Self.completionHandler
)

#if !os(watchOS) && !os(tvOS)
Expand All @@ -59,6 +63,10 @@ final class WebAuthentication: NSObject {
Self.currentSession?.start()
}
}

static func finishWithDeeplinkUrl(url: URL) {
completionHandler(url, error: nil)
}
}

#if !os(watchOS) && !os(tvOS)
Expand Down
61 changes: 52 additions & 9 deletions Sources/UI/Common/AuthSocialProvidersView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ struct AuthSocialProvidersView: View {
@Environment(\.clerkTheme) private var clerkTheme
@State private var stackWidth: CGFloat = .zero

enum UseCase {
case signIn, signUp
}

var useCase: UseCase = .signIn
var onSuccess:((_ externalAuthResult: ExternalAuthResult) -> Void)?

private var socialProviders: [OAuthProvider] {
Expand Down Expand Up @@ -61,11 +66,9 @@ struct AuthSocialProvidersView: View {

do {
if provider == .apple {
externalAuthResult = try await signInWithApple()
externalAuthResult = try await authenticateWithApple()
} else {
externalAuthResult = try await SignIn
.create(strategy: .oauth(provider))
.authenticateWithRedirect()
externalAuthResult = try await authenticateWithOAuth(provider: provider)
}

onSuccess?(externalAuthResult)
Expand All @@ -76,16 +79,56 @@ struct AuthSocialProvidersView: View {
}
}

private func signInWithApple() async throws -> ExternalAuthResult {
let appleIdCredential = try await ExternalAuthUtils.getAppleIdCredential()
private func authenticateWithOAuth(provider: OAuthProvider) async throws -> ExternalAuthResult {
var externalAuthResult: ExternalAuthResult

switch useCase {
case .signIn:
externalAuthResult = try await SignIn
.create(strategy: .oauth(provider))
.authenticateWithRedirect()
case .signUp:
externalAuthResult = try await SignUp
.create(strategy: .oauth(provider))
.authenticateWithRedirect()
}

if let signUp = externalAuthResult.signUp,
let externalAccountVerification = signUp.verifications.first(where: { $0.key == "external_account" })?.value,
let error = externalAccountVerification.error {
throw error
}

return externalAuthResult
}

private func authenticateWithApple() async throws -> ExternalAuthResult {
let appleIdCredential = try await SignInWithAppleManager.getAppleIdCredential()

guard let idToken = appleIdCredential.identityToken.flatMap({ String(data: $0, encoding: .utf8) }) else {
throw ClerkClientError(message: "Unable to get ID token from Apple ID Credential.")
}

let externalAuthResult = try await SignIn
.create(strategy: .idToken(provider: .apple, idToken: idToken))
.authenticateWithIdToken()
var externalAuthResult: ExternalAuthResult

switch useCase {
case .signIn:
externalAuthResult = try await SignIn
.create(strategy: .idToken(provider: .apple, idToken: idToken))
.authenticateWithIdToken()

case .signUp:
externalAuthResult = try await SignUp
.create(
strategy: .idToken(
.apple,
idToken: idToken,
firstName: appleIdCredential.fullName?.givenName,
lastName: appleIdCredential.fullName?.familyName
)
)
.authenticateWithIdToken()
}

return externalAuthResult
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ struct SignInFactorOneAlternativeMethodsView: View {
}

private func signInWithApple() async throws -> ExternalAuthResult {
let appleIdCredential = try await ExternalAuthUtils.getAppleIdCredential()
let appleIdCredential = try await SignInWithAppleManager.getAppleIdCredential()

guard let idToken = appleIdCredential.identityToken.flatMap({ String(data: $0, encoding: .utf8) }) else {
throw ClerkClientError(message: "Unable to get ID token from Apple ID Credential.")
Expand Down
Loading

0 comments on commit f417348

Please sign in to comment.