Merge pull request #33 from TouchInstinct/fix/pincode
Update for Swift 4. Refactor
This commit is contained in:
commit
7bc61d062c
|
|
@ -1,5 +1,8 @@
|
|||
# Changelog
|
||||
|
||||
## 0.1.4
|
||||
- **Update**: Refactor PassCode
|
||||
|
||||
## 0.1.3
|
||||
- **Update**: Typical api response keys naming
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = "LeadKitAdditions"
|
||||
s.version = "0.1.3"
|
||||
s.version = "0.1.4"
|
||||
s.summary = "iOS framework with a bunch of tools for rapid development"
|
||||
s.homepage = "https://github.com/TouchInstinct/LeadKitAdditions"
|
||||
s.license = "Apache License, Version 2.0"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// Copyright (c) 2017 Touch Instinct
|
||||
// Copyright (c) 2018 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// Copyright (c) 2017 Touch Instinct
|
||||
// Copyright (c) 2018 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// Copyright (c) 2017 Touch Instinct
|
||||
// Copyright (c) 2018 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// Copyright (c) 2017 Touch Instinct
|
||||
// Copyright (c) 2018 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
|
|
@ -24,26 +24,22 @@
|
|||
public struct PassCodeConfiguration {
|
||||
|
||||
/// Pass code length
|
||||
public var passCodeCharactersNumber: UInt = 4
|
||||
public let passCodeLength: Int
|
||||
|
||||
/// Incorrect pass code attempts count
|
||||
public var maxAttemptsLoginNumber: UInt = 5
|
||||
public let maxAttemptsNumber: Int
|
||||
|
||||
/// Clear input progress when application goes to background
|
||||
public var shouldResetWhenGoBackground: Bool = true
|
||||
public let shouldResetWhenGoBackground: Bool
|
||||
|
||||
private init() {}
|
||||
|
||||
init?(passCodeCharactersNumber: UInt) {
|
||||
guard passCodeCharactersNumber > 0 else {
|
||||
assertionFailure("passCodeCharactersNumber must be greater then 0")
|
||||
return nil
|
||||
}
|
||||
self.passCodeCharactersNumber = passCodeCharactersNumber
|
||||
public init(passCodeLength: Int = 4, maxAttemptsNumber: Int = 5, shouldResetWhenGoBackground: Bool = true) {
|
||||
self.passCodeLength = passCodeLength
|
||||
self.maxAttemptsNumber = maxAttemptsNumber
|
||||
self.shouldResetWhenGoBackground = shouldResetWhenGoBackground
|
||||
}
|
||||
|
||||
/// Returns configuration with default values
|
||||
public static var defaultConfiguration: PassCodeConfiguration {
|
||||
return PassCodeConfiguration()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// Copyright (c) 2017 Touch Instinct
|
||||
// Copyright (c) 2018 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
|
|
@ -21,8 +21,22 @@
|
|||
//
|
||||
|
||||
/// Describes error, which may occur during pass code entering
|
||||
/// - codesNotMatch: Different codes
|
||||
/// - wrongCode: Value is remaining attemps
|
||||
/// - tooManyAttempts: Attempts limit reached
|
||||
public enum PassCodeError: Error {
|
||||
case codesNotMatch
|
||||
case wrongCode
|
||||
case wrongCode(Int)
|
||||
case tooManyAttempts
|
||||
}
|
||||
|
||||
public extension PassCodeError {
|
||||
var isTooManyAttempts: Bool {
|
||||
switch self {
|
||||
case .tooManyAttempts:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// Copyright (c) 2017 Touch Instinct
|
||||
// Copyright (c) 2018 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
|
|
@ -81,7 +81,7 @@ public class PassCodeHolderCreate: PassCodeHolderProtocol {
|
|||
if let passCode = passCode {
|
||||
return .valid(passCode)
|
||||
} else {
|
||||
return .inValid(.codesNotMatch)
|
||||
return .invalid(.codesNotMatch)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -112,7 +112,7 @@ public class PassCodeHolderEnter: PassCodeHolderProtocol {
|
|||
if let passCode = passCode {
|
||||
return .valid(passCode)
|
||||
} else {
|
||||
return .inValid(nil)
|
||||
return .invalid(nil)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -178,7 +178,7 @@ public class PassCodeHolderChange: PassCodeHolderProtocol {
|
|||
if let passCode = passCode {
|
||||
return .valid(passCode)
|
||||
} else {
|
||||
return .inValid(enterStep == .newEnter ? nil : .codesNotMatch)
|
||||
return .invalid(enterStep == .newEnter ? nil : .codesNotMatch)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// Copyright (c) 2017 Touch Instinct
|
||||
// Copyright (c) 2018 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// Copyright (c) 2017 Touch Instinct
|
||||
// Copyright (c) 2018 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
|
|
@ -24,33 +24,35 @@
|
|||
public enum PassCodeValidationResult {
|
||||
|
||||
case valid(String)
|
||||
case inValid(PassCodeError?)
|
||||
case invalid(PassCodeError?)
|
||||
|
||||
public var isValid: Bool {
|
||||
}
|
||||
|
||||
public extension PassCodeValidationResult {
|
||||
var isValid: Bool {
|
||||
switch self {
|
||||
case .valid:
|
||||
return true
|
||||
default:
|
||||
case .invalid:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
public var passCode: String? {
|
||||
var passCode: String? {
|
||||
switch self {
|
||||
case let .valid(passCode):
|
||||
return passCode
|
||||
default:
|
||||
case .invalid:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public var error: PassCodeError? {
|
||||
var error: PassCodeError? {
|
||||
switch self {
|
||||
case let .inValid(error):
|
||||
case let .invalid(error):
|
||||
return error
|
||||
default:
|
||||
case .valid:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// Copyright (c) 2017 Touch Instinct
|
||||
// Copyright (c) 2018 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
|
|
@ -47,7 +47,7 @@ public enum PassCodeControllerState {
|
|||
}
|
||||
|
||||
/// Base view controller that operates with pass code
|
||||
open class BasePassCodeViewController: UIViewController {
|
||||
open class BasePassCodeViewController: UIViewController, ConfigurableController {
|
||||
|
||||
public var viewModel: BasePassCodeViewModel!
|
||||
|
||||
|
|
@ -59,7 +59,7 @@ open class BasePassCodeViewController: UIViewController {
|
|||
|
||||
public let disposeBag = DisposeBag()
|
||||
|
||||
fileprivate lazy var fakeTextField: UITextField = {
|
||||
private lazy var fakeTextField: UITextField = {
|
||||
let fakeTextField = UITextField()
|
||||
fakeTextField.isSecureTextEntry = true
|
||||
fakeTextField.keyboardType = .numberPad
|
||||
|
|
@ -76,9 +76,20 @@ open class BasePassCodeViewController: UIViewController {
|
|||
|
||||
initialLoadView()
|
||||
initialDotNumberConfiguration()
|
||||
enebleKeyboard()
|
||||
configureBackgroundNotifications()
|
||||
showTouchIdIfNeeded(with: touchIdHint)
|
||||
showBiometricsRequestIfNeeded(with: biometricsAuthorizationHint)
|
||||
}
|
||||
|
||||
override open func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
fakeTextField.becomeFirstResponder()
|
||||
}
|
||||
|
||||
override open func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
|
||||
fakeTextField.resignFirstResponder()
|
||||
}
|
||||
|
||||
// MARK: - Private functions
|
||||
|
|
@ -95,14 +106,10 @@ open class BasePassCodeViewController: UIViewController {
|
|||
.disposed(by: disposeBag)
|
||||
}
|
||||
|
||||
private func enebleKeyboard() {
|
||||
fakeTextField.becomeFirstResponder()
|
||||
}
|
||||
|
||||
private func initialDotNumberConfiguration() {
|
||||
dotStackView.arrangedSubviews.forEach { dotStackView.removeArrangedSubview($0) }
|
||||
|
||||
for _ in 0..<viewModel.passCodeConfiguration.passCodeCharactersNumber {
|
||||
for _ in 0 ..< viewModel.passCodeConfiguration.passCodeLength {
|
||||
let dotImageView = UIImageView()
|
||||
dotImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
dotImageView.widthAnchor.constraint(equalTo: dotImageView.heightAnchor, multiplier: 1)
|
||||
|
|
@ -113,7 +120,7 @@ open class BasePassCodeViewController: UIViewController {
|
|||
resetDotsUI()
|
||||
}
|
||||
|
||||
fileprivate func resetDotsUI() {
|
||||
private func resetDotsUI() {
|
||||
fakeTextField.text = nil
|
||||
dotStackView.arrangedSubviews
|
||||
.flatMap { $0 as? UIImageView }
|
||||
|
|
@ -129,10 +136,10 @@ open class BasePassCodeViewController: UIViewController {
|
|||
imageView.image = imageFor(type: state)
|
||||
}
|
||||
|
||||
fileprivate func setStates(for passCodeText: String) {
|
||||
private func setStates(for passCodeText: String) {
|
||||
var statesArray: [PinImageType] = []
|
||||
|
||||
for characterIndex in 0..<viewModel.passCodeConfiguration.passCodeCharactersNumber {
|
||||
for characterIndex in 0..<viewModel.passCodeConfiguration.passCodeLength {
|
||||
let state: PinImageType = Int(characterIndex) <= passCodeText.characters.count - 1 ? .entered : .clear
|
||||
statesArray.append(state)
|
||||
}
|
||||
|
|
@ -142,19 +149,15 @@ open class BasePassCodeViewController: UIViewController {
|
|||
}
|
||||
}
|
||||
|
||||
fileprivate func showTouchIdIfNeeded(with description: String) {
|
||||
guard viewModel.isTouchIdEnabled && viewModel.controllerType == .enter else {
|
||||
private func showBiometricsRequestIfNeeded(with description: String) {
|
||||
guard viewModel.isBiometricsEnabled && viewModel.controllerType == .enter else {
|
||||
return
|
||||
}
|
||||
|
||||
viewModel.touchIdService?.authenticateByTouchId(description: description) { [weak self] isSuccess in
|
||||
if isSuccess {
|
||||
self?.viewModel.authSucceed(.touchId)
|
||||
}
|
||||
}
|
||||
viewModel.authenticateUsingBiometrics(with: description)
|
||||
}
|
||||
|
||||
fileprivate func resetUI() {
|
||||
private func resetUI() {
|
||||
resetDotsUI()
|
||||
viewModel.reset()
|
||||
}
|
||||
|
|
@ -162,7 +165,7 @@ open class BasePassCodeViewController: UIViewController {
|
|||
// MARK: - HAVE TO OVERRIDE
|
||||
|
||||
/// Returns prompt that appears on touch id system alert
|
||||
open var touchIdHint: String {
|
||||
open var biometricsAuthorizationHint: String {
|
||||
assertionFailure("You should override this var: touchIdHint")
|
||||
return ""
|
||||
}
|
||||
|
|
@ -174,9 +177,9 @@ open class BasePassCodeViewController: UIViewController {
|
|||
}
|
||||
|
||||
/// Override to change error description
|
||||
open func errorDescription(for error: PassCodeError) -> String {
|
||||
open func errorDescription(for error: PassCodeError) -> NSAttributedString? {
|
||||
assertionFailure("You should override this method: errorDescription(for error: PassCodeError)")
|
||||
return ""
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Override to change action title text
|
||||
|
|
@ -189,7 +192,7 @@ open class BasePassCodeViewController: UIViewController {
|
|||
|
||||
/// Call to show error
|
||||
open func showError(for error: PassCodeError) {
|
||||
errorLabel?.text = errorDescription(for: error)
|
||||
errorLabel?.attributedText = errorDescription(for: error)
|
||||
errorLabel?.isHidden = false
|
||||
}
|
||||
|
||||
|
|
@ -204,11 +207,7 @@ open class BasePassCodeViewController: UIViewController {
|
|||
titleLabel?.text = actionTitle(for: passCodeControllerState)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - ConfigurableController
|
||||
// We need to implement all functions of ConfigurableController protocol to give ability to override them.
|
||||
extension BasePassCodeViewController: ConfigurableController {
|
||||
// MARK: - ConfigurableController
|
||||
|
||||
open func bindViews() {
|
||||
fakeTextField.rx.text.asDriver()
|
||||
|
|
@ -227,13 +226,13 @@ extension BasePassCodeViewController: ConfigurableController {
|
|||
|
||||
if validationResult.isValid {
|
||||
self?.hideError()
|
||||
} else if let pasCodeError = validationResult.error {
|
||||
self?.showError(for: pasCodeError)
|
||||
} else if let passCodeError = validationResult.error {
|
||||
self?.showError(for: passCodeError)
|
||||
}
|
||||
})
|
||||
.disposed(by: disposeBag)
|
||||
|
||||
viewModel.passCodeControllerState
|
||||
viewModel.passCodeControllerStateDriver
|
||||
.drive(onNext: { [weak self] controllerState in
|
||||
self?.configureUI(for: controllerState)
|
||||
})
|
||||
|
|
@ -251,6 +250,7 @@ extension BasePassCodeViewController: ConfigurableController {
|
|||
}
|
||||
|
||||
// MARK: - UITextFieldDelegate
|
||||
|
||||
extension BasePassCodeViewController: UITextFieldDelegate {
|
||||
|
||||
public func textField(_ textField: UITextField,
|
||||
|
|
@ -262,3 +262,4 @@ extension BasePassCodeViewController: UITextFieldDelegate {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// Copyright (c) 2017 Touch Instinct
|
||||
// Copyright (c) 2018 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
|
|
@ -37,36 +37,31 @@ open class BasePassCodeViewModel: BaseViewModel {
|
|||
|
||||
public let disposeBag = DisposeBag()
|
||||
|
||||
/// TouchId service, which can answer if user is authorized by finger
|
||||
public let touchIdService: TouchIDService?
|
||||
/// Service that can answer if user is authorized by biometrics
|
||||
public let biometricsService = BiometricsService()
|
||||
|
||||
/// Contains configuration for pass code operations
|
||||
public let passCodeConfiguration: PassCodeConfiguration
|
||||
|
||||
fileprivate let validationResultHolder = Variable<PassCodeValidationResult?>(nil)
|
||||
private let validationResultHolder = Variable<PassCodeValidationResult?>(nil)
|
||||
var validationResult: Driver<PassCodeValidationResult?> {
|
||||
return validationResultHolder.asDriver()
|
||||
}
|
||||
|
||||
fileprivate let passCodeControllerStateHolder = Variable<PassCodeControllerState>(.enter)
|
||||
public var passCodeControllerState: Driver<PassCodeControllerState> {
|
||||
return passCodeControllerStateHolder.asDriver()
|
||||
private let passCodeControllerStateVariable = Variable<PassCodeControllerState>(.enter)
|
||||
public var passCodeControllerStateDriver: Driver<PassCodeControllerState> {
|
||||
return passCodeControllerStateVariable.asDriver()
|
||||
}
|
||||
|
||||
private let passCodeText = Variable<String?>(nil)
|
||||
|
||||
fileprivate var attemptsNumber = 0
|
||||
private var attemptsNumber = 0
|
||||
|
||||
fileprivate lazy var passCodeHolder: PassCodeHolderProtocol = {
|
||||
return PassCodeHolderBuilder.build(with: self.controllerType)
|
||||
}()
|
||||
|
||||
public init(controllerType: PassCodeControllerType,
|
||||
passCodeConfiguration: PassCodeConfiguration,
|
||||
touchIdService: TouchIDService? = nil) {
|
||||
private lazy var passCodeHolder: PassCodeHolderProtocol = PassCodeHolderBuilder.build(with: self.controllerType)
|
||||
|
||||
public init(controllerType: PassCodeControllerType, passCodeConfiguration: PassCodeConfiguration) {
|
||||
self.controllerType = controllerType
|
||||
self.passCodeConfiguration = passCodeConfiguration
|
||||
self.touchIdService = touchIdService
|
||||
|
||||
bindViewModel()
|
||||
}
|
||||
|
|
@ -76,35 +71,14 @@ open class BasePassCodeViewModel: BaseViewModel {
|
|||
.distinctUntilChanged { $0 == $1 }
|
||||
.drive(onNext: { [weak self] passCode in
|
||||
if let passCode = passCode,
|
||||
passCode.characters.count == Int(self?.passCodeConfiguration.passCodeCharactersNumber ?? 0) {
|
||||
passCode.characters.count == Int(self?.passCodeConfiguration.passCodeLength ?? 0) {
|
||||
self?.set(passCode: passCode)
|
||||
}
|
||||
})
|
||||
.disposed(by: disposeBag)
|
||||
|
||||
validationResultHolder.asDriver()
|
||||
.drive(onNext: { [weak self] validationResult in
|
||||
guard let sSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
if sSelf.passCodeHolder.type == .change {
|
||||
if validationResult?.isValid ?? false,
|
||||
sSelf.passCodeHolder.enterStep == .repeatEnter,
|
||||
let passCode = validationResult?.passCode {
|
||||
|
||||
sSelf.authSucceed(.passCode(passCode))
|
||||
} else {
|
||||
sSelf.passCodeControllerStateHolder.value = sSelf.passCodeHolder.enterStep
|
||||
}
|
||||
} else {
|
||||
if validationResult?.isValid ?? false, let passCode = validationResult?.passCode {
|
||||
sSelf.authSucceed(.passCode(passCode))
|
||||
} else {
|
||||
sSelf.passCodeControllerStateHolder.value = sSelf.passCodeHolder.enterStep
|
||||
}
|
||||
}
|
||||
})
|
||||
validationResultHolder.asObservable()
|
||||
.bind(to: validationResultBinder)
|
||||
.disposed(by: disposeBag)
|
||||
}
|
||||
|
||||
|
|
@ -121,11 +95,21 @@ open class BasePassCodeViewModel: BaseViewModel {
|
|||
public func reset() {
|
||||
passCodeText.value = nil
|
||||
validationResultHolder.value = nil
|
||||
passCodeControllerStateHolder.value = controllerType == .change ? .oldEnter : .enter
|
||||
passCodeControllerStateVariable.value = controllerType == .change ? .oldEnter : .enter
|
||||
attemptsNumber = 0
|
||||
passCodeHolder.reset()
|
||||
}
|
||||
|
||||
public func authenticateUsingBiometrics(with description: String) {
|
||||
biometricsService.authenticateWithBiometrics(with: description) { [weak self] success, error in
|
||||
if success {
|
||||
self?.authSucceed(.touchId)
|
||||
} else {
|
||||
self?.authFailed(with: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - HAVE TO OVERRIDE
|
||||
|
||||
/// Override to check if entered pass code is equal to stored
|
||||
|
|
@ -134,40 +118,68 @@ open class BasePassCodeViewModel: BaseViewModel {
|
|||
return false
|
||||
}
|
||||
|
||||
/// Handler called after successful authentication
|
||||
/// Method is called after successful authentication
|
||||
open func authSucceed(_ type: PassCodeAuthType) {
|
||||
assertionFailure("You should override this method: authSucceed(_ type: PassCodeAuthType)")
|
||||
}
|
||||
|
||||
// MARK: - Functions that can you can override to use TouchId
|
||||
/// Called when authentication failed
|
||||
open func authFailed(with: Error?) {
|
||||
assertionFailure("You should override this method: authFailed(with: Error)")
|
||||
}
|
||||
|
||||
/// Override to be able use touchId during authentication
|
||||
open var isTouchIdEnabled: Bool {
|
||||
// MARK: - Biometrics
|
||||
|
||||
/// Posibility to use biometrics for authentication
|
||||
open var isBiometricsEnabled: Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
/// You should save user choice about authenticate by touchId
|
||||
open func activateTouchIdForUser() {
|
||||
assertionFailure("You should override this method: activateTouchIdForUser()")
|
||||
/// Notify about activation for biometrics. Remember to save user choice
|
||||
open func activateBiometricsForUser() {
|
||||
assertionFailure("You should override this method: activateBiometricsForUser()")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private extension BasePassCodeViewModel {
|
||||
var validationResultBinder: Binder<PassCodeValidationResult?> {
|
||||
return Binder(self) { model, validationResult in
|
||||
let isValid = validationResult?.isValid ?? false
|
||||
let passCode = validationResult?.passCode
|
||||
|
||||
if model.passCodeHolder.type == .change {
|
||||
if isValid, model.passCodeHolder.enterStep == .repeatEnter, let passCode = passCode {
|
||||
model.authSucceed(.passCode(passCode))
|
||||
} else {
|
||||
model.passCodeControllerStateVariable.value = model.passCodeHolder.enterStep
|
||||
}
|
||||
} else {
|
||||
if isValid, let passCode = passCode {
|
||||
model.authSucceed(.passCode(passCode))
|
||||
} else {
|
||||
model.passCodeControllerStateVariable.value = model.passCodeHolder.enterStep
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension BasePassCodeViewModel {
|
||||
|
||||
fileprivate func set(passCode: String) {
|
||||
private func set(passCode: String) {
|
||||
passCodeHolder.add(passCode: passCode)
|
||||
validateIfNeeded()
|
||||
|
||||
if shouldUpdateControllerState {
|
||||
passCodeControllerStateHolder.value = passCodeHolder.enterStep
|
||||
passCodeControllerStateVariable.value = passCodeHolder.enterStep
|
||||
}
|
||||
}
|
||||
|
||||
private var shouldUpdateControllerState: Bool {
|
||||
return !passCodeHolder.shouldValidate ||
|
||||
!(validationResultHolder.value?.isValid ?? true) ||
|
||||
validationResultHolder.value?.error == .tooManyAttempts
|
||||
validationResultHolder.value?.error?.isTooManyAttempts ?? false
|
||||
}
|
||||
|
||||
private func validateIfNeeded() {
|
||||
|
|
@ -177,16 +189,18 @@ extension BasePassCodeViewModel {
|
|||
|
||||
var validationResult = passCodeHolder.validate()
|
||||
|
||||
if passCodeHolder.type == .enter || (passCodeHolder.type == .change && passCodeHolder.enterStep == .newEnter) {
|
||||
let passCodeValidationForPassCodeChange = passCodeHolder.type == .change && passCodeHolder.enterStep == .newEnter
|
||||
|
||||
if passCodeHolder.type == .enter || passCodeValidationForPassCodeChange {
|
||||
attemptsNumber += 1
|
||||
|
||||
if let passCode = validationResult.passCode, !isEnteredPassCodeValid(passCode) {
|
||||
validationResult = .inValid(.wrongCode)
|
||||
validationResult = .invalid(.wrongCode(passCodeConfiguration.maxAttemptsNumber - attemptsNumber))
|
||||
}
|
||||
|
||||
if (!validationResult.isValid && attemptsNumber == Int(passCodeConfiguration.maxAttemptsLoginNumber)) ||
|
||||
attemptsNumber > Int(passCodeConfiguration.maxAttemptsLoginNumber) {
|
||||
validationResult = .inValid(.tooManyAttempts)
|
||||
if (!validationResult.isValid && attemptsNumber == Int(passCodeConfiguration.maxAttemptsNumber)) ||
|
||||
attemptsNumber > Int(passCodeConfiguration.maxAttemptsNumber) {
|
||||
validationResult = .invalid(.tooManyAttempts)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// Copyright (c) 2017 Touch Instinct
|
||||
// Copyright (c) 2018 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// Copyright (c) 2017 Touch Instinct
|
||||
// Copyright (c) 2018 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// Copyright (c) 2017 Touch Instinct
|
||||
// Copyright (c) 2018 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// Copyright (c) 2017 Touch Instinct
|
||||
// Copyright (c) 2018 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// Copyright (c) 2017 Touch Instinct
|
||||
// Copyright (c) 2018 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// Copyright (c) 2017 Touch Instinct
|
||||
// Copyright (c) 2018 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
|
|
@ -24,11 +24,22 @@ import KeychainAccess
|
|||
import CocoaLumberjack
|
||||
import IDZSwiftCommonCrypto
|
||||
|
||||
private enum Keys {
|
||||
static let passCodeHash = "passCodeHashKey"
|
||||
static let isBiometricsEnabled = "isBiometricsEnabledKey"
|
||||
static let isInitialLoad = "isInitialLoadKey"
|
||||
}
|
||||
|
||||
private enum Values {
|
||||
static let biometricsEnabled = "biometricsEnabled"
|
||||
static let initialLoad = "initialLoad"
|
||||
}
|
||||
|
||||
/// Represents base pass code service which encapsulates pass code storing
|
||||
open class BasePassCodeService {
|
||||
|
||||
/// Override to set specific keychain service name
|
||||
open class var keychainService: String {
|
||||
open class var keychainServiceString: String {
|
||||
return Bundle.main.bundleIdentifier ?? ""
|
||||
}
|
||||
|
||||
|
|
@ -42,47 +53,33 @@ open class BasePassCodeService {
|
|||
|
||||
// MARK: - Private stuff
|
||||
|
||||
fileprivate lazy var keychain: Keychain = {
|
||||
return Keychain(service: BasePassCodeService.keychainService)
|
||||
.synchronizable(false)
|
||||
}()
|
||||
private lazy var keychain = Keychain(service: BasePassCodeService.keychainServiceString).synchronizable(false)
|
||||
|
||||
fileprivate var passCodeHash: String? {
|
||||
private var passCodeHash: String? {
|
||||
return keychain[Keys.passCodeHash]
|
||||
}
|
||||
|
||||
fileprivate enum Keys {
|
||||
static let passCodeHash = "passCodeHash"
|
||||
static let isTouchIdEnabled = "isTouchIdEnabled"
|
||||
static let isInitialLoad = "isInitialLoad"
|
||||
}
|
||||
|
||||
fileprivate enum Values {
|
||||
static let touchIdEnabled = "touchIdEnabled"
|
||||
static let initialLoad = "initialLoad"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension BasePassCodeService {
|
||||
public extension BasePassCodeService {
|
||||
|
||||
/// Indicates is pass code already saved on this device
|
||||
public var isPassCodeSaved: Bool {
|
||||
var isPassCodeSaved: Bool {
|
||||
return keychain[Keys.passCodeHash] != nil
|
||||
}
|
||||
|
||||
/// Indicates is it possible to authenticate on this device via touch id
|
||||
public var isTouchIdEnabled: Bool {
|
||||
/// Possibility to authenticate via biometrics. TouchID or FaceID
|
||||
var isBiometricsAuthorizationEnabled: Bool {
|
||||
get {
|
||||
return keychain[Keys.isTouchIdEnabled] == Values.touchIdEnabled
|
||||
return keychain[Keys.isBiometricsEnabled] == Values.biometricsEnabled
|
||||
}
|
||||
set {
|
||||
keychain[Keys.isTouchIdEnabled] = newValue ? Values.touchIdEnabled : nil
|
||||
keychain[Keys.isBiometricsEnabled] = newValue ? Values.biometricsEnabled : nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Saves new pass code
|
||||
public func save(passCode: String?) {
|
||||
func save(passCode: String?) {
|
||||
if let passCode = passCode {
|
||||
keychain[Keys.passCodeHash] = sha256(passCode)
|
||||
} else {
|
||||
|
|
@ -91,14 +88,14 @@ extension BasePassCodeService {
|
|||
}
|
||||
|
||||
/// Check if pass code is correct
|
||||
public func check(passCode: String) -> Bool {
|
||||
func check(passCode: String) -> Bool {
|
||||
return sha256(passCode) == passCodeHash
|
||||
}
|
||||
|
||||
/// Reset pass code settings
|
||||
public func reset() {
|
||||
func reset() {
|
||||
save(passCode: nil)
|
||||
isTouchIdEnabled = false
|
||||
isBiometricsAuthorizationEnabled = false
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// Copyright (c) 2017 Touch Instinct
|
||||
// Copyright (c) 2018 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// Copyright (c) 2017 Touch Instinct
|
||||
// Copyright (c) 2018 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
|
|
@ -22,34 +22,38 @@
|
|||
|
||||
import LocalAuthentication
|
||||
|
||||
public typealias TouchIDServiceAuthHandler = (Bool) -> Void
|
||||
public typealias BiometricsAuthHandler = (Bool, Error?) -> Void
|
||||
|
||||
/// Represents service that provides access to authentication via touch id
|
||||
public class TouchIDService {
|
||||
/// Service that provide access to authentication via biometric
|
||||
public final class BiometricsService {
|
||||
|
||||
private lazy var laContext: LAContext = {
|
||||
return LAContext()
|
||||
}()
|
||||
|
||||
public init() {}
|
||||
private lazy var laContext = LAContext()
|
||||
|
||||
/// Indicates is it possible to authenticate on this device via touch id
|
||||
public var canAuthenticateByTouchId: Bool {
|
||||
public var canAuthenticateWithBiometrics: Bool {
|
||||
return laContext.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil)
|
||||
}
|
||||
|
||||
/**
|
||||
Initiates system touch id authentication process
|
||||
Initiates system biometrics authentication process
|
||||
|
||||
- parameters:
|
||||
- description: prompt on the system alert that describes what for user should attach finger to device
|
||||
- authHandler: callback, with parameter, indicates if user authenticate successfuly
|
||||
*/
|
||||
public func authenticateByTouchId(description: String, authHandler: @escaping TouchIDServiceAuthHandler) {
|
||||
laContext.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics,
|
||||
localizedReason: description) { success, _ in
|
||||
public func authenticateWithBiometrics(with description: String,
|
||||
fallback fallbackTitle: String? = nil,
|
||||
cancel cancelTitle: String? = nil,
|
||||
authHandler: @escaping BiometricsAuthHandler) {
|
||||
if #available(iOS 10.0, *), let cancel = cancelTitle {
|
||||
laContext.localizedCancelTitle = cancelTitle
|
||||
}
|
||||
if let fallback = fallbackTitle {
|
||||
laContext.localizedFallbackTitle = fallbackTitle
|
||||
}
|
||||
|
||||
authHandler(success)
|
||||
laContext.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: description) { success, error in
|
||||
authHandler(success, error)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// Copyright (c) 2017 Touch Instinct
|
||||
// Copyright (c) 2018 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// Copyright (c) 2017 Touch Instinct
|
||||
// Copyright (c) 2018 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// Copyright (c) 2017 Touch Instinct
|
||||
// Copyright (c) 2018 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
|
|
|
|||
Loading…
Reference in New Issue