Files
InvestmentTrackerApp/PortfolioJournal/Views/Security/AppLockView.swift
T
alexandrev-tibco 6774d76146 Biometría genérica (Face ID / Touch ID / Optic ID) y What's New 1.7.0
- AppLockService.biometryKind lee LAContext.biometryType; textos e icono del
  bloqueo y de la privacidad de saldos ya no asumen Face ID (Duo puede llevar
  Touch ID). Nuevas claves biometric_* en los 7 idiomas.
- Entrada 1.7.0 en el catálogo What's New (Duo, inspector, biometría) con
  strings en los 7 idiomas.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F1u4K16xy7eQVtgsYNZ9Vn
2026-09-16 11:44:50 +02:00

127 lines
4.4 KiB
Swift

import SwiftUI
struct AppLockView: View {
@Binding var isUnlocked: Bool
@AppStorage("faceIdEnabled") private var faceIdEnabled = false
@AppStorage("pinEnabled") private var pinEnabled = false
@State private var pin = ""
@State private var errorMessage: String?
@State private var didAttemptBiometrics = false
@FocusState private var pinFocused: Bool
private let biometry = AppLockService.biometryKind
var body: some View {
ZStack {
Color(.systemBackground)
.ignoresSafeArea()
VStack(spacing: 20) {
Spacer()
Image("BrandMark")
.resizable()
.scaledToFit()
.frame(width: 64, height: 64)
Text("Locked")
.font(.title.weight(.bold))
Text("Unlock Portfolio Journal to view your data.")
.font(.subheadline)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal, 30)
if faceIdEnabled {
Button {
authenticateWithBiometrics()
} label: {
Label(String(format: String(localized: "biometric_unlock_with"), biometry.displayName),
systemImage: biometry.systemImage)
.font(.headline)
.frame(maxWidth: .infinity)
.padding()
.background(Color.appPrimary)
.foregroundStyle(.white)
.clipShape(RoundedRectangle(cornerRadius: AppConstants.UI.cornerRadius))
}
.padding(.horizontal, 24)
}
if pinEnabled {
VStack(spacing: 10) {
SecureField("4-digit PIN", text: $pin)
.keyboardType(.numberPad)
.textContentType(.oneTimeCode)
.multilineTextAlignment(.center)
.font(.title3.weight(.semibold))
.padding()
.background(Color.gray.opacity(0.1))
.clipShape(RoundedRectangle(cornerRadius: 12))
.focused($pinFocused)
.onChange(of: pin) { _, newValue in
pin = String(newValue.filter(\.isNumber).prefix(4))
errorMessage = nil
if pin.count == 4 {
validatePin()
}
}
Button("Unlock") {
validatePin()
}
.font(.subheadline.weight(.semibold))
.disabled(pin.count < 4)
}
.padding(.horizontal, 24)
}
if let errorMessage {
Text(errorMessage)
.font(.caption)
.foregroundStyle(Color.negativeRed)
}
Spacer()
}
}
.onAppear {
if faceIdEnabled && !didAttemptBiometrics {
didAttemptBiometrics = true
authenticateWithBiometrics()
} else if pinEnabled {
pinFocused = true
}
}
}
private func authenticateWithBiometrics() {
AppLockService.authenticate(reason: "Unlock your portfolio") { success in
if success {
isUnlocked = true
} else if pinEnabled {
pinFocused = true
} else {
errorMessage = String(format: String(localized: "biometric_failed"), biometry.displayName)
}
}
}
private func validatePin() {
guard let storedPin = KeychainService.readPin() else {
errorMessage = "PIN not set."
pin = ""
return
}
if pin == storedPin {
isUnlocked = true
pin = ""
errorMessage = nil
} else {
errorMessage = "Incorrect PIN."
pin = ""
}
}
}