LeadKitAdditions/Sources/Controllers/PassCode/ViewModel/BasePassCodeViewModel.swift

236 lines
8.2 KiB
Swift

//
// 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
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import RxSwift
import RxCocoa
/// Describes types of authentication
public enum PassCodeAuthType {
case passCode(String)
case touchId
}
/// Base view model for passCodeViewController
open class BasePassCodeViewModel {
public let operationType: PassCodeOperationType
public let disposeBag = DisposeBag()
/// Service that can answer if user is authorized by biometrics
public let biometricsService = BiometricsService()
/// Contains configuration for pass code operations
public let passCodeConfiguration: PassCodeConfiguration
private let validationResultHolder = BehaviorRelay<PassCodeValidationResult?>(value: nil)
public var validationResultDriver: Driver<PassCodeValidationResult?> {
validationResultHolder.asDriver()
}
private let passCodeControllerStateVariable = BehaviorRelay<PassCodeControllerState>(value: .enter)
public var passCodeControllerStateDriver: Driver<PassCodeControllerState> {
passCodeControllerStateVariable.asDriver()
}
private let passCodeText = BehaviorRelay<String?>(value: nil)
private var attemptsNumber = 0
private lazy var passCodeHolder: PassCodeHolderProtocol = PassCodeHolderBuilder.build(with: self.operationType)
public init(operationType: PassCodeOperationType, passCodeConfiguration: PassCodeConfiguration) {
self.operationType = operationType
self.passCodeConfiguration = passCodeConfiguration
bindViewModel()
}
private func bindViewModel() {
passCodeText.asDriver()
.distinctUntilChanged { $0 == $1 }
.drive(onNext: { [weak self] passCode in
if let passCode = passCode,
passCode.count == Int(self?.passCodeConfiguration.passCodeLength ?? 0) {
self?.set(passCode: passCode)
}
})
.disposed(by: disposeBag)
validationResultHolder.asObservable()
.bind(to: validationResultBinder)
.disposed(by: disposeBag)
}
// MARK: - Public
public var passCodeTextValue: String? {
passCodeText.value
}
public func setPassCodeText(_ value: String?) {
passCodeText.accept(value)
}
public func reset() {
passCodeText.accept(nil)
validationResultHolder.accept(nil)
passCodeControllerStateVariable.accept(operationType == .change ? .oldEnter : .enter)
attemptsNumber = 0
passCodeHolder.reset()
}
public func authenticateUsingBiometrics(with description: String, fallback: String?, cancel: String?) {
biometricsAuthBegins()
biometricsService.authenticateWithBiometrics(with: description,
fallback: fallback,
cancel: cancel) { [weak self] success, error in
self?.biometricsAuthEnds()
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
open func isEnteredPassCodeValid(_ passCode: String) -> Bool {
assertionFailure("You should override this method: isEnteredPassCodeValid(_ passCode: String)")
return false
}
/// Method is called after successful authentication
open func authSucceed(_ type: PassCodeAuthType) {
assertionFailure("You should override this method: authSucceed(_ type: PassCodeAuthType)")
}
/// Called when authentication failed
open func authFailed(with: Error?) {
assertionFailure("You should override this method: authFailed(with: Error)")
}
// MARK: - Biometrics
/// Posibility to use biometrics for authentication
open var isBiometricsEnabled: Bool {
false
}
/// Notify about activation for biometrics. Remember to save user choice
open func activateBiometricsForUser() {
assertionFailure("You should override this method: activateBiometricsForUser()")
}
/// Notify before system alert with biometrics
open func biometricsAuthBegins() {}
/// Notify after system alert with biometrics
open func biometricsAuthEnds() {}
}
private extension BasePassCodeViewModel {
var validationResultBinder: Binder<PassCodeValidationResult?> {
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.accept(model.passCodeHolder.enterStep)
}
} else {
if isValid, let passCode = passCode {
model.authSucceed(.passCode(passCode))
} else {
model.passCodeControllerStateVariable.accept(model.passCodeHolder.enterStep)
}
}
}
}
}
extension BasePassCodeViewModel {
private func set(passCode: String) {
passCodeHolder.add(passCode: passCode)
validateIfNeeded()
if shouldUpdateControllerState {
passCodeControllerStateVariable.accept(passCodeHolder.enterStep)
}
}
private var shouldUpdateControllerState: Bool {
!passCodeHolder.shouldValidate ||
!(validationResultHolder.value?.isValid ?? true) ||
validationResultHolder.value?.error?.isTooManyAttempts ?? false
}
private func validateIfNeeded() {
guard passCodeHolder.shouldValidate else {
return
}
switch passCodeHolder.type {
case .create where passCodeHolder.enterStep == .repeatEnter:
attemptsNumber += 1
case .change where passCodeHolder.enterStep == .repeatEnter:
attemptsNumber += 1
case .enter:
attemptsNumber += 1
default:
break
}
var validationResult = passCodeHolder.validate()
// if entered (in .enter mode) code is invalid -> .wrongCode
if passCodeHolder.type == .enter,
let passCode = validationResult.passCode,
!isEnteredPassCodeValid(passCode) {
let remainingAttemptsCount = passCodeConfiguration.maxAttemptsNumber - attemptsNumber
validationResult = .invalid(.wrongCode(attemptsRemaining: remainingAttemptsCount))
}
// if entered code (in any mode) is mismatched too many times -> .tooManyAttempts
if (!validationResult.isValid && attemptsNumber == passCodeConfiguration.maxAttemptsNumber) ||
attemptsNumber > passCodeConfiguration.maxAttemptsNumber {
validationResult = .invalid(.tooManyAttempts(type: operationType))
}
if !validationResult.isValid {
passCodeHolder.reset()
}
validationResultHolder.accept(validationResult)
}
}