import LocalAuthentication import SwiftUI struct SecuritySetupView: View { var onComplete: () -> Void var changeMode: Bool = false private enum Step { case enterPin, confirmPin, done } @State private var step: Step = .enterPin @State private var pinDigits: [Int] = [] @State private var firstPin = "" @State private var hintText = "" @State private var biometricsAvailable = false @State private var biometricsEnabled = UserDefaults.standard.bool(forKey: "biometrics_enabled") var body: some View { VStack(spacing: 28) { Spacer() switch step { case .enterPin, .confirmPin: pinSetupContent case .done: doneContent } Spacer() } .animation(.easeInOut(duration: 0.2), value: step) .task { let ctx = LAContext() biometricsAvailable = ctx.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) } } // MARK: - PIN setup @ViewBuilder private var pinSetupContent: some View { VStack(spacing: 8) { Image(systemName: "lock.fill") .font(.system(size: 40)) .foregroundStyle(.secondary) Text(step == .enterPin ? "Create a PIN" : "Confirm PIN") .font(.title2).bold() Text("Use 4–8 digits") .font(.caption) .foregroundStyle(.secondary) } Text(dotsDisplay) .font(.system(size: 30, design: .monospaced)) .kerning(6) .frame(height: 44) .animation(.spring(response: 0.2), value: pinDigits.count) if !hintText.isEmpty { Text(hintText) .font(.callout) .foregroundStyle(.red) .transition(.opacity) } NumpadView { key in handleKey(key) } } // MARK: - Done screen @ViewBuilder private var doneContent: some View { VStack(spacing: 16) { Image(systemName: "checkmark.shield.fill") .font(.system(size: 64)) .foregroundStyle(.green) Text("PIN Created") .font(.title2).bold() Text("Your app is now protected.") .foregroundStyle(.secondary) } if biometricsAvailable { VStack(spacing: 4) { let label = biometricLabel() Toggle(isOn: $biometricsEnabled) { Label("Use \(label) to unlock", systemImage: biometricIcon()) } .padding(.horizontal, 32) .tint(.accentColor) .onChange(of: biometricsEnabled) { UserDefaults.standard.set(biometricsEnabled, forKey: "biometrics_enabled") } Text("Recommended for quick access") .font(.caption) .foregroundStyle(.secondary) } } Button("Continue") { onComplete() } .buttonStyle(.borderedProminent) .controlSize(.large) .padding(.horizontal, 40) } // MARK: - Logic private var dotsDisplay: String { let n = pinDigits.count let total = max(n, 4) return String(repeating: "●", count: n) + String(repeating: "○", count: total - n) } private func handleKey(_ key: String) { withAnimation { hintText = "" } switch key { case "⌫": if !pinDigits.isEmpty { pinDigits.removeLast() } case "✓": if pinDigits.count >= 4 { submitPin() } default: if let d = Int(key), pinDigits.count < 8 { pinDigits.append(d) } } } private func submitPin() { let entered = pinDigits.map(String.init).joined() switch step { case .enterPin: firstPin = entered pinDigits = [] step = .confirmPin case .confirmPin: if entered == firstPin { savePin(entered) withAnimation { step = .done } } else { withAnimation { hintText = "PINs don't match — try again" } pinDigits = [] } case .done: break } } private func savePin(_ pin: String) { let salt = PinHash.generateSalt() let hash = PinHash.hash(pin, salt: salt) try? CredentialStore.shared.save(salt.base64EncodedString(), forKey: CredentialStore.Keys.securityHashSalt) try? CredentialStore.shared.save(hash, forKey: CredentialStore.Keys.securityHash) UserDefaults.standard.set("pin", forKey: "security_method") UserDefaults.standard.set(pin.count, forKey: "pin_length") UserDefaults.standard.set(true, forKey: "auto_unlock_pin") } 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 { SecuritySetupView(onComplete: {}) }