mirror of
https://github.com/mssun/passforios.git
synced 2026-03-03 18:24:08 +01:00
* 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
243 lines
11 KiB
Swift
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
|
|
}
|
|
}
|