Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PM-11160: Add import logins success screen #1051

Merged
merged 2 commits into from
Oct 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class MasterPasswordGuidanceProcessor: StateProcessor<
) {
self.coordinator = coordinator
self.delegate = delegate
super.init(state: ())
super.init()
}

// MARK: Methods
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,12 +121,6 @@ struct MasterPasswordGuidanceView: View {

#if DEBUG
#Preview {
MasterPasswordGuidanceView(
store: Store(
processor: StateProcessor(
state: ()
)
)
)
MasterPasswordGuidanceView(store: Store(processor: StateProcessor()))
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class PreventAccountLockProcessor: StateProcessor<
///
init(coordinator: AnyCoordinator<AuthRoute, AuthEvent>) {
self.coordinator = coordinator
super.init(state: ())
super.init()
}

// MARK: Methods
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,12 +105,6 @@ struct PreventAccountLockView: View {

#if DEBUG
#Preview {
PreventAccountLockView(
store: Store(
processor: StateProcessor(
state: ()
)
)
)
PreventAccountLockView(store: Store(processor: StateProcessor()))
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"images" : [
{
"filename" : "desktop.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"images" : [
{
"filename" : "shield.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -1032,3 +1032,13 @@
"FillOutTheFormAndImportYourSavedPasswordFile" = "Fill out the form and **import your saved password file.**";
"SelectImportDataInTheWebAppThenDoneToFinishSyncing" = "**Select Import data** in the web app, then **Done** to finish syncing.";
"ForYourSecurityBeSureToDeleteYourSavedPasswordFile" = "For your security, be sure to **delete your saved password file.**";
"BitwardenTools" = "Bitwarden tools";
"ImportSuccessful" = "Import successful!";
"ManageYourLoginsFromAnywhereWithBitwardenToolsForWebAndDesktop" = "Manage your logins from anywhere with Bitwarden tools for web and desktop.";
"DownloadTheBrowserExtension" = "Download the browser extension";
"GoToBitwardenToIntegrateBitwardenIntoYourFavoriteBrowserForASeamlessExperience" = "Go to bitwarden.com/download to integrate Bitwarden into your favorite browser for a seamless experience.";
"UseTheWebApp" = "Use the web app";
"LogInAtBitwardenToEasilyManageYourAccountAndUpdateSettings" = "Log in at bitwarden.com to easily manage your account and update settings";
"AutofillPasswords" = "Autofill passwords";
"SetUpAutofillOnAllYourDevicesToLoginWithASingleTapAnywhere" = "Set up autofill on all your devices to login with a single tap anywhere.";
"GotIt" = "Got it";
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ open class StateProcessor<State: Sendable, Action: Sendable, Effect: Sendable>:
stateSubject = CurrentValueSubject(state)
}

/// Initializes a `StateProcessor` with an unused (`Void`) state.
///
public init() where State == Void {
stateSubject = CurrentValueSubject(())
}

/// Performs an asynchronous effect.
///
/// Override this method in subclasses to customize its behavior.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import SwiftUI

// MARK: - ContentBlock

/// A view that displays a block of content, where each content item is separated by a divider.
/// The block has the secondary background color applied with rounded corners.
///
/// Adapted from: https://movingparts.io/variadic-views-in-swiftui
///
struct ContentBlock<Content: View>: View {
// MARK: Properties

/// The content to display in the content block.
let content: Content

/// The amount of leading padding to apply to the divider.
let dividerLeadingPadding: CGFloat

// MARK: View

var body: some View {
// This uses SwiftUI's `VariadicView` API, which isn't part of SwiftUI's public API but
// since much of SwiftUI itself uses this, there's a low likelihood of this being removed.
_VariadicView.Tree(Layout(dividerLeadingPadding: dividerLeadingPadding)) {
content
}
}

// MARK: Initialization

/// Initialize a `ContentBlock`.
///
/// - Parameters:
/// - dividerLeadingPadding: The amount of leading padding to apply to the divider. Defaults
/// to `0` which will cause the divider to span the full width of the view.
/// - content: The content to display in the content block.
///
init(dividerLeadingPadding: CGFloat = 0, @ViewBuilder content: () -> Content) {
self.content = content()
self.dividerLeadingPadding = dividerLeadingPadding
}
}

extension ContentBlock {
/// The layout for the content block.
private struct Layout: _VariadicView_UnaryViewRoot {
// MARK: Properties

/// The amount of leading padding to apply to the divider.
let dividerLeadingPadding: CGFloat

// MARK: View

func body(children: _VariadicView.Children) -> some View {
let last = children.last?.id

VStack(spacing: 0) {
ForEach(children) { child in
VStack(alignment: .leading, spacing: 0) {
child

if child.id != last {
Divider()
.padding(.leading, dividerLeadingPadding)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}
.background(Asset.Colors.backgroundSecondary.swiftUIColor)
.clipShape(RoundedRectangle(cornerRadius: 10))
}
}
}

// MARK: Previews

#if DEBUG
#Preview {
ContentBlock {
Text("Apple ๐ŸŽ").padding()
Text("Banana ๐ŸŒ").padding()
Text("Grapes ๐Ÿ‡").padding()
}
.padding()
.background(Asset.Colors.backgroundPrimary.swiftUIColor)
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,8 @@ class ImportLoginsProcessor: StateProcessor<ImportLoginsState, ImportLoginsActio

do {
try await services.settingsRepository.fetchSync()
// TODO: PM-11160 Navigate to import successful
coordinator.hideLoadingOverlay()
coordinator.navigate(to: .importLoginsSuccess)
} catch {
coordinator.showAlert(.networkResponseError(error))
services.errorReporter.log(error: error)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ class ImportLoginsProcessorTests: BitwardenTestCase {

XCTAssertEqual(coordinator.loadingOverlaysShown, [LoadingOverlayState(title: Localizations.syncingLogins)])
XCTAssertFalse(coordinator.isLoadingOverlayShowing)
XCTAssertEqual(coordinator.routes, [.importLoginsSuccess])
XCTAssertTrue(settingsRepository.fetchSyncCalled)
}

Expand All @@ -83,6 +84,7 @@ class ImportLoginsProcessorTests: BitwardenTestCase {
XCTAssertEqual(coordinator.loadingOverlaysShown, [LoadingOverlayState(title: Localizations.syncingLogins)])
XCTAssertFalse(coordinator.isLoadingOverlayShowing)
XCTAssertEqual(coordinator.alertShown, [.networkResponseError(BitwardenTestError.example)])
XCTAssertTrue(coordinator.routes.isEmpty)
XCTAssertEqual(errorReporter.errors as? [BitwardenTestError], [BitwardenTestError.example])
XCTAssertTrue(settingsRepository.fetchSyncCalled)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// MARK: - ImportLoginsSuccessAction

/// Actions that can be processed by a `ImportLoginsSuccessProcessor`.
///
enum ImportLoginsSuccessAction {
/// Dismiss the view.
case dismiss
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// MARK: - ImportLoginsSuccessProcessor

/// The processor used to manage state and handle actions for the import logins success screen.
///
class ImportLoginsSuccessProcessor: StateProcessor<Void, ImportLoginsSuccessAction, Void> {
// MARK: Private Properties

/// The coordinator that handles navigation.
private let coordinator: AnyCoordinator<VaultRoute, AuthAction>

// MARK: Initialization

/// Creates a new `ImportLoginsSuccessProcessor`.
///
/// - Parameter coordinator: The coordinator that handles navigation.
///
init(coordinator: AnyCoordinator<VaultRoute, AuthAction>) {
self.coordinator = coordinator
super.init()
}

// MARK: Methods

override func receive(_ action: ImportLoginsSuccessAction) {
switch action {
case .dismiss:
coordinator.navigate(to: .dismiss)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import XCTest

@testable import BitwardenShared

class ImportLoginsSuccessProcessorTests: BitwardenTestCase {
// MARK: Properties

var coordinator: MockCoordinator<VaultRoute, AuthAction>!
var subject: ImportLoginsSuccessProcessor!

// MARK: Setup & Teardown

override func setUp() {
super.setUp()

coordinator = MockCoordinator()

subject = ImportLoginsSuccessProcessor(
coordinator: coordinator.asAnyCoordinator()
)
}

override func tearDown() {
super.tearDown()

coordinator = nil
subject = nil
}

// MARK: Tests

/// `receive(_:)` with `.dismiss` dismisses the view.
@MainActor
func test_receive_dismiss() {
subject.receive(.dismiss)
XCTAssertEqual(coordinator.routes.last, .dismiss)
}
}
Loading
Loading