Skip to content

Commit

Permalink
Add password dsicvoery to the b2b ui
Browse files Browse the repository at this point in the history
  • Loading branch information
nidal-stytch committed Jan 21, 2025
1 parent 6194938 commit 641455b
Show file tree
Hide file tree
Showing 16 changed files with 210 additions and 70 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public extension StytchB2BClient.Passwords {
let router: NetworkingRouter<StytchB2BClient.PasswordsRoute.DiscoveryRoute>

@Dependency(\.pkcePairManager) private var pkcePairManager
@Dependency(\.sessionManager) private var sessionManager

// sourcery: AsyncVariants, (NOTE: - must use /// doc comment styling)
///
Expand Down Expand Up @@ -42,13 +43,17 @@ public extension StytchB2BClient.Passwords {
throw StytchSDKError.missingPKCE
}

return try await router.post(
to: .resetByEmail,
parameters: CodeVerifierParameters(
codingPrefix: .pkce,
let intermediateSessionTokenParameters = IntermediateSessionTokenParameters(
intermediateSessionToken: sessionManager.intermediateSessionToken,
wrapped: CodeVerifierParameters(
codeVerifier: pkcePair.codeVerifier,
wrapped: parameters
),
)
)

return try await router.post(
to: .resetByEmail,
parameters: intermediateSessionTokenParameters,
useDFPPA: true
)
}
Expand All @@ -70,7 +75,7 @@ public extension StytchB2BClient.Passwords.Discovery {
let emailAddress: String
let discoveryRedirectUrl: URL?
let resetPasswordRedirectUrl: URL?
let resetPasswordExpirationMinutes: Int?
let resetPasswordExpirationMinutes: Minutes?
let resetPasswordTemplateId: String?

/// - Parameters:
Expand All @@ -95,7 +100,7 @@ public extension StytchB2BClient.Passwords.Discovery {
emailAddress: String,
discoveryRedirectUrl: URL? = nil,
resetPasswordRedirectUrl: URL? = nil,
resetPasswordExpirationMinutes: Int? = nil,
resetPasswordExpirationMinutes: Minutes = .defaultSessionDuration,
resetPasswordTemplateId: String? = nil
) {
self.emailAddress = emailAddress
Expand Down
7 changes: 6 additions & 1 deletion Sources/StytchCore/StytchClientType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,16 @@ extension StytchClientType {
let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
let queryItems = components.queryItems,
let typeQuery = queryItems.first(where: { $0.name == "stytch_token_type" }), let type = typeQuery.value,
let redirectTypeQuery = queryItems.first(where: { $0.name == "stytch_redirect_type" }), let redirectType = redirectTypeQuery.value,
let tokenQuery = queryItems.first(where: { $0.name == "token" }), let token = tokenQuery.value
else {
return nil
}

var redirectType: String?
if let redirectTypeQuery = queryItems.first(where: { $0.name == "stytch_redirect_type" }) {
redirectType = redirectTypeQuery.value
}

return (tokenType: type, redirectType, token)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import StytchCore
// B2BPasswordsViewModel and B2BPasswordsViewModelDelegate are shared between the home screen passwords form and the authentication screen.

protocol B2BPasswordsViewModelDelegate: AnyObject {
func didAuthenticateWithPassword()
func didAuthenticate()
func didDiscoveryAuthenticate()
func didSendEmailMagicLink()
func didError(error: Error)
}
Expand All @@ -23,7 +24,36 @@ final class B2BPasswordsViewModel {
password: String
) {
MemberManager.updateMemberEmailAddress(emailAddress)
if state.configuration.computedAuthFlowType == .discovery {
discoveryAuthenticateWithPasswordIfPossible(emailAddress: emailAddress, password: password)
} else {
organizationAuthenticateWithPasswordIfPossible(emailAddress: emailAddress, password: password)
}
}

func discoveryAuthenticateWithPasswordIfPossible(
emailAddress: String,
password: String
) {
StytchB2BUIClient.startLoading()
Task {
do {
let parameters = StytchB2BClient.Passwords.Discovery.AuthenticateParameters(emailAddress: emailAddress, password: password)
let response = try await StytchB2BClient.passwords.discovery.authenticate(parameters: parameters)
DiscoveryManager.updateDiscoveredOrganizations(newDiscoveredOrganizations: response.discoveredOrganizations)
delegate?.didDiscoveryAuthenticate()
StytchB2BUIClient.stopLoading()
} catch {
delegate?.didError(error: error)
StytchB2BUIClient.stopLoading()
}
}
}

func organizationAuthenticateWithPasswordIfPossible(
emailAddress: String,
password: String
) {
guard let organizationId = OrganizationManager.organizationId else {
delegate?.didError(error: StytchSDKError.noOrganziationId)
return
Expand All @@ -43,7 +73,7 @@ final class B2BPasswordsViewModel {
)
let response = try await StytchB2BClient.passwords.authenticate(parameters: parameters)
B2BAuthenticationManager.handlePrimaryMFAReponse(b2bMFAAuthenticateResponse: response)
delegate?.didAuthenticateWithPassword()
delegate?.didAuthenticate()
} else {
try await AuthenticationOperations.sendEmailMagicLinkIfPossible(
configuration: state.configuration,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,49 +29,55 @@ extension StytchB2BUIClient {
var productComponents = [ProductComponent]()
for product in validProducts {
switch product {
case .emailMagicLinks, .emailOtp:
if case .discovery = configuration.computedAuthFlowType {
if productComponents.contains(.email) == false {
productComponents.append(.email)
}
} else if configuration.supportsEmailAndPasswords == true {
if productComponents.contains(.emailAndPasswords) == false {
productComponents.append(.emailAndPasswords)
}
} else if productComponents.contains(.email) == false {
productComponents.append(.email)
case .emailMagicLinks, .emailOtp, .passwords:
if configuration.supportsEmailAndPasswords == true {
productComponents.appendIfNotPresent(.emailAndPasswords)
} else if configuration.supportsPasswordsWithoutEmail == true {
productComponents.appendIfNotPresent(.password)
} else if configuration.supportsEmailWithoutPasswords == true {
productComponents.appendIfNotPresent(.email)
}
case .sso:
if case .organization = configuration.computedAuthFlowType, hasSSOActiveConnections == true {
productComponents.append(.ssoButtons)
}
case .passwords:
if case .organization = configuration.computedAuthFlowType {
if configuration.supportsEmailAndPasswords == true, productComponents.contains(.emailAndPasswords) == false {
productComponents.append(.emailAndPasswords)
} else if configuration.supportsPasswordsWithoutEmail {
productComponents.append(.password)
}
productComponents.appendIfNotPresent(.ssoButtons)
}
case .oauth:
productComponents.append(.oAuthButtons)
productComponents.appendIfNotPresent(.oAuthButtons)
}
}

// If we have both buttons and input, we want to display a divider between the last 2 elements
let hasButtons = productComponents.contains(.oAuthButtons) || productComponents.contains(.ssoButtons)
let showDivider = hasButtons && (configuration.supportsEmail || configuration.supportsPasswords)
productComponents = addDividers(to: productComponents)
return productComponents
}

static func addDividers(to components: [ProductComponent]) -> [ProductComponent] {
var updatedComponents: [ProductComponent] = []

for (index, component) in components.enumerated() {
// Add dividers above and below input types
// Check if the current component is one of the target types
let shouldAddDivider = component == .email || component == .emailAndPasswords || component == .password

// Add a divider before the component if needed
if shouldAddDivider, index > 0 {
updatedComponents.append(.divider)
}

// Add the current component
updatedComponents.append(component)

if productComponents.count > 1, showDivider {
productComponents.insert(.divider, at: productComponents.count - 1)
// Add a divider after the component if needed
if shouldAddDivider, index < components.count - 1 {
updatedComponents.append(.divider)
}
}

return productComponents
return updatedComponents
}
}

extension StytchB2BUIClient {
enum ProductComponent: String {
enum ProductComponent: String, Equatable {
case email
case emailAndPasswords
case password
Expand All @@ -80,3 +86,11 @@ extension StytchB2BUIClient {
case divider
}
}

private extension Array where Element: Equatable {
mutating func appendIfNotPresent(_ element: Element) {
if contains(element) == false {
append(element)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import UIKit
final class B2BAuthHomeViewController: BaseViewController<B2BAuthHomeState, B2BAuthHomeViewModel> {
private let scrollView: UIScrollView = .init()

private let separatorView: LabelSeparatorView = .orSeparator()

private lazy var poweredByStytch: UIImageView = {
let view = UIImageView()
view.image = ImageAsset.poweredByStytch.image
Expand Down Expand Up @@ -124,6 +122,7 @@ final class B2BAuthHomeViewController: BaseViewController<B2BAuthHomeState, B2BA
constraints.append(ssoViewController.view.widthAnchor.constraint(equalTo: stackView.widthAnchor))
}
case .divider:
let separatorView = LabelSeparatorView.orSeparator()
stackView.addArrangedSubview(separatorView)
constraints.append(separatorView.widthAnchor.constraint(equalTo: stackView.widthAnchor))
}
Expand Down Expand Up @@ -190,6 +189,10 @@ extension B2BAuthHomeViewController: B2BPasswordsHomeViewControllerDelegate {
startMFAFlowIfNeeded(configuration: viewModel.state.configuration)
}

func didDiscoveryAuthenticateWithPassword() {
startDiscoveryFlowIfNeeded(configuration: viewModel.state.configuration)
}

func didSendEmailMagicLink() {
showEmailConfirmation(configuration: viewModel.state.configuration, type: .passwordResetVerify)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,7 @@ final class B2BEmailViewController: BaseViewController<B2BEmailState, B2BEmailVi
stackView.addArrangedSubview(emailInput)
stackView.addArrangedSubview(continueButton)

let isDicoveryFlow = viewModel.state.configuration.computedAuthFlowType == .discovery
if showsUsePasswordButton == true, isDicoveryFlow == false {
if showsUsePasswordButton == true {
stackView.addArrangedSubview(usePasswordInsteadButton)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import UIKit

protocol B2BPasswordsHomeViewControllerDelegate: AnyObject {
func didAuthenticateWithPassword()
func didDiscoveryAuthenticateWithPassword()
func didSendEmailMagicLink()
}

Expand Down Expand Up @@ -97,10 +98,14 @@ final class B2BPasswordsHomeViewController: BaseViewController<B2BPasswordsState
}

extension B2BPasswordsHomeViewController: B2BPasswordsViewModelDelegate {
func didAuthenticateWithPassword() {
func didAuthenticate() {
delegate?.didAuthenticateWithPassword()
}

func didDiscoveryAuthenticate() {
delegate?.didDiscoveryAuthenticateWithPassword()
}

func didSendEmailMagicLink() {
delegate?.didSendEmailMagicLink()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,14 @@ final class PasswordAuthenticateViewController: BaseViewController<B2BPasswordsS
}

extension PasswordAuthenticateViewController: B2BPasswordsViewModelDelegate {
func didAuthenticateWithPassword() {
func didAuthenticate() {
startMFAFlowIfNeeded(configuration: viewModel.state.configuration)
}

func didDiscoveryAuthenticate() {
startDiscoveryFlowIfNeeded(configuration: viewModel.state.configuration)
}

func didSendEmailMagicLink() {
showEmailConfirmation(configuration: viewModel.state.configuration, type: .passwordResetVerify)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@ extension PasswordForgotViewController: PasswordForgotViewModelDelegate {
showEmailConfirmation(configuration: viewModel.state.configuration, type: .passwordSetNew)
}

func didSendDiscoveryResetByEmailStart() {
StytchB2BUIClient.stopLoading()
showEmailConfirmation(configuration: viewModel.state.configuration, type: .passwordSetNew)
}

func didSendEmailMagicLink() {
StytchB2BUIClient.stopLoading()
showEmailConfirmation(configuration: viewModel.state.configuration, type: .passwordResetVerify)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import StytchCore

protocol PasswordForgotViewModelDelegate: AnyObject {
func didSendResetByEmailStart()
func didSendDiscoveryResetByEmailStart()
func didSendEmailMagicLink()
func didError(error: Error)
}
Expand All @@ -18,7 +19,14 @@ final class PasswordForgotViewModel {

func resetPassword(emailAddress: String) {
MemberManager.updateMemberEmailAddress(emailAddress)
if state.configuration.computedAuthFlowType == .discovery {
discoveryResetPasswordByEmail(emailAddress)
} else {
organizationResetPasswordByEmail(emailAddress)
}
}

func organizationResetPasswordByEmail(_ emailAddress: String) {
guard let organizationId = OrganizationManager.organizationId else {
delegate?.didError(error: StytchSDKError.noOrganziationId)
return
Expand Down Expand Up @@ -52,6 +60,23 @@ final class PasswordForgotViewModel {
}
}
}

func discoveryResetPasswordByEmail(_ emailAddress: String) {
Task {
do {
let parameters = StytchB2BClient.Passwords.Discovery.ResetByEmailStartParameters(
emailAddress: emailAddress,
discoveryRedirectUrl: state.configuration.redirectUrl,
resetPasswordRedirectUrl: state.configuration.redirectUrl,
resetPasswordExpirationMinutes: state.configuration.sessionDurationMinutes,
resetPasswordTemplateId: state.configuration.passwordOptions?.resetPasswordTemplateId
)
_ = try await StytchB2BClient.passwords.discovery.resetByEmailStart(parameters: parameters)
} catch {
delegate?.didError(error: error)
}
}
}
}

struct PasswordForgotState {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,11 @@ final class PasswordResetViewController: BaseViewController<PasswordResetState,
do {
try await viewModel.resetPassword(newPassword: password)
StytchB2BUIClient.stopLoading()
startMFAFlowIfNeeded(configuration: viewModel.state.configuration)
if viewModel.state.configuration.computedAuthFlowType == .discovery {
startDiscoveryFlowIfNeeded(configuration: viewModel.state.configuration)
} else {
startMFAFlowIfNeeded(configuration: viewModel.state.configuration)
}
} catch {
StytchB2BUIClient.stopLoading()
presentErrorAlert(error: error)
Expand Down
Loading

0 comments on commit 641455b

Please sign in to comment.