Adds an eye-icon toggle to the master password field (UnlockView) so users can verify what they typed before submitting. Also maps KDBX4's ERROR_HEADER_HASH_VERIFICATION_FAILED -- surfaced via the header HMAC check, which is password-derived -- to KDBXError.invalidPassword alongside the existing passwordAndOrKeyfileWrong check. KeePassKit uses this same error code for both a wrong password/keyfile and genuine file corruption (it doesn't distinguish the two), so the message hedges: "Incorrect password, or this vault file is corrupted." Found while testing against a real KDBX4 vault, which was surfacing this as a raw, unreadable NSError string before the previous commit's LocalizedError fix, and as *no* friendly message at all before this one (the error code wasn't in the recognized set yet). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WYqycDFsynHH9VnnK7LNSf
127 lines
3.7 KiB
Swift
127 lines
3.7 KiB
Swift
//
|
|
// UnlockView.swift
|
|
// MyPass
|
|
//
|
|
|
|
import SwiftUI
|
|
import UniformTypeIdentifiers
|
|
import MyPassCore
|
|
|
|
struct UnlockView: View {
|
|
@ObservedObject var vm: UnlockViewModel
|
|
|
|
var body: some View {
|
|
VStack(spacing: 24) {
|
|
Spacer()
|
|
Image(systemName: "lock.shield.fill")
|
|
.font(.system(size: 64))
|
|
.foregroundStyle(.tint)
|
|
Text("MyPass")
|
|
.font(.largeTitle.bold())
|
|
|
|
if vm.hasVault {
|
|
vaultUnlockSection
|
|
} else {
|
|
openFileSection
|
|
}
|
|
|
|
if let msg = vm.errorMessage {
|
|
Text(msg)
|
|
.foregroundStyle(.red)
|
|
.font(.caption)
|
|
.multilineTextAlignment(.center)
|
|
}
|
|
Spacer()
|
|
}
|
|
.padding(32)
|
|
.fileImporter(
|
|
isPresented: $vm.showFilePicker,
|
|
allowedContentTypes: [.init(filenameExtension: "kdbx")!],
|
|
onCompletion: { result in
|
|
if let url = try? result.get() { vm.openFile(url: url) }
|
|
}
|
|
)
|
|
}
|
|
|
|
@ViewBuilder
|
|
private var vaultUnlockSection: some View {
|
|
VStack(spacing: 16) {
|
|
if vm.canUseBiometrics {
|
|
Button(action: vm.unlockWithBiometrics) {
|
|
Label("Use Face ID / Touch ID", systemImage: "faceid")
|
|
.frame(maxWidth: .infinity)
|
|
}
|
|
.buttonStyle(.borderedProminent)
|
|
.disabled(vm.isUnlocking)
|
|
|
|
Text("or enter password")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
|
|
PasswordField(text: $vm.password, onSubmit: vm.unlockWithPassword)
|
|
|
|
Button("Unlock", action: vm.unlockWithPassword)
|
|
.buttonStyle(.bordered)
|
|
.disabled(vm.password.isEmpty || vm.isUnlocking)
|
|
|
|
Button("Choose different file…") { vm.showFilePicker = true }
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private var openFileSection: some View {
|
|
VStack(spacing: 16) {
|
|
Text("No vault selected. Open a .kdbx file to get started.")
|
|
.multilineTextAlignment(.center)
|
|
.foregroundStyle(.secondary)
|
|
Button(action: { vm.showFilePicker = true }) {
|
|
Label("Open KDBX File…", systemImage: "doc.badge.plus")
|
|
.frame(maxWidth: .infinity)
|
|
}
|
|
.buttonStyle(.borderedProminent)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A password field with a trailing eye button to reveal/hide the typed value.
|
|
private struct PasswordField: View {
|
|
@Binding var text: String
|
|
var onSubmit: () -> Void
|
|
|
|
@State private var isRevealed = false
|
|
|
|
var body: some View {
|
|
HStack {
|
|
Group {
|
|
if isRevealed {
|
|
TextField("Master Password", text: $text)
|
|
} else {
|
|
SecureField("Master Password", text: $text)
|
|
}
|
|
}
|
|
.textFieldStyle(.plain)
|
|
.autocorrectionDisabled()
|
|
#if os(iOS)
|
|
.textInputAutocapitalization(.never)
|
|
#endif
|
|
.onSubmit(onSubmit)
|
|
|
|
Button {
|
|
isRevealed.toggle()
|
|
} label: {
|
|
Image(systemName: isRevealed ? "eye.slash" : "eye")
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
.padding(8)
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 6)
|
|
.stroke(Color.secondary.opacity(0.3), lineWidth: 1)
|
|
)
|
|
}
|
|
}
|