Skip to content

Commit

Permalink
BIT-83: Adds draft login screen (#30)
Browse files Browse the repository at this point in the history
  • Loading branch information
nathan-livefront authored Sep 15, 2023
1 parent 7997124 commit a463db7
Show file tree
Hide file tree
Showing 17 changed files with 686 additions and 33 deletions.
4 changes: 4 additions & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,7 @@ type_contents_order:
identifier_name:
excluded:
- id

inclusive_language:
override_allowed_terms:
- masterPassword
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@ struct CreateAccountRequestModel: Equatable {
let keys: KeysRequestModel? = nil

/// The master password hash used to authenticate a user.
let masterPasswordHash: String // swiftlint:disable:this inclusive_language
let masterPasswordHash: String

/// The master password hint.
let masterPasswordHint: String? = nil // swiftlint:disable:this inclusive_language
let masterPasswordHint: String? = nil

/// The user's name.
let name: String? = nil
Expand Down
72 changes: 60 additions & 12 deletions BitwardenShared/UI/Auth/AuthCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,24 @@ internal final class AuthCoordinator: Coordinator {
switch route {
case .createAccount:
showCreateAccount()
case .enterpriseSingleSignOn:
showEnterpriseSingleSignOn()
case .landing:
showLanding()
case .login:
showLogin()
case let .login(username, region, isLoginWithDeviceVisible):
showLogin(
state: LoginState(
isLoginWithDeviceVisible: isLoginWithDeviceVisible,
username: username,
region: region
)
)
case .loginOptions:
showLoginOptions()
case .loginWithDevice:
showLoginWithDevice()
case .masterPasswordHint:
showMasterPasswordHint()
case .regionSelection:
showRegionSelection()
}
Expand All @@ -61,29 +75,63 @@ internal final class AuthCoordinator: Coordinator {
)
)
)
stackNavigator.push(view, animated: UI.animated)
stackNavigator.push(view)
}

/// Shows the enterprise single sign-on screen.
private func showEnterpriseSingleSignOn() {
let view = Text("Enterprise Single Sign-On")
stackNavigator.push(view)
}

/// Shows the landing screen.
private func showLanding() {
let processor = LandingProcessor(
if stackNavigator.popToRoot(animated: UI.animated).isEmpty {
let processor = LandingProcessor(
coordinator: asAnyCoordinator(),
state: LandingState()
)
let store = Store(processor: processor)
let view = LandingView(store: store)
stackNavigator.push(view)
}
}

/// Shows the login screen.
///
/// - Parameter state: The `LoginState` to initialize the login screen with.
///
private func showLogin(state: LoginState) {
let processor = LoginProcessor(
coordinator: asAnyCoordinator(),
state: LandingState()
state: state
)
let store = Store(processor: processor)
let view = LandingView(store: store)
stackNavigator.push(view, animated: UI.animated)
let view = LoginView(store: store)
stackNavigator.push(view)
}

/// Shows the login screen.
private func showLogin() {
let view = Text("Login")
stackNavigator.push(view, animated: UI.animated)
/// Shows the login options screen.
private func showLoginOptions() {
let view = Text("Login Options")
stackNavigator.push(view)
}

/// Shows the login with device screen.
private func showLoginWithDevice() {
let view = Text("Login With Device")
stackNavigator.push(view)
}

/// Shows the master password hint screen.
private func showMasterPasswordHint() {
let view = Text("Master Password Hint")
stackNavigator.push(view)
}

/// Shows the region selection screen.
private func showRegionSelection() {
let view = Text("Region")
stackNavigator.push(view, animated: UI.animated)
stackNavigator.push(view)
}
}
49 changes: 46 additions & 3 deletions BitwardenShared/UI/Auth/AuthCoordinatorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,60 @@ class AuthCoordinatorTests: BitwardenTestCase {
XCTAssertTrue(stackNavigator.actions.last?.view is CreateAccountView)
}

/// `navigate(to:)` with `.enterpriseSingleSignOn` pushes the enterprise single sign-on view onto the stack
/// navigator.
func test_navigate_enterpriseSingleSignOn() {
subject.navigate(to: .enterpriseSingleSignOn)
XCTAssertTrue(stackNavigator.actions.last?.view is Text)
}

/// `navigate(to:)` with `.landing` pushes the landing view onto the stack navigator.
func test_navigate_landing() {
subject.navigate(to: .landing)
XCTAssertTrue(stackNavigator.actions.last?.view is LandingView)
}

/// `navigate(to:)` with `.landing` from `.login` pops back to the landing view.
func test_navigate_landing_fromLogin() {
stackNavigator.viewControllersToPop = [
UIViewController(),
]
subject.navigate(to: .landing)

XCTAssertEqual(stackNavigator.actions.last?.type, .poppedToRoot)
}

/// `navigate(to:)` with `.login` pushes the login view onto the stack navigator.
func test_navigate_login() {
subject.navigate(to: .login)
func test_navigate_login() throws {
subject.navigate(to: .login(
username: "username",
region: "region",
isLoginWithDeviceVisible: true
))

XCTAssertEqual(stackNavigator.actions.last?.type, .pushed)
let view = try XCTUnwrap(stackNavigator.actions.last?.view as? LoginView)
let state = view.store.state
XCTAssertEqual(state.username, "username")
XCTAssertEqual(state.region, "region")
XCTAssertTrue(state.isLoginWithDeviceVisible)
}

/// `navigate(to:)` with `.loginOptions` pushes the login options view onto the stack navigator.
func test_navigate_loginOptions() {
subject.navigate(to: .loginOptions)
XCTAssertTrue(stackNavigator.actions.last?.view is Text)
}

/// `navigate(to:)` with `.loginWithDevice` pushes the login with device view onto the stack navigator.
func test_navigate_loginWithDevice() {
subject.navigate(to: .loginWithDevice)
XCTAssertTrue(stackNavigator.actions.last?.view is Text)
}

// Placeholder assertion until the login screen is added: BIT-83
/// `navigate(to:)` with `.masterPasswordHint` pushes the master password hint view onto the stack navigator.
func test_navigate_masterPasswordHint() {
subject.navigate(to: .masterPasswordHint)
XCTAssertTrue(stackNavigator.actions.last?.view is Text)
}

Expand Down
20 changes: 19 additions & 1 deletion BitwardenShared/UI/Auth/AuthRoute.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,29 @@ public enum AuthRoute: Equatable {
/// A route to the create account screen.
case createAccount

/// A route to the enterprise single sign-on screen.
case enterpriseSingleSignOn

/// A route to the landing screen.
case landing

/// A route to the login screen.
case login
///
/// - Parameters:
/// - username: The username to display on the login screen.
/// - region: The region the user has selected for login.
/// - isLoginWithDeviceVisible: A flag indicating if the "Login with device" button should be displayed in the
/// login screen.
case login(username: String, region: String, isLoginWithDeviceVisible: Bool)

/// A route to the login options screen.
case loginOptions

/// A route to the login with device screen.
case loginWithDevice

/// A route to the master password hint screen.
case masterPasswordHint

/// A route to the region selection screen.
case regionSelection
Expand Down
7 changes: 6 additions & 1 deletion BitwardenShared/UI/Auth/Landing/LandingProcessor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@ class LandingProcessor: StateProcessor<LandingState, LandingAction, Void> {
override func receive(_ action: LandingAction) {
switch action {
case .continuePressed:
coordinator.navigate(to: .login)
// Region placeholder until region selection support is added: BIT-268
coordinator.navigate(to: .login(
username: state.email,
region: "region",
isLoginWithDeviceVisible: false
))
case .createAccountPressed:
coordinator.navigate(to: .createAccount)
case let .emailChanged(newValue):
Expand Down
8 changes: 7 additions & 1 deletion BitwardenShared/UI/Auth/Landing/LandingProcessorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,14 @@ class LandingProcessorTests: BitwardenTestCase {

/// `receive(_:)` with `.continuePressed` navigates to the login screen.
func test_receive_continuePressed() {
subject.state.email = "[email protected]"

subject.receive(.continuePressed)
XCTAssertEqual(coordinator.routes.last, .login)
XCTAssertEqual(coordinator.routes.last, .login(
username: "[email protected]",
region: "region",
isLoginWithDeviceVisible: false
))
}

/// `receive(_:)` with `.createAccountPressed` navigates to the create account screen.
Expand Down
28 changes: 28 additions & 0 deletions BitwardenShared/UI/Auth/Login/LoginAction.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// MARK: - LoginAction

/// Actions that can be processed by a `LoginProcessor`.
enum LoginAction: Equatable {
/// The get master password hint button was pressed.
case getMasterPasswordHintPressed

/// The enterprise single sign-on button was pressed.
case enterpriseSingleSignOnPressed

/// The login with device button was pressed.
case loginWithDevicePressed

/// The login with master password button was pressed.
case loginWithMasterPasswordPressed

/// The value for the master password was changed.
case masterPasswordChanged(String)

/// The more button was pressed.
case morePressed

/// The not you? button was pressed.
case notYouPressed

/// The reveal master password field button was pressed.
case revealMasterPasswordFieldPressed
}
47 changes: 47 additions & 0 deletions BitwardenShared/UI/Auth/Login/LoginProcessor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// MARK: LoginProcessor

/// The processor used to manage state and handle actions for the login screen.
///
class LoginProcessor: StateProcessor<LoginState, LoginAction, Void> {
// MARK: Private Properties

/// The `Coordinator` that handles navigation.
private var coordinator: AnyCoordinator<AuthRoute>

// MARK: Initialization

/// Creates a new `LoginProcessor`.
///
/// - Parameters:
/// - coordinator: The coordinator that handles navigation.
/// - state: The initial state of the processor.
///
init(coordinator: AnyCoordinator<AuthRoute>, state: LoginState) {
self.coordinator = coordinator
super.init(state: state)
}

// MARK: Methods

override func receive(_ action: LoginAction) {
switch action {
case .enterpriseSingleSignOnPressed:
coordinator.navigate(to: .enterpriseSingleSignOn)
case .getMasterPasswordHintPressed:
coordinator.navigate(to: .masterPasswordHint)
case .loginWithDevicePressed:
coordinator.navigate(to: .loginWithDevice)
case .loginWithMasterPasswordPressed:
// Add login functionality here: BIT-132
print("login with master password")
case let .masterPasswordChanged(newValue):
state.masterPassword = newValue
case .morePressed:
coordinator.navigate(to: .loginOptions)
case .notYouPressed:
coordinator.navigate(to: .landing)
case .revealMasterPasswordFieldPressed:
state.isMasterPasswordRevealed.toggle()
}
}
}
Loading

0 comments on commit a463db7

Please sign in to comment.