LeadKit/TIAuth/Sources/PinCodePresenter/DefaultPinCodePresenter.swift

262 lines
7.4 KiB
Swift

//
// Copyright (c) 2022 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 KeychainAccess
import Foundation
@MainActor
public protocol PinCodeStateStorage: AnyObject {
var currentCodeInput: String { get set }
var progress: Progress { get }
var orderedPinPadActions: [PinCodeAction] { get set }
}
open class DefaultPinCodePresenter: PinCodePresenter {
open class Output {
public typealias OnLogoutClosure = () -> Void
public typealias OnRecoverClosure = () -> Void
public var onLogout: OnLogoutClosure
public var onRecover: OnRecoverClosure
public init(onLogout: @escaping OnLogoutClosure,
onRecover: @escaping OnRecoverClosure) {
self.onLogout = onLogout
self.onRecover = onRecover
}
}
public struct Config {
public enum Defaults {
public static var codeLength: Int {
4
}
public static var validator: DefaultInputValidator<DefaultViolation> {
let violations: [DefaultViolation] = [
.equalDigits(minEqualDigits: 3),
.orderedDigits(ascending: true, minLength: 3),
.orderedDigits(ascending: false, minLength: 3)
]
return DefaultInputValidator<DefaultViolation>(violations: violations) {
$0.defaultValidationRule
}
}
}
public var codeLength: Int
public var defaultInputValidator: DefaultInputValidator<DefaultViolation>
public init(codeLength: Int = Defaults.codeLength,
defaultInputValidator: DefaultInputValidator<DefaultViolation> = Defaults.validator) {
self.codeLength = codeLength
self.defaultInputValidator = defaultInputValidator
}
}
public var output: Output
public weak var stateStorage: PinCodeStateStorage? {
didSet {
updateProgress()
updateOrderedPinPadActions()
}
}
public var config: Config {
didSet {
updateProgress()
}
}
// MARK: - Scenario properties
public var filledPinCode: String {
stateStorage?.currentCodeInput ?? String()
}
public weak var pinCodePresenterDelegate: PinCodePresenterDelegate?
public var leadingPinPadAction: PinCodeAction? {
didSet {
updateOrderedPinPadActions()
}
}
public var trailingPinPadAction: PinCodeAction? {
didSet {
updateOrderedPinPadActions()
}
}
public init(output: Output,
delegate: PinCodePresenterDelegate? = nil,
config: Config = .init(),
stateStorage: PinCodeStateStorage? = nil) {
self.output = output
self.pinCodePresenterDelegate = delegate
self.config = config
self.stateStorage = stateStorage
}
// MARK: - LifecyclePresenter
open func viewDidPresented() {
pinCodePresenterDelegate?.onViewDidPresented()
}
open func viewWillDestroy() {
// cancel requests (if any)
}
// MARK: - User actions handling
open func didPerform(action: PinCodeAction) {
switch action {
case let .digit(digit):
handleDigitAction(digit: digit)
case .backspace:
handleBackspaceAction()
case .biometry:
handleBiometryAction()
case .logout:
handleLogoutAction()
case .recover:
handleRecoverAction()
case let .custom(actionId):
handleCustomAction(actionId: actionId)
}
}
// MARK: - State management
open func setCheckingState() {
// disable UI
// display animation
}
open func setIncorrectCodeState() {
resetState()
// show error message / animation
}
open func setValidCodeState() {
// display animation
}
open func resetState() {
stateStorage?.currentCodeInput = .init()
updateProgress()
}
// MARK: - Subclass override
open func handleDigitAction(digit: UInt) {
withStateStorage {
$0.currentCodeInput.append(contentsOf: format(digit: digit))
let currentCodeValid = validate(currentCode: $0.currentCodeInput)
if $0.currentCodeInput.count == config.codeLength && currentCodeValid {
pinCodePresenterDelegate?.onFill(pinCode: $0.currentCodeInput)
}
}
}
open func validate(currentCode: String) -> Bool {
let violations = config.defaultInputValidator.validate(input: currentCode)
handleDefaultValidator(violations: violations)
return violations.isEmpty
}
open func handleBackspaceAction() {
stateStorage?.currentCodeInput.removeLast()
}
open func handleBiometryAction() {
pinCodePresenterDelegate?.onBiometryRequest()
}
open func handleLogoutAction() {
output.onLogout()
}
open func handleRecoverAction() {
output.onRecover()
}
open func handleCustomAction(actionId: String) {
// handle in subclass
}
open func format(digit: UInt) -> String {
String(digit)
}
open func handleDefaultValidator(violations: Set<DefaultViolation>) {
// handle in subclass
}
open func update(progress: Progress) {
progress.totalUnitCount = Int64(config.codeLength)
progress.completedUnitCount = Int64(stateStorage?.currentCodeInput.count ?? .zero)
}
// MARK: - Private
private func updateProgress() {
withStateStorage {
update(progress: $0.progress)
}
}
private func withStateStorage(actionClosure: (PinCodeStateStorage) -> Void) {
guard let stateStorage = stateStorage else {
return
}
actionClosure(stateStorage)
}
private func updateOrderedPinPadActions() {
let first3RowsActions = (1...9).map(PinCodeAction.digit)
var lastRowActions: [PinCodeAction] = [.digit(.zero)]
if let leadingPinPadAction = leadingPinPadAction {
lastRowActions.insert(leadingPinPadAction, at: .zero)
}
if let trailingPinPadAction = trailingPinPadAction {
lastRowActions.append(trailingPinPadAction)
}
stateStorage?.orderedPinPadActions = first3RowsActions + lastRowActions
}
}