From 98fddd523286919a57e92feba06ab2f6affe736e Mon Sep 17 00:00:00 2001 From: amddg44 Date: Tue, 11 Jun 2024 11:57:52 +0200 Subject: [PATCH] Make passwords easier to discover (#2847) Task/Issue URL: https://app.asana.com/0/1199230911884351/1206531758082882/f Tech Design URL: CC: Description: UI enhancements to make passwords manager easier for users to discover --- DuckDuckGo.xcodeproj/project.pbxproj | 12 + .../Key-Color-24.imageset/Contents.json | 12 + .../Key-Color-24.imageset/Key-Color-24.svg | 4 + .../Passwords-Add-128.imageset/Contents.json | 12 + .../Passwords-Add-128.svg | 18 + .../Bookmarks-Favorites-Color-24.svg | 6 + .../Contents.json | 12 + .../Clear-Recolorable-16.svg | 12 + .../Contents.json | 12 + DuckDuckGo/Common/Localizables/UserText.swift | 39 +- .../Model/DataImportShortcutsViewModel.swift | 54 + .../Model/DataImportSummaryViewModel.swift | 5 + .../Model/DataImportViewModel.swift | 24 +- .../View/BrowserImportMoreInfoView.swift | 2 +- .../View/DataImportShortcutsView.swift | 92 + .../View/DataImportSummaryView.swift | 228 ++- .../View/DataImportTypePicker.swift | 3 +- .../DataImport/View/DataImportView.swift | 47 +- DuckDuckGo/Localizable.xcstrings | 1725 ++++++++++++++--- .../PopoverMessageViewController.swift | 2 + .../NavigationBar/View/MoreOptionsMenu.swift | 6 +- .../View/NavigationBarPopovers.swift | 2 + .../View/NavigationBarViewController.swift | 38 + .../Model/PreferencesSection.swift | 2 +- .../View/PreferencesAutofillView.swift | 15 +- .../View/PreferencesRootView.swift | 2 +- .../Extensions/UserText+PasswordManager.swift | 10 +- .../PasswordManagementViewController.swift | 8 +- .../View/PasswordManager.storyboard | 51 +- .../View/SaveCredentialsViewController.swift | 17 +- .../PopoverMessageView.swift | 2 +- .../DataImport/DataImportViewModelTests.swift | 37 +- UnitTests/Menus/MoreOptionsMenuTests.swift | 4 +- 33 files changed, 2075 insertions(+), 440 deletions(-) create mode 100644 DuckDuckGo/Assets.xcassets/Images/Autofill/Key-Color-24.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Images/Autofill/Key-Color-24.imageset/Key-Color-24.svg create mode 100644 DuckDuckGo/Assets.xcassets/Images/Autofill/Passwords-Add-128.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Images/Autofill/Passwords-Add-128.imageset/Passwords-Add-128.svg create mode 100644 DuckDuckGo/Assets.xcassets/Images/Bookmarks-Favorites-Color-24.imageset/Bookmarks-Favorites-Color-24.svg create mode 100644 DuckDuckGo/Assets.xcassets/Images/Bookmarks-Favorites-Color-24.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Images/Clear-Recolorable-16.imageset/Clear-Recolorable-16.svg create mode 100644 DuckDuckGo/Assets.xcassets/Images/Clear-Recolorable-16.imageset/Contents.json create mode 100644 DuckDuckGo/DataImport/Model/DataImportShortcutsViewModel.swift create mode 100644 DuckDuckGo/DataImport/View/DataImportShortcutsView.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 3a420a5442..39e7d25be7 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -2481,12 +2481,16 @@ C13909F52B85FD79001626ED /* AutofillDeleteAllPasswordsExecutorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C13909F32B85FD79001626ED /* AutofillDeleteAllPasswordsExecutorTests.swift */; }; C13909FB2B861039001626ED /* AutofillActionPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C13909FA2B861039001626ED /* AutofillActionPresenter.swift */; }; C13909FC2B861039001626ED /* AutofillActionPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C13909FA2B861039001626ED /* AutofillActionPresenter.swift */; }; + C16127EE2BDFB46400966BB9 /* DataImportShortcutsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C16127ED2BDFB46400966BB9 /* DataImportShortcutsView.swift */; }; + C16127EF2BDFB46400966BB9 /* DataImportShortcutsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C16127ED2BDFB46400966BB9 /* DataImportShortcutsView.swift */; }; C168B9AC2B31DC7E001AFAD9 /* AutofillNeverPromptWebsitesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C168B9AB2B31DC7E001AFAD9 /* AutofillNeverPromptWebsitesManager.swift */; }; C168B9AD2B31DC7F001AFAD9 /* AutofillNeverPromptWebsitesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C168B9AB2B31DC7E001AFAD9 /* AutofillNeverPromptWebsitesManager.swift */; }; C17CA7AD2B9B52E6008EC3C1 /* NavigationBarPopoversTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C17CA7AC2B9B52E6008EC3C1 /* NavigationBarPopoversTests.swift */; }; C17CA7AE2B9B52E6008EC3C1 /* NavigationBarPopoversTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C17CA7AC2B9B52E6008EC3C1 /* NavigationBarPopoversTests.swift */; }; C17CA7B22B9B5317008EC3C1 /* MockAutofillPopoverPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C17CA7B12B9B5317008EC3C1 /* MockAutofillPopoverPresenter.swift */; }; C17CA7B32B9B5317008EC3C1 /* MockAutofillPopoverPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C17CA7B12B9B5317008EC3C1 /* MockAutofillPopoverPresenter.swift */; }; + C1B1CBE12BE1915100B6049C /* DataImportShortcutsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1B1CBE02BE1915100B6049C /* DataImportShortcutsViewModel.swift */; }; + C1B1CBE22BE1915100B6049C /* DataImportShortcutsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1B1CBE02BE1915100B6049C /* DataImportShortcutsViewModel.swift */; }; C1DAF3B52B9A44860059244F /* AutofillPopoverPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1DAF3B42B9A44860059244F /* AutofillPopoverPresenter.swift */; }; C1DAF3B62B9A44860059244F /* AutofillPopoverPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1DAF3B42B9A44860059244F /* AutofillPopoverPresenter.swift */; }; C1E961EB2B879E79001760E1 /* MockAutofillActionPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1E961E72B879E4D001760E1 /* MockAutofillActionPresenter.swift */; }; @@ -4065,9 +4069,11 @@ C13909EE2B85FD4E001626ED /* AutofillActionExecutor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillActionExecutor.swift; sourceTree = ""; }; C13909F32B85FD79001626ED /* AutofillDeleteAllPasswordsExecutorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillDeleteAllPasswordsExecutorTests.swift; sourceTree = ""; }; C13909FA2B861039001626ED /* AutofillActionPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillActionPresenter.swift; sourceTree = ""; }; + C16127ED2BDFB46400966BB9 /* DataImportShortcutsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataImportShortcutsView.swift; sourceTree = ""; }; C168B9AB2B31DC7E001AFAD9 /* AutofillNeverPromptWebsitesManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutofillNeverPromptWebsitesManager.swift; sourceTree = ""; }; C17CA7AC2B9B52E6008EC3C1 /* NavigationBarPopoversTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarPopoversTests.swift; sourceTree = ""; }; C17CA7B12B9B5317008EC3C1 /* MockAutofillPopoverPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAutofillPopoverPresenter.swift; sourceTree = ""; }; + C1B1CBE02BE1915100B6049C /* DataImportShortcutsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataImportShortcutsViewModel.swift; sourceTree = ""; }; C1DAF3B42B9A44860059244F /* AutofillPopoverPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillPopoverPresenter.swift; sourceTree = ""; }; C1E961E72B879E4D001760E1 /* MockAutofillActionPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAutofillActionPresenter.swift; sourceTree = ""; }; C1E961EC2B879ED9001760E1 /* MockAutofillActionExecutor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAutofillActionExecutor.swift; sourceTree = ""; }; @@ -5360,6 +5366,7 @@ 4B8AC93426B3B2FD00879451 /* NSAlert+DataImport.swift */, B6B5F5832B03580A008DB58A /* RequestFilePermissionView.swift */, B677FC4E2B06376B0099EB04 /* ReportFeedbackView.swift */, + C16127ED2BDFB46400966BB9 /* DataImportShortcutsView.swift */, ); path = View; sourceTree = ""; @@ -7986,6 +7993,7 @@ B6B4D1C42B0B3B5400C26286 /* DataImportReportModel.swift */, B6A22B612B1E29D000ECD2BA /* DataImportSummaryViewModel.swift */, B6619EF82B111CBE00CD9186 /* InstructionsFormatParser.swift */, + C1B1CBE02BE1915100B6049C /* DataImportShortcutsViewModel.swift */, ); path = Model; sourceTree = ""; @@ -9828,6 +9836,7 @@ B6B5F5802B024105008DB58A /* DataImportSummaryView.swift in Sources */, 4B9DB0422A983B24000927DB /* WaitlistDialogView.swift in Sources */, 3706FB57293F65D500E42796 /* AppPrivacyConfigurationDataProvider.swift in Sources */, + C1B1CBE22BE1915100B6049C /* DataImportShortcutsViewModel.swift in Sources */, 857E5AF62A790B7000FC0FB4 /* PixelExperiment.swift in Sources */, 9F33445F2BBFA77F0040CBEB /* BookmarksBarVisibilityManager.swift in Sources */, 3706FB58293F65D500E42796 /* LinkButton.swift in Sources */, @@ -10239,6 +10248,7 @@ 3706FC62293F65D500E42796 /* ThirdPartyBrowser.swift in Sources */, 3706FC63293F65D500E42796 /* CircularProgressView.swift in Sources */, 3706FC64293F65D500E42796 /* SuggestionContainer.swift in Sources */, + C16127EF2BDFB46400966BB9 /* DataImportShortcutsView.swift in Sources */, 3706FC65293F65D500E42796 /* HomePageViewController.swift in Sources */, 3706FC67293F65D500E42796 /* OperatingSystemVersionExtension.swift in Sources */, B6F9BDE52B45CD1900677B33 /* ModalView.swift in Sources */, @@ -11280,6 +11290,7 @@ B6BCC54F2AFE4F7D002C5499 /* DataImportTypePicker.swift in Sources */, AAEEC6A927088ADB008445F7 /* FireCoordinator.swift in Sources */, B655369B268442EE00085A79 /* GeolocationProvider.swift in Sources */, + C1B1CBE12BE1915100B6049C /* DataImportShortcutsViewModel.swift in Sources */, B6C0B23C26E87D900031CB7F /* NSAlert+ActiveDownloadsTermination.swift in Sources */, AAECA42024EEA4AC00EFA63A /* IndexPathExtension.swift in Sources */, 4B9579212AC687170062CA31 /* HardwareModel.swift in Sources */, @@ -11412,6 +11423,7 @@ 4BB88B5B25B7BA50006F6B06 /* Instruments.swift in Sources */, 9812D895276CEDA5004B6181 /* ContentBlockerRulesLists.swift in Sources */, 4B0511E2262CAA8600F6079C /* NSViewControllerExtension.swift in Sources */, + C16127EE2BDFB46400966BB9 /* DataImportShortcutsView.swift in Sources */, 9FA173DF2B7A0EFE00EE4E6E /* BookmarkDialogButtonsView.swift in Sources */, F44C130225C2DA0400426E3E /* NSAppearanceExtension.swift in Sources */, 4B3B8490297A0E1000A384BD /* EmailManagerExtension.swift in Sources */, diff --git a/DuckDuckGo/Assets.xcassets/Images/Autofill/Key-Color-24.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Autofill/Key-Color-24.imageset/Contents.json new file mode 100644 index 0000000000..c30b2bb83d --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Autofill/Key-Color-24.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Key-Color-24.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/Autofill/Key-Color-24.imageset/Key-Color-24.svg b/DuckDuckGo/Assets.xcassets/Images/Autofill/Key-Color-24.imageset/Key-Color-24.svg new file mode 100644 index 0000000000..c35cfc60c1 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Autofill/Key-Color-24.imageset/Key-Color-24.svg @@ -0,0 +1,4 @@ + + + + diff --git a/DuckDuckGo/Assets.xcassets/Images/Autofill/Passwords-Add-128.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Autofill/Passwords-Add-128.imageset/Contents.json new file mode 100644 index 0000000000..71a4b8cbc9 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Autofill/Passwords-Add-128.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Passwords-Add-128.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/Autofill/Passwords-Add-128.imageset/Passwords-Add-128.svg b/DuckDuckGo/Assets.xcassets/Images/Autofill/Passwords-Add-128.imageset/Passwords-Add-128.svg new file mode 100644 index 0000000000..cb07aafa6a --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Autofill/Passwords-Add-128.imageset/Passwords-Add-128.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/DuckDuckGo/Assets.xcassets/Images/Bookmarks-Favorites-Color-24.imageset/Bookmarks-Favorites-Color-24.svg b/DuckDuckGo/Assets.xcassets/Images/Bookmarks-Favorites-Color-24.imageset/Bookmarks-Favorites-Color-24.svg new file mode 100644 index 0000000000..7dc9f7901a --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Bookmarks-Favorites-Color-24.imageset/Bookmarks-Favorites-Color-24.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/DuckDuckGo/Assets.xcassets/Images/Bookmarks-Favorites-Color-24.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Bookmarks-Favorites-Color-24.imageset/Contents.json new file mode 100644 index 0000000000..4c42ee8e0a --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Bookmarks-Favorites-Color-24.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Bookmarks-Favorites-Color-24.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/Clear-Recolorable-16.imageset/Clear-Recolorable-16.svg b/DuckDuckGo/Assets.xcassets/Images/Clear-Recolorable-16.imageset/Clear-Recolorable-16.svg new file mode 100644 index 0000000000..be459c2cd0 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Clear-Recolorable-16.imageset/Clear-Recolorable-16.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/DuckDuckGo/Assets.xcassets/Images/Clear-Recolorable-16.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Clear-Recolorable-16.imageset/Contents.json new file mode 100644 index 0000000000..bc04fe84d6 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Clear-Recolorable-16.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Clear-Recolorable-16.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index 02fb18ac46..2693a490c4 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -396,10 +396,12 @@ struct UserText { static let restartBitwarden = NSLocalizedString("restart.bitwarden", value: "Restart Bitwarden", comment: "Button to restart Bitwarden application") static let restartBitwardenInfo = NSLocalizedString("restart.bitwarden.info", value: "Bitwarden is not responding. Please restart it to initiate the communication again", comment: "This string represents a message informing the user that Bitwarden is not responding and prompts them to restart the application to initiate communication again.") - static let autofillViewContentButton = NSLocalizedString("autofill.view-autofill-content", value: "View Autofill Content…", comment: "View Autofill Content Button name in the autofill settings") - static let autofillAskToSave = NSLocalizedString("autofill.ask-to-save", value: "Save and Autofill", comment: "Autofill settings section title") + static let autofillViewContentButtonPasswords = NSLocalizedString("autofill.view-autofill-content.passwords", value: "Open Passwords…", comment: "View Password Content Button title in the autofill Settings") + static let autofillViewContentButtonPaymentMethods = NSLocalizedString("autofill.view-autofill-content.payment-methods", value: "Open Payment Methods…", comment: "View Payment Methods Content Button title in the autofill Settings") + static let autofillViewContentButtonIdentities = NSLocalizedString("autofill.view-autofill-content.identities", value: "Open Identities…", comment: "View Identities Content Button title in the autofill Settings") + static let autofillAskToSave = NSLocalizedString("autofill.ask-to-save", value: "Ask to Save and Autofill", comment: "Autofill settings section title") static let autofillAskToSaveExplanation = NSLocalizedString("autofill.ask-to-save.explanation", value: "Receive prompts to save new information and autofill online forms.", comment: "Description of Autofill autosaving feature - used in settings") - static let autofillUsernamesAndPasswords = NSLocalizedString("autofill.usernames-and-passwords", value: "Usernames and passwords", comment: "Autofill autosaved data type") + static let autofillPasswords = NSLocalizedString("autofill.passwords", value: "Passwords", comment: "Autofill autosaved data type") static let autofillAddresses = NSLocalizedString("autofill.addresses", value: "Addresses", comment: "Autofill autosaved data type") static let autofillPaymentMethods = NSLocalizedString("autofill.payment-methods", value: "Payment methods", comment: "Autofill autosaved data type") static let autofillExcludedSites = NSLocalizedString("autofill.excluded-sites", value: "Excluded Sites", comment: "Autofill settings section title") @@ -420,7 +422,6 @@ struct UserText { static let downloadsOpenPopupOnCompletion = NSLocalizedString("downloads.open.on.completion", value: "Automatically open the Downloads panel when downloads complete", comment: "Checkbox to open a Download Manager popover when downloads are completed") // MARK: Password Manager - static let passwordManagement = NSLocalizedString("passsword.management", value: "Autofill", comment: "Used as title for password management user interface") static let passwordManagementAllItems = NSLocalizedString("passsword.management.all-items", value: "All Items", comment: "Used as title for the Autofill All Items option") static let passwordManagementLogins = NSLocalizedString("passsword.management.logins", value: "Passwords", comment: "Used as title for the Autofill Logins option") static let passwordManagementIdentities = NSLocalizedString("passsword.management.identities", value: "Identities", comment: "Used as title for the Autofill Identities option") @@ -431,19 +432,19 @@ struct UserText { static let passwordManagementUnlock = NSLocalizedString("passsword.management.unlock", value: "Unlock", comment: "Unlock Logins Vault menu") static let passwordManagementSavePayment = NSLocalizedString("passsword.management.save.payment", value: "Save Payment Method?", comment: "Title of dialog that allows the user to save a payment method") static let passwordManagementSaveAddress = NSLocalizedString("passsword.management.save.address", value: "Save Address?", comment: "Title of dialog that allows the user to save an address method") - static let passwordManagementSaveCredentialsPasswordManagerTitle = NSLocalizedString("passsword.management.save.credentials.password.manager.title", value: "Save Login to Bitwarden?", comment: "Title of the passwored manager section of dialog that allows the user to save credentials") + static let passwordManagementSaveCredentialsPasswordManagerTitle = NSLocalizedString("passsword.management.save.credentials.password.manager.title", value: "Save password to Bitwarden?", comment: "Title of the passwored manager section of dialog that allows the user to save credentials") static let passwordManagementSaveCredentialsUnlockPasswordManager = NSLocalizedString("passsword.management.save.credentials.unlock.password.manager", value: "Unlock Bitwarden to Save", comment: "In the password manager dialog, alerts the user that they need to unlock Bitworden before being able to save the credential") - static let passwordManagementSaveCredentialsFireproofCheckboxTitle = NSLocalizedString("passsword.management.save.credentials.fireproof.checkbox.title", value: "Fireproof?", comment: "In the password manager dialog, title of the section that allows the user to fireproof a website via a checkbox") + static let passwordManagementSaveCredentialsFireproofCheckboxTitle = NSLocalizedString("passsword.management.save.credentials.fireproof.checkbox.title", value: "Fireproof this website", comment: "In the password manager dialog, title of the section that allows the user to fireproof a website via a checkbox") static let passwordManagementSaveCredentialsFireproofCheckboxDescription = NSLocalizedString("passsword.management.save.credentials.fireproof.checkbox.description", value: "Keeps you signed in after using the Fire Button", comment: "In the password manager dialog, description of the section that allows the user to fireproof a website via a checkbox") static func passwordManagementSaveCredentialsAccountLabel(activeVault: String) -> String { let localized = NSLocalizedString("passsword.management.save.credentials.account.label", value: "Connected to %@", comment: "In the password manager dialog, label that specifies the password manager vault we are connected with") return String(format: localized, activeVault) } + static let passwordManagementTitle = NSLocalizedString("password.management.title", value: "Passwords & Autofill", comment: "Used as the title for menu item and related Settings page") static let settingsSuspended = NSLocalizedString("Settings…", comment: "Menu item") static let passwordManagerUnlockAutofill = NSLocalizedString("passsword.manager.unlock.autofill", value: "Unlock your Autofill info", comment: "In the password manager text of button to unlock autofill info") static let passwordManagerEmptyStateTitle = NSLocalizedString("passsword.manager.empty.state.title", value: "No logins or credit card info yet", comment: "In the password manager title when there are no items") static let passwordManagerEmptyStateMessage = NSLocalizedString("passsword.manager.empty.state.message", value: "If your logins are saved in another browser, you can import them into DuckDuckGo.", comment: "In the password manager message when there are no items") - static let importData = NSLocalizedString("Import", comment: "Menu item") static let passwordManagerAlertRemovePasswordConfirmation = NSLocalizedString("passsword.manager.alert.remove-password.confirmation", value: "Are you sure you want to delete this saved password", comment: "Text of the alert that asks the user to confirm they want to delete a password") static let passwordManagerAlertSaveChanges = NSLocalizedString("passsword.manager.alert.save-changes", value: "Save the changes you made?", comment: "Text of the alert that asks the user if the want to save the changes made") static let passwordManagerAlertDuplicatePassword = NSLocalizedString("passsword.manager.alert.duplicate.password", value: "Duplicate Password", comment: "Title of the alert that the password inserted already exists") @@ -457,7 +458,11 @@ struct UserText { static let importBookmarks = NSLocalizedString("import.browser.data.bookmarks", value: "Import Bookmarks…", comment: "Opens Import Browser Data dialog") static let importPasswords = NSLocalizedString("import.browser.data.passwords", value: "Import Passwords…", comment: "Opens Import Browser Data dialog") - static let importDataTitle = NSLocalizedString("import.browser.data", value: "Import Browser Data", comment: "Import Browser Data dialog title") + static let importDataTitle = NSLocalizedString("import.browser.data", value: "Import to DuckDuckGo", comment: "Import Browser Data dialog title") + static let importDataShortcutsTitle = NSLocalizedString("import.browser.data.shortcuts", value: "Almost done!", comment: "Import Browser Data dialog title for final stage when choosing shortcuts to enable") + static let importDataShortcutsSubtitle = NSLocalizedString("import.browser.data.shortcuts.subtitle", value: "You can always right-click on the browser toolbar to find more shortcuts like these.", comment: "Subtitle explaining how users can find toolbar shortcuts.") + static let importDataSourceTitle = NSLocalizedString("import.browser.data.source.title", value: "Import From", comment: "Import Browser Data title for option to choose source browser to import from") + static let importDataSubtitle = NSLocalizedString("import.browser.data.source.subtitle", value: "Access and manage your passwords in DuckDuckGo Settings > Passwords & Autofill.", comment: "Subtitle explaining where users can find imported passwords.") static let exportLogins = NSLocalizedString("export.logins.data", value: "Export Passwords…", comment: "Opens Export Logins Data dialog") static let exportBookmarks = NSLocalizedString("export.bookmarks.menu.item", value: "Export Bookmarks…", comment: "Export bookmarks menu item") @@ -754,6 +759,11 @@ struct UserText { static let bookmarkImportBookmarks = NSLocalizedString("import.bookmarks.bookmarks", value: "Bookmarks", comment: "Title text for the Bookmarks import option") + static let importShortcutsBookmarksTitle = NSLocalizedString("import.shortcuts.bookmarks.title", value: "Show Bookmarks Bar", comment: "Title for the setting to enable the bookmarks bar") + static let importShortcutsBookmarksSubtitle = NSLocalizedString("import.shortcuts.bookmarks.subtitle", value: "Put your favorite bookmarks in easy reach", comment: "Description for the setting to enable the bookmarks bar") + static let importShortcutsPasswordsTitle = NSLocalizedString("import.shortcuts.passwords.title", value: "Show Passwords Shortcut", comment: "Title for the setting to enable the passwords shortcut") + static let importShortcutsPasswordsSubtitle = NSLocalizedString("import.shortcuts.passwords.subtitle", value: "Keep passwords nearby in the address bar", comment: "Description for the setting to enable the passwords shortcut") + static let openDeveloperTools = NSLocalizedString("main.menu.show.inspector", value: "Open Developer Tools", comment: "Show Web Inspector/Open Developer Tools") static let closeDeveloperTools = NSLocalizedString("main.menu.close.inspector", value: "Close Developer Tools", comment: "Hide Web Inspector/Close Developer Tools") @@ -951,8 +961,8 @@ struct UserText { static let bitwardenCommunicationInfo = NSLocalizedString("bitwarden.connect.communication-info", value: "All communication between Bitwarden and DuckDuckGo is encrypted and the data never leaves your device.", comment: "Warns users that all communication between the DuckDuckGo browser and the password manager Bitwarden is encrypted and doesn't leave the user device") static let bitwardenHistoryInfo = NSLocalizedString("bitwarden.connect.history-info", value: "Bitwarden will have access to your browsing history.", comment: "Warn users that the password Manager Bitwarden will have access to their browsing history") - static let showAutofillShortcut = NSLocalizedString("pinning.show-autofill-shortcut", value: "Show Autofill Shortcut", comment: "Menu item for showing the autofill shortcut") - static let hideAutofillShortcut = NSLocalizedString("pinning.hide-autofill-shortcut", value: "Hide Autofill Shortcut", comment: "Menu item for hiding the autofill shortcut") + static let showAutofillShortcut = NSLocalizedString("pinning.show-autofill-shortcut", value: "Show Passwords Shortcut", comment: "Menu item for showing the passwords shortcut") + static let hideAutofillShortcut = NSLocalizedString("pinning.hide-autofill-shortcut", value: "Hide Passwords Shortcut", comment: "Menu item for hiding the passwords shortcut") static let showBookmarksShortcut = NSLocalizedString("pinning.show-bookmarks-shortcut", value: "Show Bookmarks Shortcut", comment: "Menu item for showing the bookmarks shortcut") static let hideBookmarksShortcut = NSLocalizedString("pinning.hide-bookmarks-shortcut", value: "Hide Bookmarks Shortcut", comment: "Menu item for hiding the bookmarks shortcut") @@ -1035,6 +1045,15 @@ struct UserText { value: "View", comment: "Button to view the recently autosaved password") + static let passwordManagerAutoPinnedPopoverText = NSLocalizedString("autofill.popover.passwords.auto-pinned.text", value: "Shortcut Added!", comment: "Text confirming the password manager has been pinned to the toolbar") + + static let passwordManagerPinnedPromptPopoverText = NSLocalizedString("autofill.popover.passwords.pin-prompt.text", + value: "Add passwords shortcut?", + comment: "Text prompting user to pin the password manager shortcut to the toolbar") + static let passwordManagerPinnedPromptPopoverButtonText = NSLocalizedString("autofill.popover.passwords.pin-prompt.button.text", + value: "Add Shortcut", + comment: "Button to pin the password manager shortcut to the toolbar") + static func openPasswordManagerButton(managerName: String) -> String { let localized = NSLocalizedString("autofill.popover.open-password-manager", value: "Open %@", comment: "Open password manager button") return String(format: localized, managerName) diff --git a/DuckDuckGo/DataImport/Model/DataImportShortcutsViewModel.swift b/DuckDuckGo/DataImport/Model/DataImportShortcutsViewModel.swift new file mode 100644 index 0000000000..763c82a1b5 --- /dev/null +++ b/DuckDuckGo/DataImport/Model/DataImportShortcutsViewModel.swift @@ -0,0 +1,54 @@ +// +// DataImportShortcutsViewModel.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 SwiftUI + +final class DataImportShortcutsViewModel: ObservableObject { + typealias DataType = DataImport.DataType + + let dataTypes: Set? + private let prefs: AppearancePreferences + private let pinningManager: LocalPinningManager + + @Published var showBookmarksBarStatus: Bool { + didSet { + prefs.showBookmarksBar = showBookmarksBarStatus + } + } + + @Published var showPasswordsPinnedStatus: Bool { + didSet { + if showPasswordsPinnedStatus { + pinningManager.pin(.autofill) + NotificationCenter.default.post(name: .passwordsAutoPinned, object: nil) + } else { + pinningManager.unpin(.autofill) + } + } + } + + init(dataTypes: Set? = nil, prefs: AppearancePreferences = AppearancePreferences.shared, pinningManager: LocalPinningManager = LocalPinningManager.shared) { + self.dataTypes = dataTypes + self.prefs = prefs + self.pinningManager = pinningManager + + showBookmarksBarStatus = prefs.showBookmarksBar + showPasswordsPinnedStatus = pinningManager.isPinned(.autofill) + } +} diff --git a/DuckDuckGo/DataImport/Model/DataImportSummaryViewModel.swift b/DuckDuckGo/DataImport/Model/DataImportSummaryViewModel.swift index cb8612860a..681db6827f 100644 --- a/DuckDuckGo/DataImport/Model/DataImportSummaryViewModel.swift +++ b/DuckDuckGo/DataImport/Model/DataImportSummaryViewModel.swift @@ -55,5 +55,10 @@ struct DataImportSummaryViewModel { dataTypes.contains(dataType) ? results.last(where: { $0.dataType == dataType }) : nil } } +} +extension DataImportSummaryViewModel { + func resultsFiltered(by dataType: DataType) -> [DataTypeImportResult] { + results.filter { $0.dataType == dataType } + } } diff --git a/DuckDuckGo/DataImport/Model/DataImportViewModel.swift b/DuckDuckGo/DataImport/Model/DataImportViewModel.swift index 7e6211da9d..aa5aff6ac6 100644 --- a/DuckDuckGo/DataImport/Model/DataImportViewModel.swift +++ b/DuckDuckGo/DataImport/Model/DataImportViewModel.swift @@ -70,6 +70,7 @@ struct DataImportViewModel { case fileImport(dataType: DataType, summary: Set = []) case summary(Set, isFileImport: Bool = false) case feedback + case shortcuts(Set) var isFileImport: Bool { if case .fileImport = self { true } else { false } @@ -338,8 +339,23 @@ struct DataImportViewModel { // errors occurred during import: show feedback screen self.screen = .feedback } else { - // When we skip a manual import, and there are no next non-imported data types, we dismiss - self.dismiss(using: dismiss) + // When we skip a manual import, and there are no next non-imported data types, + // if some data was successfully imported we present the shortcuts screen, otherwise we dismiss + var dataTypes: Set = [] + + // Filter out only the successful results with a positive count of successful summaries + for dataTypeImportResult in summary { + guard case .success(let summary) = dataTypeImportResult.result, summary.successful > 0 else { + continue + } + dataTypes.insert(dataTypeImportResult.dataType) + } + + if !dataTypes.isEmpty { + self.screen = .shortcuts(dataTypes) + } else { + self.dismiss(using: dismiss) + } } } @@ -630,11 +646,13 @@ extension DataImportViewModel { if let screen = screenForNextDataTypeRemainingToImport(after: DataType.allCases.last(where: dataTypes.contains)) { return .next(screen) } else { - return .done + return .next(.shortcuts(dataTypes)) } case .feedback: return .submit + case .shortcuts: + return .done } } diff --git a/DuckDuckGo/DataImport/View/BrowserImportMoreInfoView.swift b/DuckDuckGo/DataImport/View/BrowserImportMoreInfoView.swift index 24806677bb..670a029131 100644 --- a/DuckDuckGo/DataImport/View/BrowserImportMoreInfoView.swift +++ b/DuckDuckGo/DataImport/View/BrowserImportMoreInfoView.swift @@ -30,7 +30,7 @@ struct BrowserImportMoreInfoView: View { switch source { case .chrome, .chromium, .coccoc, .edge, .brave, .opera, .operaGX, .vivaldi: Text(""" - If your computer prompts you to enter a password prior to import, DuckDuckGo will not see that password. + After clicking import, your computer may ask you to enter a password. You may need to enter your password two times before importing starts. DuckDuckGo will not see that password. Imported passwords are stored securely using encryption. """, comment: "Warning that Chromium data import would require entering system passwords.") diff --git a/DuckDuckGo/DataImport/View/DataImportShortcutsView.swift b/DuckDuckGo/DataImport/View/DataImportShortcutsView.swift new file mode 100644 index 0000000000..a0ed0b448d --- /dev/null +++ b/DuckDuckGo/DataImport/View/DataImportShortcutsView.swift @@ -0,0 +1,92 @@ +// +// DataImportShortcutsView.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 SwiftUI + +struct DataImportShortcutsView: ModalView { + + typealias DataType = DataImport.DataType + + @ObservedObject private var model: DataImportShortcutsViewModel + + init(model: DataImportShortcutsViewModel = DataImportShortcutsViewModel(), dataTypes: Set? = nil) { + self.init(model: .init(dataTypes: dataTypes)) + } + + init(model: DataImportShortcutsViewModel) { + self.model = model + } + + var body: some View { + + VStack(alignment: .leading, spacing: 8) { + VStack(spacing: 0) { + if let dataTypes = model.dataTypes, dataTypes.contains(.bookmarks) { + importShortcutsRow(image: Image(.bookmarksFavoritesColor24), + title: UserText.importShortcutsBookmarksTitle, + subtitle: UserText.importShortcutsBookmarksSubtitle, + isOn: $model.showBookmarksBarStatus) + } + + if let dataTypes = model.dataTypes, dataTypes.count > 1 { + Divider() + .padding(.leading) + } + + importShortcutsRow(image: Image(.keyColor24), + title: UserText.importShortcutsPasswordsTitle, + subtitle: UserText.importShortcutsPasswordsSubtitle, + isOn: $model.showPasswordsPinnedStatus) + } + .roundedBorder() + } + + importShortcutsSubtitle() + } +} + +private func importShortcutsRow(image: Image, title: String, subtitle: String, isOn: Binding) -> some View { + HStack { + image + VStack(alignment: .leading) { + Text(title) + Text(subtitle) + .font(.subheadline) + .foregroundColor(.greyText) + } + .padding(.top, 0) + .padding(.bottom, 1) + Spacer() + Toggle("", isOn: isOn) + .toggleStyle(.switch) + } + .padding(.horizontal) + .padding(.vertical, 10) +} + +private func importShortcutsSubtitle() -> some View { + Text(UserText.importDataShortcutsSubtitle) + .font(.subheadline) + .foregroundColor(Color(.greyText)) + .padding(.top, 8) + .padding(.leading, 8) +} + +#Preview { + DataImportShortcutsView() +} diff --git a/DuckDuckGo/DataImport/View/DataImportSummaryView.swift b/DuckDuckGo/DataImport/View/DataImportSummaryView.swift index b385a90c66..49affa7de5 100644 --- a/DuckDuckGo/DataImport/View/DataImportSummaryView.swift +++ b/DuckDuckGo/DataImport/View/DataImportSummaryView.swift @@ -22,6 +22,7 @@ struct DataImportSummaryView: View { typealias DataType = DataImport.DataType typealias Summary = DataImport.DataTypeSummary + typealias DataTypeImportResult = DataImportViewModel.DataTypeImportResult let model: DataImportSummaryViewModel @@ -33,7 +34,7 @@ struct DataImportSummaryView: View { self.model = model } - private let zeroString = "0" + private let zero = 0 var body: some View { VStack(alignment: .leading, spacing: 8) { @@ -43,7 +44,7 @@ struct DataImportSummaryView: View { Text("Import Results:", comment: "Data Import result summary headline") case .importComplete(.bookmarks), - .fileImportComplete(.bookmarks): + .fileImportComplete(.bookmarks): Text("Bookmarks Import Complete:", comment: "Bookmarks Data Import result summary headline") case .fileImportComplete(.passwords): @@ -51,105 +52,146 @@ struct DataImportSummaryView: View { } }().padding(.bottom, 4) - ForEach(model.results, id: \.dataType) { item in - switch (item.dataType, item.result) { - case (.bookmarks, .success(let summary)): - HStack { - successImage() - Text("Bookmarks:", - comment: "Data import summary format of how many bookmarks (%lld) were successfully imported.") - + Text(" " as String) - + Text(String(summary.successful)).bold() + VStack { + ForEach(model.resultsFiltered(by: .bookmarks), id: \.dataType) { item in + switch item.result { + case (.success(let summary)): + bookmarksSuccessSummary(summary) + case (.failure(let error)) where error.errorType == .noData: + importSummaryRow(image: .failed, + text: "Bookmarks:", + comment: "Data import summary format of how many bookmarks were successfully imported.", + count: zero) + case (.failure): + importSummaryRow(image: .failed, + text: "Bookmark import failed.", + comment: "Data import summary message of failed bookmarks import.", + count: nil) } - if summary.duplicate > 0 { - HStack { - skippedImage() - Text("Duplicate Bookmarks Skipped:", - comment: "Data import summary format of how many duplicate bookmarks (%lld) were skipped during import.") - + Text(" " as String) - + Text(String(summary.duplicate)).bold() - } - } - if summary.failed > 0 { - HStack { - failureImage() - Text("Bookmark import failed:", - comment: "Data import summary format of how many bookmarks (%lld) failed to import.") - + Text(" " as String) - + Text(String(summary.failed)).bold() + } + } + .applyConditionalModifiers(!model.resultsFiltered(by: .bookmarks).isEmpty) + + VStack { + ForEach(model.resultsFiltered(by: .passwords), id: \.dataType) { item in + switch item.result { + case (.failure(let error)): + if error.errorType == .noData { + importSummaryRow(image: .failed, + text: "Passwords:", + comment: "Data import summary format of how many passwords were successfully imported.", + count: zero) + } else { + importSummaryRow(image: .failed, + text: "Password import failed.", + comment: "Data import summary message of failed passwords import.", + count: nil) } - } - case (.bookmarks, .failure(let error)) where error.errorType == .noData: - HStack { - skippedImage() - Text("Bookmarks:", - comment: "Data import summary format of how many bookmarks were successfully imported.") - + Text(" " as String) - + Text(zeroString).bold() + case (.success(let summary)): + passwordsSuccessSummary(summary) } + } + } + .applyConditionalModifiers(!model.resultsFiltered(by: .passwords).isEmpty) - case (.bookmarks, .failure): - HStack { - failureImage() - Text("Bookmark import failed.", - comment: "Data import summary message of failed bookmarks import.") - } + if !model.resultsFiltered(by: .passwords).isEmpty { + importPasswordSubtitle() + } + } + } +} - case (.passwords, .failure(let error)): - if error.errorType == .noData { - HStack { - skippedImage() - Text("Passwords:", - comment: "Data import summary format of how many passwords were successfully imported.") - + Text(" " as String) - + Text(zeroString).bold() - } - } else { - HStack { - failureImage() - Text("Password import failed.", - comment: "Data import summary message of failed passwords import.") - } - } +func bookmarksSuccessSummary(_ summary: DataImport.DataTypeSummary) -> some View { + VStack { + importSummaryRow(image: .success, + text: "Bookmarks:", + comment: "Data import summary format of how many bookmarks (%lld) were successfully imported.", + count: summary.successful) + if summary.duplicate > 0 { + lineSeparator() + importSummaryRow(image: .failed, + text: "Duplicate Bookmarks Skipped:", + comment: "Data import summary format of how many duplicate bookmarks (%lld) were skipped during import.", + count: summary.duplicate) + } + if summary.failed > 0 { + lineSeparator() + importSummaryRow(image: .failed, + text: "Bookmark import failed:", + comment: "Data import summary format of how many bookmarks (%lld) failed to import.", + count: summary.failed) + } + } +} - case (.passwords, .success(let summary)): - HStack { - successImage() - Text("Passwords:", - comment: "Data import summary format of how many passwords (%lld) were successfully imported.") - + Text(" " as String) - + Text(String(summary.successful)).bold() - } - if summary.failed > 0 { - HStack { - failureImage() - Text("Password import failed: ", - comment: "Data import summary format of how many passwords (%lld) failed to import.") - + Text(" " as String) - + Text(String(summary.failed)).bold() - } - } - } - } +private func passwordsSuccessSummary(_ summary: DataImport.DataTypeSummary) -> some View { + VStack { + importSummaryRow(image: .success, + text: "Passwords:", + comment: "Data import summary format of how many passwords (%lld) were successfully imported.", + count: summary.successful) + if summary.failed > 0 { + lineSeparator() + importSummaryRow(image: .failed, + text: "Password import failed: ", + comment: "Data import summary format of how many passwords (%lld) failed to import.", + count: summary.failed) + } + } +} + +private func importPasswordSubtitle() -> some View { + Text(UserText.importDataSubtitle) + .font(.subheadline) + .foregroundColor(Color(.greyText)) + .padding(.top, -2) + .padding(.leading, 8) +} + +private func importSummaryRow(image: Image, text: LocalizedStringKey, comment: StaticString, count: Int?) -> some View { + HStack(spacing: 0) { + image + .frame(width: 16, height: 16) + .padding(.trailing, 14) + Text(text, comment: comment) + Text(verbatim: " ") + if let count = count { + Text(String(count)).bold() } + Spacer() } +} +private func lineSeparator() -> some View { + Divider() + .padding(EdgeInsets(top: 5, leading: 0, bottom: 8, trailing: 0)) } -private func successImage() -> some View { - Image(.successCheckmark) - .frame(width: 16, height: 16) +private extension Image { + static let success = Image(.successCheckmark) + static let failed = Image(.clearRecolorable16) } -private func failureImage() -> some View { - Image(.error) - .frame(width: 16, height: 16) +private struct ConditionalModifier: ViewModifier { + let applyModifiers: Bool + + func body(content: Content) -> some View { + if applyModifiers { + content + .padding([.leading, .vertical]) + .padding(.trailing, 0) + .roundedBorder() + } else { + content + } + } } -private func skippedImage() -> some View { - Image(.skipped) - .frame(width: 16, height: 16) +private extension View { + func applyConditionalModifiers(_ condition: Bool) -> some View { + modifier(ConditionalModifier(applyModifiers: condition)) + } } #if DEBUG @@ -157,17 +199,19 @@ private func skippedImage() -> some View { VStack { HStack { DataImportSummaryView(model: .init(source: .chrome, results: [ -// .init(.bookmarks, .success(.init(successful: 123, duplicate: 456, failed: 7890))), -// .init(.passwords, .success(.init(successful: 123, duplicate: 456, failed: 7890))), -// .init(.bookmarks, .failure(DataImportViewModel.TestImportError(action: .bookmarks, errorType: .dataCorrupted))), -// .init(.bookmarks, .failure(DataImportViewModel.TestImportError(action: .passwords, errorType: .keychainError))), - .init(.passwords, .failure(DataImportViewModel.TestImportError(action: .passwords, errorType: .keychainError))), - .init(.passwords, .failure(DataImportViewModel.TestImportError(action: .passwords, errorType: .keychainError))), + .init(.bookmarks, .success(.init(successful: 123, duplicate: 456, failed: 7890))), + // .init(.passwords, .success(.init(successful: 123, duplicate: 456, failed: 7890))), + // .init(.bookmarks, .failure(DataImportViewModel.TestImportError(action: .bookmarks, errorType: .dataCorrupted))), + // .init(.bookmarks, .failure(DataImportViewModel.TestImportError(action: .passwords, errorType: .keychainError))), + // .init(.passwords, .failure(DataImportViewModel.TestImportError(action: .passwords, errorType: .keychainError))), + // .init(.passwords, .failure(DataImportViewModel.TestImportError(action: .passwords, errorType: .keychainError))), + // .init(.passwords, .success(.init(successful: 100, duplicate: 0, failed: 0))) + .init(.passwords, .success(.init(successful: 100, duplicate: 30, failed: 40))) ])) .padding(EdgeInsets(top: 20, leading: 20, bottom: 16, trailing: 20)) Spacer() } } - .frame(width: 512) + .frame(width: 512, height: 400) } #endif diff --git a/DuckDuckGo/DataImport/View/DataImportTypePicker.swift b/DuckDuckGo/DataImport/View/DataImportTypePicker.swift index db6c2c9dd9..7dbb85d3c5 100644 --- a/DuckDuckGo/DataImport/View/DataImportTypePicker.swift +++ b/DuckDuckGo/DataImport/View/DataImportTypePicker.swift @@ -28,9 +28,8 @@ struct DataImportTypePicker: View { var body: some View { VStack(alignment: .leading) { - Text("Select data to import:", + Text("Select Data to Import:", comment: "Data Import section title for checkboxes of data type to import: Passwords or Bookmarks.") - .bold() ForEach(DataImport.DataType.allCases, id: \.self) { dataType in // display all types for a browser disabling unavailable options diff --git a/DuckDuckGo/DataImport/View/DataImportView.swift b/DuckDuckGo/DataImport/View/DataImportView.swift index 2302b54421..0d665affca 100644 --- a/DuckDuckGo/DataImport/View/DataImportView.swift +++ b/DuckDuckGo/DataImport/View/DataImportView.swift @@ -47,7 +47,7 @@ struct DataImportView: ModalView { viewBody() .padding(.leading, 20) .padding(.trailing, 20) - .padding(.bottom, 32) + .padding(.bottom, 20) // if import in progress… if let importProgress = model.importProgress { @@ -77,21 +77,32 @@ struct DataImportView: ModalView { private func viewHeader() -> some View { VStack(alignment: .leading, spacing: 0) { - Text(UserText.importDataTitle) - .bold() - .padding(.bottom, 16) + if case .shortcuts = model.screen { + Text(UserText.importDataShortcutsTitle) + .font(.title2.weight(.semibold)) + .padding(.bottom, 24) - // browser to import data from picker popup - if case .feedback = model.screen {} else { - DataImportSourcePicker(importSources: model.availableImportSources, selectedSource: model.importSource) { importSource in - model.update(with: importSource) + } else { + Text(UserText.importDataTitle) + .font(.title2.weight(.semibold)) + .padding(.bottom, 24) + + Text(UserText.importDataSourceTitle) + .padding(.bottom, 16) + + // browser to import data from picker popup + if case .feedback = model.screen {} else { + DataImportSourcePicker(importSources: model.availableImportSources, selectedSource: model.importSource) { importSource in + model.update(with: importSource) + } + .disabled(model.isImportSourcePickerDisabled) + .padding(.bottom, 16) } - .disabled(model.isImportSourcePickerDisabled) - .padding(.bottom, 24) } } } + // swiftlint:disable:next cyclomatic_complexity private func viewBody() -> some View { VStack(alignment: .leading, spacing: 0) { // body @@ -109,6 +120,8 @@ struct DataImportView: ModalView { DataImportTypePicker(viewModel: $model) .disabled(model.isImportSourcePickerDisabled) + importPasswordSubtitle() + case .moreInfo: // you will be asked for your keychain password blah blah... BrowserImportMoreInfoView(source: model.importSource) @@ -146,6 +159,10 @@ struct DataImportView: ModalView { model.initiateImport(fileURL: url) } + if dataType == .passwords { + importPasswordSubtitle() + } + case .summary(let dataTypes, let isFileImport): DataImportSummaryView(model, dataTypes: dataTypes, isFileImport: isFileImport) @@ -154,6 +171,9 @@ struct DataImportView: ModalView { .padding(.bottom, 20) ReportFeedbackView(model: $model.reportModel) + + case .shortcuts(let dataTypes): + DataImportShortcutsView(dataTypes: dataTypes) } } } @@ -189,6 +209,13 @@ struct DataImportView: ModalView { } } + private func importPasswordSubtitle() -> some View { + Text(UserText.importDataSubtitle) + .font(.subheadline) + .foregroundColor(Color(.greyText)) + .padding(.top, 16) + } + private func handleImportProgress(_ progress: TaskProgress) async { // receive import progress update events // the loop is completed on the import task diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index 4503c03e71..621f96ec33 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -1239,6 +1239,59 @@ } } }, + "After clicking import, your computer may ask you to enter a password. You may need to enter your password two times before importing starts. DuckDuckGo will not see that password.\n\nImported passwords are stored securely using encryption." : { + "comment" : "Warning that Chromium data import would require entering system passwords.", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nachdem du auf Importieren geklickt hast, fordert dich dein Computer möglicherweise auf, ein Passwort einzugeben. Möglicherweise musst du dein Passwort zweimal eingeben, bevor der Import beginnt. DuckDuckGo wird dieses Passwort nicht sehen.\n\nImportierte Passwörter werden durch Verschlüsselung sicher gespeichert." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Es posible que tu ordenador solicite que introduzcas una contraseña después de hacer clic en importar. Puede que tengas que introducir tu contraseña dos veces antes de empezar a importar. DuckDuckGo no verá esa contraseña.\n\nLas contraseñas importadas se almacenan de forma segura mediante encriptación." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Après avoir cliqué sur Importer, votre ordinateur pourra vous demander de saisir un mot de passe. Vous devrez peut-être saisir votre mot de passe deux fois avant le lancement de l'importation. DuckDuckGo ne verra pas ce mot de passe.\n\nLes mots de passe importés sont stockés en toute sécurité grâce à un cryptage." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dopo aver fatto clic su \"Importa\", il tuo computer potrebbe chiederti di inserire una password e potrebbe essere necessario inserirla due volte prima che l'importazione abbia inizio. DuckDuckGo non visualizzerà mai questa password.\n\nLe password importate vengono archiviate in modo sicuro utilizzando la crittografia." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nadat je op importeren hebt geklikt, kan je computer je vragen om een wachtwoord in te voeren. Mogelijk moet je je wachtwoord twee keer invoeren voordat het importeren begint. DuckDuckGo zal dat wachtwoord niet zien.\n\nGeïmporteerde wachtwoorden worden veilig opgeslagen door middel van versleuteling." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Po kliknięciu opcji importu komputer może Cię poprosić o wprowadzenie hasła. Przed rozpoczęciem importowania może być konieczne dwukrotne wprowadzenie hasła. DuckDuckGo nie zobaczy tego hasła.\n\nZaimportowane hasła są bezpiecznie przechowywane przy użyciu szyfrowania." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Depois de clicares em Importar, o teu computador pode pedir-te para introduzires uma palavra-passe. Poderás ter de a introduzir duas vezes antes a importação começar. O DuckDuckGo não verá essa palavra-passe.\n\nAs palavras-passe importadas são armazenadas com segurança por meio de encriptação." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "После нажатия кнопки «Импорт» компьютер может попросить вас ввести пароль. Прежде чем начнется импортирование, пароль, возможно, придется ввести дважды. DuckDuckGo его не увидит.\n\nИмпортированные пароли надежно хранятся в зашифрованном виде." + } + } + } + }, "after.bitwarden.installation.info" : { "comment" : "Setup of the integration with Bitwarden app", "extractionState" : "extracted_with_value", @@ -1324,7 +1377,7 @@ "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Certains signets sont mal formatés ou trop longs et n'ont pas été synchronisés.\n" + "value" : "Certains signets sont mal formatés ou trop longs et n'ont pas été synchronisés." } }, "it" : { @@ -3399,55 +3452,55 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Speichern und autovervollständigen" + "value" : "Zum Speichern und automatischen Ausfüllen auffordern" } }, "en" : { "stringUnit" : { "state" : "new", - "value" : "Save and Autofill" + "value" : "Ask to Save and Autofill" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Guardar y autocompletar" + "value" : "Solicitar guardar y autocompletar" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Enregistrement et saisie automatique" + "value" : "Demander l'enregistrement et la saisie automatique" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Salva e compila automaticamente" + "value" : "Chiedi di salvare e compilare automaticamente" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Opslaan en automatisch invullen" + "value" : "Vragen om op te slaan en automatisch in te vullen" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Zapisywanie i autouzupełnianie" + "value" : "Poproś o zapisywanie i autouzupełnianie" } }, "pt" : { "stringUnit" : { "state" : "translated", - "value" : "Guardar e preencher automaticamente" + "value" : "Pedir para guardar e preencher automaticamente" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Сохранение и автозаполнение" + "value" : "Предлагать сохранение и автозаполнение данных" } } } @@ -5252,6 +5305,66 @@ } } }, + "autofill.passwords" : { + "comment" : "Autofill autosaved data type", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Passwörter" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Passwords" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Contraseñas" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mots de passe" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Password" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wachtwoorden" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hasła" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Palavras-passe" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пароли" + } + } + } + }, "autofill.payment-methods" : { "comment" : "Autofill autosaved data type", "extractionState" : "extracted_with_value", @@ -5672,6 +5785,186 @@ } } }, + "autofill.popover.passwords.auto-pinned.text" : { + "comment" : "Text confirming the password manager has been pinned to the toolbar", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Shortcut hinzugefügt!" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Shortcut Added!" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¡Acceso directo añadido!" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Raccourci ajouté !" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Scorciatoia aggiunta." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Snelkoppeling toegevoegd!" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dodano skrót!" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Atalho adicionado!" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ярлык добавлен!" + } + } + } + }, + "autofill.popover.passwords.pin-prompt.button.text" : { + "comment" : "Button to pin the password manager shortcut to the toolbar", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Shortcut hinzufügen" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Add Shortcut" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Añadir acceso directo" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ajouter un raccourci" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aggiungi scorciatoia" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sneltoets toevoegen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dodaj skrót" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Adicionar atalho" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Добавить ярлык" + } + } + } + }, + "autofill.popover.passwords.pin-prompt.text" : { + "comment" : "Text prompting user to pin the password manager shortcut to the toolbar", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Passwort-Shortcut hinzufügen?" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Add passwords shortcut?" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Añadir acceso directo a contraseñas?" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ajouter un raccourci pour les mots de passe ?" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aggiungere una scorciatoia per le password?" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wachtwoordsnelkoppeling toevoegen?" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dodać skrót do haseł?" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Adicionar atalho de palavras-passe?" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Добавить ярлык для менеджера паролей?" + } + } + } + }, "autofill.popover.settings-button" : { "comment" : "Open Settings Button", "extractionState" : "extracted_with_value", @@ -5854,7 +6147,7 @@ }, "autofill.usernames-and-passwords" : { "comment" : "Autofill autosaved data type", - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -5914,7 +6207,7 @@ }, "autofill.view-autofill-content" : { "comment" : "View Autofill Content Button name in the autofill settings", - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -5972,6 +6265,186 @@ } } }, + "autofill.view-autofill-content.identities" : { + "comment" : "View Identities Content Button title in the autofill Settings", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Identitäten öffnen …" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Open Identities…" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abrir identidades…" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ouvrir les identités…" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apri identità..." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Identiteiten openen…" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Otwórz tożsamości…" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abrir identidades…" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Открыть учетные данные…" + } + } + } + }, + "autofill.view-autofill-content.passwords" : { + "comment" : "View Password Content Button title in the autofill Settings", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Passwörter öffnen …" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Open Passwords…" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abrir contraseñas…" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ouvrir les mots de passe…" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apri password…" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wachtwoorden openen..." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Otwórz hasła…" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abrir palavras-passe…" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Открыть пароли..." + } + } + } + }, + "autofill.view-autofill-content.payment-methods" : { + "comment" : "View Payment Methods Content Button title in the autofill Settings", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zahlungsmethoden öffnen …" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Open Payment Methods…" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abrir métodos de pago…" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ouvrir les modes de paiement…" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apri metodi di pagamento..." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Betaalmethoden openen..." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Otwórz metody płatności…" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abrir métodos de pagamento…" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Открыть способы оплаты…" + } + } + } + }, "automatically.clear.data" : { "comment" : "Label after the checkbox in Settings which configures clearing data automatically after quitting the app.", "extractionState" : "extracted_with_value", @@ -22309,6 +22782,7 @@ }, "If your computer prompts you to enter a password prior to import, DuckDuckGo will not see that password.\n\nImported passwords are stored securely using encryption." : { "comment" : "Warning that Chromium data import would require entering system passwords.", + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -22362,6 +22836,7 @@ }, "Import" : { "comment" : "Menu item", + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -22939,55 +23414,55 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Browserdaten importieren" + "value" : "In DuckDuckGo importieren" } }, "en" : { "stringUnit" : { "state" : "new", - "value" : "Import Browser Data" + "value" : "Import to DuckDuckGo" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Importar datos del navegador" + "value" : "Importar a DuckDuckGo" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Importer les données du navigateur" + "value" : "Importer dans DuckDuckGo" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Importa dati browser" + "value" : "Importa in DuckDuckGo" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Browsergegevens importeren" + "value" : "Importeren naar DuckDuckGo" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Importuj dane przeglądarki" + "value" : "Importuj do DuckDuckGo" } }, "pt" : { "stringUnit" : { "state" : "translated", - "value" : "Importar dados do navegador" + "value" : "Importar para o DuckDuckGo" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Импорт данных браузера" + "value" : "Импорт в DuckDuckGo" } } } @@ -23112,6 +23587,246 @@ } } }, + "import.browser.data.shortcuts" : { + "comment" : "Import Browser Data dialog title for final stage when choosing shortcuts to enable", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fast fertig!" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Almost done!" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¡Casi listo!" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "C'est presque terminé !" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Quasi finito!" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bijna klaar!" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Prawie gotowe!" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Quase pronto!" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Почти готово!" + } + } + } + }, + "import.browser.data.shortcuts.subtitle" : { + "comment" : "Subtitle explaining how users can find toolbar shortcuts.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Du kannst jederzeit mit der rechten Maustaste auf die Symbolleiste des Browsers klicken, um weitere Shortcuts wie diese zu finden." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "You can always right-click on the browser toolbar to find more shortcuts like these." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Siempre puedes hacer clic con el botón derecho en la barra de herramientas del navegador para encontrar más accesos directos como estos." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vous pouvez toujours faire un clic droit sur la barre d'outils du navigateur pour accéder à d'autres raccourcis de ce type." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Per trovare altre scorciatoie come queste, puoi sempre fare clic con il tasto destro del mouse sulla barra degli strumenti del browser." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Je kunt altijd met de rechtermuisknop op de werkbalk van de browser klikken om meer van deze snelkoppelingen te vinden." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zawsze możesz kliknąć prawym przyciskiem myszy pasek narzędzi przeglądarki, aby znaleźć więcej takich skrótów." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Podes sempre clicar com o botão direito do rato na barra de ferramentas do navegador para encontrares mais atalhos como estes." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Другие ярлыки можно всегда найти, щелкнув по панели инструментов браузера правой кнопкой мыши." + } + } + } + }, + "import.browser.data.source.subtitle" : { + "comment" : "Subtitle explaining where users can find imported passwords.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Du kannst deine Passwörter unter DuckDuckGo-Einstellungen > Passwörter & Autovervollständigen verwalten." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Access and manage your passwords in DuckDuckGo Settings > Passwords & Autofill." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Consulta y gestiona tus contraseñas en Ajustes de DuckDuckGo > Contraseñas y autocompletar." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Accédez à vos mots de passe et gérez-les dans Paramètres de DuckDuckGo > Mots de passe et saisie automatique." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Accedi e gestisci le tue password in Impostazioni di DuckDuckGo > Password e compilazione automatica." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Open en beheer je wachtwoorden in DuckDuckGo Instellingen > Wachtwoorden en automatisch aanvullen." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uzyskaj dostęp do swoich haseł i zarządzaj nimi w obszarze Ustawienia DuckDuckGo > Hasła i autouzupełnianie." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Acede e faz a gestão das tuas palavras-passe em Definições do DuckDuckGo > Palavras-passe e preenchimento automático." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Просмотреть и проконтролировать пароли в DuckDuckGo можно в разделе «Настройки > Пароли и автозаполнение»." + } + } + } + }, + "import.browser.data.source.title" : { + "comment" : "Import Browser Data title for option to choose source browser to import from", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Importieren aus" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Import From" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Importar desde" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Importer depuis" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Importa da" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Importeren vanuit" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Importuj z" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Importar de" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Импорт из" + } + } + } + }, "import.csv.instructions.bitwarden" : { "comment" : "Instructions to import Passwords as CSV from Bitwarden.\n%2$s - app name (Bitwarden)\n%7$@ - hamburger menu icon\n%9$@ - “Select Bitwarden CSV File” button\n**bold text**; _italic text_", "extractionState" : "extracted_with_value", @@ -25632,6 +26347,246 @@ } } }, + "import.shortcuts.bookmarks.subtitle" : { + "comment" : "Description for the setting to enable the bookmarks bar", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Halte deine Lieblingslesezeichen griffbereit" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Put your favorite bookmarks in easy reach" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pon tus marcadores favoritos a tu alcance" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Placez vos signets préférés à portée de main" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tieni i tuoi segnalibri preferiti a portata di mano" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Breng je favoriete bladwijzers binnen handbereik" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Umieść ulubione zakładki w łatwo dostępnym miejscu" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Coloca os teus marcadores favoritos num local de fácil alcance" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Любимые закладки всегда под рукой" + } + } + } + }, + "import.shortcuts.bookmarks.title" : { + "comment" : "Title for the setting to enable the bookmarks bar", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lesezeichenleiste anzeigen" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Show Bookmarks Bar" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mostrar barra de marcadores" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Afficher la barre des signets" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mostra la barra dei segnalibri" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bladwijzerbalk tonen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pokaż pasek zakładek" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mostrar barra de marcadores" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Показывать панель закладок" + } + } + } + }, + "import.shortcuts.passwords.subtitle" : { + "comment" : "Description for the setting to enable the passwords shortcut", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Passwörter in der Adressleiste aufbewahren" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Keep passwords nearby in the address bar" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mantén las contraseñas a mano en la barra de direcciones" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gardez vos mots de passe à portée de main dans la barre d'adresse" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tieni le password a portata di mano nella barra degli indirizzi" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Houd wachtwoorden bij via de adresbalk" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trzymaj hasła w pobliżu, na pasku adresu" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mantém as palavras-passe por perto na barra de endereços" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Удобное хранение паролей в адресной строке" + } + } + } + }, + "import.shortcuts.passwords.title" : { + "comment" : "Title for the setting to enable the passwords shortcut", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Passwort-Shortcut anzeigen" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Show Passwords Shortcut" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mostrar acceso directo a contraseñas" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Afficher le raccourci des mots de passe" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mostra scorciatoia per le password" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Snelkoppeling voor wachtwoorden weergeven" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pokaż skrót do haseł" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mostrar atalho de palavras-passe" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Показывать ярлык для паролей" + } + } + } + }, "invite.dialog.get.started.button" : { "comment" : "Get Started button on an invite dialog", "extractionState" : "extracted_with_value", @@ -32415,7 +33370,7 @@ "localizations" : { "de" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Du bist bereit! Du kannst mich jederzeit im Dock antreffen.\nMöchtest du sehen, wie ich dich beschütze? Versuche, eine deiner Lieblingsseiten zu besuchen 👆\n\nBehalte die Adressleiste im Auge. Ich werde Tracker blockieren und die Sicherheit deiner Verbindung verbessern, wenn möglichu{00A0}🔒" } }, @@ -32427,43 +33382,43 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "¡Ya está todo listo! Puedes encontrarme en el Dock en cualquier momento.\n¿Quieres ver cómo te protejo? Prueba a visitar uno de tus sitios favoritos 👆\n\nNo pierdas de vista la barra de direcciones al navegar. Bloquearé los rastreadores y mejoraré la seguridad de tu conexión cuando sea posible{00A0}🔒" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Tout est prêt ! Vous pouvez me trouver sur le Dock à tout moment.\nVous voulez voir comment je vous protège ? Essayez de visiter l'un de vos sites préférés 👆\n\nContinuez à regarder la barre d'adresse au fur et à mesure. Je bloquerai les traqueurs et mettrai à niveau la sécurité de votre connexion si possible 🔒" } }, "it" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Tutto pronto! Puoi trovarmi nel dock in qualsiasi momento.\nVuoi vedere come ti proteggo? Prova a visitare uno dei tuoi siti preferiti 👆\n\nContinua a controllare la barra degli indirizzi mentre esplori. Bloccherò i sistemi di tracciamento e aggiornerò la sicurezza della tua connessione quando possibile{00A0} 🔒" } }, "nl" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Je bent helemaal klaar! Je kunt me altijd vinden in het Dock.\nWil je zien hoe ik je bescherm? Ga eens naar een van je favoriete websites 👆\n\nKijk tijdens het surfen goed naar de adresbalk. Ik blokkeer trackers en werk de beveiliging van je verbinding bij wanneer mogelijk 🔒" } }, "pl" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Wszystko gotowe! W każdej chwili możesz mnie znaleźć w Docku.\nChcesz zobaczyć, jak Cię chronię? Spróbuj odwiedzić jedną z ulubionych stron 👆\n\nW międzyczasie obserwuj pasek adresu. Będę blokować mechanizmy śledzące i w miarę możliwości poprawiać bezpieczeństwo połączenia 🔒" } }, "pt" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Está tudo pronto! Podes encontrar-me na Dock em qualquer altura.\nQueres ver como te protejo? Experimenta visitar um dos teus sites favoritos 👆\n\nContinua a observar a barra de endereço à medida que vais avançando. Vou bloquear os rastreadores e melhorar a segurança da tua ligação sempre que possível 🔒" } }, "ru" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Готово! Теперь меня всегда можно найти на док-панели.\nВам интересно, как я защищаю вашу конфиденциальность? Зайдите на свой любимый сайт...👆\n\nИ следите за адресной строкой. По возможности я заблокирую все трекеры и сделаю соединение более безопасным {00A0}🔒" } } @@ -34024,7 +34979,7 @@ }, "passsword.management" : { "comment" : "Used as title for password management user interface", - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -34629,55 +35584,55 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Fireproof?" + "value" : "„Fireproof“ diese Website?" } }, "en" : { "stringUnit" : { "state" : "new", - "value" : "Fireproof?" + "value" : "Fireproof this website" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "¿Marcar como Fireproof?" + "value" : "Marcar el sitio web como Fireproof" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Mode coupe-feu (Fireproof) ?" + "value" : "Placer ce site Web en mode coupe-feu" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Attivare Fireproof?" + "value" : "Fireproof questo sito" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Fireproof?" + "value" : "Deze website fireproof maken" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Ustawić jako Fireproof?" + "value" : "Zabezpiecz tę witrynę funkcją Fireproof" } }, "pt" : { "stringUnit" : { "state" : "translated", - "value" : "Utilizar o Fireproof?" + "value" : "Faz o fireproof deste site" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Сделать огнеупорным?" + "value" : "Fireproof-защита для сайта" } } } @@ -34689,55 +35644,55 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Login bei Bitwarden speichern?" + "value" : "Passwort bei Bitwarden speichern?" } }, "en" : { "stringUnit" : { "state" : "new", - "value" : "Save Login to Bitwarden?" + "value" : "Save password to Bitwarden?" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "¿Guardar inicio de sesión en Bitwarden?" + "value" : "¿Guardar contraseña en Bitwarden?" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Enregistrer la connexion à Bitwarden ?" + "value" : "Enregistrer le mot de passe dans Bitwarden ?" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Salvare l'accesso a Bitwarden?" + "value" : "Salvare la password su Bitwarden?" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Login op Bitwarden opslaan?" + "value" : "Wachtwoord opslaan in Bitwarden?" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Zapisać dane logowania w aplikacji Bitwarden?" + "value" : "Zapisać hasło w aplikacji Bitwarden?" } }, "pt" : { "stringUnit" : { "state" : "translated", - "value" : "Guardar início de sessão no Bitwarden?" + "value" : "Guardar palavra-passe no Bitwarden?" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Сохранить логин в Bitwarden?" + "value" : "Сохранить пароль в Bitwarden?" } } } @@ -35741,6 +36696,66 @@ } } }, + "password.management.title" : { + "comment" : "Used as the title for menu item and related Settings page", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Passwörter und Autovervollständigen" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Passwords & Autofill" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Contraseñas y autocompletar" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mots de passe et saisie automatique" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Password e compilazione automatica" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wachtwoorden en automatisch invullen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hasła i autouzupełnianie" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Palavras-passe e preenchimento automático" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пароли и автозаполнение" + } + } + } + }, "password.manager" : { "comment" : "Section header", "extractionState" : "extracted_with_value", @@ -37595,61 +38610,61 @@ } }, "pinning.hide-autofill-shortcut" : { - "comment" : "Menu item for hiding the autofill shortcut", + "comment" : "Menu item for hiding the passwords shortcut", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", - "value" : "Autovervollständigungs-Verknüpfung ausblenden" + "value" : "Passwort-Shortcut ausblenden" } }, "en" : { "stringUnit" : { "state" : "new", - "value" : "Hide Autofill Shortcut" + "value" : "Hide Passwords Shortcut" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Ocultar acceso directo a Autocompletar" + "value" : "Ocultar acceso directo a contraseñas" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Masquer le raccourci de saisie automatique" + "value" : "Masquer le raccourci des mots de passe" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Nascondi scorciatoia compilazione automatica" + "value" : "Nascondi scorciatoia per le password" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Snelkoppeling voor automatisch invullen verbergen" + "value" : "Snelkoppeling voor wachtwoorden verbergen" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Ukryj skrót do autouzupełniania" + "value" : "Ukryj skrót do haseł" } }, "pt" : { "stringUnit" : { "state" : "translated", - "value" : "Ocultar atalho de preenchimento automático" + "value" : "Ocultar atalho de palavras-passe" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Скрыть ярлык для автозаполнения" + "value" : "Скрыть ярлык для паролей" } } } @@ -37835,61 +38850,61 @@ } }, "pinning.show-autofill-shortcut" : { - "comment" : "Menu item for showing the autofill shortcut", + "comment" : "Menu item for showing the passwords shortcut", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", - "value" : "Autofill-Verknüpfung anzeigen" + "value" : "Passwort-Shortcut anzeigen" } }, "en" : { "stringUnit" : { "state" : "new", - "value" : "Show Autofill Shortcut" + "value" : "Show Passwords Shortcut" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Mostrar acceso directo a Autocompletar" + "value" : "Mostrar acceso directo a contraseñas" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Afficher le raccourci de saisie automatique" + "value" : "Afficher le raccourci des mots de passe" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Mostra scorciatoia compilazione automatica" + "value" : "Mostra scorciatoia per le password" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Snelkoppeling voor automatisch invullen weergeven" + "value" : "Snelkoppeling voor wachtwoorden weergeven" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Pokaż skrót do autouzupełniania" + "value" : "Pokaż skrót do haseł" } }, "pt" : { "stringUnit" : { "state" : "translated", - "value" : "Mostrar atalho de preenchimento automático" + "value" : "Mostrar atalho de palavras-passe" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Показывать ярлык для автозаполнения" + "value" : "Показывать ярлык для паролей" } } } @@ -39567,6 +40582,66 @@ } } }, + "pm.empty.default.button.title" : { + "comment" : "Import passwords button title for default empty state", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Passwörter importieren" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Import Passwords" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Importar contraseñas" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Importer les mots de passe" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Importa password" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wachtwoorden importeren" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Importuj hasła" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Importar palavras-passe" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Импорт паролей" + } + } + } + }, "pm.empty.default.description" : { "comment" : "Label for default empty state description", "extractionState" : "extracted_with_value", @@ -39754,55 +40829,55 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Keine Passwörter" + "value" : "Noch keine Passwörter gespeichert" } }, "en" : { "stringUnit" : { "state" : "new", - "value" : "No passwords" + "value" : "No passwords saved yet" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Sin contraseñas" + "value" : "Aún no hay contraseñas guardadas" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Aucun mot de passe" + "value" : "Aucun mot de passe n'a été enregistré" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Nessuna password" + "value" : "Nessuna password ancora salvata" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Geen wachtwoorden" + "value" : "Nog geen wachtwoorden opgeslagen" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Brak haseł" + "value" : "Nie zapisano jeszcze żadnych haseł" } }, "pt" : { "stringUnit" : { "state" : "translated", - "value" : "Sem palavras-passe" + "value" : "Ainda não há palavras-passe guardadas" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Нет паролей" + "value" : "Сохраненных паролей пока нет" } } } @@ -40342,187 +41417,187 @@ "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Разблокировать доступ к автозаполняемым данным" - } - } - } - }, - "pm.lock-screen.prompt.change-settings" : { - "comment" : "Label presented when changing Auto-Lock settings", - "extractionState" : "extracted_with_value", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Einstellungen für das automatische Ausfüllen von Informationen ändern" - } - }, - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "change your autofill info access settings" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "cambiar la configuración de acceso a la información de autocompletar" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "modifier les paramètres d'accès à vos informations de saisie automatique" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "modifica le impostazioni di accesso alla compilazione automatica delle informazioni" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "toegangsinstellingen voor automatisch ingevulde gegevens wijzigen" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "zmień ustawienia dostępu do informacji autouzupełniania" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "alterar as tuas definições de acesso às informações de preenchimento automático" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Изменить настройки доступа к автозаполняемым данным" - } - } - } - }, - "pm.lock-screen.prompt.export-logins" : { - "comment" : "Label presented when exporting logins", - "extractionState" : "extracted_with_value", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Deine Benutzernamen und Passwörter exportieren" - } - }, - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "export your usernames and passwords" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "exporta tus nombres de usuario y contraseñas" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "exporter vos noms d'utilisateur et mots de passe" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "esporta i tuoi nomi utente e le tue password" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "je gebruikersnamen en wachtwoorden exporteren" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "eksportuj swoje nazwy użytkownika i hasła" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "exportar os nomes de utilizador e as palavras-passe" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Экспортировать имена пользователей и пароли" - } - } - } - }, - "pm.lock-screen.prompt.unlock-logins" : { - "comment" : "Label presented when unlocking Autofill", - "extractionState" : "extracted_with_value", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zugang zu deinen Autovervollständigungs-Infos freischalten" - } - }, - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "unlock access to your autofill info" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "desbloquear el acceso a tu información de autocompletar" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "déverrouiller l'accès à vos informations de saisie automatique" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "sblocca l'accesso alla compilazione automatica delle informazioni" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "toegang tot automatisch ingevulde gegevens ontgrendelen" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "odblokuj dostęp do informacji autouzupełniania" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "desbloquear o acesso às tuas informações de preenchimento automático" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "разблокировать доступ к автозаполняемым данным" + "value" : "Разблокировать доступ к автозаполняемым данным" + } + } + } + }, + "pm.lock-screen.prompt.change-settings" : { + "comment" : "Label presented when changing Auto-Lock settings", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Einstellungen für das automatische Ausfüllen von Informationen ändern" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "change your autofill info access settings" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "cambiar la configuración de acceso a la información de autocompletar" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "modifier les paramètres d'accès à vos informations de saisie automatique" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "modifica le impostazioni di accesso alla compilazione automatica delle informazioni" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "toegangsinstellingen voor automatisch ingevulde gegevens wijzigen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "zmień ustawienia dostępu do informacji autouzupełniania" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "alterar as tuas definições de acesso às informações de preenchimento automático" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Изменить настройки доступа к автозаполняемым данным" + } + } + } + }, + "pm.lock-screen.prompt.export-logins" : { + "comment" : "Label presented when exporting logins", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deine Benutzernamen und Passwörter exportieren" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "export your usernames and passwords" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "exporta tus nombres de usuario y contraseñas" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "exporter vos noms d'utilisateur et mots de passe" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "esporta i tuoi nomi utente e le tue password" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "je gebruikersnamen en wachtwoorden exporteren" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "eksportuj swoje nazwy użytkownika i hasła" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "exportar os nomes de utilizador e as palavras-passe" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Экспортировать имена пользователей и пароли" + } + } + } + }, + "pm.lock-screen.prompt.unlock-logins" : { + "comment" : "Label presented when unlocking Autofill", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "deine Passwörter freischalten und Informationen autovervollständigen" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "unlock your passwords and autofill info for you" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "desbloquea tus contraseñas y completa automáticamente tu información" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "débloque vos mots de passe et saisit automatiquement les informations pour vous" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "sblocca le tue password e compila automaticamente le informazioni" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "ontgrendel je wachtwoorden en vul gegevens automatisch in" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "odblokuj hasła i automatycznie uzupełniaj informacje" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "desbloquear as palavras-passe e preencher automaticamente informações por ti" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Разблокирует ваши пароли и автоматически заполнит информацию" } } } @@ -42334,55 +43409,55 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Passwort speichern?" + "value" : "Passwort in DuckDuckGo speichern?" } }, "en" : { "stringUnit" : { "state" : "new", - "value" : "Save password?" + "value" : "Save password in DuckDuckGo?" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "¿Guardar contraseña?" + "value" : "¿Guardar contraseña en DuckDuckGo?" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Enregistrer le mot de passe ?" + "value" : "Enregistrer le mot de passe dans DuckDuckGo ?" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Salvare password?" + "value" : "Salvare la password in DuckDuckGo?" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Wachtwoord opslaan?" + "value" : "Wachtwoord opslaan in DuckDuckGo?" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Zapisać hasło?" + "value" : "Zapisać hasło w DuckDuckGo?" } }, "pt" : { "stringUnit" : { "state" : "translated", - "value" : "Guardar palavra-passe?" + "value" : "Guardar palavra-passe no DuckDuckGo?" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Сохранить пароль?" + "value" : "Сохранить пароль в DuckDuckGo?" } } } @@ -42400,7 +43475,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "New Password Saved" + "value" : "New password saved" } }, "es" : { @@ -42927,6 +44002,66 @@ } } }, + "pm.update-credentials.title" : { + "comment" : "Title for the Update Credentials popover", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Passwort aktualisieren?" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Update password?" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Actualizar contraseña?" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Modifier le mot de passe ?" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aggiornare password?" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wachtwoord bijwerken?" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zaktualizować hasło?" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Atualizar palavra-passe?" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Обновить пароль?" + } + } + } + }, "pm.username" : { "comment" : "Label for username edit field", "extractionState" : "extracted_with_value", @@ -48531,6 +49666,60 @@ } }, "Select data to import:" : { + "comment" : "Data Import section title for checkboxes of data type to import: Passwords or Bookmarks.", + "extractionState" : "stale", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zu importierende Daten auswählen:" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Selecciona los datos a importar:" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sélectionner les données à importer :" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seleziona i dati da importare:" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Selecteer gegevens om te importeren:" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wybierz dane do zaimportowania:" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Selecionar dados para importar:" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Выберите данные для импорта:" + } + } + } + }, + "Select Data to Import:" : { "comment" : "Data Import section title for checkboxes of data type to import: Passwords or Bookmarks.", "localizations" : { "de" : { diff --git a/DuckDuckGo/MessageViews/PopoverMessageViewController.swift b/DuckDuckGo/MessageViews/PopoverMessageViewController.swift index b0533f8712..02c119a703 100644 --- a/DuckDuckGo/MessageViews/PopoverMessageViewController.swift +++ b/DuckDuckGo/MessageViews/PopoverMessageViewController.swift @@ -77,6 +77,8 @@ final class PopoverMessageViewController: NSHostingController AnyCancellable? { @@ -987,8 +987,8 @@ final class PasswordManagementViewController: NSViewController { private func showEmptyState(category: SecureVaultSorting.Category) { switch category { - case .allItems: showEmptyState(image: .loginsEmpty, title: UserText.pmEmptyStateDefaultTitle, message: UserText.pmEmptyStateDefaultDescription, hideMessage: false, hideButton: false) - case .logins: showEmptyState(image: .loginsEmpty, title: UserText.pmEmptyStateLoginsTitle, hideMessage: false, hideButton: false) + case .allItems: showEmptyState(image: .passwordsAdd128, title: UserText.pmEmptyStateDefaultTitle, message: UserText.pmEmptyStateDefaultDescription, hideMessage: false, hideButton: false) + case .logins: showEmptyState(image: .passwordsAdd128, title: UserText.pmEmptyStateLoginsTitle, hideMessage: false, hideButton: false) case .identities: showEmptyState(image: .identitiesEmpty, title: UserText.pmEmptyStateIdentitiesTitle) case .cards: showEmptyState(image: .creditCardsEmpty, title: UserText.pmEmptyStateCardsTitle) } diff --git a/DuckDuckGo/SecureVault/View/PasswordManager.storyboard b/DuckDuckGo/SecureVault/View/PasswordManager.storyboard index 2fd6c58259..e8f73afdc7 100644 --- a/DuckDuckGo/SecureVault/View/PasswordManager.storyboard +++ b/DuckDuckGo/SecureVault/View/PasswordManager.storyboard @@ -1,7 +1,7 @@ - + - + @@ -429,14 +429,35 @@ - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -742,18 +763,19 @@ DQ - + + - + @@ -782,11 +804,10 @@ DQ + - - @@ -794,6 +815,7 @@ DQ + @@ -1073,6 +1095,7 @@ DQ + diff --git a/DuckDuckGo/SecureVault/View/SaveCredentialsViewController.swift b/DuckDuckGo/SecureVault/View/SaveCredentialsViewController.swift index 0d7bc4bc6d..cddee05928 100644 --- a/DuckDuckGo/SecureVault/View/SaveCredentialsViewController.swift +++ b/DuckDuckGo/SecureVault/View/SaveCredentialsViewController.swift @@ -39,6 +39,7 @@ final class SaveCredentialsViewController: NSViewController { return controller } + @IBOutlet var ddgPasswordManagerTitle: NSView! @IBOutlet var titleLabel: NSTextField! @IBOutlet var passwordManagerTitle: NSView! @IBOutlet var passwordManagerAccountLabel: NSTextField! @@ -81,6 +82,8 @@ final class SaveCredentialsViewController: NSViewController { private var saveButtonAction: (() -> Void)? + private var shouldFirePinPromptNotification = false + var passwordData: Data { let string = hiddenPasswordField.isHidden ? visiblePasswordField.stringValue : hiddenPasswordField.stringValue return string.data(using: .utf8)! @@ -104,6 +107,9 @@ final class SaveCredentialsViewController: NSViewController { override func viewWillDisappear() { passwordManagerStateCancellable = nil + if shouldFirePinPromptNotification { + NotificationCenter.default.post(name: .passwordsPinningPrompt, object: nil) + } } private func setUpStrings() { @@ -166,11 +172,11 @@ final class SaveCredentialsViewController: NSViewController { editButton.isHidden = true doneButton.isHidden = true - titleLabel.isHidden = passwordManagerCoordinator.isEnabled + ddgPasswordManagerTitle.isHidden = passwordManagerCoordinator.isEnabled passwordManagerTitle.isHidden = !passwordManagerCoordinator.isEnabled || passwordManagerCoordinator.isLocked passwordManagerAccountLabel.stringValue = UserText.passwordManagementSaveCredentialsAccountLabel(activeVault: passwordManagerCoordinator.activeVaultEmail ?? "") unlockPasswordManagerTitle.isHidden = !passwordManagerCoordinator.isEnabled || !passwordManagerCoordinator.isLocked - titleLabel.stringValue = UserText.pmSaveCredentialsEditableTitle + titleLabel.stringValue = credentials?.account.id == nil ? UserText.pmSaveCredentialsEditableTitle : UserText.pmUpdateCredentialsTitle usernameField.makeMeFirstResponder() } else { notNowSegmentedControl.isHidden = true @@ -232,9 +238,14 @@ final class SaveCredentialsViewController: NSViewController { } } } else { - _ = try AutofillSecureVaultFactory.makeVault(reporter: SecureVaultReporter.shared).storeWebsiteCredentials(credentials) + let vault = try AutofillSecureVaultFactory.makeVault(reporter: SecureVaultReporter.shared) + _ = try vault.storeWebsiteCredentials(credentials) NSApp.delegateTyped.syncService?.scheduler.notifyDataChanged() os_log(.debug, log: OSLog.sync, "Requesting sync if enabled") + + if existingCredentials?.account.id == nil, !LocalPinningManager.shared.isPinned(.autofill), let count = try? vault.accountsCount(), count == 1 { + shouldFirePinPromptNotification = true + } } } catch { os_log("%s:%s: failed to store credentials %s", type: .error, className, #function, error.localizedDescription) diff --git a/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/PopoverMessageView.swift b/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/PopoverMessageView.swift index d08f3e7f48..cd6cf0184e 100644 --- a/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/PopoverMessageView.swift +++ b/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/PopoverMessageView.swift @@ -24,7 +24,7 @@ public final class PopoverMessageViewModel: ObservableObject { @Published var message: String @Published var image: NSImage? @Published var buttonText: String? - @Published var buttonAction: (() -> Void)? + @Published public var buttonAction: (() -> Void)? public init(message: String, image: NSImage? = nil, diff --git a/UnitTests/DataImport/DataImportViewModelTests.swift b/UnitTests/DataImport/DataImportViewModelTests.swift index 81f5fca4a7..31ac01269b 100644 --- a/UnitTests/DataImport/DataImportViewModelTests.swift +++ b/UnitTests/DataImport/DataImportViewModelTests.swift @@ -1142,7 +1142,7 @@ import XCTest xctDescr = "\(source): " + xctDescr XCTAssertEqual(model.description, expectation.description, xctDescr) - XCTAssertEqual(model.actionButton, .done, xctDescr) + XCTAssertEqual(model.actionButton, .next(.shortcuts([.bookmarks])), xctDescr) XCTAssertNil(model.secondaryButton, xctDescr) } } @@ -1181,7 +1181,7 @@ import XCTest // expect Final Summary let expectation = DataImportViewModel(importSource: source, screen: .summary([.bookmarks], isFileImport: true), summary: [bookmarksSummary, result.map { .init(.bookmarks, $0) }].compactMap { $0 }) XCTAssertEqual(model.description, expectation.description, xctDescr) - XCTAssertEqual(model.actionButton, .done, xctDescr) + XCTAssertEqual(model.actionButton, .next(.shortcuts([.bookmarks])), xctDescr) XCTAssertNil(model.secondaryButton, xctDescr) } } @@ -1231,7 +1231,7 @@ import XCTest } } - func testWhenBrowsersBookmarksImportFailsNoDataAndFileImportSkippedAndNoPasswordsFileImportNeeded_dialogDismissed() throws { + func testWhenBrowsersBookmarksImportFailsNoDataAndFileImportSkippedAndNoPasswordsFileImportNeeded_shortcutsShown() throws { for source in Source.allCases where source.initialScreen == .profileAndDataTypesPicker && source.supportedDataTypes.contains(.passwords) { let bookmarksSummary = bookmarkSummaryNoData @@ -1246,12 +1246,9 @@ import XCTest screen: .fileImport(dataType: .bookmarks, summary: []), summary: [bookmarksSummary, passwordsSummary].compactMap { $0 }) - let expectation = expectation(description: "dismissed") - model.performAction(for: .skip) { - expectation.fulfill() - } + model.performAction(for: .skip) {} - waitForExpectations(timeout: 0) + XCTAssertEqual(model.screen, .shortcuts([.passwords])) } } } @@ -1410,7 +1407,7 @@ import XCTest // expect Final Summary let expectation = DataImportViewModel(importSource: source, screen: .summary([.passwords], isFileImport: true), summary: [bookmarksSummary, passwordsSummary, bookmarksFileImportSummary, result.map { .init(.passwords, $0) }].compactMap { $0 }) XCTAssertEqual(model.description, expectation.description, xctDescr) - XCTAssertEqual(model.actionButton, .done, xctDescr) + XCTAssertEqual(model.actionButton, .next(.shortcuts([.passwords])), xctDescr) XCTAssertNil(model.secondaryButton, xctDescr) } } @@ -1447,7 +1444,7 @@ import XCTest // expect Final Summary let expectation = DataImportViewModel(importSource: source, screen: .summary([.passwords], isFileImport: true), summary: [passwordsSummary, result.map { .init(.passwords, $0) }].compactMap { $0 }) XCTAssertEqual(model.description, expectation.description, xctDescr) - XCTAssertEqual(model.actionButton, .done, xctDescr) + XCTAssertEqual(model.actionButton, .next(.shortcuts([.passwords])), xctDescr) XCTAssertNil(model.secondaryButton, xctDescr) } } @@ -1503,7 +1500,7 @@ import XCTest } } - func testWhenBrowsersPasswordsImportFailNoDataAndFileImportSkipped_dialogDismissed() throws { + func testWhenBrowsersPasswordsImportFailNoDataAndFileImportSkipped_dialogDismissedOrShortcutsShown() throws { for source in Source.allCases where source.initialScreen == .profileAndDataTypesPicker && source.supportedDataTypes.contains(.passwords) { for bookmarksSummary in bookmarksSummaries { @@ -1522,12 +1519,19 @@ import XCTest screen: .fileImport(dataType: .passwords, summary: []), summary: [bookmarksSummary, passwordsSummary, bookmarksFileImportSummary].compactMap { $0 }) - let expectation = expectation(description: "dismissed") - model.performAction(for: .skip) { - expectation.fulfill() + if let result = bookmarksSummary?.result as? DataImportResult, result.isSuccess, let successful = try? result.get().successful, successful > 0 { + model.performAction(for: .skip) {} + XCTAssertEqual(model.screen, .shortcuts([.bookmarks])) + } else if let result = bookmarksFileImportSummary?.result as? DataImportResult, result.isSuccess, let successful = try? result.get().successful, successful > 0 { + model.performAction(for: .skip) {} + XCTAssertEqual(model.screen, .shortcuts([.bookmarks])) + } else { + let expectation = expectation(description: "dismissed") + model.performAction(for: .skip) { + expectation.fulfill() + } + waitForExpectations(timeout: 0) } - - waitForExpectations(timeout: 0) } } } @@ -1828,6 +1832,7 @@ extension DataImportViewModel.Screen: CustomStringConvertible { case .summary(let dataTypes, isFileImport: true): ".summary([\(dataTypes.map { "." + $0.rawValue }.sorted().joined(separator: ", "))], isFileImport: true)" case .feedback: ".feedback" + case .shortcuts: ".shortcuts" } } } diff --git a/UnitTests/Menus/MoreOptionsMenuTests.swift b/UnitTests/Menus/MoreOptionsMenuTests.swift index f6d7e42781..0140a43a9f 100644 --- a/UnitTests/Menus/MoreOptionsMenuTests.swift +++ b/UnitTests/Menus/MoreOptionsMenuTests.swift @@ -83,7 +83,7 @@ final class MoreOptionsMenuTests: XCTestCase { XCTAssertTrue(moreOptionsMenu.items[7].isSeparatorItem) XCTAssertEqual(moreOptionsMenu.items[8].title, UserText.bookmarks) XCTAssertEqual(moreOptionsMenu.items[9].title, UserText.downloads) - XCTAssertEqual(moreOptionsMenu.items[10].title, UserText.passwordManagement) + XCTAssertEqual(moreOptionsMenu.items[10].title, UserText.passwordManagementTitle) XCTAssertTrue(moreOptionsMenu.items[11].isSeparatorItem) XCTAssertEqual(moreOptionsMenu.items[12].title, UserText.emailOptionsMenuItem) @@ -115,7 +115,7 @@ final class MoreOptionsMenuTests: XCTestCase { XCTAssertTrue(moreOptionsMenu.items[7].isSeparatorItem) XCTAssertEqual(moreOptionsMenu.items[8].title, UserText.bookmarks) XCTAssertEqual(moreOptionsMenu.items[9].title, UserText.downloads) - XCTAssertEqual(moreOptionsMenu.items[10].title, UserText.passwordManagement) + XCTAssertEqual(moreOptionsMenu.items[10].title, UserText.passwordManagementTitle) XCTAssertTrue(moreOptionsMenu.items[11].isSeparatorItem) XCTAssertEqual(moreOptionsMenu.items[12].title, UserText.emailOptionsMenuItem)