Skip to content

Commit 9ae99da

Browse files
PM-13433: Import login flow from settings (#1066)
1 parent 7467e72 commit 9ae99da

File tree

53 files changed

+477
-95
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+477
-95
lines changed

BitwardenShared/UI/Platform/Application/AppCoordinator.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,14 @@ extension AppCoordinator: SendItemDelegate {
363363
// MARK: - SettingsCoordinatorDelegate
364364

365365
extension AppCoordinator: SettingsCoordinatorDelegate {
366+
func didCompleteLoginsImport() {
367+
navigate(to: .tab(.vault(.list)))
368+
showToast(
369+
Localizations.loginsImported,
370+
subtitle: Localizations.rememberToDeleteYourImportedPasswordFileFromYourComputer
371+
)
372+
}
373+
366374
func didDeleteAccount() {
367375
Task {
368376
await handleAuthEvent(.didDeleteAccount)

BitwardenShared/UI/Platform/Application/AppCoordinatorTests.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,14 @@ class AppCoordinatorTests: BitwardenTestCase { // swiftlint:disable:this type_bo
9292
XCTAssertNil(subject.authCompletionRoute)
9393
}
9494

95+
/// `didCompleteLoginsImport()` navigates to the vault list.
96+
@MainActor
97+
func test_didCompleteLoginsImport() {
98+
subject.didCompleteLoginsImport()
99+
XCTAssertTrue(module.tabCoordinator.isStarted)
100+
XCTAssertEqual(module.tabCoordinator.routes, [.vault(.list)])
101+
}
102+
95103
/// `didDeleteAccount(otherAccounts:)` navigates to the `didDeleteAccount` route.
96104
@MainActor
97105
func test_didDeleteAccount() throws {

BitwardenShared/UI/Platform/Application/AppModuleTests.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,19 @@ class AppModuleTests: BitwardenTestCase {
7676
XCTAssertTrue(navigationController.viewControllers[0] is UIHostingController<ExtensionActivationView>)
7777
}
7878

79+
/// `makeImportLoginsCoordinator` builds the import logins coordinator.
80+
@MainActor
81+
func test_makeImportLoginsCoordinator() {
82+
let navigationController = UINavigationController()
83+
let coordinator = subject.makeImportLoginsCoordinator(
84+
delegate: MockImportLoginsCoordinatorDelegate(),
85+
stackNavigator: navigationController
86+
)
87+
coordinator.navigate(to: .importLogins(.vault))
88+
XCTAssertEqual(navigationController.viewControllers.count, 1)
89+
XCTAssertTrue(navigationController.viewControllers[0] is UIHostingController<ImportLoginsView>)
90+
}
91+
7992
/// `makeSendCoordinator()` builds the send coordinator.
8093
@MainActor
8194
func test_makeSendCoordinator() {

BitwardenShared/UI/Platform/Application/Support/Localizations/en.lproj/Localizable.strings

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1049,3 +1049,5 @@
10491049
"NoAccountFoundPleaseLogInAgainIfYouContinueToSeeThisError" = "No account found. Please log in again if you continue to see this error.";
10501050
"ImportError" = "Import error";
10511051
"NoLoginsWereImported" = "No logins were imported";
1052+
"LoginsImported" = "Logins imported";
1053+
"RememberToDeleteYourImportedPasswordFileFromYourComputer" = "Remember to delete your imported password file from your computer.";

BitwardenShared/UI/Platform/Application/Utilities/Navigator.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,14 @@ extension Navigator {
2727
///
2828
func showLoadingOverlay(_ state: LoadingOverlayState) {
2929
guard let rootViewController else { return }
30-
LoadingOverlayDisplayHelper.show(in: rootViewController.topmostViewController(), state: state)
30+
LoadingOverlayDisplayHelper.show(in: rootViewController, state: state)
3131
}
3232

3333
/// Hides the loading overlay view.
3434
///
3535
func hideLoadingOverlay() {
3636
guard let rootViewController else { return }
37-
LoadingOverlayDisplayHelper.hide(from: rootViewController.topmostViewController())
37+
LoadingOverlayDisplayHelper.hide(from: rootViewController)
3838
}
3939

4040
/// Shows the toast.

BitwardenShared/UI/Platform/Application/Views/ToastDisplayHelper.swift

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,12 @@ enum ToastDisplayHelper {
3333

3434
// Position the toast view on the window with appropriate bottom padding above the tab bar.
3535
window.addSubview(viewController.view)
36-
let bottomPadding = window.safeAreaInsets.bottom + getSafeArea(from: parentViewController).bottom + 14
36+
let bottomPadding = getSafeArea(from: parentViewController).bottom + 16
3737
viewController.view.translatesAutoresizingMaskIntoConstraints = false
3838
viewController.view.bottomAnchor.constraint(equalTo: window.bottomAnchor, constant: -bottomPadding)
3939
.isActive = true
40-
viewController.view.centerXAnchor.constraint(equalTo: window.centerXAnchor).isActive = true
40+
viewController.view.leadingAnchor.constraint(equalTo: window.leadingAnchor).isActive = true
41+
viewController.view.trailingAnchor.constraint(equalTo: window.trailingAnchor).isActive = true
4142

4243
// Animate the toast in.
4344
UIView.animate(withDuration: UI.duration(transitionDuration)) {
@@ -46,7 +47,7 @@ enum ToastDisplayHelper {
4647

4748
// Dismiss the toast after 3 seconds.
4849
Timer.scheduledTimer(withTimeInterval: duration, repeats: false) { _ in
49-
hide(from: parentViewController)
50+
hide(viewController.view)
5051
}
5152
}
5253

@@ -65,19 +66,16 @@ enum ToastDisplayHelper {
6566
let selected = tabBarController?.selectedViewController,
6667
let topViewController = (selected as? UINavigationController)?.topViewController,
6768
!topViewController.hidesBottomBarWhenPushed {
68-
let height = tabBar.bounds.height - tabBar.safeAreaInsets.bottom
69-
return UIEdgeInsets(top: 0, left: 0, bottom: height, right: 0)
69+
return UIEdgeInsets(top: 0, left: 0, bottom: tabBar.bounds.height, right: 0)
7070
}
71-
return .zero
71+
return parentViewController.view.safeAreaInsets
7272
}
7373

7474
/// Hides the toast from showing over the specified view controller
7575
///
76-
/// - Parameter parentViewController: The parent view controller that the toast is shown in.
76+
/// - Parameter view: The toast view to hide.
7777
///
78-
private static func hide(from parentViewController: UIViewController) {
79-
guard let view = parentViewController.view.window?.viewWithTag(toastTag) else { return }
80-
78+
private static func hide(_ view: UIView) {
8179
UIView.animate(withDuration: UI.duration(transitionDuration)) {
8280
view.layer.opacity = 0
8381
} completion: { _ in

BitwardenShared/UI/Platform/Settings/Settings/Vault/VaultSettingsProcessor.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,7 @@ final class VaultSettingsProcessor: StateProcessor<VaultSettingsState, VaultSett
6363
self.state.url = self.services.environmentService.importItemsURL
6464
})
6565
case .showImportLogins:
66-
// TODO: PM-13467 Navigate to import logins
67-
// coordinator.navigate(to: .importLogins)
68-
break
66+
coordinator.navigate(to: .importLogins)
6967
}
7068
}
7169

BitwardenShared/UI/Platform/Settings/Settings/Vault/VaultSettingsProcessorTests.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,4 +132,12 @@ class VaultSettingsProcessorTests: BitwardenTestCase {
132132
try await alert.tapAction(title: Localizations.continue)
133133
XCTAssertEqual(subject.state.url, environmentService.importItemsURL)
134134
}
135+
136+
/// `receive(_:)` with `.showImportLogins` navigates to the import logins screen.
137+
@MainActor
138+
func test_receive_showImportLogins() {
139+
subject.receive(.showImportLogins)
140+
141+
XCTAssertEqual(coordinator.routes.last, .importLogins)
142+
}
135143
}

BitwardenShared/UI/Platform/Settings/SettingsCoordinator.swift

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ import SwiftUI
99
///
1010
@MainActor
1111
public protocol SettingsCoordinatorDelegate: AnyObject {
12+
/// Called when the user completes the import navigation flow and should be navigated to the vault tab.
13+
///
14+
func didCompleteLoginsImport()
15+
1216
/// Called when the active user's account has been deleted.
1317
///
1418
func didDeleteAccount()
@@ -45,6 +49,7 @@ final class SettingsCoordinator: Coordinator, HasStackNavigator { // swiftlint:d
4549

4650
/// The module types required by this coordinator for creating child coordinators.
4751
typealias Module = AuthModule
52+
& ImportLoginsModule
4853
& LoginRequestModule
4954

5055
typealias Services = HasAccountAPIService
@@ -149,6 +154,8 @@ final class SettingsCoordinator: Coordinator, HasStackNavigator { // swiftlint:d
149154
showExportVault()
150155
case .folders:
151156
showFolders()
157+
case .importLogins:
158+
showImportLogins()
152159
case let .loginRequest(loginRequest):
153160
showLoginRequest(loginRequest, delegate: context as? LoginRequestDelegate)
154161
case .other:
@@ -350,6 +357,21 @@ final class SettingsCoordinator: Coordinator, HasStackNavigator { // swiftlint:d
350357
stackNavigator?.push(viewController, navigationTitle: Localizations.folders)
351358
}
352359

360+
/// Shows the import login items screen.
361+
///
362+
private func showImportLogins() {
363+
let navigationController = UINavigationController()
364+
navigationController.modalPresentationStyle = .overFullScreen
365+
let coordinator = module.makeImportLoginsCoordinator(
366+
delegate: self,
367+
stackNavigator: navigationController
368+
)
369+
coordinator.start()
370+
coordinator.navigate(to: .importLogins(.settings))
371+
372+
stackNavigator?.present(navigationController)
373+
}
374+
353375
/// Shows the login request.
354376
///
355377
/// - Parameters:
@@ -446,7 +468,17 @@ final class SettingsCoordinator: Coordinator, HasStackNavigator { // swiftlint:d
446468
}
447469
}
448470

449-
// MARK: SettingsProcessorDelegate
471+
// MARK: - ImportLoginsCoordinatorDelegate
472+
473+
extension SettingsCoordinator: ImportLoginsCoordinatorDelegate {
474+
func didCompleteLoginsImport() {
475+
stackNavigator?.dismiss {
476+
self.delegate?.didCompleteLoginsImport()
477+
}
478+
}
479+
}
480+
481+
// MARK: - SettingsProcessorDelegate
450482

451483
extension SettingsCoordinator: SettingsProcessorDelegate {
452484
func updateSettingsTabBadge(_ badgeValue: String?) {

BitwardenShared/UI/Platform/Settings/SettingsCoordinatorTests.swift

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,17 @@ class SettingsCoordinatorTests: BitwardenTestCase {
3939

4040
// MARK: Tests
4141

42+
/// `didCompleteLoginsImport()` notifies the delegate that the user completed importing their
43+
/// logins and dismisses the import logins flow.
44+
@MainActor
45+
func test_didCompleteLoginsImport() throws {
46+
subject.didCompleteLoginsImport()
47+
48+
XCTAssertTrue(delegate.didCompleteLoginsImportCalled)
49+
let action = try XCTUnwrap(stackNavigator.actions.last)
50+
XCTAssertEqual(action.type, .dismissedWithCompletionHandler)
51+
}
52+
4253
/// `navigate(to:)` with `.about` pushes the about view onto the stack navigator.
4354
@MainActor
4455
func test_navigateTo_about() throws {
@@ -165,6 +176,18 @@ class SettingsCoordinatorTests: BitwardenTestCase {
165176
XCTAssertTrue(navigationController.viewControllers.first is UIHostingController<ExportVaultView>)
166177
}
167178

179+
/// `navigate(to:)` with `.importLogins` presents the import logins flow.
180+
@MainActor
181+
func test_navigateTo_importLogins() throws {
182+
subject.navigate(to: .importLogins)
183+
184+
let action = try XCTUnwrap(stackNavigator.actions.last)
185+
XCTAssertEqual(action.type, .presented)
186+
XCTAssertTrue(action.view is UINavigationController)
187+
XCTAssertTrue(module.importLoginsCoordinator.isStarted)
188+
XCTAssertEqual(module.importLoginsCoordinator.routes.last, .importLogins(.settings))
189+
}
190+
168191
/// `navigate(to:)` with `.lockVault` navigates the user to the login view.
169192
@MainActor
170193
func test_navigateTo_lockVault() async throws {
@@ -331,6 +354,7 @@ class SettingsCoordinatorTests: BitwardenTestCase {
331354
}
332355

333356
class MockSettingsCoordinatorDelegate: SettingsCoordinatorDelegate {
357+
var didCompleteLoginsImportCalled = false
334358
var didDeleteAccountCalled = false
335359
var didLockVaultCalled = false
336360
var didLogoutCalled = false
@@ -341,6 +365,10 @@ class MockSettingsCoordinatorDelegate: SettingsCoordinatorDelegate {
341365
var wasLogoutUserInitiated: Bool?
342366
var wasSwitchAutomatic: Bool?
343367

368+
func didCompleteLoginsImport() {
369+
didCompleteLoginsImportCalled = true
370+
}
371+
344372
func didDeleteAccount() {
345373
didDeleteAccountCalled = true
346374
}

BitwardenShared/UI/Platform/Settings/SettingsRoute.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ public enum SettingsRoute: Equatable, Hashable {
4040
/// A route to view the folders in the vault.
4141
case folders
4242

43+
/// A route to the import logins screen.
44+
case importLogins
45+
4346
/// A route to view a login request.
4447
///
4548
/// - Parameter loginRequest: The login request to display.

BitwardenShared/UI/Vault/Vault/ImportLogins/ImportLoginsProcessor.swift renamed to BitwardenShared/UI/Vault/ImportLogins/ImportLogins/ImportLoginsProcessor.swift

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ class ImportLoginsProcessor: StateProcessor<ImportLoginsState, ImportLoginsActio
1313
// MARK: Private Properties
1414

1515
/// The coordinator that handles navigation.
16-
private let coordinator: AnyCoordinator<VaultRoute, AuthAction>
16+
private let coordinator: AnyCoordinator<ImportLoginsRoute, ImportLoginsEvent>
1717

1818
/// The services used by this processor.
1919
private let services: Services
@@ -28,7 +28,7 @@ class ImportLoginsProcessor: StateProcessor<ImportLoginsState, ImportLoginsActio
2828
/// - state: The initial state of the processor.
2929
///
3030
init(
31-
coordinator: AnyCoordinator<VaultRoute, AuthAction>,
31+
coordinator: AnyCoordinator<ImportLoginsRoute, ImportLoginsEvent>,
3232
services: Services,
3333
state: ImportLoginsState
3434
) {
@@ -135,7 +135,6 @@ class ImportLoginsProcessor: StateProcessor<ImportLoginsState, ImportLoginsActio
135135

136136
do {
137137
try await services.settingsRepository.fetchSync()
138-
coordinator.hideLoadingOverlay()
139138

140139
guard try await !services.vaultRepository.isVaultEmpty() else {
141140
showImportLoginsEmptyAlert()

BitwardenShared/UI/Vault/Vault/ImportLogins/ImportLoginsProcessorTests.swift renamed to BitwardenShared/UI/Vault/ImportLogins/ImportLogins/ImportLoginsProcessorTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import XCTest
55
class ImportLoginsProcessorTests: BitwardenTestCase {
66
// MARK: Properties
77

8-
var coordinator: MockCoordinator<VaultRoute, AuthAction>!
8+
var coordinator: MockCoordinator<ImportLoginsRoute, ImportLoginsEvent>!
99
var errorReporter: MockErrorReporter!
1010
var settingsRepository: MockSettingsRepository!
1111
var stateService: MockStateService!
@@ -31,7 +31,7 @@ class ImportLoginsProcessorTests: BitwardenTestCase {
3131
stateService: stateService,
3232
vaultRepository: vaultRepository
3333
),
34-
state: ImportLoginsState()
34+
state: ImportLoginsState(mode: .vault)
3535
)
3636
}
3737

BitwardenShared/UI/Vault/Vault/ImportLogins/ImportLoginsState.swift renamed to BitwardenShared/UI/Vault/ImportLogins/ImportLogins/ImportLoginsState.swift

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,19 @@
22

33
/// An object that defines the current state of a `ImportLoginsView`.
44
///
5-
struct ImportLoginsState: Equatable, Sendable {
5+
public struct ImportLoginsState: Equatable, Sendable {
66
// MARK: Types
77

8+
/// The modes that define where the import logins flow started from.
9+
///
10+
public enum Mode: Equatable, Sendable {
11+
/// Import logins from the app's settings.
12+
case settings
13+
14+
/// Import logins from the vault list.
15+
case vault
16+
}
17+
818
/// An enumeration of the instruction pages that the user can navigate between.
919
///
1020
enum Page: Int {
@@ -26,9 +36,19 @@ struct ImportLoginsState: Equatable, Sendable {
2636

2737
// MARK: Properties
2838

39+
/// The mode of the view based on where the import logins flow was started from.
40+
var mode: Mode
41+
2942
/// The current page.
3043
var page = Page.intro
3144

3245
/// The hostname of the web vault URL.
3346
var webVaultHost = Constants.defaultWebVaultHost
47+
48+
// MARK: Computed Properties
49+
50+
/// Whether the import logins later button should be shown.
51+
var shouldShowImportLoginsLater: Bool {
52+
mode == .vault
53+
}
3454
}

0 commit comments

Comments
 (0)