Skip to content

Commit

Permalink
[BITAU-131] Add Popup to QR Code Scan to Add Code to Bitwarden (#177)
Browse files Browse the repository at this point in the history
  • Loading branch information
brant-livefront authored Nov 7, 2024
1 parent e50d624 commit 5a699f3
Show file tree
Hide file tree
Showing 20 changed files with 821 additions and 49 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import Foundation

// MARK: - DefaultSaveOption

/// The default location for saving newly added keys via QR code scan and Manual entry.
///
enum DefaultSaveOption: String, Equatable, Menuable {
/// Ask where to save a code each time a QR code is scanned.
case none

/// Save the code locally without showing any prompt.
case saveLocally

/// Take the user to the Bitwarden PM app to save the code without prompt.
case saveToBitwarden

/// All of the cases to show in the menu, in order.
public static let allCases: [Self] = [
.saveToBitwarden,
.saveLocally,
.none,
]

/// The name of the value to display in the menu.
var localizedName: String {
switch self {
case .none:
Localizations.none
case .saveLocally:
Localizations.saveLocally
case .saveToBitwarden:
Localizations.saveToBitwarden
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import XCTest

@testable import AuthenticatorShared

class DefaultSaveOptionTests: AuthenticatorTestCase {
// MARK: Tests

/// `allCases` returns all of the cases in the correct order.
func test_allCases() {
XCTAssertEqual(
DefaultSaveOption.allCases,
[
.saveToBitwarden,
.saveLocally,
.none,
]
)
}

/// `localizedName` returns the correct values.
func test_localizedName() {
XCTAssertEqual(DefaultSaveOption.none.localizedName, Localizations.none)
XCTAssertEqual(DefaultSaveOption.saveLocally.localizedName, Localizations.saveLocally)
XCTAssertEqual(DefaultSaveOption.saveToBitwarden.localizedName, Localizations.saveToBitwarden)
}

/// `rawValue` returns the correct values.
func test_rawValues() {
XCTAssertEqual(DefaultSaveOption.none.rawValue, "none")
XCTAssertEqual(DefaultSaveOption.saveLocally.rawValue, "saveLocally")
XCTAssertEqual(DefaultSaveOption.saveToBitwarden.rawValue, "saveToBitwarden")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ protocol AppSettingsStore: AnyObject {
/// Whether to disable the website icons.
var disableWebIcons: Bool { get set }

/// The default save location for new keys.
var defaultSaveOption: DefaultSaveOption { get set }

/// Whether the user has seen the default save options prompt.
var hasSeenDefaultSaveOptionPrompt: Bool { get }

/// Whether the user has seen the welcome tutorial.
var hasSeenWelcomeTutorial: Bool { get set }

Expand Down Expand Up @@ -296,6 +302,7 @@ extension DefaultAppSettingsStore: AppSettingsStore {
case cardClosedState(card: ItemListCard)
case clearClipboardValue(userId: String)
case debugFeatureFlag(name: String)
case defaultSaveOption
case disableWebIcons
case hasSeenWelcomeTutorial
case hasSyncedAccount(name: String)
Expand Down Expand Up @@ -324,6 +331,8 @@ extension DefaultAppSettingsStore: AppSettingsStore {
key = "clearClipboard_\(userId)"
case let .debugFeatureFlag(name):
key = "debugFeatureFlag_\(name)"
case .defaultSaveOption:
key = "defaultSaveOption"
case .disableWebIcons:
key = "disableFavicon"
case .hasSeenWelcomeTutorial:
Expand Down Expand Up @@ -363,6 +372,21 @@ extension DefaultAppSettingsStore: AppSettingsStore {
set { store(newValue, for: .disableWebIcons) }
}

var defaultSaveOption: DefaultSaveOption {
get {
guard let rawValue: String = fetch(for: .defaultSaveOption),
let value = DefaultSaveOption(rawValue: rawValue)
else { return .none }

return value
}
set { store(newValue.rawValue, for: .defaultSaveOption) }
}

var hasSeenDefaultSaveOptionPrompt: Bool {
fetch(for: .defaultSaveOption) != nil
}

var hasSeenWelcomeTutorial: Bool {
get { fetch(for: .hasSeenWelcomeTutorial) }
set { store(newValue, for: .hasSeenWelcomeTutorial) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,39 @@ class AppSettingsStoreTests: AuthenticatorTestCase {
XCTAssertEqual(userDefaults.integer(forKey: "bwaPreferencesStorage:clearClipboard_2"), -1)
}

/// `defaultSaveOption` returns `.none` if there isn't a previously stored value or if a previously
/// stored value is not a valid option
func test_defaultSaveOption_isInitiallyNone() {
XCTAssertEqual(subject.defaultSaveOption, .none)

userDefaults.set("An invalid value", forKey: "bwaPreferencesStorage:defaultSaveOption")
XCTAssertEqual(subject.defaultSaveOption, .none)
}

/// `defaultSaveOption` can be used to get and set the default save option..
func test_defaultSaveOption_withValue() {
subject.defaultSaveOption = .saveToBitwarden
XCTAssertEqual(subject.defaultSaveOption, .saveToBitwarden)
XCTAssertEqual(userDefaults.string(forKey: "bwaPreferencesStorage:defaultSaveOption"), "saveToBitwarden")

subject.defaultSaveOption = .saveLocally
XCTAssertEqual(subject.defaultSaveOption, .saveLocally)
XCTAssertEqual(userDefaults.string(forKey: "bwaPreferencesStorage:defaultSaveOption"), "saveLocally")

subject.defaultSaveOption = .none
XCTAssertEqual(subject.defaultSaveOption, .none)
XCTAssertEqual(userDefaults.string(forKey: "bwaPreferencesStorage:defaultSaveOption"), "none")
}

/// `hasSeenDefaultSaveOptionPrompt` returns `false` if there isn't a 'defaultSaveOption` value stored, and `true`
/// when there is a value stored.
func test_hasSeenDefaultSaveOptionPrompt() {
XCTAssertFalse(subject.hasSeenDefaultSaveOptionPrompt)

subject.defaultSaveOption = .none
XCTAssertTrue(subject.hasSeenDefaultSaveOptionPrompt)
}

/// `disableWebIcons` returns `false` if there isn't a previously stored value.
func test_disableWebIcons_isInitiallyFalse() {
XCTAssertFalse(subject.disableWebIcons)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ class MockAppSettingsStore: AppSettingsStore {
var appLocale: String?
var appTheme: String?
var disableWebIcons = false
var defaultSaveOption: DefaultSaveOption = .none
var hasSeenDefaultSaveOptionPrompt = false
var hasSeenWelcomeTutorial = false
var lastUserShouldConnectToWatch = false
var localUserId: String = "localtest"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,16 @@
"MoveToBitwarden" = "Move to Bitwarden";
"AddCodeToBitwarden" = "Add code to Bitwarden";
"AddCodeLocally" = "Add code locally";
"ScanComplete" = "Scan complete";
"SaveThisAuthenticatorKeyHereOrAddItToALoginInYourBitwardenApp" = "Save this authenticator key here, or add it to a login in your Bitwarden app.";
"SaveHere" = "Save here";
"TakeMeToBitwarden" = "Take me to Bitwarden";
"SetSaveToBitwardenAsYourDefaultSaveOption" = "Set \"Save to Bitwarden\" as your default save option?";
"SetSaveLocallyAsYourDefaultSaveOption" = "Set \"Save locally\" as your default save option?";
"YouCanUpdateYourDefaultAnytimeInSettings" = "You can update your default anytime in settings.";
"YesSetDefault" = "Yes, set default";
"NoAskMe" = "No, ask me";
"DefaultSaveOption" = "Default save option";
"SaveToBitwarden" = "Save to Bitwarden";
"SaveLocally" = "Save locally";
"None" = "None";
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ enum SettingsAction: Equatable {
/// The url has been opened so clear the value in the state.
case clearURL

/// The default save option was changed.
case defaultSaveChanged(DefaultSaveOption)

/// The export items button was tapped.
case exportItemsTapped

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import OSLog
final class SettingsProcessor: StateProcessor<SettingsState, SettingsAction, SettingsEffect> {
// MARK: Types

typealias Services = HasBiometricsRepository
typealias Services = HasAppSettingsStore
& HasAuthenticatorItemRepository
& HasBiometricsRepository
& HasConfigService
& HasErrorReporter
& HasExportItemsService
Expand Down Expand Up @@ -65,6 +67,9 @@ final class SettingsProcessor: StateProcessor<SettingsState, SettingsAction, Set
})
case .clearURL:
state.url = nil
case let .defaultSaveChanged(option):
state.defaultSaveOption = option
services.appSettingsStore.defaultSaveOption = option
case .exportItemsTapped:
coordinator.navigate(to: .exportItems)
case .helpCenterTapped:
Expand Down Expand Up @@ -118,6 +123,10 @@ final class SettingsProcessor: StateProcessor<SettingsState, SettingsAction, Set
state.appTheme = await services.stateService.getAppTheme()
state.biometricUnlockStatus = await loadBiometricUnlockPreference()
state.shouldShowSyncButton = await services.configService.getFeatureFlag(.enablePasswordManagerSync)
if state.shouldShowSyncButton {
state.shouldShowDefaultSaveOption = await services.authenticatorItemRepository.isPasswordManagerSyncActive()
state.defaultSaveOption = services.appSettingsStore.defaultSaveOption
}
}

/// Sets the user's biometric auth
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import XCTest
class SettingsProcessorTests: AuthenticatorTestCase {
// MARK: Properties

var appSettingsStore: MockAppSettingsStore!
var authItemRepository: MockAuthenticatorItemRepository!
var configService: MockConfigService!
var coordinator: MockCoordinator<SettingsRoute, SettingsEvent>!
var subject: SettingsProcessor!
Expand All @@ -14,11 +16,15 @@ class SettingsProcessorTests: AuthenticatorTestCase {
override func setUp() {
super.setUp()

appSettingsStore = MockAppSettingsStore()
authItemRepository = MockAuthenticatorItemRepository()
configService = MockConfigService()
coordinator = MockCoordinator()
subject = SettingsProcessor(
coordinator: coordinator.asAnyCoordinator(),
services: ServiceContainer.withMocks(
appSettingsStore: appSettingsStore,
authenticatorItemRepository: authItemRepository,
configService: configService
),
state: SettingsState()
Expand All @@ -28,27 +34,65 @@ class SettingsProcessorTests: AuthenticatorTestCase {
override func tearDown() {
super.tearDown()

appSettingsStore = nil
authItemRepository = nil
configService = nil
coordinator = nil
subject = nil
}

// MARK: Tests

/// Performing `.loadData` with the password manager sync disabled sets
/// `state.shouldShowSyncButton` to `false`.
func test_perform_loadData_syncDisabled() async throws {
/// Performing `.loadData` sets the 'defaultSaveOption' to the current value in 'AppSettingsStore'.
func test_perform_loadData_defaultSaveOption() async throws {
configService.featureFlagsBool[.enablePasswordManagerSync] = true
appSettingsStore.defaultSaveOption = .saveToBitwarden
await subject.perform(.loadData)

XCTAssertEqual(subject.state.defaultSaveOption, .saveToBitwarden)
}

/// Performing `.loadData` sets the sync related flags correctly when the feature flag is
/// disabled and the sync is off.
func test_perform_loadData_syncFlagDisabled_syncOff() async throws {
configService.featureFlagsBool[.enablePasswordManagerSync] = false
authItemRepository.pmSyncEnabled = false
await subject.perform(.loadData)

XCTAssertFalse(subject.state.shouldShowDefaultSaveOption)
XCTAssertFalse(subject.state.shouldShowSyncButton)
}

/// Performing `.loadData` sets the sync related flags correctly when the feature flag is
/// enabled and the sync is off.
func test_perform_loadData_syncFlagEnabled_syncOff() async throws {
configService.featureFlagsBool[.enablePasswordManagerSync] = true
authItemRepository.pmSyncEnabled = false
await subject.perform(.loadData)

XCTAssertFalse(subject.state.shouldShowDefaultSaveOption)
XCTAssertTrue(subject.state.shouldShowSyncButton)
}

/// Performing `.loadData` sets the sync related flags correctly when the feature flag is
/// disabled and the sync is on.
func test_perform_loadData_syncFlagDisabled_syncOn() async throws {
configService.featureFlagsBool[.enablePasswordManagerSync] = false
authItemRepository.pmSyncEnabled = true
await subject.perform(.loadData)

XCTAssertFalse(subject.state.shouldShowDefaultSaveOption)
XCTAssertFalse(subject.state.shouldShowSyncButton)
}

/// Performing `.loadData` with the password manager sync enabled sets
/// `state.shouldShowSyncButton` to `true`.
func test_perform_loadData_syncEnabled() async throws {
/// Performing `.loadData` sets the sync related flags correctly when the feature flag is
/// enabled and the sync is on.
func test_perform_loadData_syncFlagEnabled_syncOn() async throws {
configService.featureFlagsBool[.enablePasswordManagerSync] = true
authItemRepository.pmSyncEnabled = true
await subject.perform(.loadData)

XCTAssertTrue(subject.state.shouldShowDefaultSaveOption)
XCTAssertTrue(subject.state.shouldShowSyncButton)
}

Expand All @@ -61,6 +105,15 @@ class SettingsProcessorTests: AuthenticatorTestCase {
XCTAssertEqual(subject.state.url, ExternalLinksConstants.backupInformation)
}

/// Receiving `.defaultSaveChanged` updates the user's `defaultSaveOption` app setting.
func test_receive_defaultSaveChanged() {
subject.state.defaultSaveOption = .none
subject.receive(.defaultSaveChanged(.saveLocally))

XCTAssertEqual(appSettingsStore.defaultSaveOption, .saveLocally)
XCTAssertEqual(subject.state.defaultSaveOption, .saveLocally)
}

/// Receiving `.exportItemsTapped` navigates to the export vault screen.
func test_receive_exportVaultTapped() {
subject.receive(.exportItemsTapped)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ struct SettingsState: Equatable {
/// The current language selection.
var currentLanguage: LanguageOption = .default

/// The current default save option.
var defaultSaveOption: DefaultSaveOption = .none

/// A flag to indicate if we should show the default save option menu.
/// Defaults to false, which indicates we should not show the menu.
var shouldShowDefaultSaveOption = false

/// A flag to indicate if we should show the "Sync with the Bitwarden app" button
/// Defaults to false, which indicates we should not show the button.
var shouldShowSyncButton = false
Expand Down
Loading

0 comments on commit 5a699f3

Please sign in to comment.