Skip to content

Commit

Permalink
Address autofill security concerns (#3321)
Browse files Browse the repository at this point in the history
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.

<!--
Before submitting a PR, please ensure you have tested the combinations
you expect the reviewer to test, then delete configurations you *know*
do not need explicit testing.

Using a simulator where a physical device is unavailable is acceptable.
-->

**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)
  • Loading branch information
graeme authored Sep 19, 2024
1 parent 7b3cca1 commit d77035f
Show file tree
Hide file tree
Showing 11 changed files with 98 additions and 28 deletions.
1 change: 1 addition & 0 deletions Core/AppURLs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")!

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "Lock-Solid-16.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}
Binary file not shown.
1 change: 0 additions & 1 deletion DuckDuckGo/AutofillItemsEmptyView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ struct AutofillItemsEmptyView: View {
}
.buttonStyle(PrimaryButtonStyle(fullWidth: false))
.padding(.top, 24)

}
.frame(maxWidth: 300.0)
.padding(.top, 16)
Expand Down
9 changes: 5 additions & 4 deletions DuckDuckGo/AutofillLoginSettingsListViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,17 @@ final class AutofillLoginSettingsListViewController: UIViewController {
}

let hostingController = UIHostingController(rootView: emptyView)
hostingController.view.frame = CGRect(origin: .zero, size: hostingController.sizeThatFits(in: UIScreen.main.bounds.size))
var size = hostingController.sizeThatFits(in: UIScreen.main.bounds.size)
size.height += 50
hostingController.view.frame = CGRect(origin: .zero, size: size)
hostingController.view.layoutIfNeeded()
hostingController.view.backgroundColor = .clear

self.tableView.tableFooterView?.frame.size.height = hostingController.view.frame.height

return hostingController.view
}()

private let lockedView = AutofillItemsLockedView()
private let enableAutofillFooterView = AutofillSettingsEnableFooterView()
private let emptySearchView = AutofillEmptySearchView()
Expand Down Expand Up @@ -854,9 +857,7 @@ extension AutofillLoginSettingsListViewController: UITableViewDelegate {

func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> 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
Expand Down
42 changes: 33 additions & 9 deletions DuckDuckGo/AutofillSettingsEnableFooterView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
//

import UIKit
import DesignResourcesKit

class AutofillSettingsEnableFooterView: UIView {

Expand All @@ -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() {
Expand All @@ -67,3 +85,9 @@ class AutofillSettingsEnableFooterView: UIView {
])
}
}

extension AutofillSettingsEnableFooterView: UITextViewDelegate {
func textViewDidChangeSelection(_ textView: UITextView) {
textView.selectedTextRange = nil
}
}
27 changes: 22 additions & 5 deletions DuckDuckGo/AutofillViews.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down
2 changes: 1 addition & 1 deletion DuckDuckGo/PasswordGenerationPromptView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions DuckDuckGo/SaveLoginView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
8 changes: 6 additions & 2 deletions DuckDuckGo/UserText.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)")
Expand All @@ -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")
Expand Down
17 changes: 13 additions & 4 deletions DuckDuckGo/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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";

Expand Down Expand Up @@ -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?";

Expand Down

0 comments on commit d77035f

Please sign in to comment.