Skip to content

Commit

Permalink
Add Email OTP To B2B UI
Browse files Browse the repository at this point in the history
  • Loading branch information
nidal-stytch committed Jan 9, 2025
1 parent ed8b49c commit e742d93
Show file tree
Hide file tree
Showing 17 changed files with 514 additions and 119 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import StytchCore
import UIKit

class MFAEnrollmentSelectionTableViewCell: UITableViewCell {
class SelectionTableViewCell: UITableViewCell {
private let descriptionLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
Expand Down Expand Up @@ -47,11 +47,7 @@ class MFAEnrollmentSelectionTableViewCell: UITableViewCell {
fatalError("init(coder:) has not been implemented")
}

func configure(with mfaMethod: StytchB2BClient.MfaMethod, image _: UIImage?) {
if mfaMethod == .sms {
descriptionLabel.text = "Text me a code"
} else {
descriptionLabel.text = "Use an authenticator app"
}
func configure(with label: String) {
descriptionLabel.text = label
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import StytchCore
import UIKit

protocol MFAMethodSelectionViewControllerDelegate: AnyObject {
func didSelectMFAMethod(mfaMethod: StytchB2BClient.MfaMethod)
protocol SelectionViewControllerDelegate: AnyObject {
func didSelectCell(label: String)
}

class MFAMethodSelectionViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
class SelectionViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
private let tableView = UITableView()
private let mfaMethods: [StytchB2BClient.MfaMethod]
weak var delegate: MFAMethodSelectionViewControllerDelegate?
private let labels: [String]
weak var delegate: SelectionViewControllerDelegate?

init(mfaMethods: [StytchB2BClient.MfaMethod]) {
self.mfaMethods = mfaMethods
init(labels: [String]) {
self.labels = labels
super.init(nibName: nil, bundle: nil)
}

Expand All @@ -35,7 +35,7 @@ class MFAMethodSelectionViewController: UIViewController, UITableViewDataSource,
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
])

tableView.register(MFAEnrollmentSelectionTableViewCell.self, forCellReuseIdentifier: "MFAEnrollmentSelectionTableViewCell")
tableView.register(SelectionTableViewCell.self, forCellReuseIdentifier: "SelectionTableViewCell")
tableView.dataSource = self
tableView.delegate = self

Expand All @@ -45,19 +45,17 @@ class MFAMethodSelectionViewController: UIViewController, UITableViewDataSource,
}

func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int {
mfaMethods.count
labels.count
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "MFAEnrollmentSelectionTableViewCell", for: indexPath) as? MFAEnrollmentSelectionTableViewCell else {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "SelectionTableViewCell", for: indexPath) as? SelectionTableViewCell else {
return UITableViewCell()
}

cell.separatorInset = UIEdgeInsets.zero
cell.layoutMargins = UIEdgeInsets.zero

let mfaMethod = mfaMethods[indexPath.row]
cell.configure(with: mfaMethod, image: nil)
cell.configure(with: labels[indexPath.row])
return cell
}

Expand All @@ -66,8 +64,7 @@ class MFAMethodSelectionViewController: UIViewController, UITableViewDataSource,
}

func tableView(_: UITableView, didSelectRowAt indexPath: IndexPath) {
let mfaMethod = mfaMethods[indexPath.row]
delegate?.didSelectMFAMethod(mfaMethod: mfaMethod)
delegate?.didSelectCell(label: labels[indexPath.row])
tableView.deselectRow(at: indexPath, animated: true)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,51 @@ struct AuthenticationOperations {
_ = try await StytchB2BClient.magicLinks.email.loginOrSignup(parameters: parameters)
}

static func sendEmailMagicLinkForAuthFlowType(configuration: StytchB2BUIClient.Configuration, emailAddress: String) async throws {
if configuration.computedAuthFlowType == .discovery {
let parameters = StytchB2BClient.MagicLinks.Email.DiscoveryParameters(
emailAddress: emailAddress,
discoveryRedirectUrl: configuration.redirectUrl
)
_ = try await StytchB2BClient.magicLinks.email.discoverySend(parameters: parameters)
} else {
guard let organizationId = OrganizationManager.organizationId else {
throw StytchSDKError.noOrganziationId
}

try await sendEmailMagicLink(
configuration: configuration,
emailAddress: emailAddress,
organizationId: organizationId,
redirectUrl: configuration.redirectUrl
)
}
}

static func sendEmailOTPForAuthFlowType(configuration: StytchB2BUIClient.Configuration, emailAddress: String) async throws {
if configuration.computedAuthFlowType == .discovery {
let parameters = StytchB2BClient.OTP.Email.Discovery.SendParameters(
emailAddress: emailAddress,
loginTemplateId: configuration.emailOtpOptions?.loginTemplateId,
locale: nil
)
_ = try await StytchB2BClient.otp.email.discovery.send(parameters: parameters)
} else {
guard let organizationId = OrganizationManager.organizationId else {
throw StytchSDKError.noOrganziationId
}

let parameters = StytchB2BClient.OTP.Email.LoginOrSignupParameters(
organizationId: organizationId,
emailAddress: emailAddress,
loginTemplateId: configuration.emailOtpOptions?.loginTemplateId,
signupTemplateId: configuration.emailOtpOptions?.signupTemplateId,
locale: nil
)
_ = try await StytchB2BClient.otp.email.loginOrSignup(parameters: parameters)
}
}

static func smsSend(phoneNumberE164: String) async throws {
guard let organizationId = OrganizationManager.organizationId else {
throw StytchSDKError.noOrganziationId
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ import StytchCore
import UIKit

extension BaseViewController {
func startDiscoveryFlowIfNeeded(configuration: StytchB2BUIClient.Configuration) {
func startDiscoveryFlowIfNeeded(configuration: StytchB2BUIClient.Configuration, shouldPopToRoot: Bool = true) {
Task { @MainActor in
let discoveredOrganizations = DiscoveryManager.discoveredOrganizations
if let singleDiscoveredOrganization = discoveredOrganizations.shouldAllowDirectLoginToOrganization(configuration.directLoginForSingleMembershipOptions) {
selectDiscoveredOrganization(configuration: configuration, discoveredOrganization: singleDiscoveredOrganization)
} else {
navigationController?.popToRootViewController(animated: false)
if shouldPopToRoot == true {
navigationController?.popToRootViewController(animated: false)
}

let viewController: UIViewController
if DiscoveryManager.discoveredOrganizations.isEmpty {
if configuration.allowCreateOrganization == true, StytchB2BClient.createOrganizationEnabled == true {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,6 @@ public extension StytchB2BUIClient {
products.contains(.emailMagicLinks)
}

public var supportsEmailMagicLinksWithoutPasswords: Bool {
supportsEmailMagicLinks && !supportsPasswords
}

public var supportsEmailOTP: Bool {
products.contains(.emailOtp)
}
Expand All @@ -49,16 +45,28 @@ public extension StytchB2BUIClient {
products.contains(.passwords)
}

public var supportsPasswordsWithoutEmailMagiclinks: Bool {
!supportsEmailMagicLinks && supportsPasswords
}

public var supportsOauth: Bool {
products.contains(.oauth) && !oauthProviders.isEmpty
}

public var supportsEmailMagicLinksAndPasswords: Bool {
supportsEmailMagicLinks && supportsPasswords
public var supportsEmail: Bool {
supportsEmailMagicLinks || supportsEmailOTP
}

public var supportsEmailMagicLinksAndEmailOTP: Bool {
supportsEmailMagicLinks && supportsEmailOTP
}

public var supportsEmailAndPasswords: Bool {
supportsEmail && supportsPasswords
}

public var supportsEmailWithoutPasswords: Bool {
supportsEmail && !supportsPasswords
}

public var supportsPasswordsWithoutEmail: Bool {
!supportsEmail && supportsPasswords
}

public var organizationSlug: String? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,25 +29,27 @@ extension StytchB2BUIClient {
var productComponents = [ProductComponent]()
for product in validProducts {
switch product {
case .emailMagicLinks:
case .emailMagicLinks, .emailOtp:
if case .discovery = configuration.computedAuthFlowType {
productComponents.append(.emailMagicLink)
} else if configuration.supportsEmailMagicLinksAndPasswords == true, productComponents.contains(.emailMagicLinkAndPasswords) == false {
productComponents.append(.emailMagicLinkAndPasswords)
} else {
productComponents.append(.emailMagicLink)
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 .emailOtp:
break
case .sso:
if case .organization = configuration.computedAuthFlowType, hasSSOActiveConnections == true {
productComponents.append(.ssoButtons)
}
case .passwords:
if case .organization = configuration.computedAuthFlowType {
if configuration.supportsEmailMagicLinksAndPasswords == true, productComponents.contains(.emailMagicLinkAndPasswords) == false {
productComponents.append(.emailMagicLinkAndPasswords)
} else if configuration.supportsPasswordsWithoutEmailMagiclinks {
if configuration.supportsEmailAndPasswords == true, productComponents.contains(.emailAndPasswords) == false {
productComponents.append(.emailAndPasswords)
} else if configuration.supportsPasswordsWithoutEmail {
productComponents.append(.password)
}
}
Expand All @@ -58,7 +60,7 @@ extension StytchB2BUIClient {

// 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.supportsEmailMagicLinks || configuration.supportsPasswords)
let showDivider = hasButtons && (configuration.supportsEmail || configuration.supportsPasswords)

if productComponents.count > 1, showDivider {
productComponents.insert(.divider, at: productComponents.count - 1)
Expand All @@ -70,8 +72,8 @@ extension StytchB2BUIClient {

extension StytchB2BUIClient {
enum ProductComponent: String {
case emailMagicLink
case emailMagicLinkAndPasswords
case email
case emailAndPasswords
case password
case oAuthButtons
case ssoButtons
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,10 @@ final class B2BAuthHomeViewController: BaseViewController<B2BAuthHomeState, B2BA

for productComponent in productComponents {
switch productComponent {
case .emailMagicLink, .emailMagicLinkAndPasswords:
let emailMagicLinksViewController = B2BEmailMagicLinksViewController(
case .email, .emailAndPasswords:
let emailMagicLinksViewController = B2BEmailViewController(
state: .init(configuration: viewModel.state.configuration),
showsUsePasswordButton: productComponent == .emailMagicLinkAndPasswords,
showsUsePasswordButton: productComponent == .emailAndPasswords,
delegate: self
)
addChild(emailMagicLinksViewController)
Expand Down Expand Up @@ -158,11 +158,25 @@ extension B2BAuthHomeViewController: B2BOAuthViewControllerDelegate {
}
}

extension B2BAuthHomeViewController: B2BEmailMagicLinksViewControllerDelegate {
extension B2BAuthHomeViewController: B2BEmailViewControllerDelegate {
func emailOTPSent() {
Task { @MainActor in
let emailOTPEntryViewController = EmailOTPEntryViewController(state: .init(configuration: viewModel.state.configuration, didSendCode: true))
navigationController?.pushViewController(emailOTPEntryViewController, animated: true)
}
}

func emailMagicLinkSent() {
showEmailConfirmation(configuration: viewModel.state.configuration, type: .emailConfirmation)
}

func showEmailMethodSelection() {
Task { @MainActor in
let emailMethodSelectionViewController = EmailMethodSelectionViewController(state: .init(configuration: viewModel.state.configuration))
navigationController?.pushViewController(emailMethodSelectionViewController, animated: true)
}
}

func usePasswordInstead() {
Task { @MainActor in
let passwordAuthenticateViewController = PasswordAuthenticateViewController(state: .init(configuration: viewModel.state.configuration))
Expand Down

This file was deleted.

Loading

0 comments on commit e742d93

Please sign in to comment.