86 lines
2.7 KiB
Swift
86 lines
2.7 KiB
Swift
import Foundation
|
|
import Observation
|
|
|
|
@Observable
|
|
final class LockViewModel {
|
|
private static let maxAttempts = 5
|
|
private static let lockoutDuration = 30.0
|
|
|
|
var pinDigits: [Int] = []
|
|
var hintMessage = ""
|
|
var isVerifying = false
|
|
|
|
private let autoUnlock: Bool
|
|
private(set) var pinLength: Int
|
|
|
|
init() {
|
|
self.autoUnlock = UserDefaults.standard.bool(forKey: "auto_unlock_pin")
|
|
self.pinLength = max(4, UserDefaults.standard.integer(forKey: "pin_length"))
|
|
}
|
|
|
|
// MARK: - Display
|
|
|
|
var dotsDisplay: String {
|
|
let n = pinDigits.count
|
|
let total = autoUnlock ? pinLength : max(n, 4)
|
|
return String(repeating: "●", count: n)
|
|
+ String(repeating: "○", count: max(total - n, 0))
|
|
}
|
|
|
|
// MARK: - Lockout
|
|
|
|
var lockoutRemaining: TimeInterval {
|
|
let fails = UserDefaults.standard.integer(forKey: "lock_fail_count")
|
|
guard fails >= Self.maxAttempts else { return 0 }
|
|
let lastFail = UserDefaults.standard.double(forKey: "lock_last_fail_time")
|
|
let elapsed = Date().timeIntervalSince1970 - lastFail
|
|
return max(0, Self.lockoutDuration - elapsed)
|
|
}
|
|
|
|
var isLockedOut: Bool { lockoutRemaining > 0 }
|
|
|
|
func resetFailures() {
|
|
UserDefaults.standard.removeObject(forKey: "lock_fail_count")
|
|
UserDefaults.standard.removeObject(forKey: "lock_last_fail_time")
|
|
}
|
|
|
|
@discardableResult
|
|
func incrementFailures() -> Int {
|
|
let fails = UserDefaults.standard.integer(forKey: "lock_fail_count") + 1
|
|
UserDefaults.standard.set(fails, forKey: "lock_fail_count")
|
|
UserDefaults.standard.set(Date().timeIntervalSince1970, forKey: "lock_last_fail_time")
|
|
return fails
|
|
}
|
|
|
|
// MARK: - Key handling
|
|
|
|
// Returns true when PIN should be submitted
|
|
func handleKey(_ key: String) -> Bool {
|
|
guard !isVerifying else { return false }
|
|
switch key {
|
|
case "⌫":
|
|
if !pinDigits.isEmpty { pinDigits.removeLast() }
|
|
case "✓":
|
|
if pinDigits.count >= 4 { return true }
|
|
default:
|
|
if let d = Int(key), pinDigits.count < 8 {
|
|
pinDigits.append(d)
|
|
if autoUnlock && pinDigits.count == pinLength { return true }
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func currentPin() -> String { pinDigits.map(String.init).joined() }
|
|
func clearPin() { pinDigits = [] }
|
|
|
|
func failureHint() -> String {
|
|
let fails = incrementFailures()
|
|
let remaining = Self.maxAttempts - fails
|
|
if remaining <= 0 {
|
|
return "Too many attempts. Try again in \(Int(Self.lockoutDuration))s."
|
|
}
|
|
return "\(remaining) attempt\(remaining == 1 ? "" : "s") remaining"
|
|
}
|
|
}
|