From d77035f99020facfa7a54190b62760bc2d146a85 Mon Sep 17 00:00:00 2001 From: Graeme Arthur Date: Thu, 19 Sep 2024 12:37:21 +0200 Subject: [PATCH] Address autofill security concerns (#3321) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/414235014887631/1207411921782781/f **Description**: [✓ Implement Survey for Password Manager Users](https://app.asana.com/0/72649045549333/1206568003117818) showed that a proportion of users are hesitant to use our Password Manager because they don't know how secure it is. Easing these concerns should increase the adoption of DuckDuckGo's Password Manager. These changes update and add copy to more clearly explain the security safe-guards of using the Password Manager. **Steps to test this PR**: - Go to the screens from the designs in [Figma](https://www.figma.com/design/wAWx1a0mAooj6sDCmoTFbS/Password-Manager-security?node-id=192-10952&node-type=FRAME&t=0sLE1hdNaQCkC7A8-0) and check they match. Double check Ship Review for any copy divergences. **Definition of Done (Internal Only)**: * [ ] Does this PR satisfy our [Definition of Done](https://app.asana.com/0/1202500774821704/1207634633537039/f)? **Copy Testing**: * [ ] Use of correct apostrophes in new copy, ie `’` rather than `'` **Orientation Testing**: * [x] Portrait * [ ] Landscape **Device Testing**: * [x] iPhone SE (1st Gen) * [x] iPhone 8 * [ ] iPhone X * [ ] iPhone 14 Pro * [ ] iPad **OS Testing**: * [ ] iOS 15 * [ ] iOS 16 * [x] iOS 17 **Theme Testing**: * [x] Light theme * [x] Dark theme --- ###### Internal references: [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) --- Core/AppURLs.swift | 1 + .../16px/Lock-Solid-16.imageset/Contents.json | 15 +++++++ .../Lock-Solid-16.imageset/Lock-Solid-16.pdf | Bin 0 -> 2602 bytes DuckDuckGo/AutofillItemsEmptyView.swift | 1 - ...ofillLoginSettingsListViewController.swift | 9 ++-- .../AutofillSettingsEnableFooterView.swift | 42 ++++++++++++++---- DuckDuckGo/AutofillViews.swift | 27 ++++++++--- DuckDuckGo/PasswordGenerationPromptView.swift | 2 +- DuckDuckGo/SaveLoginView.swift | 4 +- DuckDuckGo/UserText.swift | 8 +++- DuckDuckGo/en.lproj/Localizable.strings | 17 +++++-- 11 files changed, 98 insertions(+), 28 deletions(-) create mode 100644 DuckDuckGo/Assets.xcassets/DesignSystemIcons/16px/Lock-Solid-16.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/DesignSystemIcons/16px/Lock-Solid-16.imageset/Lock-Solid-16.pdf diff --git a/Core/AppURLs.swift b/Core/AppURLs.swift index 7851870c26..e07d771f2a 100644 --- a/Core/AppURLs.swift +++ b/Core/AppURLs.swift @@ -37,6 +37,7 @@ public extension URL { static let aboutLink = URL(string: AppDeepLinkSchemes.quickLink.appending("\(ddg.host!)/about"))! static let apps = URL(string: AppDeepLinkSchemes.quickLink.appending("\(ddg.host!)/apps"))! static let searchSettings = URL(string: AppDeepLinkSchemes.quickLink.appending("\(ddg.host!)/settings"))! + static let autofillHelpPageLink = URL(string: AppDeepLinkSchemes.quickLink.appending("\(ddg.host!)/duckduckgo-help-pages/sync-and-backup/password-manager-security/"))! static let surrogates = URL(string: "\(staticBase)/surrogates.txt")! diff --git a/DuckDuckGo/Assets.xcassets/DesignSystemIcons/16px/Lock-Solid-16.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/DesignSystemIcons/16px/Lock-Solid-16.imageset/Contents.json new file mode 100644 index 0000000000..ce371320cc --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/DesignSystemIcons/16px/Lock-Solid-16.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Lock-Solid-16.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/DuckDuckGo/Assets.xcassets/DesignSystemIcons/16px/Lock-Solid-16.imageset/Lock-Solid-16.pdf b/DuckDuckGo/Assets.xcassets/DesignSystemIcons/16px/Lock-Solid-16.imageset/Lock-Solid-16.pdf new file mode 100644 index 0000000000000000000000000000000000000000..b1a5584f9c8e20ce5de1f860ce2ce42208ada181 GIT binary patch literal 2602 zcmd^B&rjPh6u$ef@C6AbEwPh0sU=hiTBxQC!P*^=&<-IrrL0Z5B%NS?{hl4?IUz-C z=lKB6=lAn__Io|LnO=V6UP1_`j28D#gwoSf>VJFLDeB)|-#@9W1`L+}Mdhpdkp>_* zx6rIC_PdUP`EJgPPNTR0du5teyR=k!_R>2akFBZ2{=PO&FN<<(vAL>>eVM6ByFkhF z^WvG_QRXqT+=d4&Sl`m;|K;ev_4a7FUwKaiQXY=|94P>rH7_GMTIw=OF5Z5Ig>eW&-3=u1@PXvyD)*n~)|Gb6`Qo{r zE$XyZ=KO-2e~YWM`eAm>T+yGsDd2`hf9KDrzH)Zl^v?!gKFpc3&F(RM-*h`m1~xQc zhaYD0mbvk`NgIr{?<4LpWd~-`GW1tN$Oq0=74d=EX>s^%YJ*Lcq8MF|5Y$ieEYFL& zqIY`yxs-0wm9oH UIView? { switch viewModel.viewState { - case .empty: - return viewModel.sections[section] == .enableAutofill ? enableAutofillFooterView : nil - case .showItems: + case .showItems, .empty: return viewModel.sections[section] == .enableAutofill ? enableAutofillFooterView : nil default: return nil diff --git a/DuckDuckGo/AutofillSettingsEnableFooterView.swift b/DuckDuckGo/AutofillSettingsEnableFooterView.swift index aa2011be22..1d2f26d26f 100644 --- a/DuckDuckGo/AutofillSettingsEnableFooterView.swift +++ b/DuckDuckGo/AutofillSettingsEnableFooterView.swift @@ -18,6 +18,7 @@ // import UIKit +import DesignResourcesKit class AutofillSettingsEnableFooterView: UIView { @@ -36,16 +37,33 @@ class AutofillSettingsEnableFooterView: UIView { fatalError("init(coder:) has not been implemented") } - private lazy var title: UILabel = { - let label = UILabel(frame: CGRect.zero) - label.font = .preferredFont(forTextStyle: .footnote) - label.numberOfLines = 0 - label.textAlignment = .left - label.lineBreakMode = .byWordWrapping - label.textColor = UIColor(designSystemColor: .textSecondary) - label.text = UserText.autofillSettingsFooter + private lazy var title: UITextView = { + let textView = UITextView(frame: CGRect.zero) + textView.delegate = self + textView.textAlignment = .left - return label + var attributedText = NSMutableAttributedString() + let attributedTextDescription = (try? NSMutableAttributedString(markdown: UserText.autofillLoginListSettingsFooterMarkdown)) ?? NSMutableAttributedString(string: UserText.autofillLoginListSettingsFooterFallback) + let attachment = NSTextAttachment() + attachment.image = UIImage(resource: .lockSolid16).withTintColor(UIColor(designSystemColor: .textSecondary)) + attachment.bounds = CGRect(x: 0, y: -1, width: 12, height: 12) + let attributedTextImage = NSMutableAttributedString(attachment: attachment) + attributedText.append(attributedTextImage) + attributedText.append(.init(string: " ")) + attributedText.append(attributedTextDescription) + let wholeRange = NSRange(location: 0, length: attributedText.length) + attributedText.addAttribute(.foregroundColor, value: UIColor(designSystemColor: .textSecondary), range: wholeRange) + attributedText.addAttribute(.font, value: UIFont.daxFootnoteRegular(), range: wholeRange) + + textView.attributedText = attributedText + textView.linkTextAttributes = [.foregroundColor: UIColor(designSystemColor: .accent)] + textView.isEditable = false + textView.isScrollEnabled = false + textView.backgroundColor = .clear + textView.textContainerInset = .zero + textView.textContainer.lineFragmentPadding = 0 + + return textView }() private func installSubviews() { @@ -67,3 +85,9 @@ class AutofillSettingsEnableFooterView: UIView { ]) } } + +extension AutofillSettingsEnableFooterView: UITextViewDelegate { + func textViewDidChangeSelection(_ textView: UITextView) { + textView.selectedTextRange = nil + } +} diff --git a/DuckDuckGo/AutofillViews.swift b/DuckDuckGo/AutofillViews.swift index ffb219210c..5ea7061702 100644 --- a/DuckDuckGo/AutofillViews.swift +++ b/DuckDuckGo/AutofillViews.swift @@ -82,11 +82,28 @@ struct AutofillViews { var body: some View { Text(text) - .daxFootnoteRegular() - .foregroundColor(Color(designSystemColor: .textSecondary)) - .multilineTextAlignment(.center) - .fixedSize(horizontal: false, vertical: true) - .frame(maxWidth: Const.Size.maxWidth) + .daxFootnoteRegular() + .foregroundColor(Color(designSystemColor: .textSecondary)) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + .frame(maxWidth: Const.Size.maxWidth) + } + } + + struct SecureDescription: View { + let text: String + + var body: some View { + ( + Text("\(Image(.lockSolid16)) ").baselineOffset(-1.0) + + + Text(text) + ) + .daxFootnoteRegular() + .foregroundColor(Color(designSystemColor: .textSecondary)) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + .frame(maxWidth: Const.Size.maxWidth) } } diff --git a/DuckDuckGo/PasswordGenerationPromptView.swift b/DuckDuckGo/PasswordGenerationPromptView.swift index 6743d47f12..ed257a27d4 100644 --- a/DuckDuckGo/PasswordGenerationPromptView.swift +++ b/DuckDuckGo/PasswordGenerationPromptView.swift @@ -56,7 +56,7 @@ struct PasswordGenerationPromptView: View { passwordView AutofillViews.LegacySpacerView() } - AutofillViews.Description(text: UserText.autofillPasswordGenerationPromptSubtitle) + AutofillViews.SecureDescription(text: UserText.autofillSaveLoginSecurityMessage) contentViewSpacer ctaView .padding(.bottom, AutofillViews.isIPad(verticalSizeClass, horizontalSizeClass) ? Const.Size.bottomPaddingIPad diff --git a/DuckDuckGo/SaveLoginView.swift b/DuckDuckGo/SaveLoginView.swift index 601441e663..9d15b185d4 100644 --- a/DuckDuckGo/SaveLoginView.swift +++ b/DuckDuckGo/SaveLoginView.swift @@ -216,8 +216,8 @@ struct SaveLoginView: View { private var contentView: some View { switch layoutType { case .newUser, .saveLogin, .savePassword, .updatePassword: - let text = layoutType == .updatePassword ? UserText.autoUpdatePasswordMessage : UserText.autofillSaveLoginMessageNewUser - AutofillViews.Description(text: text) + let text = layoutType == .updatePassword ? UserText.autoUpdatePasswordMessage : UserText.autofillSaveLoginSecurityMessage + AutofillViews.SecureDescription(text: text) case .updateUsername: updateUsernameContentView } diff --git a/DuckDuckGo/UserText.swift b/DuckDuckGo/UserText.swift index 0f6a604a29..b586b9f55d 100644 --- a/DuckDuckGo/UserText.swift +++ b/DuckDuckGo/UserText.swift @@ -396,7 +396,7 @@ public struct UserText { public static let autofillSaveLoginTitle = NSLocalizedString("autofill.save-login.title", value: "Save password?", comment: "Title displayed on modal asking for the user to save the login") public static let autofillUpdateUsernameTitle = NSLocalizedString("autofill.update-usernamr.title", value: "Update username?", comment: "Title displayed on modal asking for the user to update the username") - public static let autofillSaveLoginMessageNewUser = NSLocalizedString("autofill.save-login.new-user.message", value: "DuckDuckGo Passwords & Autofill stores passwords securely on your device.", comment: "Message displayed on modal asking for the user to save the login for the first time") + public static let autofillSaveLoginSecurityMessage = NSLocalizedString("autofill.save-login.security.message", value: "Securely store your password on device with DuckDuckGo Passwords & Autofill.", comment: "Message displayed on modal asking for the user to save the login for the first time") public static let autofillSaveLoginNeverPromptCTA = NSLocalizedString("autofill.save-login.never-prompt.CTA", value: "Never Ask for This Site", comment: "CTA displayed on modal asking if the user never wants to be prompted to save a login for this website agin") public static func autofillUpdatePassword(for title: String) -> String { @@ -701,9 +701,11 @@ public struct UserText { public static let autofillLoginDetailsAddress = NSLocalizedString("autofill.logins.details.address", value:"Website URL", comment: "Address label for login details on autofill") public static let autofillLoginDetailsNotes = NSLocalizedString("autofill.logins.details.notes", value:"Notes", comment: "Notes label for login details on autofill") public static let autofillEmptyViewTitle = NSLocalizedString("autofill.logins.empty-view.title", value:"No passwords saved yet", comment: "Title for view displayed when autofill has no items") - public static let autofillEmptyViewSubtitle = NSLocalizedString("autofill.logins.empty-view.subtitle", value:"Passwords from other browsers or apps can be imported using the desktop version of the DuckDuckGo browser.", comment: "Subtitle for view displayed when no autofill passwords have been saved") + public static let autofillEmptyViewSubtitle = NSLocalizedString("autofill.logins.empty-view.subtitle.first.paragraph", value:"You can import saved passwords from another browser into DuckDuckGo.", comment: "Subtitle for view displayed when no autofill passwords have been saved") public static let autofillEmptyViewButtonTitle = NSLocalizedString("autofill.logins.empty-view.button.title", value:"Import Passwords", comment: "Title for button to Import Passwords when autofill has no items") + public static let autofillLearnMoreLinkTitle = NSLocalizedString("autofill.learn.more.link.title", value: "Learn More", comment: "A link that takes the user to the DuckDuckGo help pages explaining password managers") + public static let autofillSearchNoResultTitle = NSLocalizedString("autofill.logins.search.no-results.title", value:"No Results", comment: "Title displayed when there are no results on Autofill search") public static func autofillSearchNoResultSubtitle(for query: String) -> String { let message = NSLocalizedString("autofill.logins.search.no-results.subtitle", value: "for '%@'", comment: "Subtitle displayed when there are no results on Autofill search, example : No Result (Title) for Duck (Subtitle)") @@ -726,6 +728,8 @@ But if you *do* want a peek under the hood, you can find more information about public static let autofillLoginListTitle = NSLocalizedString("autofill.logins.list.title", value:"Passwords", comment: "Title for screen listing autofill logins") public static let autofillLoginListSearchPlaceholder = NSLocalizedString("autofill.logins.list.search-placeholder", value:"Search passwords", comment: "Placeholder for search field on autofill login listing") public static let autofillLoginListSuggested = NSLocalizedString("autofill.logins.list.suggested", value:"Suggested", comment: "Section title for group of suggested saved logins") + public static let autofillLoginListSettingsFooterMarkdown = NSLocalizedString("autofill.logins.list.settings.footer.markdown", value: "Passwords are encrypted. Nobody but you can see them, not even us. [Learn More](ddgQuickLink://duckduckgo.com/duckduckgo-help-pages/sync-and-backup/password-manager-security/)", comment: "Subtext under Autofill Settings briefly explaining security to alleviate user concerns. Has a URL link by clicking Learn More.") + public static let autofillLoginListSettingsFooterFallback = NSLocalizedString("autofill.logins.list.settings.footer.fallback", value: "Passwords are encrypted. Nobody but you can see them, not even us.", comment: "Subtext under Autofill Settings briefly explaining security to alleviate user concerns.") public static let autofillResetNeverSavedActionTitle = NSLocalizedString("autofill.logins.list.never.saved.reset.action.title", value:"If you reset excluded sites, you will be prompted to save your password next time you sign in to any of these sites.", comment: "Alert title") public static let autofillResetNeverSavedActionConfirmButton = NSLocalizedString("autofill.logins.list.never.saved.reset.action.confirm", value: "Reset Excluded Sites", comment: "Confirm button to reset list of never saved sites") diff --git a/DuckDuckGo/en.lproj/Localizable.strings b/DuckDuckGo/en.lproj/Localizable.strings index 1bdd754039..affd90745c 100644 --- a/DuckDuckGo/en.lproj/Localizable.strings +++ b/DuckDuckGo/en.lproj/Localizable.strings @@ -322,6 +322,9 @@ /* Disable action for alert when asking the user if they want to keep using autofill */ "autofill.keep-enabled.alert.disable" = "Disable"; +/* A link that takes the user to the DuckDuckGo help pages explaining password managers */ +"autofill.learn.more.link.title" = "Learn More"; + /* Button displayed after saving/updating an autofill login that takes the user to the saved login */ "autofill.login-save-action-button.toast" = "View"; @@ -416,7 +419,7 @@ "autofill.logins.empty-view.button.title" = "Import Passwords"; /* Subtitle for view displayed when no autofill passwords have been saved */ -"autofill.logins.empty-view.subtitle" = "Passwords from other browsers or apps can be imported using the desktop version of the DuckDuckGo browser."; +"autofill.logins.empty-view.subtitle.first.paragraph" = "You can import saved passwords from another browser into DuckDuckGo."; /* Title for view displayed when autofill has no items */ "autofill.logins.empty-view.title" = "No passwords saved yet"; @@ -460,6 +463,12 @@ /* Placeholder for search field on autofill login listing */ "autofill.logins.list.search-placeholder" = "Search passwords"; +/* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. */ +"autofill.logins.list.settings.footer.fallback" = "Passwords are encrypted. Nobody but you can see them, not even us."; + +/* Subtext under Autofill Settings briefly explaining security to alleviate user concerns. Has a URL link by clicking Learn More. */ +"autofill.logins.list.settings.footer.markdown" = "Passwords are encrypted. Nobody but you can see them, not even us. [Learn More](ddgQuickLink://duckduckgo.com/duckduckgo-help-pages/sync-and-backup/password-manager-security/)"; + /* Section title for group of suggested saved logins */ "autofill.logins.list.suggested" = "Suggested"; @@ -578,12 +587,12 @@ /* CTA displayed on modal asking if the user never wants to be prompted to save a login for this website agin */ "autofill.save-login.never-prompt.CTA" = "Never Ask for This Site"; -/* Message displayed on modal asking for the user to save the login for the first time */ -"autofill.save-login.new-user.message" = "DuckDuckGo Passwords & Autofill stores passwords securely on your device."; - /* Title displayed on modal asking for the user to save the login for the first time */ "autofill.save-login.new-user.title" = "Save this password?"; +/* Message displayed on modal asking for the user to save the login for the first time */ +"autofill.save-login.security.message" = "Securely store your password on device with DuckDuckGo Passwords & Autofill."; + /* Title displayed on modal asking for the user to save the login */ "autofill.save-login.title" = "Save password?";