import LocalAuthentication import SwiftUI struct LockScreenView: View { var onUnlocked: () -> Void @State private var vm = LockViewModel() @State private var hintText = "" @State private var lockoutSeconds = 0 @State private var lockoutTimer: Timer? private let biometricsEnabled = UserDefaults.standard.bool(forKey: "biometrics_enabled") var body: some View { VStack(spacing: 28) { Spacer() VStack(spacing: 8) { Image(systemName: "lock.fill") .font(.system(size: 48)) .foregroundStyle(.secondary) Text("Enter PIN") .font(.title2).bold() } Text(vm.dotsDisplay) .font(.system(size: 30, design: .monospaced)) .kerning(6) .frame(height: 44) .animation(.spring(response: 0.2), value: vm.pinDigits.count) Group { if lockoutSeconds > 0 { Text("Try again in \(lockoutSeconds)s") .foregroundStyle(.red) } else if !hintText.isEmpty { Text(hintText) .foregroundStyle(.red) } else { Text(" ") // placeholder to keep layout stable } } .font(.callout) NumpadView { key in handleKey(key) } .disabled(lockoutSeconds > 0 || vm.isVerifying) .opacity(lockoutSeconds > 0 ? 0.4 : 1) if biometricsEnabled { Button { triggerBiometric() } label: { Label(biometricLabel(), systemImage: biometricIcon()) .font(.callout) } .foregroundStyle(.secondary) .padding(.top, 4) } Spacer() } .task { if biometricsEnabled { triggerBiometric() } // Resume any active lockout on re-appearance if vm.isLockedOut { startLockoutTimer() } } .onDisappear { lockoutTimer?.invalidate() lockoutTimer = nil } } // MARK: - Key handling private func handleKey(_ key: String) { guard !vm.isLockedOut else { startLockoutTimer(); return } hintText = "" if vm.handleKey(key) { verifyPin() } } // MARK: - Verification private func verifyPin() { guard !vm.isVerifying else { return } vm.isVerifying = true let entered = vm.currentPin() vm.clearPin() Task { let ok = await verifyInBackground(entered) await MainActor.run { vm.isVerifying = false if ok { vm.resetFailures() onUnlocked() } else { showFailure() } } } } private func verifyInBackground(_ input: String) async -> Bool { await Task.detached(priority: .userInitiated) { guard let saltB64 = CredentialStore.shared.load(forKey: CredentialStore.Keys.securityHashSalt), let salt = Data(base64Encoded: saltB64), let stored = CredentialStore.shared.load(forKey: CredentialStore.Keys.securityHash) else { return false } return PinHash.verify(input, against: stored, salt: salt) }.value } private func showFailure() { withAnimation { hintText = vm.failureHint() } if vm.isLockedOut { startLockoutTimer() } else { DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { withAnimation { hintText = "" } } } } // MARK: - Lockout timer private func startLockoutTimer() { lockoutSeconds = Int(ceil(vm.lockoutRemaining)) guard lockoutSeconds > 0 else { return } hintText = "" lockoutTimer?.invalidate() lockoutTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in lockoutSeconds = Int(ceil(vm.lockoutRemaining)) if lockoutSeconds <= 0 { lockoutTimer?.invalidate() lockoutTimer = nil } } } // MARK: - Biometric private func triggerBiometric() { let ctx = LAContext() var err: NSError? guard ctx.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &err) else { return } ctx.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: "Unlock Thijooree") { success, _ in guard success else { return } Task { @MainActor in vm.resetFailures() onUnlocked() } } } private func biometricLabel() -> String { let ctx = LAContext() _ = ctx.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) return ctx.biometryType == .faceID ? "Face ID" : "Touch ID" } private func biometricIcon() -> String { let ctx = LAContext() _ = ctx.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) return ctx.biometryType == .faceID ? "faceid" : "touchid" } } #Preview { LockScreenView(onUnlocked: {}) }