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-131] Add Popup to QR Code Scan to Add Code to Bitwarden #177

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
2e5aa95
[BITAU-132] Move to Bitwarden Long Press on Local Items
brant-livefront Oct 24, 2024
5207553
[BITAU-130] Add Option to Manual Key Entry for Adding to Bitwarden
brant-livefront Oct 24, 2024
85c59a8
Updated per PR review: removed BW shortening and renamed hash function
brant-livefront Oct 25, 2024
f664ce3
Merge in latest from brant/BITAU-132-move-to-bitwarden-long-press-on-โ€ฆ
brant-livefront Oct 25, 2024
dfe9ef0
Fixed one missed merge conflict
brant-livefront Oct 25, 2024
016ad2f
Merged in latest from main, fixed conflicts
brant-livefront Oct 25, 2024
7837fe7
[BITAU-131] Add Popup to QR Code Scan to Add Code to Bitwarden
brant-livefront Oct 28, 2024
d060aca
Increase sleep time to allow test to succeed on CI
brant-livefront Oct 28, 2024
46a383f
Respond to PR feedback
brant-livefront Oct 29, 2024
c2acc5c
Removed dead code that Codecov revealed
brant-livefront Oct 29, 2024
bf971bb
Copy changes from the UX team
brant-livefront Oct 29, 2024
67bae02
Merge branch 'main' into brant/BITAU-130-add-option-to-manual-key-entโ€ฆ
brant-livefront Oct 31, 2024
d50a68b
Merge in latest from main; fix conflicts
brant-livefront Oct 31, 2024
3f01f91
Add tests and handle case where QR code scan is done with the sync noโ€ฆ
brant-livefront Oct 31, 2024
2983145
Updated strings to put quotes around default save options
brant-livefront Oct 31, 2024
836e087
Merge branch 'main' into brant/BITAU-130-add-option-to-manual-key-entโ€ฆ
brant-livefront Nov 1, 2024
bcc76d3
Merge branch 'brant/BITAU-130-add-option-to-manual-key-entry-for-addiโ€ฆ
brant-livefront Nov 1, 2024
2937ba0
Merge in latest from main; fix conflicts
brant-livefront Nov 6, 2024
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
@@ -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
fedemkr marked this conversation as resolved.
Show resolved Hide resolved
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
Loading