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:
@@ -18,6 +18,7 @@ struct SettingsView: View {
|
|||||||
@State private var showingBiometricAlert = false
|
@State private var showingBiometricAlert = false
|
||||||
@State private var showingPinRequiredAlert = false
|
@State private var showingPinRequiredAlert = false
|
||||||
@State private var showingPinDisableAlert = false
|
@State private var showingPinDisableAlert = false
|
||||||
|
@State private var showingPinVerifyForFaceId = false
|
||||||
@State private var showingRestartAlert = false
|
@State private var showingRestartAlert = false
|
||||||
@State private var didLoadCloudSync = false
|
@State private var didLoadCloudSync = false
|
||||||
@State private var backupToRestore: BackupRecord?
|
@State private var backupToRestore: BackupRecord?
|
||||||
@@ -163,6 +164,13 @@ struct SettingsView: View {
|
|||||||
_ = KeychainService.savePin(pin)
|
_ = KeychainService.savePin(pin)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.sheet(isPresented: $showingPinVerifyForFaceId) {
|
||||||
|
PinVerifyView(title: "Enter PIN to Disable Face ID") { success in
|
||||||
|
if success {
|
||||||
|
faceIdEnabled = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
didLoadCloudSync = true
|
didLoadCloudSync = true
|
||||||
if viewModel.backupsEnabled {
|
if viewModel.backupsEnabled {
|
||||||
@@ -495,6 +503,9 @@ struct SettingsView: View {
|
|||||||
faceIdEnabled = false
|
faceIdEnabled = false
|
||||||
showingPinRequiredAlert = true
|
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 {
|
#Preview {
|
||||||
SettingsView(iapService: IAPService())
|
SettingsView(iapService: IAPService())
|
||||||
.environmentObject(AccountStore(iapService: IAPService()))
|
.environmentObject(AccountStore(iapService: IAPService()))
|
||||||
|
|||||||
Reference in New Issue
Block a user