Skip to content

Commit

Permalink
[feature/biometrical-button] Biometrical Authentication Button (#1098)
Browse files Browse the repository at this point in the history
* #1004 added biometrical unlock button, which appears by default as fallback, if biometrical unlock does not work (in file provider or not detected)

* - added fallback image for face-id too
- added changelog entry

* Calens changelog updated

* - fixed visibility of biometrical button in edit mode and, if feature is not enabled
- removed unneeded code

* - AppLockManager: add new property biometricCancelLabel to provide a label for the Cancel-option when presenting biometric authentication
- FileProviderUI / DocumentActionViewController: use new property to display "Cancel" instead of "Enter code" for biometric authentication

Co-authored-by: hosy <hosy@users.noreply.github.com>
Co-authored-by: Felix Schwarz <fs-git@iospirit.com>
  • Loading branch information
3 people committed Feb 23, 2022
1 parent a1f8e63 commit b6032f4
Show file tree
Hide file tree
Showing 12 changed files with 160 additions and 11 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,19 @@ ownCloud admins and users.
Summary
-------

* Change - Biometrical Authentication Button: [#1004](https://github.com/owncloud/ios-app/issues/1004)
* Change - Rename Account (without re-authentication): [#972](https://github.com/owncloud/ios-app/issues/972)

Details
-------

* Change - Biometrical Authentication Button: [#1004](https://github.com/owncloud/ios-app/issues/1004)

Added biometrical authentication button to provide a fallback for the fileprovider or app, if
the automatically biometrical unlock does not work, or the user cancel the biometrical
authentication flow.

https://github.com/owncloud/ios-app/issues/1004
* Change - Rename Account (without re-authentication): [#972](https://github.com/owncloud/ios-app/issues/972)

Check if only the account name was changed in edit mode: save and dismiss without
Expand Down
5 changes: 5 additions & 0 deletions changelog/unreleased/1004
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Change: Biometrical Authentication Button

Added biometrical authentication button to provide a fallback for the fileprovider or app, if the automatically biometrical unlock does not work, or the user cancel the biometrical authentication flow.

https://github.com/owncloud/ios-app/issues/1004
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ class DocumentActionViewController: FPUIActionExtensionViewController {
override func prepare(forError error: Error) {
if AppLockManager.supportedOnDevice {
AppLockManager.shared.passwordViewHostViewController = self
AppLockManager.shared.biometricCancelLabel = "Cancel".localized
AppLockManager.shared.cancelAction = { [weak self] in
self?.complete(cancelWith: NSError(domain: FPUIErrorDomain, code: Int(FPUIExtensionErrorCode.userCancelled.rawValue), userInfo: nil))
}
Expand Down
6 changes: 3 additions & 3 deletions ownCloud/Resources/Assets.xcassets/Contents.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
"author" : "xcode",
"version" : 1
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "biometrical-faceid.pdf",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"images" : [
{
"filename" : "biometrical.pdf",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}
Binary file not shown.
23 changes: 20 additions & 3 deletions ownCloudAppShared/AppLock/AppLockManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,14 @@ public class AppLockManager: NSObject {
self.userDefaults.set(newValue, forKey: "applock-locked-until-date")
}
}
private var biometricalAuthenticationSucceeded: Bool {
get {
return userDefaults.bool(forKey: "applock-biometrical-authentication-succeeded")
}
set(newValue) {
self.userDefaults.set(newValue, forKey: "applock-biometrical-authentication-succeeded")
}
}

private let maximumPasscodeAttempts: Int = 3
private let powBaseDelay: Double = 1.5
Expand Down Expand Up @@ -146,7 +154,7 @@ public class AppLockManager: NSObject {
lockscreenOpen = true

// Show biometrical
if !forceShow, !self.shouldDisplayCountdown {
if !forceShow, !self.shouldDisplayCountdown, self.biometricalAuthenticationSucceeded {
showBiometricalAuthenticationInterface(context: context)
}
}
Expand Down Expand Up @@ -187,6 +195,8 @@ public class AppLockManager: NSObject {
private var passcodeControllerByWindow : NSMapTable<ThemeWindow, PasscodeViewController> = NSMapTable.weakToStrongObjects()
private var applockWindowByWindow : NSMapTable<ThemeWindow, AppLockWindow> = NSMapTable.weakToStrongObjects()

open var biometricCancelLabel : String?

open var cancelAction : (() -> Void)?
open var successAction : (() -> Void)?

Expand Down Expand Up @@ -282,7 +292,12 @@ public class AppLockManager: NSObject {
func passwordViewController() -> PasscodeViewController {
var passcodeViewController : PasscodeViewController

passcodeViewController = PasscodeViewController(completionHandler: { (viewController: PasscodeViewController, passcode: String) in
passcodeViewController = PasscodeViewController(biometricalHandler: { (passcodeViewController) in
if !self.shouldDisplayCountdown {
let context = LAContext()
self.showBiometricalAuthenticationInterface(context: context)
}
}, completionHandler: { (viewController: PasscodeViewController, passcode: String) in
self.attemptUnlock(with: passcode, passcodeViewController: viewController)
}, requiredLength: AppLockManager.shared.passcode?.count ?? AppLockSettings.shared.requiredPasscodeDigits)

Expand Down Expand Up @@ -452,7 +467,7 @@ public class AppLockManager: NSObject {
}
}

context.localizedCancelTitle = "Enter code".localized
context.localizedCancelTitle = biometricCancelLabel ?? "Enter code".localized
context.localizedFallbackTitle = ""

self.biometricalAuthenticationInterfaceShown = true
Expand All @@ -461,6 +476,7 @@ public class AppLockManager: NSObject {
self.biometricalAuthenticationInterfaceShown = false

if success {
self.biometricalAuthenticationSucceeded = true
// Fill the passcode dots
OnMainThread {
self.performPasscodeViewControllerUpdates { (passcodeViewController) in
Expand All @@ -478,6 +494,7 @@ public class AppLockManager: NSObject {
self.attemptUnlock(with: self.passcode)
}
} else {
self.biometricalAuthenticationSucceeded = false
if let error = error {
switch error {
case LAError.biometryLockout:
Expand Down
26 changes: 25 additions & 1 deletion ownCloudAppShared/AppLock/PasscodeViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@
*/

import UIKit
import ownCloudApp
import LocalAuthentication

public typealias PasscodeViewControllerCancelHandler = ((_ passcodeViewController: PasscodeViewController) -> Void)
public typealias PasscodeViewControllerBiometricalHandler = ((_ passcodeViewController: PasscodeViewController) -> Void)
public typealias PasscodeViewControllerCompletionHandler = ((_ passcodeViewController: PasscodeViewController, _ passcode: String) -> Void)

public class PasscodeViewController: UIViewController, Themeable {
Expand All @@ -39,6 +42,8 @@ public class PasscodeViewController: UIViewController, Themeable {
@IBOutlet private var keypadButtons: [ThemeRoundedButton]?
@IBOutlet private var deleteButton: ThemeButton?
@IBOutlet public var cancelButton: ThemeButton?
@IBOutlet public var biometricalButton: ThemeButton?
@IBOutlet public var biometricalImageView: UIImageView?
@IBOutlet public var compactHeightPasscodeTextField: UITextField?

// MARK: - Properties
Expand Down Expand Up @@ -109,6 +114,15 @@ public class PasscodeViewController: UIViewController, Themeable {
}
}

var biometricalButtonHidden: Bool = false {
didSet {
biometricalButton?.isEnabled = biometricalButtonHidden
biometricalButton?.isHidden = !biometricalButtonHidden
biometricalImageView?.isHidden = !biometricalButtonHidden
biometricalImageView?.image = LAContext().biometricsAuthenticationImage()
}
}

var hasCompactHeight: Bool {
if self.traitCollection.verticalSizeClass == .compact {
return true
Expand All @@ -119,11 +133,13 @@ public class PasscodeViewController: UIViewController, Themeable {

// MARK: - Handlers
public var cancelHandler: PasscodeViewControllerCancelHandler?
public var biometricalHandler: PasscodeViewControllerBiometricalHandler?
public var completionHandler: PasscodeViewControllerCompletionHandler?

// MARK: - Init
public init(cancelHandler: PasscodeViewControllerCancelHandler? = nil, completionHandler: @escaping PasscodeViewControllerCompletionHandler, hasCancelButton: Bool = true, keypadButtonsEnabled: Bool = true, requiredLength: Int) {
public init(cancelHandler: PasscodeViewControllerCancelHandler? = nil, biometricalHandler: PasscodeViewControllerBiometricalHandler? = nil, completionHandler: @escaping PasscodeViewControllerCompletionHandler, hasCancelButton: Bool = true, keypadButtonsEnabled: Bool = true, requiredLength: Int) {
self.cancelHandler = cancelHandler
self.biometricalHandler = biometricalHandler
self.completionHandler = completionHandler
self.keypadButtonsEnabled = keypadButtonsEnabled
self.cancelButtonHidden = hasCancelButton
Expand Down Expand Up @@ -157,6 +173,7 @@ public class PasscodeViewController: UIViewController, Themeable {
self.screenBlurringEnabled = { self.screenBlurringEnabled }()
self.errorMessageLabel?.minimumScaleFactor = 0.5
self.errorMessageLabel?.adjustsFontSizeToFitWidth = true
self.biometricalButtonHidden = !(!AppLockSettings.shared.biometricalSecurityEnabled || self.cancelButtonHidden)
updateKeypadButtons()

if #available(iOS 13.4, *) {
Expand All @@ -165,6 +182,7 @@ public class PasscodeViewController: UIViewController, Themeable {
}
PointerEffect.install(on: cancelButton!, effectStyle: .highlight)
PointerEffect.install(on: deleteButton!, effectStyle: .highlight)
PointerEffect.install(on: biometricalButton!, effectStyle: .highlight)
}
}

Expand Down Expand Up @@ -283,6 +301,10 @@ public class PasscodeViewController: UIViewController, Themeable {
cancelHandler?(self)
}

@IBAction func biometricalAction(_ sender: UIButton) {
biometricalHandler?(self)
}

// MARK: - Themeing
public override var preferredStatusBarStyle : UIStatusBarStyle {
if VendorServices.shared.isBranded {
Expand Down Expand Up @@ -311,6 +333,8 @@ public class PasscodeViewController: UIViewController, Themeable {

deleteButton?.themeColorCollection = ThemeColorPairCollection(fromPair: ThemeColorPair(foreground: collection.neutralColors.normal.background, background: .clear))

biometricalImageView?.tintColor = collection.tintColor

cancelButton?.applyThemeCollection(collection, itemStyle: .defaultForItem)
}
}
Expand Down
34 changes: 32 additions & 2 deletions ownCloudAppShared/AppLock/PasscodeViewController.xib
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="18122" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="19529" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina4_0" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18093"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19519"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="PasscodeViewController" customModule="ownCloudAppShared" customModuleProvider="target">
<connections>
<outlet property="backgroundBlurView" destination="ZQ4-NE-Hrm" id="GBv-lU-WeS"/>
<outlet property="biometricalButton" destination="M8y-Pc-oda" id="VZj-jp-Ioo"/>
<outlet property="biometricalImageView" destination="uLV-Sc-Alc" id="JbF-51-aGp"/>
<outlet property="cancelButton" destination="g0I-oo-iOZ" id="vc0-3w-JYu"/>
<outlet property="compactHeightPasscodeTextField" destination="SOo-MB-t0E" id="BWm-9n-6ao"/>
<outlet property="deleteButton" destination="F9C-Vg-uCq" id="Dw1-Fd-gaD"/>
Expand Down Expand Up @@ -199,6 +201,21 @@
<action selector="appendDigit:" destination="-1" eventType="touchUpInside" id="fxw-0Q-s08"/>
</connections>
</button>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="faceid" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="uLV-Sc-Alc">
<rect key="frame" x="8" y="265.5" width="50" height="38.5"/>
</imageView>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="M8y-Pc-oda" customClass="ThemeRoundedButton" customModule="ownCloudAppShared" customModuleProvider="target">
<rect key="frame" x="8" y="255" width="60" height="60"/>
<accessibility key="accessibilityConfiguration" identifier="number0Button"/>
<constraints>
<constraint firstAttribute="width" secondItem="M8y-Pc-oda" secondAttribute="height" multiplier="1:1" id="47x-wi-FWl"/>
<constraint firstAttribute="height" relation="lessThanOrEqual" constant="700" id="ZuU-m8-h8R"/>
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="60" id="vWA-oc-OPO"/>
</constraints>
<connections>
<action selector="biometricalAction:" destination="-1" eventType="touchUpInside" id="eXX-Zz-g0B"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="F9C-Vg-uCq" customClass="ThemeRoundedButton" customModule="ownCloudAppShared" customModuleProvider="target">
<rect key="frame" x="170" y="255" width="60" height="60"/>
<accessibility key="accessibilityConfiguration" identifier="deleteButton"/>
Expand All @@ -215,22 +232,29 @@
</button>
</subviews>
<constraints>
<constraint firstItem="M8y-Pc-oda" firstAttribute="top" secondItem="fMx-64-bYq" secondAttribute="bottom" constant="25" id="0m2-MP-6Yy"/>
<constraint firstItem="qMI-IV-KI3" firstAttribute="top" secondItem="fMx-64-bYq" secondAttribute="top" id="18A-ZW-jWc"/>
<constraint firstItem="hgh-7z-Fto" firstAttribute="top" secondItem="IVZ-la-esK" secondAttribute="top" id="2eX-x7-uSc"/>
<constraint firstItem="TIO-8z-Ags" firstAttribute="leading" secondItem="M8y-Pc-oda" secondAttribute="trailing" constant="17" id="2mW-yz-103"/>
<constraint firstItem="gBp-H8-606" firstAttribute="top" secondItem="85k-O3-nSF" secondAttribute="top" id="3Tp-Ni-Zn9"/>
<constraint firstItem="hgh-7z-Fto" firstAttribute="leading" secondItem="gBp-H8-606" secondAttribute="leading" id="4eO-HV-3x8"/>
<constraint firstItem="85k-O3-nSF" firstAttribute="top" secondItem="C0s-hA-da3" secondAttribute="top" id="6b4-xi-l10"/>
<constraint firstItem="9xg-xz-Tph" firstAttribute="leading" secondItem="VzK-5I-Ct8" secondAttribute="leading" id="8Dd-LE-Tdo"/>
<constraint firstItem="IVZ-la-esK" firstAttribute="leading" secondItem="85k-O3-nSF" secondAttribute="leading" id="8La-7P-NEs"/>
<constraint firstItem="uLV-Sc-Alc" firstAttribute="bottom" secondItem="M8y-Pc-oda" secondAttribute="bottom" constant="-10" id="A2a-6f-VVQ"/>
<constraint firstItem="TIO-8z-Ags" firstAttribute="leading" secondItem="qMI-IV-KI3" secondAttribute="leading" id="LEo-oa-jZZ"/>
<constraint firstItem="48Q-nf-LTg" firstAttribute="leading" secondItem="gBp-H8-606" secondAttribute="leading" id="LFW-Rw-tRv"/>
<constraint firstItem="hgh-7z-Fto" firstAttribute="top" secondItem="gBp-H8-606" secondAttribute="bottom" constant="25" id="MYC-9Y-80h"/>
<constraint firstItem="qMI-IV-KI3" firstAttribute="leading" secondItem="VzK-5I-Ct8" secondAttribute="leading" id="NHk-xt-rZG"/>
<constraint firstItem="M8y-Pc-oda" firstAttribute="width" secondItem="M8y-Pc-oda" secondAttribute="height" multiplier="1:1" id="NYK-8F-LkF"/>
<constraint firstItem="VzK-5I-Ct8" firstAttribute="top" secondItem="85k-O3-nSF" secondAttribute="top" id="WBr-Xg-iqr"/>
<constraint firstItem="uLV-Sc-Alc" firstAttribute="leading" secondItem="M8y-Pc-oda" secondAttribute="leading" id="ZId-Xc-K0a"/>
<constraint firstItem="uLV-Sc-Alc" firstAttribute="trailing" secondItem="M8y-Pc-oda" secondAttribute="trailing" constant="-10" id="aJW-ss-ZVn"/>
<constraint firstItem="fMx-64-bYq" firstAttribute="leading" secondItem="85k-O3-nSF" secondAttribute="leading" id="aZi-2h-OhS"/>
<constraint firstItem="48Q-nf-LTg" firstAttribute="top" secondItem="hgh-7z-Fto" secondAttribute="bottom" constant="25" id="f3A-Aw-sE9"/>
<constraint firstItem="9xg-xz-Tph" firstAttribute="top" secondItem="IVZ-la-esK" secondAttribute="top" id="jZI-GP-nv9"/>
<constraint firstItem="F9C-Vg-uCq" firstAttribute="top" secondItem="TIO-8z-Ags" secondAttribute="top" id="jch-1Y-paT"/>
<constraint firstItem="uLV-Sc-Alc" firstAttribute="top" secondItem="M8y-Pc-oda" secondAttribute="top" constant="10" id="k3L-AD-mU9"/>
<constraint firstItem="85k-O3-nSF" firstAttribute="leading" secondItem="C0s-hA-da3" secondAttribute="leading" id="k8s-77-W6O"/>
<constraint firstAttribute="bottom" secondItem="F9C-Vg-uCq" secondAttribute="bottom" id="nPA-7A-BV0"/>
<constraint firstItem="F9C-Vg-uCq" firstAttribute="top" secondItem="48Q-nf-LTg" secondAttribute="bottom" constant="25" id="nUX-x9-pef"/>
Expand Down Expand Up @@ -348,6 +372,9 @@
<designable name="IVZ-la-esK">
<size key="intrinsicContentSize" width="60" height="39"/>
</designable>
<designable name="M8y-Pc-oda">
<size key="intrinsicContentSize" width="60" height="39"/>
</designable>
<designable name="TIO-8z-Ags">
<size key="intrinsicContentSize" width="60" height="39"/>
</designable>
Expand All @@ -370,4 +397,7 @@
<size key="intrinsicContentSize" width="60" height="39"/>
</designable>
</designables>
<resources>
<image name="faceid" catalog="system" width="128" height="115"/>
</resources>
</document>
23 changes: 21 additions & 2 deletions ownCloudAppShared/UIKit Extension/LAContext+Extension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@
*/

import LocalAuthentication
import UIKit

extension LAContext {

public func supportedBiometricsAuthenticationName() -> String? {

if canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) {
switch self.biometryType {
case .faceID : return "Face ID".localized
Expand All @@ -30,5 +30,24 @@ extension LAContext {
}
}
return nil
}
}

public func biometricsAuthenticationImage() -> UIImage? {
if canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) {
switch self.biometryType {
case .faceID : if #available(iOSApplicationExtension 13.0, *) {
return UIImage(systemName: "faceid")
} else {
return UIImage(named: "biometrical-faceid")
}
case .touchID: if #available(iOSApplicationExtension 13.0, *) {
return UIImage(systemName: "touchid")
} else {
return UIImage(named: "biometrical-touchid")
}
case .none: return nil
}
}
return nil
}
}

0 comments on commit b6032f4

Please sign in to comment.