From edbe5f6b965fd2c89a917590f24de49690168038 Mon Sep 17 00:00:00 2001 From: Matt Czech Date: Wed, 16 Oct 2024 10:53:59 -0500 Subject: [PATCH] PM-11159: Add vault sync for importing logins (#1047) --- .../en.lproj/Localizable.strings | 1 + .../Application/Utilities/Navigator.swift | 4 +- .../ImportLogins/ImportLoginsAction.swift | 3 - .../ImportLogins/ImportLoginsEffect.swift | 3 + .../ImportLogins/ImportLoginsProcessor.swift | 27 ++++++-- .../ImportLoginsProcessorTests.swift | 68 +++++++++++++------ .../Vault/ImportLogins/ImportLoginsView.swift | 4 +- .../ImportLogins/ImportLoginsViewTests.swift | 8 +-- .../UI/Vault/Vault/VaultCoordinator.swift | 1 + 9 files changed, 83 insertions(+), 36 deletions(-) diff --git a/BitwardenShared/UI/Platform/Application/Support/Localizations/en.lproj/Localizable.strings b/BitwardenShared/UI/Platform/Application/Support/Localizations/en.lproj/Localizable.strings index 540493991..5ae8fba81 100644 --- a/BitwardenShared/UI/Platform/Application/Support/Localizations/en.lproj/Localizable.strings +++ b/BitwardenShared/UI/Platform/Application/Support/Localizations/en.lproj/Localizable.strings @@ -170,6 +170,7 @@ "ItemUpdated" = "Item saved"; "Submitting" = "Submitting..."; "Syncing" = "Syncing..."; +"SyncingLogins" = "Syncing logins..."; "SyncingComplete" = "Syncing complete"; "SyncingFailed" = "Syncing failed"; "SyncVaultNow" = "Sync vault now"; diff --git a/BitwardenShared/UI/Platform/Application/Utilities/Navigator.swift b/BitwardenShared/UI/Platform/Application/Utilities/Navigator.swift index 67f74b277..c558faaff 100644 --- a/BitwardenShared/UI/Platform/Application/Utilities/Navigator.swift +++ b/BitwardenShared/UI/Platform/Application/Utilities/Navigator.swift @@ -27,14 +27,14 @@ extension Navigator { /// func showLoadingOverlay(_ state: LoadingOverlayState) { guard let rootViewController else { return } - LoadingOverlayDisplayHelper.show(in: rootViewController, state: state) + LoadingOverlayDisplayHelper.show(in: rootViewController.topmostViewController(), state: state) } /// Hides the loading overlay view. /// func hideLoadingOverlay() { guard let rootViewController else { return } - LoadingOverlayDisplayHelper.hide(from: rootViewController) + LoadingOverlayDisplayHelper.hide(from: rootViewController.topmostViewController()) } /// Shows the toast. diff --git a/BitwardenShared/UI/Vault/Vault/ImportLogins/ImportLoginsAction.swift b/BitwardenShared/UI/Vault/Vault/ImportLogins/ImportLoginsAction.swift index 72ed3cc72..9f481cb57 100644 --- a/BitwardenShared/UI/Vault/Vault/ImportLogins/ImportLoginsAction.swift +++ b/BitwardenShared/UI/Vault/Vault/ImportLogins/ImportLoginsAction.swift @@ -3,9 +3,6 @@ /// Actions that can be processed by a `ImportLoginsProcessor`. /// enum ImportLoginsAction: Equatable { - /// Advance to the next page of instructions. - case advanceNextPage - /// Advance to the previous page of instructions. case advancePreviousPage diff --git a/BitwardenShared/UI/Vault/Vault/ImportLogins/ImportLoginsEffect.swift b/BitwardenShared/UI/Vault/Vault/ImportLogins/ImportLoginsEffect.swift index 4346a6aef..f5839e6ec 100644 --- a/BitwardenShared/UI/Vault/Vault/ImportLogins/ImportLoginsEffect.swift +++ b/BitwardenShared/UI/Vault/Vault/ImportLogins/ImportLoginsEffect.swift @@ -3,6 +3,9 @@ /// Effects handled by the `ImportLoginsProcessor`. /// enum ImportLoginsEffect: Equatable { + /// Advance to the next page of instructions. + case advanceNextPage + /// The view appeared on screen. case appeared diff --git a/BitwardenShared/UI/Vault/Vault/ImportLogins/ImportLoginsProcessor.swift b/BitwardenShared/UI/Vault/Vault/ImportLogins/ImportLoginsProcessor.swift index c3eaa0642..17c1a9371 100644 --- a/BitwardenShared/UI/Vault/Vault/ImportLogins/ImportLoginsProcessor.swift +++ b/BitwardenShared/UI/Vault/Vault/ImportLogins/ImportLoginsProcessor.swift @@ -6,6 +6,7 @@ class ImportLoginsProcessor: StateProcessor! var errorReporter: MockErrorReporter! + var settingsRepository: MockSettingsRepository! var stateService: MockStateService! var subject: ImportLoginsProcessor! @@ -17,12 +18,14 @@ class ImportLoginsProcessorTests: BitwardenTestCase { coordinator = MockCoordinator() errorReporter = MockErrorReporter() + settingsRepository = MockSettingsRepository() stateService = MockStateService() subject = ImportLoginsProcessor( coordinator: coordinator.asAnyCoordinator(), services: ServiceContainer.withMocks( errorReporter: errorReporter, + settingsRepository: settingsRepository, stateService: stateService ), state: ImportLoginsState() @@ -34,48 +37,54 @@ class ImportLoginsProcessorTests: BitwardenTestCase { coordinator = nil errorReporter = nil + settingsRepository = nil stateService = nil subject = nil } // MARK: Tests - /// `receive(_:)` with `.advanceNextPage` advances to the next page. + /// `perform(_:)` with `.advanceNextPage` advances to the next page. @MainActor - func test_receive_advanceNextPage() { + func test_perform_advanceNextPage() async { XCTAssertEqual(subject.state.page, .intro) - subject.receive(.advanceNextPage) + await subject.perform(.advanceNextPage) XCTAssertEqual(subject.state.page, .step1) - subject.receive(.advanceNextPage) + await subject.perform(.advanceNextPage) XCTAssertEqual(subject.state.page, .step2) - subject.receive(.advanceNextPage) - XCTAssertEqual(subject.state.page, .step3) - - // TODO: PM-11159 Sync vault - subject.receive(.advanceNextPage) + await subject.perform(.advanceNextPage) XCTAssertEqual(subject.state.page, .step3) } - /// `receive(_:)` with `.advancePreviousPage` advances to the previous page. + /// `perform(_:)` with `.advanceNextPage` initiates a vault sync when on the last page. @MainActor - func test_receive_advancePreviousPage() { + func test_perform_advanceNextPage_sync() async { subject.state.page = .step3 - subject.receive(.advancePreviousPage) - XCTAssertEqual(subject.state.page, .step2) + await subject.perform(.advanceNextPage) - subject.receive(.advancePreviousPage) - XCTAssertEqual(subject.state.page, .step1) + XCTAssertEqual(coordinator.loadingOverlaysShown, [LoadingOverlayState(title: Localizations.syncingLogins)]) + XCTAssertFalse(coordinator.isLoadingOverlayShowing) + XCTAssertTrue(settingsRepository.fetchSyncCalled) + } - subject.receive(.advancePreviousPage) - XCTAssertEqual(subject.state.page, .intro) + /// `perform(_:)` with `.advanceNextPage` initiates a vault sync when on the last page and + /// handles a sync error. + @MainActor + func test_perform_advanceNextPage_syncError() async { + subject.state.page = .step3 + settingsRepository.fetchSyncResult = .failure(BitwardenTestError.example) - // Advancing again stays at the first page. - subject.receive(.advancePreviousPage) - XCTAssertEqual(subject.state.page, .intro) + await subject.perform(.advanceNextPage) + + XCTAssertEqual(coordinator.loadingOverlaysShown, [LoadingOverlayState(title: Localizations.syncingLogins)]) + XCTAssertFalse(coordinator.isLoadingOverlayShowing) + XCTAssertEqual(coordinator.alertShown, [.networkResponseError(BitwardenTestError.example)]) + XCTAssertEqual(errorReporter.errors as? [BitwardenTestError], [BitwardenTestError.example]) + XCTAssertTrue(settingsRepository.fetchSyncCalled) } /// `perform(_:)` with `.appeared` loads the user's web vault host. @@ -141,6 +150,25 @@ class ImportLoginsProcessorTests: BitwardenTestCase { XCTAssertEqual(errorReporter.errors as? [StateServiceError], [.noActiveAccount]) } + /// `receive(_:)` with `.advancePreviousPage` advances to the previous page. + @MainActor + func test_receive_advancePreviousPage() { + subject.state.page = .step3 + + subject.receive(.advancePreviousPage) + XCTAssertEqual(subject.state.page, .step2) + + subject.receive(.advancePreviousPage) + XCTAssertEqual(subject.state.page, .step1) + + subject.receive(.advancePreviousPage) + XCTAssertEqual(subject.state.page, .intro) + + // Advancing again stays at the first page. + subject.receive(.advancePreviousPage) + XCTAssertEqual(subject.state.page, .intro) + } + /// `receive(_:)` with `.dismiss` dismisses the view. @MainActor func test_receive_dismiss() { diff --git a/BitwardenShared/UI/Vault/Vault/ImportLogins/ImportLoginsView.swift b/BitwardenShared/UI/Vault/Vault/ImportLogins/ImportLoginsView.swift index ef888e4fa..08fa5ed33 100644 --- a/BitwardenShared/UI/Vault/Vault/ImportLogins/ImportLoginsView.swift +++ b/BitwardenShared/UI/Vault/Vault/ImportLogins/ImportLoginsView.swift @@ -132,8 +132,8 @@ struct ImportLoginsView: View { .tint(Asset.Colors.textInteraction.swiftUIColor) VStack(spacing: 12) { - Button(Localizations.continue) { - store.send(.advanceNextPage) + AsyncButton(Localizations.continue) { + await store.perform(.advanceNextPage) } .buttonStyle(.primary()) diff --git a/BitwardenShared/UI/Vault/Vault/ImportLogins/ImportLoginsViewTests.swift b/BitwardenShared/UI/Vault/Vault/ImportLogins/ImportLoginsViewTests.swift index 1a361dc63..ba3cbcbc7 100644 --- a/BitwardenShared/UI/Vault/Vault/ImportLogins/ImportLoginsViewTests.swift +++ b/BitwardenShared/UI/Vault/Vault/ImportLogins/ImportLoginsViewTests.swift @@ -64,11 +64,11 @@ class ImportLoginsViewTests: BitwardenTestCase { /// Tapping the continue button for a step dispatches the `advanceNextPage` action. @MainActor - func test_step_continue_tap() throws { + func test_step_continue_tap() async throws { processor.state.page = .step1 - let button = try subject.inspect().find(button: Localizations.continue) - try button.tap() - XCTAssertEqual(processor.dispatchedActions.last, .advanceNextPage) + let button = try subject.inspect().find(asyncButton: Localizations.continue) + try await button.tap() + XCTAssertEqual(processor.effects.last, .advanceNextPage) } // MARK: Snapshots diff --git a/BitwardenShared/UI/Vault/Vault/VaultCoordinator.swift b/BitwardenShared/UI/Vault/Vault/VaultCoordinator.swift index 4ae00c7e7..05c0974b3 100644 --- a/BitwardenShared/UI/Vault/Vault/VaultCoordinator.swift +++ b/BitwardenShared/UI/Vault/Vault/VaultCoordinator.swift @@ -71,6 +71,7 @@ final class VaultCoordinator: Coordinator, HasStackNavigator { & HasFido2UserInterfaceHelper & HasLocalAuthService & HasNotificationService + & HasSettingsRepository & HasStateService & HasTimeProvider & HasVaultRepository