Skip to content

Commit

Permalink
BIT-155: Adds the landing screen and Auth module (#20)
Browse files Browse the repository at this point in the history
  • Loading branch information
nathan-livefront authored Sep 12, 2023
1 parent ca45d50 commit 416a672
Show file tree
Hide file tree
Showing 17 changed files with 565 additions and 35 deletions.
7 changes: 2 additions & 5 deletions Bitwarden/Application/SceneDelegateTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,14 @@ import XCTest
class SceneDelegateTests: BitwardenTestCase {
// MARK: Properties

var appCoordinator: MockCoordinator<AppRoute>!
var appModule: MockAppModule!
var subject: SceneDelegate!

// MARK: Setup & Teardown

override func setUp() {
super.setUp()
appCoordinator = MockCoordinator<AppRoute>()
appModule = MockAppModule()
appModule.appCoordinator = appCoordinator.asAnyCoordinator()
subject = SceneDelegate()
subject.appModule = appModule
}
Expand All @@ -42,7 +39,7 @@ class SceneDelegateTests: BitwardenTestCase {

XCTAssertNotNil(subject.appCoordinator)
XCTAssertNotNil(subject.window)
XCTAssertTrue(appCoordinator.isStarted)
XCTAssertTrue(appModule.appCoordinator.isStarted)
}

/// `scene(_:willConnectTo:options:)` without a `UIWindowScene` fails to create the app's UI.
Expand All @@ -56,6 +53,6 @@ class SceneDelegateTests: BitwardenTestCase {

XCTAssertNil(subject.appCoordinator)
XCTAssertNil(subject.window)
XCTAssertFalse(appCoordinator.isStarted)
XCTAssertFalse(appModule.appCoordinator.isStarted)
}
}
82 changes: 82 additions & 0 deletions BitwardenShared/UI/Auth/AuthCoordinator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import SwiftUI
import UIKit

// MARK: - AuthCoordinator

/// A coordinator that manages navigation in the authentication flow.
///
internal final class AuthCoordinator: Coordinator {
// MARK: Properties

/// The root navigator used to display this coordinator's interface.
weak var rootNavigator: (any RootNavigator)?

/// The stack navigator that is managed by this coordinator.
var stackNavigator: StackNavigator

// MARK: Initialization

/// Creates a new `AuthCoordinator`.
///
/// - Parameters:
/// - rootNavigator: The root navigator used to display this coordinator's interface.
/// - stackNavigator: The stack navigator that is managed by this coordinator.
///
init(
rootNavigator: RootNavigator,
stackNavigator: StackNavigator
) {
self.rootNavigator = rootNavigator
self.stackNavigator = stackNavigator
}

// MARK: Methods

func navigate(to route: AuthRoute, context: AnyObject?) {
switch route {
case .createAccount:
showCreateAccount()
case .landing:
showLanding()
case .login:
showLogin()
case .regionSelection:
showRegionSelection()
}
}

func start() {
rootNavigator?.show(child: stackNavigator)
}

// MARK: Private Methods

/// Shows the create account screen.
private func showCreateAccount() {
let view = Text("Create Account")
stackNavigator.push(view, animated: UI.animated)
}

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

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

/// Shows the region selection screen.
private func showRegionSelection() {
let view = Text("Region")
stackNavigator.push(view, animated: UI.animated)
}
}
84 changes: 84 additions & 0 deletions BitwardenShared/UI/Auth/AuthCoordinatorTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import SwiftUI
import XCTest

@testable import BitwardenShared

// MARK: - AuthCoordinatorTests

class AuthCoordinatorTests: BitwardenTestCase {
// MARK: Properties

var rootNavigator: MockRootNavigator!
var stackNavigator: MockStackNavigator!
var subject: AuthCoordinator!

// MARK: Setup & Teardown

override func setUp() {
super.setUp()
rootNavigator = MockRootNavigator()
stackNavigator = MockStackNavigator()
subject = AuthCoordinator(
rootNavigator: rootNavigator,
stackNavigator: stackNavigator
)
}

override func tearDown() {
super.tearDown()
rootNavigator = nil
stackNavigator = nil
subject = nil
}

// MARK: Tests

/// `navigate(to:)` with `.createAccount` pushes the create account view onto the stack navigator.
func test_navigate_createAccount() {
subject.navigate(to: .createAccount)

// Placeholder assertion until the create account screen is added: BIT-157
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 `.login` pushes the login view onto the stack navigator.
func test_navigate_login() {
subject.navigate(to: .login)

// Placeholder assertion until the login screen is added: BIT-83
XCTAssertTrue(stackNavigator.actions.last?.view is Text)
}

/// `navigate(to:)` with `.regionSelection` pushes the region selection view onto the stack navigator.
func test_navigate_regionSelection() {
subject.navigate(to: .regionSelection)

// Placeholder assertion until the region selection screen is added: BIT-268
XCTAssertTrue(stackNavigator.actions.last?.view is Text)
}

/// `rootNavigator` uses a weak reference and does not retain a value once the root navigator has been erased.
func test_rootNavigator_resetWeakReference() {
var rootNavigator: MockRootNavigator? = MockRootNavigator()
subject = AuthCoordinator(
rootNavigator: rootNavigator!,
stackNavigator: stackNavigator
)
XCTAssertNotNil(subject.rootNavigator)

rootNavigator = nil
XCTAssertNil(subject.rootNavigator)
}

/// `start()` presents the stack navigator within the root navigator.
func test_start() {
subject.start()
XCTAssertIdentical(rootNavigator.navigatorShown, stackNavigator)
}
}
32 changes: 32 additions & 0 deletions BitwardenShared/UI/Auth/AuthModule.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import UIKit

// MARK: - AuthModule

/// An object that builds coordinators for the auth flow.
@MainActor
public protocol AuthModule {
/// Initializes a coordinator for navigating between `AuthRoute`s.
///
/// - rootNavigator: The root navigator used to display this coordinator's interface.
/// - stackNavigator: The stack navigator that will be used to navigate between routes.
/// - Returns: A coordinator that can navigate to `AuthRoute`s.
///
func makeAuthCoordinator(
rootNavigator: RootNavigator,
stackNavigator: StackNavigator
) -> AnyCoordinator<AuthRoute>
}

// MARK: - DefaultAppModule

extension DefaultAppModule: AuthModule {
public func makeAuthCoordinator(
rootNavigator: RootNavigator,
stackNavigator: StackNavigator
) -> AnyCoordinator<AuthRoute> {
AuthCoordinator(
rootNavigator: rootNavigator,
stackNavigator: stackNavigator
).asAnyCoordinator()
}
}
16 changes: 16 additions & 0 deletions BitwardenShared/UI/Auth/AuthRoute.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// MARK: - AuthRoute

/// A route to a specific screen in the authentication flow.
public enum AuthRoute: Equatable {
/// A route to the create account screen.
case createAccount

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

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

/// A route to the region selection screen.
case regionSelection
}
19 changes: 19 additions & 0 deletions BitwardenShared/UI/Auth/Landing/LandingAction.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// MARK: - LandingAction

/// Actions that can be processed by a `LandingProcessor`.
enum LandingAction {
/// The continue button was pressed.
case continuePressed

/// The create account button was pressed.
case createAccountPressed

/// The value for the email was changed.
case emailChanged(String)

/// The region button was pressed.
case regionPressed

/// The value for the remember me toggle was changed.
case rememberMeChanged(Bool)
}
42 changes: 42 additions & 0 deletions BitwardenShared/UI/Auth/Landing/LandingProcessor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import Combine

// MARK: - LandingProcessor

/// The processor used to manage state and handle actions for the landing screen.
///
class LandingProcessor: StateProcessor<LandingState, LandingAction, Void> {
// MARK: Private Properties

/// The coordinator that handles navigation.
private let coordinator: AnyCoordinator<AuthRoute>

// MARK: Initialization

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

// MARK: Methods

override func receive(_ action: LandingAction) {
switch action {
case .continuePressed:
coordinator.navigate(to: .login)
case .createAccountPressed:
coordinator.navigate(to: .createAccount)
case let .emailChanged(newValue):
state.email = newValue
case .regionPressed:
coordinator.navigate(to: .regionSelection)
case let .rememberMeChanged(newValue):
state.isRememberMeOn = newValue
}
}
}
67 changes: 67 additions & 0 deletions BitwardenShared/UI/Auth/Landing/LandingProcessorTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import XCTest

@testable import BitwardenShared

// MARK: - LandingProcessorTests

class LandingProcessorTests: BitwardenTestCase {
// MARK: Properties

var coordinator: MockCoordinator<AuthRoute>!
var subject: LandingProcessor!

// MARK: Setup & Teardown

override func setUp() {
super.setUp()
coordinator = MockCoordinator<AuthRoute>()

let state = LandingState()
subject = LandingProcessor(
coordinator: coordinator.asAnyCoordinator(),
state: state
)
}

override func tearDown() {
super.tearDown()
coordinator = nil
subject = nil
}

// MARK: Tests

/// `receive(_:)` with `.continuePressed` navigates to the login screen.
func test_receive_continuePressed() {
subject.receive(.continuePressed)
XCTAssertEqual(coordinator.routes.last, .login)
}

/// `receive(_:)` with `.createAccountPressed` navigates to the create account screen.
func test_receive_createAccountPressed() {
subject.receive(.createAccountPressed)
XCTAssertEqual(coordinator.routes.last, .createAccount)
}

/// `receive(_:)` with `.emailChanged` updates the state to reflect the changes.
func test_receive_emailChanged() {
XCTAssertEqual(subject.state.email, "")

subject.receive(.emailChanged("[email protected]"))
XCTAssertEqual(subject.state.email, "[email protected]")
}

/// `receive(_:)` with `.regionPressed` navigates to the region selection screen.
func test_receive_regionPressed() {
subject.receive(.regionPressed)
XCTAssertEqual(coordinator.routes.last, .regionSelection)
}

/// `receive(_:)` with `.emailChanged` updates the state to reflect the changes.
func test_receive_rememberMeChanged() {
XCTAssertFalse(subject.state.isRememberMeOn)

subject.receive(.rememberMeChanged(true))
XCTAssertTrue(subject.state.isRememberMeOn)
}
}
Loading

0 comments on commit 416a672

Please sign in to comment.