diff --git a/pass.xcodeproj/project.pbxproj b/pass.xcodeproj/project.pbxproj index 8821919..de182dd 100644 --- a/pass.xcodeproj/project.pbxproj +++ b/pass.xcodeproj/project.pbxproj @@ -38,6 +38,7 @@ A2A61C201EEFABAD00CFE063 /* UtilsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2A61C1F1EEFABAD00CFE063 /* UtilsExtension.swift */; }; A2A61C2C1EEFDF3300CFE063 /* ExtensionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2A61C2B1EEFDF3300CFE063 /* ExtensionViewController.swift */; }; A2A7813F1E97DBD9001311F5 /* QRScannerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2A7813E1E97DBD9001311F5 /* QRScannerController.swift */; }; + A2BEC1BB207D2EFE00F3051C /* UIViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2BEC1BA207D2EFE00F3051C /* UIViewExtension.swift */; }; A2C532BB201E5A9600DB9F53 /* PasscodeLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2C532BA201E5A9600DB9F53 /* PasscodeLock.swift */; }; A2C532BE201E5AA100DB9F53 /* PasscodeLockViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2C532BC201E5AA000DB9F53 /* PasscodeLockViewController.swift */; }; A2C532BF201E5AA100DB9F53 /* PasscodeLockPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2C532BD201E5AA100DB9F53 /* PasscodeLockPresenter.swift */; }; @@ -200,6 +201,7 @@ A2A61C2B1EEFDF3300CFE063 /* ExtensionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExtensionViewController.swift; sourceTree = ""; }; A2A7813E1E97DBD9001311F5 /* QRScannerController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QRScannerController.swift; sourceTree = ""; }; A2BC54C71EEE5669001FAFBD /* Objective-CBridgingHeader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "Objective-CBridgingHeader.h"; sourceTree = ""; }; + A2BEC1BA207D2EFE00F3051C /* UIViewExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = UIViewExtension.swift; path = Helpers/UIViewExtension.swift; sourceTree = ""; }; A2C532BA201E5A9600DB9F53 /* PasscodeLock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PasscodeLock.swift; path = Models/PasscodeLock.swift; sourceTree = ""; }; A2C532BC201E5AA000DB9F53 /* PasscodeLockViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PasscodeLockViewController.swift; path = Controllers/PasscodeLockViewController.swift; sourceTree = ""; }; A2C532BD201E5AA100DB9F53 /* PasscodeLockPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PasscodeLockPresenter.swift; path = Controllers/PasscodeLockPresenter.swift; sourceTree = ""; }; @@ -410,6 +412,7 @@ A2F4E21B1EED80160011986E /* NotificationNames.swift */, A2F4E21C1EED80160011986E /* UITextFieldExtension.swift */, A2F4E21D1EED80160011986E /* Utils.swift */, + A2BEC1BA207D2EFE00F3051C /* UIViewExtension.swift */, ); name = Helpers; sourceTree = ""; @@ -1032,6 +1035,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + A2BEC1BB207D2EFE00F3051C /* UIViewExtension.swift in Sources */, A2C532BB201E5A9600DB9F53 /* PasscodeLock.swift in Sources */, A2F4E2151EED800F0011986E /* Password.swift in Sources */, A26075AD1EEC7125005DB03E /* pass.xcdatamodeld in Sources */, diff --git a/passExtension/PasscodeExtensionDisplay.swift b/passExtension/PasscodeExtensionDisplay.swift index d0ea6c0..24258af 100644 --- a/passExtension/PasscodeExtensionDisplay.swift +++ b/passExtension/PasscodeExtensionDisplay.swift @@ -9,15 +9,35 @@ import Foundation import passKit +// cancel means cancel the extension +class PasscodeLockViewControllerForExtension: PasscodeLockViewController { + var originalExtensionContest: NSExtensionContext? + public convenience init(extensionContext: NSExtensionContext?) { + self.init() + originalExtensionContest = extensionContext + } + override func viewDidLoad() { + super.viewDidLoad() + cancelButton?.removeTarget(nil, action: nil, for: .allEvents) + cancelButton?.addTarget(self, action: #selector(cancelExtension), for: .touchUpInside) + } + @objc func cancelExtension() { + originalExtensionContest?.completeRequest(returningItems: [], completionHandler: nil) + } +} + class PasscodeExtensionDisplay { private var isPasscodePresented = false - private let passcodeLockVC: PasscodeLockViewController + private let passcodeLockVC: PasscodeLockViewControllerForExtension + private let extensionContext: NSExtensionContext? init(extensionContext: NSExtensionContext?) { - passcodeLockVC = PasscodeLockViewController() + self.extensionContext = extensionContext + passcodeLockVC = PasscodeLockViewControllerForExtension(extensionContext: extensionContext) passcodeLockVC.dismissCompletionCallback = { [weak self] in self?.dismiss() } + passcodeLockVC.setCancellable(true) } // present the passcode lock view if passcode is set and the view controller is not presented diff --git a/passKit/Controllers/PasscodeLockViewController.swift b/passKit/Controllers/PasscodeLockViewController.swift index b433c99..7be4362 100644 --- a/passKit/Controllers/PasscodeLockViewController.swift +++ b/passKit/Controllers/PasscodeLockViewController.swift @@ -21,8 +21,10 @@ open class PasscodeLockViewController: UIViewController, UITextFieldDelegate { weak var passcodeWrongAttemptsLabel: UILabel? weak var passcodeTextField: UITextField? weak var biometryAuthButton: UIButton? + open weak var cancelButton: UIButton? var passcodeFailedAttempts = 0 + var isCancellable: Bool = false open override func loadView() { super.loadView() @@ -80,6 +82,16 @@ open class PasscodeLockViewController: UIViewController, UITextFieldDelegate { } } + let cancelButton = UIButton(type: .custom) + cancelButton.setTitle("Cancel", for: .normal) + cancelButton.setTitleColor(Globals.blue, for: .normal) + cancelButton.addTarget(self, action: #selector(passcodeLockDidCancel), for: .touchUpInside) + cancelButton.isHidden = !self.isCancellable + cancelButton.translatesAutoresizingMaskIntoConstraints = false + cancelButton.contentHorizontalAlignment = UIControlContentHorizontalAlignment.left + self.view.addSubview(cancelButton) + self.cancelButton = cancelButton + NSLayoutConstraint.activate([ passcodeTextField.widthAnchor.constraint(equalToConstant: 300), passcodeTextField.heightAnchor.constraint(equalToConstant: 40), @@ -99,7 +111,12 @@ open class PasscodeLockViewController: UIViewController, UITextFieldDelegate { biometryAuthButton.widthAnchor.constraint(equalToConstant: 150), biometryAuthButton.heightAnchor.constraint(equalToConstant: 40), biometryAuthButton.centerXAnchor.constraint(equalTo: self.view.centerXAnchor), - biometryAuthButton.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -40), + biometryAuthButton.bottomAnchor.constraint(equalTo: self.view.safeBottomAnchor, constant: -40), + // cancel (top-left of the screen) + cancelButton.widthAnchor.constraint(equalToConstant: 150), + cancelButton.heightAnchor.constraint(equalToConstant: 40), + cancelButton.topAnchor.constraint(equalTo: self.view.safeTopAnchor), + cancelButton.leftAnchor.constraint(equalTo: self.view.safeLeftAnchor, constant: 20) ]) } @@ -144,11 +161,11 @@ open class PasscodeLockViewController: UIViewController, UITextFieldDelegate { dismissPasscodeLock(completionHandler: successCallback) } - open func passcodeLockDidCancel() { + @objc func passcodeLockDidCancel() { dismissPasscodeLock(completionHandler: cancelCallback) } - @objc public func bioButtonPressedAction(_ uiButton: UIButton) { + @objc func bioButtonPressedAction(_ uiButton: UIButton) { let myContext = LAContext() let myLocalizedReasonString = "Authentication is needed to access Pass." var authError: NSError? @@ -182,9 +199,14 @@ open class PasscodeLockViewController: UIViewController, UITextFieldDelegate { return true } - @objc public func passcodeTextFieldDidChange(_ textField: UITextField) { + @objc func passcodeTextFieldDidChange(_ textField: UITextField) { if PasscodeLock.shared.check(passcode: textField.text ?? "") { self.passcodeLockDidSucceed() } } + + public func setCancellable(_ isCancellable: Bool) { + self.isCancellable = isCancellable + cancelButton?.isHidden = !isCancellable + } } diff --git a/passKit/Helpers/UIViewExtension.swift b/passKit/Helpers/UIViewExtension.swift new file mode 100644 index 0000000..649b032 --- /dev/null +++ b/passKit/Helpers/UIViewExtension.swift @@ -0,0 +1,45 @@ +// +// UIViewExtension.swift +// passKit +// +// Created by Yishi Lin on 2018/4/11. +// Copyright © 2018 Yishi Lin. All rights reserved. +// + +import Foundation + +extension UIView { + + // Save anchors: https://stackoverflow.com/questions/46317061/use-safe-area-layout-programmatically + var safeTopAnchor: NSLayoutYAxisAnchor { + if #available(iOS 11.0, *) { + return self.safeAreaLayoutGuide.topAnchor + } else { + return self.topAnchor + } + } + + var safeLeftAnchor: NSLayoutXAxisAnchor { + if #available(iOS 11.0, *){ + return self.safeAreaLayoutGuide.leftAnchor + } else { + return self.leftAnchor + } + } + + var safeRightAnchor: NSLayoutXAxisAnchor { + if #available(iOS 11.0, *){ + return self.safeAreaLayoutGuide.rightAnchor + } else { + return self.rightAnchor + } + } + + var safeBottomAnchor: NSLayoutYAxisAnchor { + if #available(iOS 11.0, *) { + return self.safeAreaLayoutGuide.bottomAnchor + } else { + return self.bottomAnchor + } + } +}