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 0000000000..b1a5584f9c Binary files /dev/null and b/DuckDuckGo/Assets.xcassets/DesignSystemIcons/16px/Lock-Solid-16.imageset/Lock-Solid-16.pdf differ diff --git a/DuckDuckGo/AutofillItemsEmptyView.swift b/DuckDuckGo/AutofillItemsEmptyView.swift index 99f50e31e5..f69e1635b6 100644 --- a/DuckDuckGo/AutofillItemsEmptyView.swift +++ b/DuckDuckGo/AutofillItemsEmptyView.swift @@ -53,7 +53,6 @@ struct AutofillItemsEmptyView: View { } .buttonStyle(PrimaryButtonStyle(fullWidth: false)) .padding(.top, 24) - } .frame(maxWidth: 300.0) .padding(.top, 16) diff --git a/DuckDuckGo/AutofillLoginSettingsListViewController.swift b/DuckDuckGo/AutofillLoginSettingsListViewController.swift index 65a3c5c59a..11e2934c0a 100644 --- a/DuckDuckGo/AutofillLoginSettingsListViewController.swift +++ b/DuckDuckGo/AutofillLoginSettingsListViewController.swift @@ -58,7 +58,9 @@ 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 @@ -66,6 +68,7 @@ final class AutofillLoginSettingsListViewController: UIViewController { return hostingController.view }() + private let lockedView = AutofillItemsLockedView() private let enableAutofillFooterView = AutofillSettingsEnableFooterView() private let emptySearchView = AutofillEmptySearchView() @@ -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 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?";