125 lines
4.1 KiB
Swift
125 lines
4.1 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
|
|
|
|
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)
|
|
.foregroundColor(.secondary)
|
|
.multilineTextAlignment(.center)
|
|
.padding(.horizontal, 30)
|
|
|
|
if faceIdEnabled {
|
|
Button {
|
|
authenticateWithBiometrics()
|
|
} label: {
|
|
Label("Unlock with Face ID", systemImage: "faceid")
|
|
.font(.headline)
|
|
.frame(maxWidth: .infinity)
|
|
.padding()
|
|
.background(Color.appPrimary)
|
|
.foregroundColor(.white)
|
|
.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))
|
|
.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)
|
|
.foregroundColor(.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 = "Face ID failed. Please try again."
|
|
}
|
|
}
|
|
}
|
|
|
|
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 = ""
|
|
}
|
|
}
|
|
}
|