-
Notifications
You must be signed in to change notification settings - Fork 13
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
Add Ability to Delete All Saved Passwords #2265
Changes from 20 commits
cfc7281
0be6ca3
2569353
5847268
b146c7d
b121f4a
288cfb8
47db351
3079176
d79e256
675b426
a259d4e
6427f97
b5be13b
26da4cc
5f259ef
09a96ed
418020e
41d45c7
eaed1af
ab3d54b
40461cc
9ee16cc
935ec43
3ece6ea
609e3bd
8611884
64808f8
48611d5
e03edcd
aac7d7c
c08178d
51ff740
7d91ccc
04cd79e
16cf922
df094e0
b871ee1
8a57841
adc5d6e
e6d65a8
73331ad
91e4507
635f550
c9cbd5b
9ff31f4
670101f
34b885b
01452cb
76b7372
4187dee
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
// | ||
// AutofillActionBuilder.swift | ||
// | ||
// Copyright © 2024 DuckDuckGo. All rights reserved. | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
// | ||
|
||
import BrowserServicesKit | ||
import Foundation | ||
|
||
/// Conforming types provide methods to build an `AutofillActionExecutor` and an `AutofillActionPresenter` | ||
protocol AutofillActionBuilder { | ||
func buildExecutor() -> AutofillActionExecutor? | ||
func buildPresenter() -> AutofillActionPresenter | ||
} | ||
|
||
extension AutofillActionBuilder { | ||
func buildPresenter() -> AutofillActionPresenter { | ||
DefaultAutofillActionPresenter() | ||
} | ||
} | ||
|
||
/// Builds an `AutofillActionExecutor` | ||
struct AutofillDeleteAllPasswordsBuilder: AutofillActionBuilder { | ||
@MainActor | ||
func buildExecutor() -> AutofillActionExecutor? { | ||
guard let secureVault = try? AutofillSecureVaultFactory.makeVault(errorReporter: SecureVaultErrorReporter.shared), | ||
let syncService = NSApp.delegateTyped.syncService else { return nil } | ||
|
||
return AutofillDeleteAllPasswordsExecutor(userAuthenticator: DeviceAuthenticator.shared, | ||
secureVault: secureVault, | ||
syncService: syncService) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
// | ||
// AutofillActionExecutor.swift | ||
// | ||
// Copyright © 2024 DuckDuckGo. All rights reserved. | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
// | ||
|
||
import Foundation | ||
import BrowserServicesKit | ||
import DDGSync | ||
import AppKit | ||
|
||
/// Conforming types provide an `execute` method which performs some action on autofill types (e.g delete all passwords) | ||
protocol AutofillActionExecutor { | ||
init(userAuthenticator: UserAuthenticating, secureVault: any AutofillSecureVault, syncService: DDGSyncing) | ||
/// NSAlert to display asking a user to confirm the action | ||
var confirmationAlert: NSAlert { get } | ||
/// NSAlert to display when the action is complete | ||
var completionAlert: NSAlert { get } | ||
/// Executes the action | ||
func execute(_ onSuccess: (() -> Void)?) | ||
} | ||
|
||
/// Concrete `AutofillActionExecutor` for deletion of all autofill passwords | ||
struct AutofillDeleteAllPasswordsExecutor: AutofillActionExecutor { | ||
|
||
var confirmationAlert: NSAlert { | ||
let accounts = (try? secureVault.accounts()) ?? [] | ||
return NSAlert.deleteAllPasswordsConfirmationAlert(count: accounts.count, syncEnabled: syncEnabled) | ||
} | ||
|
||
var completionAlert: NSAlert { | ||
let accounts = (try? secureVault.accounts()) ?? [] | ||
return NSAlert.deleteAllPasswordsCompletionAlert(count: accounts.count, syncEnabled: syncEnabled) | ||
} | ||
|
||
private var syncEnabled: Bool { | ||
syncService.authState != .inactive | ||
} | ||
|
||
private var userAuthenticator: UserAuthenticating | ||
private var secureVault: any AutofillSecureVault | ||
private var syncService: DDGSyncing | ||
|
||
init(userAuthenticator: UserAuthenticating, secureVault: any AutofillSecureVault, syncService: DDGSyncing) { | ||
self.userAuthenticator = userAuthenticator | ||
self.secureVault = secureVault | ||
self.syncService = syncService | ||
} | ||
|
||
func execute(_ onSuccess: (() -> Void)? = nil) { | ||
userAuthenticator.authenticateUser(reason: .deleteAllPasswords) { authenticationResult in | ||
guard authenticationResult.authenticated else { return } | ||
|
||
do { | ||
try secureVault.deleteAllWebsiteCredentials() | ||
syncService.scheduler.notifyDataChanged() | ||
NotificationCenter.default.post(name: .PasswordManagerChanged, object: nil) | ||
onSuccess?() | ||
} catch { | ||
Pixel.fire(.debug(event: .secureVaultError, error: error)) | ||
} | ||
|
||
return | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
// | ||
// AutofillActionPresenter.swift | ||
// | ||
// Copyright © 2024 DuckDuckGo. All rights reserved. | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
// | ||
|
||
import Foundation | ||
import AppKit | ||
|
||
/// Conforming types handles presentation of `NSAlert`s associated with an `AutofillActionExecutor` | ||
protocol AutofillActionPresenter { | ||
func show(actionExecutor: AutofillActionExecutor) | ||
} | ||
|
||
/// Handles presentation of an alert associated with an `AutofillActionExecutor` | ||
struct DefaultAutofillActionPresenter: AutofillActionPresenter { | ||
|
||
@MainActor | ||
func show(actionExecutor: AutofillActionExecutor) { | ||
guard let window else { return } | ||
|
||
let confirmationAlert = actionExecutor.confirmationAlert | ||
let completionAlert = actionExecutor.completionAlert | ||
|
||
confirmationAlert.beginSheetModal(for: window) { response in | ||
switch response { | ||
case .alertFirstButtonReturn: | ||
actionExecutor.execute { | ||
show(completionAlert) | ||
} | ||
default: | ||
break | ||
} | ||
} | ||
} | ||
} | ||
|
||
private extension DefaultAutofillActionPresenter { | ||
|
||
@MainActor | ||
func show(_ alert: NSAlert) { | ||
guard let window else { return } | ||
alert.beginSheetModal(for: window) | ||
} | ||
|
||
@MainActor | ||
var window: NSWindow? { | ||
WindowControllersManager.shared.lastKeyMainWindowController?.window | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1059,6 +1059,63 @@ struct UserText { | |
} | ||
} | ||
|
||
// MARK: Autofill Item Deletion (Autofill -> More Menu, Settings -> Autofill) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note: It’s possible copy with change after review. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is actually a dedicated |
||
static let deleteAllPasswords = NSLocalizedString("autofill.items.delete-all-passwords", value: "Delete All Passwords…", comment: "Opens Delete All Passwords dialog") | ||
|
||
// Confirmation Message Text | ||
static func deleteAllPasswordsConfirmationMessageText(count: Int) -> String { | ||
let localized = NSLocalizedString("autofill.items.delete-all-passwords-confirmation-message-text", value: "Are you sure you want to delete all passwords (%d)?", comment: "Message displayed on dialog asking user to confirm deletion of all passwords") | ||
return String(format: localized, count) | ||
} | ||
|
||
// Confirmation Information Text | ||
static func deleteAllPasswordsConfirmationInformationText(count: Int, syncEnabled: Bool) -> String { | ||
switch(count, syncEnabled) { | ||
case (1, true): | ||
return NSLocalizedString("autofill.items.delete-one-password-synced-confirmation-information-text", value: "Your password will be deleted from all synced devices. Make sure you still have a way to access your accounts.", comment: "Information message displayed when deleting one password on a synced device") | ||
case (1, false): | ||
return NSLocalizedString("autofill.items.delete-one-password-device-confirmation-information-text", value: "Your password will be deleted from this device.", comment: "Information message displayed when deleting one password on a device") | ||
case (_, true): | ||
return NSLocalizedString("autofill.items.delete-all-passwords-synced-confirmation-information-text", value: "Your passwords will be deleted from all synced devices. Make sure you still have a way to access your accounts.", comment: "Information message displayed when deleting all passwords on a synced device") | ||
default: | ||
return NSLocalizedString("autofill.items.delete-all-passwords-device-confirmation-information-text", value: "Your passwords will be deleted from this device.", comment: "Information message displayed when deleting all passwords on a device") | ||
} | ||
} | ||
|
||
// Completion Message Text | ||
static func deleteAllPasswordsCompletionMessageText(count: Int) -> String { | ||
if count == 1 { | ||
return NSLocalizedString("autofill.items.delete-one-password-confirmation-message-text", value: "Password deleted", comment: "Message displayed on completion of single password deletion") | ||
} else { | ||
let localized = NSLocalizedString("autofill.items.delete-all-passwords-confirmation-message-text", value: "Passwords deleted (%d)", comment: "Message displayed on completion of multiple password deletion") | ||
return String(format: localized, count) | ||
} | ||
} | ||
|
||
// Completion Information Text | ||
static func deleteAllPasswordsCompletionInformationText(count: Int, syncEnabled: Bool) -> String { | ||
switch(count, syncEnabled) { | ||
case (1, true): | ||
return NSLocalizedString("autofill.items.delete-one-password-synced-completion-information-text", | ||
value: "Your password have been deleted from all synced devices.", | ||
comment: "Information message displayed on completion of single password deletion when devices are synced") | ||
case (1, false): | ||
return NSLocalizedString("autofill.items.delete-one-password-device-completion-information-text", | ||
value: "Your password have been deleted from this device.", | ||
comment: "Information message displayed on completion of single password deletion when devices are not synced") | ||
case (_, true): | ||
return NSLocalizedString("autofill.items.delete-all-passwords-synced-completion-information-text", | ||
value: "Your passwords have been deleted from all synced devices.", | ||
comment: "Information message displayed on completion of multiple password deletion when devices are synced") | ||
default: | ||
return NSLocalizedString("autofill.items.delete-all-passwords-device-completion-information-text", | ||
value: "Your passwords have been deleted from this device.", | ||
comment: "Information message displayed on completion of multiple password deletion when no devices are not synced") | ||
} | ||
} | ||
|
||
static let deleteAllPasswordsPermissionText = NSLocalizedString("autofill.items.delete-all-passwords-permisson-text", value: "Authenticate to confirm you want to delete all passwords", comment: "Message displayed in system authentication dialog") | ||
|
||
#if SUBSCRIPTION | ||
// Key: "subscription.menu.item" | ||
// Comment: "Title for Subscription item in the options menu" | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I’ll update this to point to the latest version once the related BSK PR is merged.