Files
passforios-mirror/passKit/Controllers/PasscodeLockViewController.swift
Danny Mösch d9bd0f3014 Update SwiftLint and SwiftFormat (#613)
* Update Swift version used by SwiftFormat

* Update SwiftLint version

* Rely on new virtual 'all' rule in SwiftLint

* Enable SwiftLint rule 'direct_return' rule and fix all violations

* Enable SwiftLint rule 'shorthand_optional_binding' rule and fix all violations

* Enable SwiftLint rule 'blanket_disable_command' rule and fix all violations
2023-04-23 13:01:37 -07:00

243 lines
11 KiB
Swift

//
// PasscodeLockViewController.swift
// passKit
//
// Created by Yishi Lin on 10/04/2018.
// Copyright © 2018 Yishi Lin. All rights reserved.
//
// Inspired by SwiftPasscodeLock created by Yanko Dimitrov.
//
import LocalAuthentication
import UIKit
open class PasscodeLockViewController: UIViewController, UITextFieldDelegate {
open var dismissCompletionCallback: (() -> Void)?
open var successCallback: (() -> Void)?
open var cancelCallback: (() -> Void)?
weak var passcodeTextField: UITextField?
weak var biometryAuthButton: UIButton?
open weak var cancelButton: UIButton?
var isCancellable = false
private let passwordStore = PasswordStore.shared
override open func loadView() {
super.loadView()
let passcodeTextField = UITextField()
passcodeTextField.borderStyle = UITextField.BorderStyle.roundedRect
passcodeTextField.placeholder = "EnterPasscode".localize()
passcodeTextField.isSecureTextEntry = true
passcodeTextField.clearButtonMode = UITextField.ViewMode.whileEditing
passcodeTextField.delegate = self
passcodeTextField.addTarget(self, action: #selector(passcodeTextFieldDidChange), for: UIControl.Event.editingChanged)
passcodeTextField.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(passcodeTextField)
self.passcodeTextField = passcodeTextField
view.backgroundColor = Colors.systemBackground
passcodeTextField.backgroundColor = Colors.secondarySystemBackground
passcodeTextField.textColor = Colors.secondaryLabel
let biometryAuthButton = UIButton(type: .custom)
biometryAuthButton.setTitle("", for: .normal)
biometryAuthButton.setTitleColor(Colors.systemBlue, for: .normal)
biometryAuthButton.addTarget(self, action: #selector(bioButtonPressedAction), for: .touchUpInside)
biometryAuthButton.isHidden = true
biometryAuthButton.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(biometryAuthButton)
self.biometryAuthButton = biometryAuthButton
let myContext = LAContext()
var authError: NSError?
if myContext.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &authError) {
if myContext.biometryType == .faceID {
biometryAuthButton.setTitle("FaceId".localize(), for: .normal)
} else {
biometryAuthButton.setTitle("TouchId".localize(), for: .normal)
}
biometryAuthButton.titleLabel?.font = UIFont.systemFont(ofSize: UIFont.systemFontSize)
biometryAuthButton.isHidden = false
}
let forgotPasscodeButton = UIButton(type: .custom)
forgotPasscodeButton.setTitle("ForgotYourPasscode?".localize(), for: .normal)
forgotPasscodeButton.titleLabel?.font = UIFont.systemFont(ofSize: UIFont.systemFontSize)
forgotPasscodeButton.setTitleColor(Colors.systemBlue, for: .normal)
forgotPasscodeButton.addTarget(self, action: #selector(forgotPasscodeButtonPressedAction), for: .touchUpInside)
// hide the forgotPasscodeButton if the native app is running
forgotPasscodeButton.isHidden = isCancellable
forgotPasscodeButton.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(forgotPasscodeButton)
let cancelButton = UIButton(type: .custom)
cancelButton.setTitle("Cancel".localize(), for: .normal)
cancelButton.setTitleColor(Colors.systemBlue, for: .normal)
cancelButton.addTarget(self, action: #selector(passcodeLockDidCancel), for: .touchUpInside)
cancelButton.isHidden = !isCancellable
cancelButton.translatesAutoresizingMaskIntoConstraints = false
cancelButton.contentHorizontalAlignment = UIControl.ContentHorizontalAlignment.left
view.addSubview(cancelButton)
self.cancelButton = cancelButton
// Display the Pass icon in the middle of the screen
let bundle = Bundle(for: Self.self)
let appIcon = UIImage(named: "PasscodeLockViewIcon", in: bundle, compatibleWith: nil)
let appIconSize = (appIcon?.size.height) ?? 0
let appIconView = UIImageView(image: appIcon)
appIconView.translatesAutoresizingMaskIntoConstraints = false
appIconView.layer.cornerRadius = appIconSize / 5
appIconView.layer.masksToBounds = true
view?.addSubview(appIconView)
NSLayoutConstraint.activate(
[
passcodeTextField.widthAnchor.constraint(equalToConstant: 250),
passcodeTextField.heightAnchor.constraint(equalToConstant: 40),
passcodeTextField.centerXAnchor.constraint(equalTo: view.centerXAnchor),
passcodeTextField.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: -20),
// above passocde
appIconView.widthAnchor.constraint(equalToConstant: appIconSize),
appIconView.heightAnchor.constraint(equalToConstant: appIconSize),
appIconView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
appIconView.bottomAnchor.constraint(equalTo: passcodeTextField.topAnchor, constant: -appIconSize),
// below passcode
biometryAuthButton.widthAnchor.constraint(equalToConstant: 250),
biometryAuthButton.heightAnchor.constraint(equalToConstant: 40),
biometryAuthButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
biometryAuthButton.topAnchor.constraint(equalTo: passcodeTextField.bottomAnchor),
// cancel (top-left of the screen)
cancelButton.widthAnchor.constraint(equalToConstant: 150),
cancelButton.heightAnchor.constraint(equalToConstant: 40),
cancelButton.topAnchor.constraint(equalTo: view.safeTopAnchor),
cancelButton.leftAnchor.constraint(equalTo: view.safeLeftAnchor, constant: 20),
// bottom of the screen
forgotPasscodeButton.widthAnchor.constraint(equalToConstant: 250),
forgotPasscodeButton.heightAnchor.constraint(equalToConstant: 40),
forgotPasscodeButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
forgotPasscodeButton.bottomAnchor.constraint(equalTo: view.safeBottomAnchor, constant: -40),
]
)
// dismiss keyboard when tapping anywhere
let tap = UITapGestureRecognizer(target: view, action: #selector(UIView.endEditing))
view.addGestureRecognizer(tap)
}
override open func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if let biometryAuthButton {
bioButtonPressedAction(biometryAuthButton)
}
}
private func dismissPasscodeLock(completionHandler: (() -> Void)? = nil) {
// clean up the textfield
DispatchQueue.main.async {
self.passcodeTextField?.text = ""
}
completionHandler?()
// pop
if presentingViewController?.presentedViewController == self {
// if presented as modal
dismiss(animated: true) { [weak self] in
self?.dismissCompletionCallback?()
}
} else {
// if pushed in a navigation controller
_ = navigationController?.popViewController(animated: true)
dismissCompletionCallback?()
}
}
// MARK: - PasscodeLockDelegate
open func passcodeLockDidSucceed() {
dismissPasscodeLock(completionHandler: successCallback)
}
@objc
func passcodeLockDidCancel() {
dismissPasscodeLock(completionHandler: cancelCallback)
}
@objc
func bioButtonPressedAction(_: UIButton) {
let myContext = LAContext()
let myLocalizedReasonString = "AuthenticationNeeded.".localize()
var authError: NSError?
if myContext.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &authError) {
myContext.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: myLocalizedReasonString) { success, _ in
if success {
DispatchQueue.main.async {
// user authenticated successfully, take appropriate action
self.passcodeLockDidSucceed()
}
}
}
}
}
@objc
func forgotPasscodeButtonPressedAction(_: UIButton) {
let alert = UIAlertController(title: "ResetPass".localize(), message: "ResetPassExplanation.".localize(), preferredStyle: UIAlertController.Style.alert)
alert.addAction(
UIAlertAction(title: "ErasePasswordStoreData".localize(), style: UIAlertAction.Style.destructive) { [unowned self] _ in
let myContext = LAContext()
// If the device passcode is not set, reset the app.
guard myContext.canEvaluatePolicy(.deviceOwnerAuthentication, error: nil) else {
passwordStore.erase()
passcodeLockDidSucceed()
return
}
// If the device passcode is set, authentication is required.
myContext.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: "ErasePasswordStoreData".localize()) { success, error in
if success {
DispatchQueue.main.async {
// User authenticated successfully, take appropriate action
self.passwordStore.erase()
self.passcodeLockDidSucceed()
}
} else {
DispatchQueue.main.async {
Utils.alert(title: "Error".localize(), message: error?.localizedDescription ?? "", controller: self)
}
}
}
}
)
alert.addAction(UIAlertAction.dismiss())
present(alert, animated: true, completion: nil)
}
override public func textFieldShouldReturn(_ textField: UITextField) -> Bool {
if textField == passcodeTextField {
if !PasscodeLock.shared.check(passcode: textField.text ?? "") {
passcodeTextField?.placeholder =
"TryAgain".localize()
passcodeTextField?.text = ""
passcodeTextField?.shake()
}
}
textField.resignFirstResponder()
return true
}
@objc
func passcodeTextFieldDidChange(_ textField: UITextField) {
if PasscodeLock.shared.check(passcode: textField.text ?? "") {
passcodeLockDidSucceed()
}
}
public func setCancellable(_ isCancellable: Bool) {
self.isCancellable = isCancellable
cancelButton?.isHidden = !isCancellable
}
}