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

[PM-15996] [BEEEP] PoC Implement Autofill vault filter for favorites/folders. #1209

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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 @@ -35,6 +35,8 @@ protocol SettingsRepository: AnyObject {
/// Get the current value of the allow sync on refresh value.
func getAllowSyncOnRefresh() async throws -> Bool

func getAutofillFilter() async throws -> AutofillFilter

/// Get the current value of the connect to watch setting.
func getConnectToWatch() async throws -> Bool

Expand Down Expand Up @@ -62,6 +64,8 @@ protocol SettingsRepository: AnyObject {
///
func updateAllowSyncOnRefresh(_ allowSyncOnRefresh: Bool) async throws

func updateAutofillFilter(_ filter: AutofillFilter) async throws

/// Update the cached value of the connect to watch setting.
///
/// - Parameter connectToWatch: Whether to connect to the watch app.
Expand Down Expand Up @@ -179,6 +183,14 @@ extension DefaultSettingsRepository: SettingsRepository {
try await stateService.getAllowSyncOnRefresh()
}

func getAutofillFilter() async throws -> AutofillFilter {
let filter = try await stateService.getAutofillFilter()
guard let filter else {
return AutofillFilter(idType: .none)
}
return filter
}

func getConnectToWatch() async throws -> Bool {
try await stateService.getConnectToWatch()
}
Expand All @@ -203,6 +215,14 @@ extension DefaultSettingsRepository: SettingsRepository {
try await stateService.setAllowSyncOnRefresh(allowSyncOnRefresh)
}

func updateAutofillFilter(_ filter: AutofillFilter) async throws {
guard filter.idType != .none else {
try await stateService.setAutofillFilter(nil)
return
}
try await stateService.setAutofillFilter(filter)
}

func updateConnectToWatch(_ connectToWatch: Bool) async throws {
try await stateService.setConnectToWatch(connectToWatch)
}
Expand Down
22 changes: 22 additions & 0 deletions BitwardenShared/Core/Platform/Services/StateService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@
/// - Returns: The app theme.
///
func getAppTheme() async -> AppTheme

func getAutofillFilter(userId: String?) async throws -> AutofillFilter?

/// Get the active user's Biometric Authentication Preference.
///
Expand Down Expand Up @@ -442,6 +444,8 @@
/// - Parameter appTheme: The new app theme.
///
func setAppTheme(_ appTheme: AppTheme) async

func setAutofillFilter(_ filter: AutofillFilter?, userId: String?) async throws

/// Sets the user's Biometric Authentication Preference.
///
Expand Down Expand Up @@ -822,6 +826,10 @@
func getAppRehydrationState() async throws -> AppRehydrationState? {
try await getAppRehydrationState(userId: nil)
}

func getAutofillFilter() async throws -> AutofillFilter? {
try await getAutofillFilter(userId: nil)
}

/// Gets the clear clipboard value for the active account.
///
Expand Down Expand Up @@ -1050,6 +1058,10 @@
func setAllowSyncOnRefresh(_ allowSyncOnRefresh: Bool) async throws {
try await setAllowSyncOnRefresh(allowSyncOnRefresh, userId: nil)
}

func setAutofillFilter(_ filter: AutofillFilter?) async throws {
try await setAutofillFilter(filter, userId: nil)
}

/// Sets the clear clipboard value for the active account.
///
Expand Down Expand Up @@ -1301,7 +1313,7 @@
showWebIconsSubject = CurrentValueSubject(!appSettingsStore.disableWebIcons)

Task {
for await activeUserId in self.appSettingsStore.activeAccountIdPublisher().values {

Check warning on line 1316 in BitwardenShared/Core/Platform/Services/StateService.swift

View workflow job for this annotation

GitHub Actions / Test

expression is 'async' but is not marked with 'await'; this is an error in the Swift 6 language mode
errorReporter.setUserId(activeUserId)
}
}
Expand Down Expand Up @@ -1425,6 +1437,11 @@
AppTheme(appSettingsStore.appTheme)
}

func getAutofillFilter(userId: String?) async throws -> AutofillFilter? {
let userId = try userId ?? getActiveAccountUserId()
return appSettingsStore.autofillFilter(userId: userId)
}

func getClearClipboardValue(userId: String?) async throws -> ClearClipboardValue {
let userId = try userId ?? getActiveAccountUserId()
return appSettingsStore.clearClipboardValue(userId: userId)
Expand Down Expand Up @@ -1687,6 +1704,11 @@
appSettingsStore.appTheme = appTheme.value
appThemeSubject.send(appTheme)
}

func setAutofillFilter(_ filter: AutofillFilter?, userId: String?) async throws {
let userId = try userId ?? getActiveAccountUserId()
appSettingsStore.setAutofillFilter(filter, userId: userId)
}

func setClearClipboardValue(_ clearClipboardValue: ClearClipboardValue?, userId: String?) async throws {
let userId = try userId ?? getActiveAccountUserId()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ protocol AppSettingsStore: AnyObject {
/// - Parameter userId: The user ID associated with this state.
/// - Returns: The rehydration state.
func appRehydrationState(userId: String) -> AppRehydrationState?

func autofillFilter(userId: String) -> AutofillFilter?

/// Gets the time after which the clipboard should be cleared.
///
Expand Down Expand Up @@ -283,6 +285,8 @@ protocol AppSettingsStore: AnyObject {
/// - userId: The user ID associated with the sync on refresh setting.
///
func setAllowSyncOnRefresh(_ allowSyncOnRefresh: Bool?, userId: String)

func setAutofillFilter(_ filter: AutofillFilter?, userId: String)

/// Sets the user's Biometric Authentication Preference.
///
Expand Down Expand Up @@ -677,6 +681,7 @@ extension DefaultAppSettingsStore: AppSettingsStore {
case accountSetupVaultUnlock(userId: String)
case addSitePromptShown
case allowSyncOnRefresh(userId: String)
case autofillFilter(userId: String)
case appId
case appLocale
case appRehydrationState(userId: String)
Expand Down Expand Up @@ -734,6 +739,8 @@ extension DefaultAppSettingsStore: AppSettingsStore {
key = "addSitePromptShown"
case let .allowSyncOnRefresh(userId):
key = "syncOnRefresh_\(userId)"
case let .autofillFilter(userId):
key = "autofillFilter_\(userId)"
case .appId:
key = "appId"
case .appLocale:
Expand Down Expand Up @@ -924,6 +931,10 @@ extension DefaultAppSettingsStore: AppSettingsStore {
func appRehydrationState(userId: String) -> AppRehydrationState? {
fetch(for: .appRehydrationState(userId: userId))
}

func autofillFilter(userId: String) -> AutofillFilter? {
fetch(for: .autofillFilter(userId: userId))
}

func clearClipboardValue(userId: String) -> ClearClipboardValue {
if let rawValue: Int = fetch(for: .clearClipboardValue(userId: userId)),
Expand Down Expand Up @@ -1026,6 +1037,10 @@ extension DefaultAppSettingsStore: AppSettingsStore {
func setAllowSyncOnRefresh(_ allowSyncOnRefresh: Bool?, userId: String) {
store(allowSyncOnRefresh, for: .allowSyncOnRefresh(userId: userId))
}

func setAutofillFilter(_ filter: AutofillFilter?, userId: String) {
store(filter, for: .autofillFilter(userId: userId))
}

func setAppRehydrationState(_ state: AppRehydrationState?, userId: String) {
store(state, for: .appRehydrationState(userId: userId))
Expand Down
28 changes: 28 additions & 0 deletions BitwardenShared/Core/Platform/Utilities/AutofillFilter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import BitwardenSdk

struct AutofillFilter: Menuable, Codable {
var idType: AutofillFilterType
var folderName: String?

var localizedName: String {
switch idType {
case .none:
"None"
case .favorites:
Localizations.favorites
case .folder:
folderName ?? "Unknown"
}
}

init(idType: AutofillFilterType, folderName: String? = nil) {
self.idType = idType
self.folderName = folderName
}
}

enum AutofillFilterType: Equatable, Hashable, Codable {
case none
case favorites
case folder(Uuid)
}
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,7 @@ class DefaultVaultRepository { // swiftlint:disable:this type_body_length
searchText: String,
filterType: VaultFilterType,
isActive: Bool,
isAutofilling: Bool = false,
cipherFilter: ((CipherView) -> Bool)? = nil
) async throws -> AnyPublisher<[CipherView], Error> {
let query = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
Expand All @@ -519,7 +520,7 @@ class DefaultVaultRepository { // swiftlint:disable:this type_body_length
cipher.type != .sshKey || isSSHKeyVaultItemEnabled
}

return try await cipherService.ciphersPublisher().asyncTryMap { ciphers -> [CipherView] in
return try await cipherService.ciphersPublisher(isAutofilling: isAutofilling).asyncTryMap { ciphers -> [CipherView] in
// Convert the Ciphers to CipherViews and filter appropriately.
let matchingCiphers = try await ciphers.asyncMap { cipher in
try await self.clientService.vault().ciphers().decrypt(cipher: cipher)
Expand Down Expand Up @@ -1226,7 +1227,7 @@ extension DefaultVaultRepository: VaultRepository {
}

return try await Publishers.CombineLatest(
cipherService.ciphersPublisher(),
cipherService.ciphersPublisher(isAutofilling: true),
availableFido2CredentialsPublisher
)
.asyncTryMap { ciphers, availableFido2Credentials in
Expand Down Expand Up @@ -1266,7 +1267,8 @@ extension DefaultVaultRepository: VaultRepository {
searchPublisher(
searchText: searchText,
filterType: filterType,
isActive: true
isActive: true,
isAutofilling: true
) { cipher in
cipher.type == .login
},
Expand Down
32 changes: 29 additions & 3 deletions BitwardenShared/Core/Vault/Services/CipherService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,13 @@ protocol CipherService {
///
/// - Returns: The list of encrypted ciphers.
///
func ciphersPublisher() async throws -> AnyPublisher<[Cipher], Error>
func ciphersPublisher(isAutofilling: Bool) async throws -> AnyPublisher<[Cipher], Error>
}

extension CipherService {
func ciphersPublisher() async throws -> AnyPublisher<[Cipher], Error> {
try await ciphersPublisher(isAutofilling: false)
}
}

// MARK: - DefaultCipherService
Expand Down Expand Up @@ -362,8 +368,28 @@ extension DefaultCipherService {

// MARK: Publishers

func ciphersPublisher() async throws -> AnyPublisher<[Cipher], Error> {
func ciphersPublisher(isAutofilling: Bool) async throws -> AnyPublisher<[Cipher], Error> {
let userId = try await stateService.getActiveAccountId()
return cipherDataStore.cipherPublisher(userId: userId)
let ciphersPublisher = cipherDataStore.cipherPublisher(userId: userId)
if !isAutofilling {
return ciphersPublisher
}
let autofillFilter = try await stateService.getAutofillFilter(userId: userId)
guard let autofillFilter else {
return ciphersPublisher
}
return switch autofillFilter.idType {
case .none:
ciphersPublisher
case .favorites:
ciphersPublisher.map { ciphers in
ciphers.filter{ $0.favorite }
}
.eraseToAnyPublisher()
case let .folder(folderId):
ciphersPublisher.map { ciphers in
ciphers.filter { $0.folderId == folderId }
}.eraseToAnyPublisher()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
enum AutoFillAction: Equatable {
/// The app extension button was tapped.
case appExtensionTapped

/// The autofill filter was changed.
case autofillFilterChanged(AutofillFilter)

/// The default URI match type was changed.
case defaultUriMatchTypeChanged(UriMatchType)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,24 @@ final class AutoFillProcessor: StateProcessor<AutoFillState, AutoFillAction, Aut
await fetchSettingValues()
case .streamSettingsBadge:
await streamSettingsBadge()
await streamFolders()
}
}

override func receive(_ action: AutoFillAction) {
switch action {
case .appExtensionTapped:
coordinator.navigate(to: .appExtension)
case let .autofillFilterChanged(filter):
state.autofillFilter = filter
Task {
do {
try await services.settingsRepository.updateAutofillFilter(filter)
} catch {
coordinator.showAlert(.defaultAlert(title: Localizations.anErrorHasOccurred))
services.errorReporter.log(error: error)
}
}
case let .defaultUriMatchTypeChanged(newValue):
state.defaultUriMatchType = newValue
Task {
Expand Down Expand Up @@ -88,6 +99,7 @@ final class AutoFillProcessor: StateProcessor<AutoFillState, AutoFillAction, Aut
///
private func fetchSettingValues() async {
do {
state.autofillFilter = try await services.settingsRepository.getAutofillFilter()
state.defaultUriMatchType = try await services.settingsRepository.getDefaultUriMatchType()
state.isCopyTOTPToggleOn = try await !services.settingsRepository.getDisableAutoTotpCopy()
} catch {
Expand All @@ -96,6 +108,34 @@ final class AutoFillProcessor: StateProcessor<AutoFillState, AutoFillAction, Aut
}
}

/// Stream the list of folders
private func streamFolders() async {
do {
let publisher = try await services.settingsRepository.foldersListPublisher()
for try await value in publisher {
let folderOptions = value.compactMap { folder -> AutofillFilter? in
guard let id = folder.id else {
return nil
}
return AutofillFilter(
idType: .folder(id),
folderName: folder.name
)
}
var autofillFilterOptions = [
AutofillFilter(idType: .none),
AutofillFilter(idType: .favorites)
]
if !folderOptions.isEmpty {
autofillFilterOptions.append(contentsOf: folderOptions)
}
state.autofillFilterOptions = autofillFilterOptions
}
} catch {
services.errorReporter.log(error: error)
}
}

/// Streams the state of the badges in the settings tab.
///
private func streamSettingsBadge() async {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@
struct AutoFillState {
// MARK: Properties

var autofillFilter: AutofillFilter = AutofillFilter(idType: .none)

var autofillFilterOptions: [AutofillFilter] = [
AutofillFilter(idType: .none),
AutofillFilter(idType: .favorites)
]

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,21 @@ struct AutoFillView: View {
.styleGuide(.subheadline)
.foregroundColor(Color(asset: Asset.Colors.textSecondary))
}

VStack(spacing: 2) {
SettingsMenuField(
title: "Autofill vault filter",
options: store.state.autofillFilterOptions,
hasDivider: false,
selection: store.binding(
get: \.autofillFilter,
send: AutoFillAction.autofillFilterChanged
)
)
.cornerRadius(10)
.padding(.bottom, 8)
.accessibilityIdentifier("AutofillFilterChooser")
}
}
}

Expand Down
Loading