// // 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 LeadKit import RxSwift import RxCocoa /// Describes types of authentication public enum PassCodeAuthType { case passCode(String) case touchId } /// Base view model for passCodeViewController open class BasePassCodeViewModel: BaseViewModel { public let controllerType: PassCodeControllerType 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 = Variable(nil) var validationResult: Driver { return validationResultHolder.asDriver() } private let passCodeControllerStateVariable = Variable(.enter) public var passCodeControllerStateDriver: Driver { return passCodeControllerStateVariable.asDriver() } private let passCodeText = Variable(nil) private var attemptsNumber = 0 private lazy var passCodeHolder: PassCodeHolderProtocol = PassCodeHolderBuilder.build(with: self.controllerType) public init(controllerType: PassCodeControllerType, passCodeConfiguration: PassCodeConfiguration) { self.controllerType = controllerType 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? { return passCodeText.value } public func setPassCodeText(_ value: String?) { passCodeText.value = value } public func reset() { passCodeText.value = nil validationResultHolder.value = nil passCodeControllerStateVariable.value = controllerType == .change ? .oldEnter : .enter attemptsNumber = 0 passCodeHolder.reset() } public func authenticateUsingBiometrics(with description: String, fallback: String?, cancel: String?) { biometricsService.authenticateWithBiometrics(with: description, fallback: fallback, cancel: cancel) { [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 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 { return false } /// 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 { 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 { private func set(passCode: String) { passCodeHolder.add(passCode: passCode) validateIfNeeded() if shouldUpdateControllerState { passCodeControllerStateVariable.value = passCodeHolder.enterStep } } private var shouldUpdateControllerState: Bool { return !passCodeHolder.shouldValidate || !(validationResultHolder.value?.isValid ?? true) || validationResultHolder.value?.error?.isTooManyAttempts ?? false } private func validateIfNeeded() { guard passCodeHolder.shouldValidate else { return } var validationResult = passCodeHolder.validate() 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(passCodeConfiguration.maxAttemptsNumber - attemptsNumber)) } if (!validationResult.isValid && attemptsNumber == Int(passCodeConfiguration.maxAttemptsNumber)) || attemptsNumber > Int(passCodeConfiguration.maxAttemptsNumber) { validationResult = .invalid(.tooManyAttempts) } } if !validationResult.isValid { passCodeHolder.reset() } validationResultHolder.value = validationResult } }