Files

181 lines
5.4 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 48 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: {})
}