Require PIN to disable Face ID

When toggling Face ID off, users must now verify their PIN first.
Adds a PinVerifyView component for PIN verification.

Fixes #23

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
alexandrev-tibco
2026-02-11 23:50:44 +01:00
parent 5dc9eb109f
commit 9d2ed68dcc
@@ -18,6 +18,7 @@ struct SettingsView: View {
@State private var showingBiometricAlert = false
@State private var showingPinRequiredAlert = false
@State private var showingPinDisableAlert = false
@State private var showingPinVerifyForFaceId = false
@State private var showingRestartAlert = false
@State private var didLoadCloudSync = false
@State private var backupToRestore: BackupRecord?
@@ -163,6 +164,13 @@ struct SettingsView: View {
_ = KeychainService.savePin(pin)
}
}
.sheet(isPresented: $showingPinVerifyForFaceId) {
PinVerifyView(title: "Enter PIN to Disable Face ID") { success in
if success {
faceIdEnabled = false
}
}
}
.onAppear {
didLoadCloudSync = true
if viewModel.backupsEnabled {
@@ -495,6 +503,9 @@ struct SettingsView: View {
faceIdEnabled = false
showingPinRequiredAlert = true
}
} else {
faceIdEnabled = true
showingPinVerifyForFaceId = true
}
}
@@ -860,6 +871,62 @@ struct PinSetupView: View {
}
}
struct PinVerifyView: View {
@Environment(\.dismiss) private var dismiss
let title: String
let onResult: (Bool) -> Void
@State private var pin = ""
@State private var errorMessage: String?
var body: some View {
NavigationStack {
Form {
Section {
SecureField("Enter PIN", text: $pin)
.keyboardType(.numberPad)
.onChange(of: pin) { _, newValue in
pin = String(newValue.filter(\.isNumber).prefix(4))
}
} header: {
Text("4-Digit PIN")
}
if let errorMessage {
Text(errorMessage)
.font(.caption)
.foregroundColor(.negativeRed)
}
}
.navigationTitle(title)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") {
onResult(false)
dismiss()
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button("Verify") { verifyPin() }
.disabled(pin.count < 4)
}
}
}
.presentationDetents([.medium])
}
private func verifyPin() {
guard let savedPin = KeychainService.readPin(), pin == savedPin else {
errorMessage = "Incorrect PIN."
pin = ""
return
}
onResult(true)
dismiss()
}
}
#Preview {
SettingsView(iapService: IAPService())
.environmentObject(AccountStore(iapService: IAPService()))