Skip to content

Commit

Permalink
PM-11150: Add import logins step 1 (#1031)
Browse files Browse the repository at this point in the history
  • Loading branch information
matt-livefront authored Oct 14, 2024
1 parent 87d6e41 commit 5a47964
Show file tree
Hide file tree
Showing 21 changed files with 354 additions and 15 deletions.
4 changes: 4 additions & 0 deletions BitwardenShared/Core/Platform/Models/Enum/FeatureFlag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ enum FeatureFlag: String, CaseIterable, Codable {
/// A flag that enables individual cipher encryption.
case enableCipherKeyEncryption

/// A feature flag for the import logins flow for new accounts.
case importLoginsFlow = "import-logins-flow"

/// A feature flag for the intro carousel flow.
case nativeCarouselFlow = "native-carousel-flow"

Expand Down Expand Up @@ -75,6 +78,7 @@ enum FeatureFlag: String, CaseIterable, Codable {
switch self {
case .enableAuthenticatorSync,
.enableCipherKeyEncryption,
.importLoginsFlow,
.nativeCarouselFlow,
.nativeCreateAccountFlow,
.testLocalFeatureFlag,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ enum ExternalLinksConstants {
/// A link to Bitwarden's general help and feedback page.
static let helpAndFeedback = URL(string: "https://bitwarden.com/help/")!

/// A link to the import logins help page.
static let importHelp = URL(string: "https://bitwarden.com/help/import-data/")!

/// A link to the password options within the passwords section of the settings menu.
static let passwordOptions = URL(string: "App-prefs:PASSWORDS&path=PASSWORD_OPTIONS")!

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1016,3 +1016,10 @@
"YouCanReturnToCompleteThisStepAnytimeInVaultUnderSettings" = "You can return to complete this step anytime in Vault under Settings.";
"DoYouHaveAComputerAvailable" = "Do you have a computer available?";
"DoYouHaveAComputerAvailableDescriptionLong" = "The following instructions will guide you through importing logins from your desktop or laptop computer.";
"StepXOfY" = "Step %1$@ of %2$@";
"ExportYourSavedLogins" = "Export your saved logins";
"ExportLoginsStep1" = "On your computer, **log in to your current browser or password manager.**";
"ExportLoginsStep2" = "**Export your passwords.** This option is usually found in your settings.";
"ExportLoginsStep3" = "**Select Import data** in the web app, then Done to finish syncing.";
"ExportLoginsStep3Subtitle" = "You’ll delete this file after import is complete.";
"NeedHelpCheckOutImportHelp" = "Need help? Check out **[import help](%1$@)**";
79 changes: 79 additions & 0 deletions BitwardenShared/UI/Platform/Application/Views/NumberedList.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import SwiftUI

// MARK: - NumberedList

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

/// The content to display in the numbered list. Each child view will receive it's own number.
let content: Content

// MARK: View

var body: some View {
_VariadicView.Tree(Layout()) {
content
}
}

// MARK: Initialization

/// Initialize a `NumberedList`.
///
/// - Parameter content: The content to display in the numbered list.
///
init(@ViewBuilder content: () -> Content) {
self.content = content()
}
}

extension NumberedList {
/// The layout for the numbered list.
private struct Layout: _VariadicView_UnaryViewRoot {
func body(children: _VariadicView.Children) -> some View {
let last = children.last?.id

VStack(spacing: 0) {
ForEachIndexed(children) { index, child in
HStack(spacing: 12) {
Text(String(index + 1))
.styleGuide(.title2, weight: .bold)
.foregroundStyle(Asset.Colors.textInteraction.swiftUIColor)
.frame(minWidth: 24, alignment: .center)
.padding(.leading, 12)

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

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

// MARK: Previews

#if DEBUG
#Preview {
NumberedList {
NumberedListRow(title: "Apple 🍎")
NumberedListRow(title: "Banana 🍌")
NumberedListRow(title: "Grapes 🍇")
}
.padding()
.background(Asset.Colors.backgroundPrimary.swiftUIColor)
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import SwiftUI

/// A view that displays a single row within a `NumberedList`.
///
struct NumberedListRow: View {
// MARK: Properties

/// The title to display in the row.
let title: String

/// An optional subtitle to display in the row.
let subtitle: String?

// MARK: View

var body: some View {
VStack(alignment: .leading, spacing: 2) {
Text(LocalizedStringKey(title))
.styleGuide(.body)
.foregroundStyle(Asset.Colors.textPrimary.swiftUIColor)

if let subtitle {
Text(LocalizedStringKey(subtitle))
.styleGuide(.subheadline)
.foregroundStyle(Asset.Colors.textSecondary.swiftUIColor)
}
}
.padding(.vertical, 12)
.padding(.trailing, 16) // Leading padding is handled by `NumberedList`.
}

// MARK: Initialization

/// Initializes a `NumberedListRow`.
///
/// - Parameters:
/// - title: The title to display in the row.
/// - subtitle: An optional subtitle to display in the row.
///
init(title: String, subtitle: String? = nil) {
self.title = title
self.subtitle = subtitle
}
}

// MARK: Previews

#if DEBUG
#Preview {
NumberedList {
NumberedListRow(title: "Apple 🍎")
NumberedListRow(title: "Banana 🍌")
NumberedListRow(title: "Grapes 🍇")
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
/// 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

/// Dismiss the view.
case dismiss

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ class ImportLoginsProcessor: StateProcessor<ImportLoginsState, ImportLoginsActio

override func receive(_ action: ImportLoginsAction) {
switch action {
case .advanceNextPage:
advanceNextPage()
case .advancePreviousPage:
advancePreviousPage()
case .dismiss:
coordinator.navigate(to: .dismiss)
case .getStarted:
Expand All @@ -55,11 +59,28 @@ class ImportLoginsProcessor: StateProcessor<ImportLoginsState, ImportLoginsActio

// MARK: Private

/// Advances the view to show the next page of instructions.
///
private func advanceNextPage() {
guard let next = state.page.next else {
// TODO: PM-11159 Sync vault
return
}
state.page = next
}

/// Advances the view to show the previous page of instructions.
///
private func advancePreviousPage() {
guard let previous = state.page.previous else { return }
state.page = previous
}

/// Shows the alert confirming the user wants to get started on importing logins.
///
private func showGetStartAlert() {
coordinator.showAlert(.importLoginsComputerAvailable {
// TODO: PM-11150 Show step 1
self.advanceNextPage()
})
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,32 @@ class ImportLoginsProcessorTests: BitwardenTestCase {

// MARK: Tests

/// `receive(_:)` with `.advanceNextPage` advances to the next page.
@MainActor
func test_receive_advanceNextPage() {
XCTAssertEqual(subject.state.page, .intro)

subject.receive(.advanceNextPage)
XCTAssertEqual(subject.state.page, .step1)

// TODO: PM-11159 Sync vault
subject.receive(.advanceNextPage)
XCTAssertEqual(subject.state.page, .step1)
}

/// `receive(_:)` with `.advancePreviousPage` advances to the previous page.
@MainActor
func test_receive_advancePreviousPage() {
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)
}

/// `perform(_:)` with `.importLoginsLater` shows an alert for confirming the user wants to
/// import logins later.
@MainActor
Expand Down Expand Up @@ -80,10 +106,13 @@ class ImportLoginsProcessorTests: BitwardenTestCase {
/// `receive(_:)` with `.getStarted` shows an alert for the user to confirm they have a
/// computer available.
@MainActor
func test_receive_getStarted() throws {
func test_receive_getStarted() async throws {
subject.receive(.getStarted)

let alert = try XCTUnwrap(coordinator.alertShown.last)
XCTAssertEqual(alert, .importLoginsComputerAvailable {})

try await alert.tapAction(title: Localizations.continue)
XCTAssertEqual(subject.state.page, .step1)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,27 @@
/// An object that defines the current state of a `ImportLoginsView`.
///
struct ImportLoginsState: Equatable, Sendable {
// MARK: Types

/// An enumeration of the instruction pages that the user can navigate between.
///
enum Page: Int {
case intro
case step1

/// The page before the current one.
var previous: Page? {
Page(rawValue: rawValue - 1)
}

/// The page after the current one.
var next: Page? {
Page(rawValue: rawValue + 1)
}
}

// MARK: Properties

/// The current page.
var page = Page.intro
}
Loading

0 comments on commit 5a47964

Please sign in to comment.