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" } }