Skip to content

Commit

Permalink
Mike/UI component data observing refactor (#140)
Browse files Browse the repository at this point in the history
* model changes

* remove passed param in favor of observing shared instance

* remove button public body
  • Loading branch information
mikepitre authored Sep 24, 2024
1 parent 7c85a35 commit ddb5ff5
Show file tree
Hide file tree
Showing 32 changed files with 412 additions and 409 deletions.
88 changes: 51 additions & 37 deletions Sources/API/Models/SignIn.swift
Original file line number Diff line number Diff line change
Expand Up @@ -226,31 +226,33 @@ public struct SignIn: Codable, Sendable, Equatable, Hashable {
}

public enum PrepareFirstFactorStrategy {
case emailCode
case phoneCode
case emailCode(emailAddressId: String? = nil)
case phoneCode(phoneNumberId: String? = nil)
case saml
case passkey
case resetPasswordEmailCode
case resetPasswordPhoneCode
case resetPasswordEmailCode(emailAddressId: String? = nil)
case resetPasswordPhoneCode(phoneNumberId: String? = nil)

var strategy: Strategy {
switch self {
case .emailCode: .emailCode
case .phoneCode: .phoneCode
case .saml: .saml
case .passkey: .passkey
case .resetPasswordEmailCode: .resetPasswordEmailCode
case .resetPasswordPhoneCode: .resetPasswordPhoneCode
}
}
}

private func prepareFirstFactorParams(for prepareFirstFactorStrategy: PrepareFirstFactorStrategy) -> PrepareFirstFactorParams {
let strategy: Strategy = switch prepareFirstFactorStrategy {
case .emailCode: .emailCode
case .phoneCode: .phoneCode
case .saml: .saml
case .passkey: .passkey
case .resetPasswordEmailCode: .resetPasswordEmailCode
case .resetPasswordPhoneCode: .resetPasswordPhoneCode
}

switch prepareFirstFactorStrategy {
case .emailCode, .resetPasswordEmailCode:
return .init(strategy: strategy.stringValue, emailAddressId: factorId(for: strategy))
return .init(strategy: prepareFirstFactorStrategy.strategy.stringValue, emailAddressId: factorId(for: prepareFirstFactorStrategy))
case .phoneCode, .resetPasswordPhoneCode:
return .init(strategy: strategy.stringValue, phoneNumberId: factorId(for: strategy))
return .init(strategy: prepareFirstFactorStrategy.strategy.stringValue, phoneNumberId: factorId(for: prepareFirstFactorStrategy))
case .saml, .passkey:
return .init(strategy: strategy.stringValue)
return .init(strategy: prepareFirstFactorStrategy.strategy.stringValue)
}
}

Expand Down Expand Up @@ -421,7 +423,7 @@ public struct SignIn: Codable, Sendable, Equatable, Hashable {
}
#endif

@MainActor
@discardableResult @MainActor
static func handleOAuthCallbackUrl(_ url: URL) async throws -> ExternalAuthResult {
if let nonce = ExternalAuthUtils.nonceFromCallbackUrl(url: url) {

Expand Down Expand Up @@ -547,7 +549,11 @@ extension SignIn {
@MainActor
var currentFirstFactor: SignInFactor? {
if let firstFactorVerification,
let currentFirstFactor = supportedFirstFactors?.first(where: { $0.strategyEnum == firstFactorVerification.strategyEnum }) {
firstFactorVerification.strategyEnum != .passkey,
let currentFirstFactor = supportedFirstFactors?.first(where: {
$0.strategyEnum == firstFactorVerification.strategyEnum &&
$0.safeIdentifier == identifier
}) {
return currentFirstFactor
}

Expand All @@ -563,20 +569,16 @@ extension SignIn {
guard let preferredStrategy = Clerk.shared.environment?.displayConfig.preferredSignInStrategy else { return nil }
let firstFactors = alternativeFirstFactors(currentFactor: nil) // filters out reset strategies and oauth

// Passkey should be prioritized, but the environment `preferredSignInStrategy` doesnt account for that yet
// this hardcodes passkeys to be preferred
if let passkeyFactor = firstFactors.first(where: { $0.strategyEnum == .passkey }) {
return passkeyFactor
}

switch preferredStrategy {

case .password:
let sortedFactors = firstFactors.sorted { $0.sortOrderPasswordPreferred < $1.sortOrderPasswordPreferred }
if let passwordFactor = sortedFactors.first(where: { $0.strategyEnum == .password }) {
return passwordFactor
}

return sortedFactors.first(where: { $0.safeIdentifier == identifier }) ?? firstFactors.first

case .otp:
let sortedFactors = firstFactors.sorted { $0.sortOrderOTPPreferred < $1.sortOrderOTPPreferred }
return sortedFactors.first(where: { $0.safeIdentifier == identifier }) ?? firstFactors.first
Expand All @@ -593,9 +595,10 @@ extension SignIn {
func alternativeFirstFactors(currentFactor: SignInFactor?) -> [SignInFactor] {
// Remove the current factor, reset factors, oauth factors
let firstFactors = supportedFirstFactors?.filter { factor in
factor.strategy != currentFactor?.strategy &&
factor != currentFactor &&
factor.strategyEnum?.isResetStrategy == false &&
!(factor.strategy).hasPrefix("oauth_")
!(factor.strategy).hasPrefix("oauth_") &&
factor.strategyEnum != .passkey
}

return firstFactors?.sorted(by: { $0.sortOrderPasswordPreferred < $1.sortOrderPasswordPreferred }) ?? []
Expand All @@ -605,13 +608,21 @@ extension SignIn {
supportedFirstFactors?.first(where: { $0.strategyEnum == strategy })
}

var resetFactor: SignInFactor? {
supportedFirstFactors?.first(where: {
$0.strategyEnum?.isResetStrategy == true
})
}

// Second SignInFactor

var currentSecondFactor: SignInFactor? {
guard status == .needsSecondFactor else { return nil }

if let secondFactorVerification,
let currentSecondFactor = supportedSecondFactors?.first(where: { $0.strategy == secondFactorVerification.strategy })
let currentSecondFactor = supportedSecondFactors?.first(where: {
$0.strategy == secondFactorVerification.strategy
})
{
return currentSecondFactor
}
Expand All @@ -637,22 +648,25 @@ extension SignIn {
}

func alternativeSecondFactors(currentFactor: SignInFactor?) -> [SignInFactor] {
supportedSecondFactors?.filter { $0.strategy != currentFactor?.strategy } ?? []
supportedSecondFactors?.filter { $0 != currentFactor } ?? []
}

func secondFactor(for strategy: Strategy) -> SignInFactor? {
supportedSecondFactors?.first(where: { $0.strategyEnum == strategy })
supportedSecondFactors?.first(where: {
$0.strategyEnum == strategy &&
$0.safeIdentifier == identifier
})
}

func factorId(for strategy: Strategy) -> String? {
private func factorId(for strategy: PrepareFirstFactorStrategy) -> String? {
let signInFactors = (supportedFirstFactors ?? []) + (supportedSecondFactors ?? [])
let signInFactor = signInFactors.first(where: { $0.strategyEnum == strategy })
let defaultSignInFactor = signInFactors.first(where: { $0.strategyEnum == strategy.strategy && $0.safeIdentifier == identifier })

switch strategy {
case .emailCode, .resetPasswordEmailCode:
return signInFactor?.emailAddressId
case .phoneCode, .resetPasswordPhoneCode:
return signInFactor?.phoneNumberId
case .emailCode(let emailAddressId), .resetPasswordEmailCode(let emailAddressId):
return emailAddressId ?? defaultSignInFactor?.emailAddressId
case .phoneCode(let phoneNumberId), .resetPasswordPhoneCode(let phoneNumberId):
return phoneNumberId ?? defaultSignInFactor?.phoneNumberId
default:
return nil
}
Expand All @@ -664,11 +678,11 @@ extension SignIn {
guard let supportedFirstFactors else { return nil }

if supportedFirstFactors.contains(where: { $0.strategyEnum == .resetPasswordEmailCode }) {
return .resetPasswordEmailCode
return .resetPasswordEmailCode()
}

if supportedFirstFactors.contains(where: { $0.strategyEnum == .resetPasswordPhoneCode }) {
return .resetPasswordPhoneCode
return .resetPasswordPhoneCode()
}

return nil
Expand Down
10 changes: 8 additions & 2 deletions Sources/API/Models/SignInFactor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,15 @@ extension SignInFactor {
var prepareFirstFactorStrategy: SignIn.PrepareFirstFactorStrategy? {
switch strategyEnum {
case .phoneCode:
return .phoneCode
return .phoneCode(phoneNumberId: phoneNumberId)
case .emailCode:
return .emailCode
return .emailCode(emailAddressId: emailAddressId)
case .resetPasswordPhoneCode:
return .resetPasswordPhoneCode(phoneNumberId: phoneNumberId)
case .resetPasswordEmailCode:
return .resetPasswordEmailCode(emailAddressId: emailAddressId)
case .passkey:
return .passkey
default:
return nil
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/API/Models/SignUp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,7 @@ public struct SignUp: Codable, Sendable, Equatable, Hashable {
}
#endif

@MainActor
@discardableResult @MainActor
static func handleOAuthCallbackUrl(_ url: URL) async throws -> ExternalAuthResult {
if let nonce = ExternalAuthUtils.nonceFromCallbackUrl(url: url) {

Expand Down
13 changes: 0 additions & 13 deletions Sources/API/Models/Strategy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -112,17 +112,4 @@ extension Strategy {
}
}

var signInPrepareStrategy: SignIn.PrepareFirstFactorStrategy? {
switch self {
case .phoneCode:
return .phoneCode
case .emailCode:
return .emailCode
case .saml:
return .saml
default:
return nil
}
}

}
72 changes: 41 additions & 31 deletions Sources/UI/ClerkUIState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,17 @@ final class ClerkUIState: ObservableObject {

enum AuthStep: Equatable, Hashable {
case signInStart
case signInFactorOne(signIn: SignIn, factor: SignInFactor?)
case signInFactorOneUseAnotherMethod(signIn: SignIn, currentFactor: SignInFactor?)
case signInFactorTwo(signIn: SignIn, factor: SignInFactor?)
case signInFactorTwoUseAnotherMethod(signIn: SignIn, currentFactor: SignInFactor?)
case signInForgotPassword(signIn: SignIn)
case signInResetPassword(signIn: SignIn)
case ssoCallback(signIn: SignIn)
case signInFactorOne(factor: SignInFactor)
case signInFactorOneUseAnotherMethod(currentFactor: SignInFactor)
case signInFactorTwo(factor: SignInFactor)
case signInFactorTwoUseAnotherMethod(currentFactor: SignInFactor)
case signInForgotPassword(factor: SignInFactor)
case signInResetPassword
case ssoCallback

case signUpStart
case signUpVerification(signUp: SignUp)
case signUpCreatePasskey(signUp: SignUp, user: User)
case signUpVerification
case signUpCreatePasskey
}

@Published var presentedAuthStep: AuthStep = .signInStart {
Expand All @@ -43,58 +43,69 @@ extension ClerkUIState {

/// Sets the current auth step to the status determined by the API
@MainActor
func setAuthStepToCurrentStatus(for signIn: SignIn) {
if signIn.firstFactorVerification?.status == .transferable, Clerk.shared.environment?.displayConfig.botProtectionIsEnabled == true {
presentedAuthStep = .ssoCallback(signIn: signIn)
func setAuthStepToCurrentSignInStatus() {
let signIn = Clerk.shared.client?.signIn

if signIn?.firstFactorVerification?.status == .transferable,
Clerk.shared.environment?.displayConfig.botProtectionIsEnabled == true
{
presentedAuthStep = .ssoCallback
return
}

switch signIn.status {
case .needsIdentifier, .abandoned:
switch signIn?.status {

case .needsIdentifier:
presentedAuthStep = .signInStart

case .needsFirstFactor:
guard let currentFirstFactor = signIn.currentFirstFactor else { authIsPresented = false; return }
presentedAuthStep = .signInFactorOne(signIn: signIn, factor: currentFirstFactor)
guard let currentFirstFactor = signIn?.currentFirstFactor else { presentedAuthStep = .signInStart; return }
presentedAuthStep = .signInFactorOne(factor: currentFirstFactor)

case .needsSecondFactor:
guard let currentSecondFactor = signIn.currentSecondFactor else { authIsPresented = false; return }
presentedAuthStep = .signInFactorTwo(signIn: signIn, factor: currentSecondFactor)
guard let currentSecondFactor = signIn?.currentSecondFactor else { presentedAuthStep = .signInStart; return }
presentedAuthStep = .signInFactorTwo(factor: currentSecondFactor)

case .needsNewPassword:
presentedAuthStep = .signInResetPassword(signIn: signIn)
presentedAuthStep = .signInResetPassword

case .complete:
case .complete, .none:
authIsPresented = false

case .abandoned:
presentedAuthStep = .signInStart

case .unknown:
authIsPresented = false
}
}

/// Sets the current auth step to the status determined by the API
@MainActor
func setAuthStepToCurrentStatus(for signUp: SignUp) {
switch signUp.status {
func setAuthStepToCurrentSignUpStatus() {
let signUp = Clerk.shared.client?.signUp

switch signUp?.status {

case .missingRequirements:
if (signUp.unverifiedFields ?? []).contains(where: { $0 == "email_address" || $0 == "phone_number" }) {
presentedAuthStep = .signUpVerification(signUp: signUp)
if (signUp?.unverifiedFields ?? []).contains(where: { $0 == "email_address" || $0 == "phone_number" }) {
presentedAuthStep = .signUpVerification
} else {
presentedAuthStep = .signUpStart
}

case .abandoned:
presentedAuthStep = .signUpStart

case .complete:
case .complete, .none:

// if a user just signed up, passkeys are enabled and they dont have any passkeys on their account
// then ask them if they would like to create one
if
Clerk.shared.environment?.userSettings.config(for: "passkey")?.enabled == true,
let user = Clerk.shared.user,
user.passkeys.isEmpty
if Clerk.shared.environment?.userSettings.config(for: "passkey")?.enabled == true,
let user = Clerk.shared.user,
user.passkeys.isEmpty
{
presentedAuthStep = .signUpCreatePasskey(signUp: signUp, user: user)
presentedAuthStep = .signUpCreatePasskey
return
}

Expand All @@ -104,7 +115,6 @@ extension ClerkUIState {
authIsPresented = false
}
}

}

#endif
Loading

0 comments on commit ddb5ff5

Please sign in to comment.