From 9d2ed68dccd46b401607d97826260391026b9240 Mon Sep 17 00:00:00 2001 From: alexandrev-tibco Date: Wed, 11 Feb 2026 23:50:44 +0100 Subject: [PATCH] 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 --- .../Views/Settings/SettingsView.swift | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/PortfolioJournal/Views/Settings/SettingsView.swift b/PortfolioJournal/Views/Settings/SettingsView.swift index b1c13fb..f9fb318 100644 --- a/PortfolioJournal/Views/Settings/SettingsView.swift +++ b/PortfolioJournal/Views/Settings/SettingsView.swift @@ -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()))