8000 PM-12734: Add set up autofill action card by matt-livefront · Pull Request #996 · bitwarden/ios · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

PM-12734: Add set up autofill action card #996

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

Merged
merged 2 commits into from
Oct 3, 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
@@ -1,5 +1,4 @@
@testable import BitwardenShared

#if DEBUG
extension SettingsBadgeState {
static func fixture(
autofillSetupProgress: AccountSetupProgress? = nil,
Expand All @@ -13,3 +12,4 @@ extension SettingsBadgeState {
)
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -262,4 +262,12 @@ struct AccountSecurityView: View {
)
}
}

#Preview("Vault Unlock Action Card") {
NavigationView {
AccountSecurityView(store: Store(processor: StateProcessor(state: AccountSecurityState(
badgeState: .fixture(vaultUnlockSetupProgress: .setUpLater)
))))
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ class AccountSecurityViewTests: BitwardenTestCase {

// MARK: Snapshots

/// THe view renders correctly with the vault unlock action card displayed.
/// The view renders correctly with the vault unlock action card is displayed.
@MainActor
func test_snapshot_actionCardVaultUnlock() async {
processor.state.badgeState = .fixture(vaultUnlockSetupProgress: .setUpLater)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ enum AutoFillAction: Equatable {
/// The password auto-fill button was tapped.
case passwordAutoFillTapped

/// The user tapped the get started button on the set up autofill action card.
case showSetUpAutofill

/// The copy TOTP automatically toggle value changed.
case toggleCopyTOTPToggle(Bool)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
/// Effects emitted by the `AutoFillView`.
///
enum AutoFillEffect: Equatable {
/// The user tapped the dismiss button on the set up autofill action card.
case dismissSetUpAutofillActionCard

/// The view appears and the initial values should be fetched.
case fetchSettingValues

/// Stream the state of the badges in the settings tab.
case streamSettingsBadge
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
final class AutoFillProcessor: StateProcessor<AutoFillState, AutoFillAction, AutoFillEffect> {
// MARK: Types

typealias Services = HasErrorReporter
typealias Services = HasConfigService
& HasErrorReporter
& HasSettingsRepository
& HasStateService

// MARK: Properties

Expand Down Expand Up @@ -39,8 +41,12 @@ final class AutoFillProcessor: StateProcessor<AutoFillState, AutoFillAction, Aut

override func perform(_ effect: AutoFillEffect) async {
switch effect {
case .dismissSetUpAutofillActionCard:
await dismissSetUpAutofillActionCard()
case .fetchSettingValues:
await fetchSettingValues()
case .streamSettingsBadge:
await streamSettingsBadge()
}
}

Expand All @@ -55,6 +61,8 @@ final class AutoFillProcessor: StateProcessor<AutoFillState, AutoFillAction, Aut
}
case .passwordAutoFillTapped:
coordinator.navigate(to: .passwordAutoFill)
case .showSetUpAutofill:
coordinator.navigate(to: .passwordAutoFill)
case let .toggleCopyTOTPToggle(isOn):
state.isCopyTOTPToggleOn = isOn
Task {
Expand All @@ -65,6 +73,17 @@ final class AutoFillProcessor: StateProcessor<AutoFillState, AutoFillAction, Aut

// MARK: Private

/// Dismisses the set up autofill action card by marking the user's vault autofill setup progress complete.
///
private func dismissSetUpAutofillActionCard() async {
do {
try await services.stateService.setAccountSetupAutofill(.complete)
} catch {
services.errorReporter.log(error: error)
coordinator.showAlert(.defaultAlert(title: Localizations.anErrorHasOccurred))
}
}

/// Fetches the initial stored setting values for the view.
///
private func fetchSettingValues() async {
Expand All @@ -77,6 +96,19 @@ final class AutoFillProcessor: StateProcessor<AutoFillState, AutoFillAction, Aut
}
}

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

/// Updates the default URI match type value for the user.
///
/// - Parameter defaultUriMatchType: The default URI match type.
Expand Down
F438
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,31 @@ import XCTest
class AutoFillProcessorTests: BitwardenTestCase {
// MARK: Properties

var configService: MockConfigService!
var coordinator: MockCoordinator<SettingsRoute, SettingsEvent>!
var errorReporter: MockErrorReporter!
var settingsRepository: MockSettingsRepository!
var stateService: MockStateService!
var subject: AutoFillProcessor!

// MARK: Setup and Teardown

override func setUp() {
super.setUp()

configService = MockConfigService()
coordinator = MockCoordinator<SettingsRoute, SettingsEvent>()
errorReporter = MockErrorReporter()
settingsRepository = MockSettingsRepository()
stateService = MockStateService()

subject = AutoFillProcessor(
coordinator: coordinator.asAnyCoordinator(),
services: ServiceContainer.withMocks(
configService: configService,
errorReporter: errorReporter,
settingsRepository: settingsRepository
settingsRepository: settingsRepository,
stateService: stateService
),
state: AutoFillState()
)
Expand All @@ -32,14 +38,38 @@ class AutoFillProcessorTests: BitwardenTestCase {
override func tearDown() {
super.tearDown()

configService = nil
coordinator = nil
errorReporter = nil
settingsRepository = nil
stateService = nil
subject = nil
}

// MARK: Tests

/// `perform(_:)` with `.dismissSetUpAutofillActionCard` sets the user's vault autofill setup
/// progress to complete.
@MainActor
func test_perform_dismissSetUpAutofillActionCard() async {
stateService.activeAccount = .fixture()
stateService.accountSetupAutofill["1"] = .setUpLater

await subject.perform(.dismissSetUpAutofillActionCard)

XCTAssertEqual(stateService.accountSetupAutofill["1"], .complete)
}

/// `perform(_:)` with `.dismissSetUpAutofillActionCard` logs an error and shows an alert if an
/// error occurs.
@MainActor
func test_perform_dismissSetUpAutofillActionCard_error() async {
await subject.perform(.dismissSetUpAutofillActionCard)

XCTAssertEqual(coordinator.alertShown, [.defaultAlert(title: Localizations.anErrorHasOccurred)])
XCTAssertEqual(errorReporter.errors as? [StateServiceError], [.noActiveAccount])
}

/// `perform(_:)` with `.fetchSettingValues` fetches the setting values to display and updates the state.
@MainActor
func test_perform_fetchSettingValues() async {
Expand Down Expand Up @@ -67,6 +97,47 @@ class AutoFillProcessorTests: BitwardenTestCase {
XCTAssertEqual(errorReporter.errors.last as? StateServiceError, StateServiceError.noActiveAccount)
}

/// `perform(_:)` with `.streamSettingsBadge` updates the state's badge state whenever it changes.
@MainActor
func test_perform_streamSettingsBadge() {
configService.featureFlagsBool[.nativeCreateAccountFlow] = true
stateService.activeAccount = .fixture()

let task = Task {
await subject.perform(.streamSettingsBadge)
}
defer { task.cancel() }

let badgeState = SettingsBadgeState.fixture(vaultUnlockSetupProgress: .setUpLater)
stateService.settingsBadgeSubject.send(badgeState)
waitFor { subject.state.badgeState == badgeState }

XCTAssertEqual(subject.state.badgeState, badgeState)
}

/// `perform(_:)` with `.streamSettingsBadge` logs an error if streaming the settings badge state fails.
@MainActor
func test_perform_streamSettingsBadge_error() async {
configService.featureFlagsBool[.nativeCreateAccountFlow] = true

await subject.perform(.streamSettingsBadge)

XCTAssertEqual(errorReporter.errors as? [StateServiceError], [.noActiveAccount])
}

/// `perform(_:)` with `.streamSettingsBadge` doesn't load the badge state if the create account
/// feature flag is disabled.
@MainActor
func test_perform_streamSettingsBadge_nativeCreateAccountFlowDisabled() async {
configService.featureFlagsBool[.nativeCreateAccountFlow] = false
stateService.activeAccount = .fixture()
stateService.settingsBadgeSubject.send(.fixture())

await subject.perform(.streamSettingsBadge)

XCTAssertNil(subject.state.badgeState)
}

/// `receive(_:)` with `.appExtensionTapped` navigates to the app extension view.
@MainActor
func test_receive_appExtensionTapped() {
Expand All @@ -91,6 +162,15 @@ class AutoFillProcessorTests: BitwardenTestCase {
XCTAssertEqual(coordinator.routes.last, .passwordAutoFill)
}

/// `receive(_:)` with `showSetUpAutofill(:)` has the coordinator navigate to the password
/// autofill screen.
@MainActor
func test_receive_showSetUpAutofill() throws {
subject.receive(.showSetUpAutofill)

XCTAssertEqual(coordinator.routes, [.passwordAutoFill])
}

/// `.receive(_:)` with `.toggleCopyTOTPToggle` updates the state.
@MainActor
func test_receive_toggleCopyTOTPToggle() throws {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,22 @@
/// An object that defines the current state of the `AutoFillView`.
///
struct AutoFillState {
// MARK: Properties

/// The state of the badges in the settings tab.
var badgeState: SettingsBadgeState?

/// The default URI match type.
var defaultUriMatchType: UriMatchType = .domain

/// Whether or not the copy TOTP automatically toggle is on.
var isCopyTOTPToggleOn: Bool = false

// MARK: Computed Properties

/// Whether the autofill action card should be shown.
var shouldShowAutofillActionCard: Bool {
guard let badgeState, badgeState.autofillSetupProgress != .complete else { return false }
return true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,42 @@ struct AutoFillView: View {

var body: some View {
VStack(spacing: 20) {
autofillActionCard

autoFillSection

additionalOptionsSection
}
.animation(.easeInOut, value: store.state.badgeState?.autofillSetupProgress == .complete)
.scrollView()
.navigationBar(title: Localizations.autofill, titleDisplayMode: .inline)
.task {
await store.perform(.fetchSettingValues)
}
.task {
await store.perform(.streamSettingsBadge)
}
}

// MARK: Private views

/// The action card for setting up autofill.
@ViewBuilder private var autofillActionCard: some View {
if store.state.shouldShowAutofillActionCard {
ActionCard(
title: Localizations.setUpAutofill,
actionButtonState: ActionCard.ButtonState(title: Localizations.getStarted) {
store.send(.showSetUpAutofill)
},
dismissButtonState: ActionCard.ButtonState(title: Localizations.dismiss) {
await store.perform(.dismissSetUpAutofillActionCard)
}
) {
BitwardenBadge(badgeValue: "1")
}
}
}

/// The additional options section.
private var additionalOptionsSection: some View {
VStack(alignment: .leading) {
Expand Down Expand Up @@ -94,6 +117,18 @@ struct AutoFillView: View {

// MARK: - Previews

#if DEBUG
#Preview {
AutoFillView(store: Store(processor: StateProcessor(state: AutoFillState())))
NavigationView {
AutoFillView(store: Store(processor: StateProcessor(state: AutoFillState())))
}
}

#Preview("Autofill Action Card") {
NavigationView {
AutoFillView(store: Store(processor: StateProcessor(state: AutoFillState(
badgeState: .fixture(autofillSetupProgress: .setUpLater)
))))
}
}
#endif
Loading
Loading
0