Skip to content

Instantly share code, notes, and snippets.

@lalkrishna
Created August 24, 2020 03:55
Show Gist options
  • Save lalkrishna/a37f8bf061b23c5a7e54b983e9417046 to your computer and use it in GitHub Desktop.
Save lalkrishna/a37f8bf061b23c5a7e54b983e9417046 to your computer and use it in GitHub Desktop.
//
// LocalAuthorization.swift
// LocalAuthorization
//
// Created by lalkrishna.
//
// Copyright © 2020 Lal Krishna. All rights reserved.
//
import Foundation
import LocalAuthentication
//
// Add `NSFaceIDUsageDescription` to info.plist.
//
class LKLocalAuthentication: NSObject {
// private override init() { fatalError() }
@objc static let shared: LKLocalAuthentication = LKLocalAuthentication()
/// LAPolicy default value is `.deviceOwnerAuthentication`
public var policy = LAPolicy.deviceOwnerAuthentication
/// Timeout interval in minutes.
@objc public let timeoutInterval: TimeInterval = 60
/// Reason Message.
@objc public var reason = NSLocalizedString("To enabling user authentication using biometrics", comment: "Reason for using device authentication feature")
typealias Completion = ((Bool, String?) -> Void)
public var lastUnlockedTimeInterval: TimeInterval?
@objc var completionHandler: Completion?
@objc public var isUnlocked: Bool {
guard let lastUnlockedTimeInterval = lastUnlockedTimeInterval else { return false }
let currentInterval = Date().timeIntervalSince1970
let timeElapsed = currentInterval - lastUnlockedTimeInterval
return timeElapsed < timeoutInterval * 60
}
/// Returns bool that indicates whether the device passcode is Set.
@objc public var isAuthenticationAvailable: Bool = {
var error: NSError?
if LAContext().canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) {
return true
} else {
guard let laError = error as? LAError else { return false }
if laError.code == LAError.passcodeNotSet {
return false
} else {
return true
}
}
}()
@objc public var availableMethodText: String? = {
let context = LAContext()
var error: NSError?
let evaluated = context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error)
if let laError = error {
LKLocalAuthentication.log("laError - \(laError)")
}
if #available(iOS 11.0, *) {
let type = context.biometryType
switch type {
case .touchID:
return NSLocalizedString("Touch ID", comment: "")
case .faceID:
return NSLocalizedString("Face ID", comment: "")
default:
return NSLocalizedString("Passcode", comment: "")
}
} else {
if evaluated || (error?.code != LAError.touchIDNotAvailable.rawValue) {
return NSLocalizedString("Touch ID", comment: "")
}
}
return nil
}()
// MARK: -
@objc func requestAuth() {
requestAuth(policy: policy)
}
@objc func requestAuth(completionHandler: Completion?) {
self.completionHandler = completionHandler
requestAuth(policy: policy)
}
@objc func requestAuth(reason: String, completionHandler: Completion?) {
self.reason = reason
requestAuth(completionHandler: completionHandler)
}
private func requestAuth(policy: LAPolicy) {
let authContext = LAContext()
authContext.localizedFallbackTitle = NSLocalizedString("Use Passcode", comment: "")
authContext.localizedCancelTitle = NSLocalizedString("Cancel", comment: "")
var authError: NSError?
if authContext.canEvaluatePolicy(policy, error: &authError) {
evaluatePolicy(policy, context: authContext)
} else {
guard let error = authError else { return }
LKLocalAuthentication.log("LKAuthorization Error: \(error.code)")
checkError(error)
}
}
@discardableResult
private func checkError(_ error: NSError) -> String? {
guard let error = error as? LAError else { return "Authorization Failed" }
switch error.code {
case .authenticationFailed:
// print("authenticationFailed")
requestAuth(policy: .deviceOwnerAuthentication)
// NSLocalizedString(@"User failed after a few attempts", "")
case .userFallback:
// print("userFallback")
requestAuth(policy: .deviceOwnerAuthentication)
case .userCancel:
LKLocalAuthentication.log("userCancel")
return nil
case .systemCancel:
LKLocalAuthentication.log("systemCancel")
return NSLocalizedString("System canceled auth request due to app coming to foreground or background.", comment: "");
case .passcodeNotSet:
return NSLocalizedString("Please set device passcode to continue", comment: "")
case .appCancel:
LKLocalAuthentication.log("appCancel")
return nil
case .invalidContext:
LKLocalAuthentication.log("invalidContext")
return nil
case .notInteractive:
LKLocalAuthentication.log("notInteractive")
return nil
default:
checkBioMetricError(error)
}
return nil
}
private func checkBioMetricError(_ error: LAError) {
if #available(iOS 11.0, *) {
switch error.code {
case .biometryNotAvailable,
.biometryNotEnrolled,
.biometryLockout:
requestAuth(policy: .deviceOwnerAuthentication)
default: break
}
} else {
switch error.code {
case .touchIDNotAvailable,
.touchIDNotEnrolled,
.touchIDLockout:
requestAuth(policy: .deviceOwnerAuthentication)
default: break
}
}
}
private func evaluatePolicy(_ policy: LAPolicy, context: LAContext) {
context.evaluatePolicy(policy, localizedReason: reason) { (success, error) in
if success {
self.lastUnlockedTimeInterval = Date().timeIntervalSince1970
if let completion = self.completionHandler {
DispatchQueue.main.async {
completion(true, nil)
}
}
} else {
guard let error = error else { return }
let errorMessage = self.checkError(error as NSError)
if let completion = self.completionHandler {
DispatchQueue.main.async {
completion(false, errorMessage)
}
}
}
}
}
private static func log(_ message: String) {
#if DEBUG
print(message)
#endif
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment