UnlockViewModel.openFile(url:) called unlockWithPassword() immediately after saving the bookmark, but the first-open UI (openFileSection) has no password field yet -- password is still "" at that point, so every first file pick failed before the user could type anything. Picking a file now only saves the bookmark; the view naturally switches to the password-entry state for the user to unlock explicitly. Also makes KDBXError conform to LocalizedError with readable messages per case, so any error path that reaches the generic catch clause shows something useful instead of the default "The operation couldn't be completed (MyPassCore.KDBXError error N)". Found while testing the Phase 4 UI against a real KDBX file in the simulator. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WYqycDFsynHH9VnnK7LNSf
88 lines
2.8 KiB
Swift
88 lines
2.8 KiB
Swift
//
|
|
// UnlockViewModel.swift
|
|
// MyPass
|
|
//
|
|
|
|
import Foundation
|
|
import Combine
|
|
import MyPassCore
|
|
|
|
@MainActor
|
|
final class UnlockViewModel: ObservableObject {
|
|
@Published var password: String = ""
|
|
@Published var errorMessage: String?
|
|
@Published var isUnlocking: Bool = false
|
|
@Published var showFilePicker: Bool = false
|
|
|
|
private let session: VaultSession
|
|
private let bookmarkService: FileBookmarkService
|
|
private let keychainStore: KeychainStore
|
|
private let biometricService: BiometricAuthService
|
|
|
|
private let keychainAccount = "masterPassword"
|
|
|
|
init(
|
|
session: VaultSession,
|
|
bookmarkService: FileBookmarkService = .init(),
|
|
keychainStore: KeychainStore = .init(accessGroup: "org.antiloop222.mypass"),
|
|
biometricService: BiometricAuthService = .init()
|
|
) {
|
|
self.session = session
|
|
self.bookmarkService = bookmarkService
|
|
self.keychainStore = keychainStore
|
|
self.biometricService = biometricService
|
|
}
|
|
|
|
var canUseBiometrics: Bool {
|
|
biometricService.isAvailable && keychainStore.exists(for: keychainAccount)
|
|
}
|
|
|
|
var hasVault: Bool { bookmarkService.hasBookmark }
|
|
|
|
func unlockWithBiometrics() {
|
|
Task {
|
|
isUnlocking = true
|
|
errorMessage = nil
|
|
do {
|
|
try await biometricService.authenticate(reason: "Unlock MyPass")
|
|
let storedPassword = try keychainStore.load(for: keychainAccount)
|
|
let url = try bookmarkService.resolveURL()
|
|
defer { bookmarkService.stopAccess(url: url) }
|
|
try session.unlock(url: url, password: storedPassword)
|
|
} catch {
|
|
errorMessage = error.localizedDescription
|
|
}
|
|
isUnlocking = false
|
|
}
|
|
}
|
|
|
|
func unlockWithPassword() {
|
|
Task {
|
|
isUnlocking = true
|
|
errorMessage = nil
|
|
do {
|
|
let url = try bookmarkService.resolveURL()
|
|
defer { bookmarkService.stopAccess(url: url) }
|
|
try session.unlock(url: url, password: password)
|
|
try keychainStore.save(password: password, for: keychainAccount)
|
|
password = ""
|
|
} catch KDBXError.invalidPassword {
|
|
errorMessage = "Incorrect password."
|
|
} catch {
|
|
errorMessage = error.localizedDescription
|
|
}
|
|
isUnlocking = false
|
|
}
|
|
}
|
|
|
|
func openFile(url: URL) {
|
|
do {
|
|
try bookmarkService.save(url: url)
|
|
// Don't attempt to unlock yet -- picking a file only saves the bookmark.
|
|
// The view now shows the password field for the user to enter their password.
|
|
} catch {
|
|
errorMessage = error.localizedDescription
|
|
}
|
|
}
|
|
}
|