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-13467: Add import saved logins action card and badge to vault settings #1055

Merged
merged 9 commits into from
Oct 22, 2024
4 changes: 3 additions & 1 deletion BitwardenShared/Core/Auth/Services/AuthService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -602,7 +602,9 @@ class DefaultAuthService: AuthService { // swiftlint:disable:this type_body_leng
do {
let isAutofillEnabled = await credentialIdentityStore.isAutofillEnabled()
try await stateService.setAccountSetupAutofill(isAutofillEnabled ? .complete : .incomplete)
try await stateService.setAccountSetupImportLogins(.incomplete)
if await configService.getFeatureFlag(.importLoginsFlow) {
try await stateService.setAccountSetupImportLogins(.incomplete)
}
try await stateService.setAccountSetupVaultUnlock(.incomplete)
} catch {
errorReporter.log(error: error)
Expand Down
2 changes: 2 additions & 0 deletions BitwardenShared/Core/Auth/Services/AuthServiceTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,7 @@ class AuthServiceTests: BitwardenTestCase { // swiftlint:disable:this type_body_
]
appSettingsStore.appId = "App id"
clientService.mockAuth.hashPasswordResult = .success("hashed password")
configService.featureFlagsBool[.importLoginsFlow] = true
configService.featureFlagsBool[.nativeCreateAccountFlow] = true
credentialIdentityStore.state.mockIsEnabled = false
stateService.preAuthEnvironmentUrls = EnvironmentUrlData(base: URL(string: "https://vault.bitwarden.com"))
Expand Down Expand Up @@ -509,6 +510,7 @@ class AuthServiceTests: BitwardenTestCase { // swiftlint:disable:this type_body_
]
appSettingsStore.appId = "App id"
clientService.mockAuth.hashPasswordResult = .success("hashed password")
configService.featureFlagsBool[.importLoginsFlow] = true
configService.featureFlagsBool[.nativeCreateAccountFlow] = true
credentialIdentityStore.state.mockIsEnabled = true
stateService.preAuthEnvironmentUrls = EnvironmentUrlData(base: URL(string: "https://vault.bitwarden.com"))
Expand Down
16 changes: 14 additions & 2 deletions BitwardenShared/Core/Platform/Services/StateService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1144,7 +1144,7 @@

/// The errors thrown from a `StateService`.
///
enum StateServiceError: Error {
enum StateServiceError: LocalizedError {
/// There are no known accounts.
case noAccounts

Expand All @@ -1159,6 +1159,15 @@

/// The user has no user key.
case noEncUserKey

var errorDescription: String? {
phil-livefront marked this conversation as resolved.
Show resolved Hide resolved
switch self {
case .noActiveAccount:
Localizations.noAccountFoundPleaseLogInAgainIfYouContinueToSeeThisError
default:
nil

Check warning on line 1168 in BitwardenShared/Core/Platform/Services/StateService.swift

View check run for this annotation

Codecov / codecov/patch

BitwardenShared/Core/Platform/Services/StateService.swift#L1168

Added line #L1168 was not covered by tests
}
}
}

// MARK: - DefaultStateService
Expand Down Expand Up @@ -1850,10 +1859,13 @@
let autofillSetupProgress = await getAccountSetupAutofill(userId: userId)
let importLoginsSetupProgress = await getAccountSetupImportLogins(userId: userId)
let vaultUnlockSetupProgress = await getAccountSetupVaultUnlock(userId: userId)
let badgeCount = [autofillSetupProgress, vaultUnlockSetupProgress]
var badgeCount = [autofillSetupProgress, vaultUnlockSetupProgress]
.compactMap { $0 }
.filter { $0 != .complete }
.count
if importLoginsSetupProgress == .setUpLater {
badgeCount += 1
}
settingsBadgeByUserIdSubject.value[userId] = SettingsBadgeState(
autofillSetupProgress: autofillSetupProgress,
badgeValue: badgeCount > 0 ? String(badgeCount) : nil,
Expand Down
35 changes: 30 additions & 5 deletions BitwardenShared/Core/Platform/Services/StateServiceTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1730,7 +1730,7 @@ class StateServiceTests: BitwardenTestCase { // swiftlint:disable:this type_body
}

/// `settingsBadgePublisher()` publishes the settings badge value for the active user.
func test_settingsBadgePublisher() async throws {
func test_settingsBadgePublisher() async throws { // swiftlint:disable:this function_body_length
await subject.addAccount(.fixture())

var publishedValues = [SettingsBadgeState]()
Expand All @@ -1741,33 +1741,58 @@ class StateServiceTests: BitwardenTestCase { // swiftlint:disable:this type_body
defer { publisher.cancel() }

try await subject.setAccountSetupAutofill(.setUpLater)
try await subject.setAccountSetupImportLogins(.setUpLater)
try await subject.setAccountSetupVaultUnlock(.setUpLater)

try await subject.setAccountSetupAutofill(.complete)
try await subject.setAccountSetupImportLogins(.complete)
try await subject.setAccountSetupVaultUnlock(.complete)

XCTAssertEqual(publishedValues.count, 5)
XCTAssertEqual(publishedValues.count, 7)
XCTAssertEqual(publishedValues[0], .fixture())
XCTAssertEqual(publishedValues[1], .fixture(autofillSetupProgress: .setUpLater, badgeValue: "1"))
XCTAssertEqual(
publishedValues[2],
.fixture(
autofillSetupProgress: .setUpLater,
badgeValue: "2",
vaultUnlockSetupProgress: .setUpLater
importLoginsSetupProgress: .setUpLater
)
)
XCTAssertEqual(
publishedValues[3],
.fixture(
autofillSetupProgress: .setUpLater,
badgeValue: "3",
importLoginsSetupProgress: .setUpLater,
vaultUnlockSetupProgress: .setUpLater
)
)
XCTAssertEqual(
publishedValues[4],
.fixture(
autofillSetupProgress: .complete,
badgeValue: "2",
importLoginsSetupProgress: .setUpLater,
vaultUnlockSetupProgress: .setUpLater
)
)
XCTAssertEqual(
publishedValues[5],
.fixture(
autofillSetupProgress: .complete,
badgeValue: "1",
importLoginsSetupProgress: .complete,
vaultUnlockSetupProgress: .setUpLater
)
)
XCTAssertEqual(
publishedValues[4],
.fixture(autofillSetupProgress: .complete, vaultUnlockSetupProgress: .complete)
publishedValues[6],
.fixture(
autofillSetupProgress: .complete,
importLoginsSetupProgress: .complete,
vaultUnlockSetupProgress: .complete
)
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1046,3 +1046,4 @@
"GenerateErrorReport" = "Generate error report";
"AllowAuthenticatorSyncing" = "Allow authenticator syncing";
"AuthenticatorSync" = "Authenticator sync";
"NoAccountFoundPleaseLogInAgainIfYouContinueToSeeThisError" = "No account found. Please log in again if you continue to see this error.";
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,25 @@ import Foundation
extension Alert {
// MARK: Methods

/// The default alert style for a given error with a standard ok button to dismiss.
///
/// - Parameters:
/// - error: The error that prompted the alert.
/// - alertActions: A list of actions that the user can tap on in the alert.
///
/// - Returns a default styled alert.
///
static func defaultAlert(
error: Error,
alertActions: [AlertAction]? = nil
) -> Alert {
defaultAlert(
title: Localizations.anErrorHasOccurred,
message: error.localizedDescription,
alertActions: alertActions
)
}

/// The default alert style with a standard ok button to dismiss.
///
/// - Parameters:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@ class AlertErrorTests: BitwardenTestCase {
XCTAssertEqual(subject.alertActions, [AlertAction(title: "Ok", style: .cancel)])
}

/// `defaultAlert(error:)` constructs an `Alert` with the title and message based on the error,
/// and an OK button.
func test_defaultAlertError() {
let subject = Alert.defaultAlert(error: StateServiceError.noActiveAccount)

XCTAssertEqual(subject.title, Localizations.anErrorHasOccurred)
XCTAssertEqual(subject.message, StateServiceError.noActiveAccount.errorDescription)
XCTAssertEqual(subject.alertActions, [AlertAction(title: "Ok", style: .cancel)])
}

/// `inputValidationAlert(error:)` creates an `Alert` for an input validation error.
func test_inputValidationAlert() {
let subject = Alert.inputValidationAlert(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,11 @@ struct SettingsState: Equatable {
let isComplete = badgeState?.autofillSetupProgress?.isComplete ?? true
return isComplete ? nil : "1"
}

/// The badge value for the vault row.
var vaultBadgeValue: String? {
// Since the action card displays on the vault when the progress is incomplete, only show a
// badge value if the user wants to set it up later.
badgeState?.importLoginsSetupProgress == .setUpLater ? "1" : nil
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,22 @@ class SettingsStateTests: BitwardenTestCase {
subject = SettingsState(badgeState: .fixture(autofillSetupProgress: .setUpLater, badgeValue: "2"))
XCTAssertEqual(subject.autofillBadgeValue, "1")
}

/// `vaultBadgeValue` returns `nil` if no badge should be shown for the vault row.
func test_vaultBadgeValue_nil() {
var subject = SettingsState(badgeState: .fixture())
XCTAssertNil(subject.vaultBadgeValue)

subject = SettingsState(badgeState: .fixture(importLoginsSetupProgress: .incomplete))
XCTAssertNil(subject.vaultBadgeValue)

subject = SettingsState(badgeState: .fixture(importLoginsSetupProgress: .complete))
XCTAssertNil(subject.vaultBadgeValue)
}

/// `vaultBadgeValue` returns the badge value for the vault row if the user wants to import logins later.
func test_vaultBadgeValue_value() {
let subject = SettingsState(badgeState: .fixture(importLoginsSetupProgress: .setUpLater))
XCTAssertEqual(subject.vaultBadgeValue, "1")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ struct SettingsView: View {
}
.accessibilityIdentifier("AutofillSettingsButton")

SettingsListItem(Localizations.vault) {
SettingsListItem(Localizations.vault, badgeValue: store.state.vaultBadgeValue) {
store.send(.vaultPressed)
} trailingContent: {
chevron
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ class SettingsViewTests: BitwardenTestCase {
func test_settingsView_badges() {
processor.state.badgeState = .fixture(
autofillSetupProgress: .setUpLater,
importLoginsSetupProgress: .setUpLater,
vaultUnlockSetupProgress: .setUpLater
)
assertSnapshots(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,7 @@ enum VaultSettingsAction {

/// The import items button was tapped.
case importItemsTapped

/// The user tapped the get started button on the import logins action card.
case showImportLogins
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// MARK: - VaultSettingsEffect

/// Effects that can be processed by a `VaultSettingsProcessor`.
///
enum VaultSettingsEffect: Equatable {
/// The user tapped the dismiss button on the import logins action card.
case dismissImportLoginsActionCard

/// Stream the state of the badges in the settings tab.
case streamSettingsBadge
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@

/// The processor used to manage state and handle actions for the `VaultSettingsView`.
///
final class VaultSettingsProcessor: StateProcessor<VaultSettingsState, VaultSettingsAction, Void> {
final class VaultSettingsProcessor: StateProcessor<VaultSettingsState, VaultSettingsAction, VaultSettingsEffect> {
// MARK: Types

typealias Services = HasEnvironmentService
typealias Services = HasConfigService
& HasEnvironmentService
& HasErrorReporter
& HasStateService

// MARK: Properties

Expand Down Expand Up @@ -36,6 +39,15 @@

// MARK: Methods

override func perform(_ effect: VaultSettingsEffect) async {
switch effect {
case .dismissImportLoginsActionCard:
await dismissImportLoginsActionCard()
case .streamSettingsBadge:
await streamSettingsBadge()
}
}

override func receive(_ action: VaultSettingsAction) {
switch action {
case .clearUrl:
Expand All @@ -50,6 +62,36 @@
) {
self.state.url = self.services.environmentService.importItemsURL
})
case .showImportLogins:
// TODO: PM-13467 Navigate to import logins
// coordinator.navigate(to: .importLogins)
break

Check warning on line 68 in BitwardenShared/UI/Platform/Settings/Settings/Vault/VaultSettingsProcessor.swift

View check run for this annotation

Codecov / codecov/patch

BitwardenShared/UI/Platform/Settings/Settings/Vault/VaultSettingsProcessor.swift#L66-L68

Added lines #L66 - L68 were not covered by tests
}
}

// MARK: Private

/// Dismisses the import logins action card by marking the user's import logins progress complete.
///
private func dismissImportLoginsActionCard() async {
do {
try await services.stateService.setAccountSetupImportLogins(.complete)
} catch {
services.errorReporter.log(error: error)
coordinator.showAlert(.defaultAlert(error: error))
}
}

/// Streams the state of the badges in the settings tab.
///
private func streamSettingsBadge() async {
guard await services.configService.getFeatureFlag(.importLoginsFlow) else { return }
do {
for await badgeState in try await services.stateService.settingsBadgePublisher().values {
state.badgeState = badgeState
}
} catch {
services.errorReporter.log(error: error)
}
}
}
Loading
Loading