Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Breach Alerts Feature #7136

Merged
merged 20 commits into from
Aug 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
6110fd0
Base BreachAlertsManager with loadBreaches() and compareToBreaches() …
vphong May 26, 2020
65f6e9d
Revert "Base BreachAlertsManager with loadBreaches() and compareToBre…
vphong May 26, 2020
6325822
Base BreachAlertsManager class with loadBreaches() + compare() (#6699)
vphong May 28, 2020
995fa21
Test BreachAlertsManager.loadBreaches() and compareToBreaches() (#6715)
vphong Jun 1, 2020
189e109
Merge branch 'master' into vphong/breachalerts
vphong Jun 17, 2020
c0b16b0
Refactor LoginListViewController to MVVM (#6779)
vphong Jun 26, 2020
6d54ef8
Refactor LoginListViewController to MVVM (again) (#6871)
vphong Jun 30, 2020
ce08213
Merge branch 'main' into vphong/breachalerts
kaylagalway Jul 6, 2020
7472cee
Merge branch 'main' into vphong/breachalerts
kaylagalway Jul 10, 2020
43b9c69
Test LoginsList-related refactored classes (#6897)
vphong Jul 11, 2020
8264d1f
Incorporate BreachAlertsManager in to LoginsListViewController (#6934)
vphong Jul 16, 2020
c607822
Merge branch 'main' into vphong/breachalerts
kaylagalway Jul 16, 2020
8dab047
Add breach alert icon to Logins List cells and display if item is bre…
vphong Jul 21, 2020
db71ae0
Merge branch 'main' into vphong/breachalerts
kaylagalway Jul 21, 2020
86280c8
Fix margins within LoginListTableViewCell (#7022)
vphong Jul 27, 2020
e4f1565
FXIOS-710 ⁃ Create breach details view (#7041)
vphong Aug 6, 2020
b29cb9c
UX updates (#7127)
vphong Aug 13, 2020
ab66183
FXIOS-731 ⁃ HTTP HEAD etags to cut down on data requests from Breach …
vphong Aug 14, 2020
5081c8f
Merge branch 'main' into vphong/breachalerts
vphong Aug 14, 2020
e27ce23
Lint
vphong Aug 14, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 57 additions & 4 deletions Client.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,7 @@
<dict>
<key>BuildSystemType</key>
<string>Latest</string>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "warning icon.pdf",
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ extension BrowserViewController: TabToolbarDelegate, PhotonActionSheetProtocol {
}

func tabToolbarDidPressMenu(_ tabToolbar: TabToolbarProtocol, button: UIButton) {
var whatsNewAction: PhotonActionSheetItem? = nil
var whatsNewAction: PhotonActionSheetItem?
let showBadgeForWhatsNew = shouldShowWhatsNew()
if showBadgeForWhatsNew {
// Set the version number of the app, so the What's new will stop showing
Expand All @@ -87,7 +87,11 @@ extension BrowserViewController: TabToolbarDelegate, PhotonActionSheetProtocol {
let viewLogins: PhotonActionSheetItem? = !isLoginsButtonShowing ? nil :
PhotonActionSheetItem(title: Strings.LoginsAndPasswordsTitle, iconString: "key", iconType: .Image, iconAlignment: .left, isEnabled: true) { _, _ in
guard let navController = self.navigationController else { return }
LoginListViewController.create(authenticateInNavigationController: navController, profile: self.profile, settingsDelegate: self).uponQueue(.main) { loginsVC in
let navigationHandler: ((_ url: URL?) -> Void) = { url in
UIApplication.shared.keyWindow?.rootViewController?.dismiss(animated: true, completion: nil)
self.openURLInNewTab(url)
}
LoginListViewController.create(authenticateInNavigationController: navController, profile: self.profile, settingsDelegate: self, webpageNavigationHandler: navigationHandler).uponQueue(.main) { loginsVC in
guard let loginsVC = loginsVC else { return }
loginsVC.shownFromAppMenu = true
let navController = ThemedNavigationController(rootViewController: loginsVC)
Expand Down
91 changes: 91 additions & 0 deletions Client/Frontend/Login Management/BreachAlertsClient.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import Foundation
import Shared

/// Errors related to BreachAlertsClient and BreachAlertsManager.
struct BreachAlertsError: MaybeErrorType {
public let description: String
}
/// For mocking and testing BreachAlertsClient.
protocol BreachAlertsClientProtocol {
func fetchEtag(endpoint: BreachAlertsClient.Endpoint, profile: Profile, completion: @escaping (_ etag: String?) -> Void)
func fetchData(endpoint: BreachAlertsClient.Endpoint, profile: Profile, completion: @escaping (_ result: Maybe<Data>) -> Void)
}

/// Handles all network requests for BreachAlertsManager.
public class BreachAlertsClient: BreachAlertsClientProtocol {
private var dataTask: URLSessionDataTask?
public enum Endpoint: String {
case breachedAccounts = "https://monitor.firefox.com/hibp/breaches"
}
static let etagKey = "BreachAlertsDataEtag"
static let etagDateKey = "BreachAlertsDataDate"

/// Makes a header-only request to an endpoint and hands off the endpoint's etag to a completion handler.
func fetchEtag(endpoint: Endpoint, profile: Profile, completion: @escaping (_ etag: String?) -> Void) {
guard let url = URL(string: endpoint.rawValue) else { return }
var request = URLRequest(url: url)
request.httpMethod = "HEAD"

dataTask?.cancel()
dataTask = URLSession.shared.dataTask(with: request) { _, response, _ in
guard let response = response as? HTTPURLResponse else { return }
guard response.statusCode < 400 else {
Sentry.shared.send(message: "BreachAlerts: fetchEtag: HTTP status code: \(response.statusCode)")
completion(nil)
return
}
guard let etag = response.allHeaderFields["Etag"] as Any as? String else {
completion(nil)
assert(false)
return
}
DispatchQueue.main.async {
completion(etag)
}
}
dataTask?.resume()
}

/// Makes a network request to an endpoint and hands off the result to a completion handler.
func fetchData(endpoint: Endpoint, profile: Profile, completion: @escaping (_ result: Maybe<Data>) -> Void) {
guard let url = URL(string: endpoint.rawValue) else { return }

dataTask?.cancel()
dataTask = URLSession.shared.dataTask(with: url) { data, response, error in
guard let response = response as? HTTPURLResponse else { return }
guard response.statusCode < 400 else {
Sentry.shared.send(message: "BreachAlerts: fetchData: HTTP status code: \(response.statusCode)")
return
}
if let error = error {
completion(Maybe(failure: BreachAlertsError(description: error.localizedDescription)))
Sentry.shared.send(message: "BreachAlerts: fetchData: \(error)")
return
}
guard let data = data else {
completion(Maybe(failure: BreachAlertsError(description: "invalid data")))
Sentry.shared.send(message: "BreachAlerts: fetchData: invalid data")
assert(false)
return
}

guard let etag = response.allHeaderFields["Etag"] as Any as? String else { return }
let date = Date.now()

if profile.prefs.stringForKey(BreachAlertsClient.etagKey) != etag {
profile.prefs.setString(etag, forKey: BreachAlertsClient.etagKey)
}
if profile.prefs.timestampForKey(BreachAlertsClient.etagDateKey) != date {
profile.prefs.setTimestamp(date, forKey: BreachAlertsClient.etagDateKey)
}
DispatchQueue.main.async {
completion(Maybe(success: data))
}
}
dataTask?.resume()
}
}
208 changes: 208 additions & 0 deletions Client/Frontend/Login Management/BreachAlertsDetailView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import UIKit
import Shared

class BreachAlertsDetailView: UIView {

private let textColor = UIColor.white
private let titleIconSize: CGFloat = 24
private lazy var titleIconContainerSize: CGFloat = {
return titleIconSize + LoginTableViewCellUX.HorizontalMargin * 2
}()

lazy var titleIcon: UIImageView = {
let imageView = UIImageView(image: BreachAlertsManager.icon)
imageView.tintColor = textColor
imageView.accessibilityTraits = .image
imageView.accessibilityLabel = "Breach Alert Icon"
return imageView
}()

lazy var titleIconContainer: UIView = {
let container = UIView()
container.addSubview(titleIcon)
titleIcon.snp.makeConstraints { make in
make.width.height.equalTo(titleIconSize)
make.center.equalToSuperview()
}
container.snp.makeConstraints { make in
make.width.height.equalTo(titleIconContainerSize)
}
return container
}()

lazy var titleLabel: UILabel = {
let label = UILabel()
label.font = DynamicFontHelper.defaultHelper.DeviceFontLargeBold
label.textColor = textColor
label.text = Strings.BreachAlertsTitle
label.sizeToFit()
label.isAccessibilityElement = true
label.accessibilityTraits = .staticText
label.accessibilityLabel = Strings.BreachAlertsTitle
return label
}()

lazy var learnMoreButton: UIButton = {
let button = UIButton()
let attributes: [NSAttributedString.Key: Any] = [
.foregroundColor: UIColor.white,
.underlineStyle: NSUnderlineStyle.single.rawValue
]
let attributedText = NSMutableAttributedString(string: Strings.BreachAlertsLearnMore, attributes: attributes)
button.titleLabel?.font = DynamicFontHelper.defaultHelper.DeviceFontSmallBold
button.setAttributedTitle(attributedText, for: .normal)
button.setTitleColor(textColor, for: .normal)
button.tintColor = .white
button.isAccessibilityElement = true
button.accessibilityTraits = .button
button.accessibilityLabel = Strings.BreachAlertsLearnMore
return button
}()

lazy var titleStack: UIStackView = {
let container = UIStackView(arrangedSubviews: [titleIconContainer, titleLabel, learnMoreButton])
container.axis = .horizontal
return container
}()

lazy var breachDateLabel: UILabel = {
let label = UILabel()
label.text = Strings.BreachAlertsBreachDate
label.textColor = textColor
label.numberOfLines = 0
label.font = DynamicFontHelper.defaultHelper.DeviceFontSmallBold
label.isAccessibilityElement = true
label.accessibilityTraits = .staticText
label.accessibilityLabel = Strings.BreachAlertsBreachDate
return label
}()

lazy var descriptionLabel: UILabel = {
let label = UILabel()
label.text = Strings.BreachAlertsDescription
label.numberOfLines = 0
label.textColor = textColor
label.font = DynamicFontHelper.defaultHelper.DeviceFontSmall
label.isAccessibilityElement = true
label.accessibilityTraits = .staticText
return label
}()

lazy var goToButton: UILabel = {
let button = UILabel()
button.font = DynamicFontHelper.defaultHelper.DeviceFontSmallBold
button.textColor = textColor
button.numberOfLines = 0
button.isUserInteractionEnabled = true
button.text = Strings.BreachAlertsLink
button.isAccessibilityElement = true
button.accessibilityTraits = .button
button.accessibilityLabel = Strings.BreachAlertsLink
return button
}()

private lazy var infoStack: UIStackView = {
let stack = UIStackView(arrangedSubviews: [breachDateLabel, descriptionLabel, goToButton])
stack.distribution = .fill
stack.axis = .vertical
stack.setCustomSpacing(8.0, after: descriptionLabel)
return stack
}()

private lazy var contentStack: UIStackView = {
let stack = UIStackView(arrangedSubviews: [titleStack, infoStack])
stack.axis = .vertical
return stack
}()

override init(frame: CGRect) {
super.init(frame: frame)

self.backgroundColor = BreachAlertsManager.lightMode
self.layer.cornerRadius = 5
self.layer.masksToBounds = true

self.isAccessibilityElement = false
self.accessibilityElements = [titleLabel, learnMoreButton, breachDateLabel, descriptionLabel, goToButton]

self.addSubview(contentStack)
self.configureView(for: self.traitCollection)
}

required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func layoutSubviews() {
titleStack.snp.remakeConstraints { make in
make.leading.trailing.equalToSuperview()
}
infoStack.snp.remakeConstraints { make in
make.leading.equalToSuperview().inset(titleIconContainerSize)
make.trailing.equalToSuperview()
}
contentStack.snp.remakeConstraints { make in
make.bottom.trailing.equalToSuperview().inset(LoginTableViewCellUX.HorizontalMargin)
make.leading.top.equalToSuperview()
}
self.setContentCompressionResistancePriority(.defaultHigh, for: .vertical)
}

// Populate the view with information from a BreachRecord.
public func setup(_ breach: BreachRecord) {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"
guard let date = dateFormatter.date(from: breach.breachDate) else { return }
dateFormatter.dateStyle = .medium
self.breachDateLabel.text! += " \(dateFormatter.string(from: date))."
let goToText = Strings.BreachAlertsLink + " \(breach.domain)"
let attributes: [NSAttributedString.Key: Any] = [
.foregroundColor: UIColor.white,
.underlineStyle: NSUnderlineStyle.single.rawValue
]
let attributedText = NSMutableAttributedString(string: goToText, attributes: attributes)
self.goToButton.attributedText = attributedText
self.goToButton.sizeToFit()
self.layoutIfNeeded()

self.goToButton.accessibilityValue = breach.domain
self.breachDateLabel.accessibilityValue = "\(dateFormatter.string(from: date))."
}

// MARK: - Dynamic Type Support
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
if previousTraitCollection?.preferredContentSizeCategory != self.traitCollection.preferredContentSizeCategory {
configureView(for: self.traitCollection)
}
}

// If large fonts are enabled, set the title stack vertically.
// Else, set the title stack horizontally.
private func configureView(for traitCollection: UITraitCollection) {
let contentSize = traitCollection.preferredContentSizeCategory
if contentSize.isAccessibilityCategory {
self.titleStack.axis = .vertical
self.titleStack.alignment = .leading
self.titleLabel.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(self.titleIconSize)
}
self.learnMoreButton.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(self.titleIconSize)
}
} else {
self.titleStack.axis = .horizontal
self.titleStack.alignment = .leading
self.titleLabel.snp.makeConstraints { make in
make.centerY.equalToSuperview()
}
self.learnMoreButton.snp.makeConstraints { make in
make.centerY.equalToSuperview()
}
}
}
}
Loading