Files

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