Skip to content

Commit

Permalink
[BITAU-139] Add Menu Item to Turn on Authenticator Sync (#1048)
Browse files Browse the repository at this point in the history
  • Loading branch information
brant-livefront authored Oct 17, 2024
1 parent 67af2ec commit 1bc1dda
Show file tree
Hide file tree
Showing 8 changed files with 164 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1042,3 +1042,5 @@
"AutofillPasswords" = "Autofill passwords";
"SetUpAutofillOnAllYourDevicesToLoginWithASingleTapAnywhere" = "Set up autofill on all your devices to login with a single tap anywhere.";
"GotIt" = "Got it";
"AllowAuthenticatorSyncing" = "Allow authenticator syncing";
"AuthenticatorSync" = "Authenticator sync";
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ enum AccountSecurityEffect: Equatable {
/// Stream the state of the badges in the settings tab.
case streamSettingsBadge

/// Sync with Authenticator was toggled.
case toggleSyncWithAuthenticator(Bool)

/// Unlock with Biometrics was toggled.
case toggleUnlockWithBiometrics(Bool)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import OSLog

/// The processor used to manage state and handle actions for the account security screen.
///
final class AccountSecurityProcessor: StateProcessor<
final class AccountSecurityProcessor: StateProcessor<// swiftlint:disable:this type_body_length
AccountSecurityState,
AccountSecurityAction,
AccountSecurityEffect
Expand Down Expand Up @@ -77,6 +77,8 @@ final class AccountSecurityProcessor: StateProcessor<
)
case .streamSettingsBadge:
await streamSettingsBadge()
case let .toggleSyncWithAuthenticator(isOn):
await setSyncToAuthenticator(isOn)
case let .toggleUnlockWithBiometrics(isOn):
await setBioMetricAuth(isOn)
case let .toggleUnlockWithPINCode(isOn):
Expand Down Expand Up @@ -166,6 +168,11 @@ final class AccountSecurityProcessor: StateProcessor<
if try await services.authRepository.isPinUnlockAvailable() {
state.isUnlockWithPINCodeOn = true
}
state.shouldShowAuthenticatorSyncSection =
await services.configService.getFeatureFlag(.enableAuthenticatorSync)
if state.shouldShowAuthenticatorSyncSection {
state.isAuthenticatorSyncEnabled = try await services.stateService.getSyncToAuthenticator()
}
} catch {
services.errorReporter.log(error: error)
}
Expand Down Expand Up @@ -196,6 +203,19 @@ final class AccountSecurityProcessor: StateProcessor<
}
}

/// Sets the user's sync with Authenticator setting
///
/// - Parameter enabled: Whether or not the the user wants to enable sync with Authenticator.
///
private func setSyncToAuthenticator(_ enabled: Bool) async {
do {
try await services.stateService.setSyncToAuthenticator(enabled)
state.isAuthenticatorSyncEnabled = enabled
} catch {
services.errorReporter.log(error: error)
}
}

/// Sets the session timeout action.
///
/// - Parameter action: The action that occurs upon a session timeout.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,37 @@ class AccountSecurityProcessorTests: BitwardenTestCase { // swiftlint:disable:th
XCTAssertEqual(errorReporter.errors.last as? StateServiceError, .noActiveAccount)
}

/// `perform(_:)` with `.loadData` updates the state. The `isAuthenticatorSyncEnabled`
/// should be set to the user's current `syncToAuthenticator` setting.
@MainActor
func test_perform_loadData_isAuthenticatorSyncEnabled() async {
stateService.activeAccount = .fixture()
configService.featureFlagsBool[.enableAuthenticatorSync] = true

stateService.syncToAuthenticatorByUserId[Account.fixture().profile.userId] = false
await subject.perform(.loadData)
XCTAssertFalse(subject.state.isAuthenticatorSyncEnabled)

stateService.syncToAuthenticatorByUserId[Account.fixture().profile.userId] = true
await subject.perform(.loadData)
XCTAssertTrue(subject.state.isAuthenticatorSyncEnabled)
}

/// `perform(_:)` with `.loadData` updates the state. The processor should update
/// `shouldShowAuthenticatorSyncSection` based on the value of the `enableAuthenticatorSync`
/// feature flag.
@MainActor
func test_perform_loadData_shouldShowAuthenticatorSync() async {
stateService.activeAccount = .fixture()
configService.featureFlagsBool[.enableAuthenticatorSync] = true
await subject.perform(.loadData)
XCTAssertTrue(subject.state.shouldShowAuthenticatorSyncSection)

configService.featureFlagsBool[.enableAuthenticatorSync] = false
await subject.perform(.loadData)
XCTAssertFalse(subject.state.shouldShowAuthenticatorSyncSection)
}

/// `perform(_:)` with `.lockVault` locks the user's vault.
@MainActor
func test_perform_lockVault() async {
Expand Down Expand Up @@ -334,6 +365,44 @@ class AccountSecurityProcessorTests: BitwardenTestCase { // swiftlint:disable:th
)
}

/// `perform(_:)` with `.toggleSyncWithAuthenticator` disables authenticator sync and updates the state.
@MainActor
func test_perform_toggleSyncWithAuthenticator_disable() async throws {
configService.featureFlagsBool[.enableAuthenticatorSync] = true
stateService.activeAccount = .fixture()
subject.state.isAuthenticatorSyncEnabled = true

await subject.perform(.toggleSyncWithAuthenticator(false))
waitFor { !subject.state.isAuthenticatorSyncEnabled }

let syncEnabled = try await stateService.getSyncToAuthenticator()
XCTAssertFalse(syncEnabled)
}

/// `perform(_:)` with `.toggleSyncWithAuthenticator` enables authenticator sync and updates the state.
@MainActor
func test_perform_toggleSyncWithAuthenticator_enable() async throws {
configService.featureFlagsBool[.enableAuthenticatorSync] = true
stateService.activeAccount = .fixture()
subject.state.isAuthenticatorSyncEnabled = false

await subject.perform(.toggleSyncWithAuthenticator(true))
waitFor { subject.state.isAuthenticatorSyncEnabled }

let syncEnabled = try await stateService.getSyncToAuthenticator()
XCTAssertTrue(syncEnabled)
}

/// `perform(_:)` with `.toggleSyncWithAuthenticator` correctly handles and logs errors.
@MainActor
func test_perform_toggleSyncWithAuthenticator_error() async throws {
subject.state.isAuthenticatorSyncEnabled = false
stateService.syncToAuthenticatorResult = .failure(BitwardenTestError.example)
await subject.perform(.toggleSyncWithAuthenticator(true))
waitFor { !errorReporter.errors.isEmpty }
XCTAssertFalse(subject.state.isAuthenticatorSyncEnabled)
}

/// `perform(_:)` with `.toggleUnlockWithBiometrics` disables biometrics unlock and updates the state.
@MainActor
func test_perform_toggleUnlockWithBiometrics_disable() async {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,9 @@ struct AccountSecurityState: Equatable {
/// Whether the user has a master password.
var hasMasterPassword = true

/// Whether the user has enabled the sync with the authenticator app..
var isAuthenticatorSyncEnabled = false

/// Whether the timeout policy is in effect.
var isTimeoutPolicyEnabled = false

Expand Down Expand Up @@ -213,6 +216,9 @@ struct AccountSecurityState: Equatable {
/// The length of time before a session timeout occurs.
var sessionTimeoutValue: SessionTimeoutValue = .immediately

/// Whether the sync with the authenticator app section should be included.
var shouldShowAuthenticatorSyncSection = false

/// The URL for two step login external link.
var twoStepLoginUrl: URL?

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ struct AccountSecurityView: View {

unlockOptionsSection

authenticatorSyncSection

sessionTimeoutSection

otherSection
Expand Down Expand Up @@ -215,6 +217,26 @@ struct AccountSecurityView: View {
}
}

/// The authenticator sync section.
@ViewBuilder private var authenticatorSyncSection: some View {
if store.state.shouldShowAuthenticatorSyncSection {
VStack(alignment: .leading, spacing: 16) {
SectionHeaderView(Localizations.authenticatorSync)

VStack(spacing: 24) {
Toggle(isOn: store.bindingAsync(
get: \.isAuthenticatorSyncEnabled,
perform: AccountSecurityEffect.toggleSyncWithAuthenticator
)) {
Text(Localizations.allowAuthenticatorSyncing)
}
.toggleStyle(.bitwarden)
.accessibilityLabel(Localizations.allowAuthenticatorSyncing)
}
}
}
}

/// A view for the user's biometrics setting
///
@ViewBuilder private var biometricsSetting: some View {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import XCTest

@testable import BitwardenShared

class AccountSecurityViewTests: BitwardenTestCase {
class AccountSecurityViewTests: BitwardenTestCase { // swiftlint:disable:this type_body_length
// MARK: Properties

var processor: MockProcessor<AccountSecurityState, AccountSecurityAction, AccountSecurityEffect>!
Expand Down Expand Up @@ -41,6 +41,33 @@ class AccountSecurityViewTests: BitwardenTestCase {
task.cancel()
}

/// The view hides the authenticator sync section when appropriate.
@MainActor
func test_authenticatorSync_hidden() throws {
processor.state.shouldShowAuthenticatorSyncSection = false
XCTAssertNil(
try? subject.inspect().find(
toggleWithAccessibilityLabel: Localizations.allowAuthenticatorSyncing
)
)
}

/// Tapping the sync with authenticator switch should send `.toggleSyncWithAuthenticator(enabled)` with the
/// new value of enabled.
@MainActor
func test_authenticatorSync_tap() throws {
processor.state.shouldShowAuthenticatorSyncSection = true
processor.state.isAuthenticatorSyncEnabled = false
let toggle = try subject.inspect().find(toggleWithAccessibilityLabel: Localizations.allowAuthenticatorSyncing)
XCTAssertFalse(try toggle.isOn())

let task = Task {
try toggle.tap()
}
defer { task.cancel() }
waitFor(processor.effects.last == .toggleSyncWithAuthenticator(true))
}

/// The action card is hidden if the vault unlock setup progress is complete.
@MainActor
func test_setUpUnlockActionCard_hidden() {
Expand Down Expand Up @@ -239,6 +266,19 @@ class AccountSecurityViewTests: BitwardenTestCase {
assertSnapshot(of: subject, as: .defaultPortrait)
}

/// The view renders correctly when the `shouldShowAuthenticatorSyncSection` is `true`.
@MainActor
func test_snapshot_shouldShowAuthenticatorSyncSection() {
let subject = AccountSecurityView(
store: Store(
processor: StateProcessor(
state: AccountSecurityState(shouldShowAuthenticatorSyncSection: true)
)
)
)
assertSnapshot(of: subject, as: .defaultPortrait)
}

/// The view renders correctly when the timeout policy is enabled.
@MainActor
func test_snapshot_timeoutPolicy() {
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 1bc1dda

Please sign in to comment.