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

[BITAU-139] Add Menu Item to Turn on Authenticator Sync #1048

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 @@ -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() }
victor-livefront marked this conversation as resolved.
Show resolved Hide resolved
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.
Loading