Navigate password folders on iPhone

This commit is contained in:
2026-08-11 17:30:57 +02:00
parent 8e883f5ccf
commit 3295761bcf
7 changed files with 1144 additions and 5 deletions

View File

@@ -1523,6 +1523,138 @@ public func FfiConverterTypeMobilePage_lower(_ value: MobilePage) -> RustBuffer
}
public struct MobilePasswordPage: Equatable, Hashable {
public var id: String
public var path: String
public var title: String
public var rows: [MobilePasswordRow]
// Default memberwise initializers are never public by default, so we
// declare one manually.
public init(id: String, path: String, title: String, rows: [MobilePasswordRow]) {
self.id = id
self.path = path
self.title = title
self.rows = rows
}
}
#if compiler(>=6)
extension MobilePasswordPage: Sendable {}
#endif
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public struct FfiConverterTypeMobilePasswordPage: FfiConverterRustBuffer {
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobilePasswordPage {
return
try MobilePasswordPage(
id: FfiConverterString.read(from: &buf),
path: FfiConverterString.read(from: &buf),
title: FfiConverterString.read(from: &buf),
rows: FfiConverterSequenceTypeMobilePasswordRow.read(from: &buf)
)
}
public static func write(_ value: MobilePasswordPage, into buf: inout [UInt8]) {
FfiConverterString.write(value.id, into: &buf)
FfiConverterString.write(value.path, into: &buf)
FfiConverterString.write(value.title, into: &buf)
FfiConverterSequenceTypeMobilePasswordRow.write(value.rows, into: &buf)
}
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeMobilePasswordPage_lift(_ buf: RustBuffer) throws -> MobilePasswordPage {
return try FfiConverterTypeMobilePasswordPage.lift(buf)
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeMobilePasswordPage_lower(_ value: MobilePasswordPage) -> RustBuffer {
return FfiConverterTypeMobilePasswordPage.lower(value)
}
public struct MobilePasswordRow: Equatable, Hashable {
public var id: String
public var path: String
public var title: String
public var detail: String
public var systemImage: String
public var kind: MobilePasswordRowKind
// Default memberwise initializers are never public by default, so we
// declare one manually.
public init(id: String, path: String, title: String, detail: String, systemImage: String, kind: MobilePasswordRowKind) {
self.id = id
self.path = path
self.title = title
self.detail = detail
self.systemImage = systemImage
self.kind = kind
}
}
#if compiler(>=6)
extension MobilePasswordRow: Sendable {}
#endif
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public struct FfiConverterTypeMobilePasswordRow: FfiConverterRustBuffer {
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobilePasswordRow {
return
try MobilePasswordRow(
id: FfiConverterString.read(from: &buf),
path: FfiConverterString.read(from: &buf),
title: FfiConverterString.read(from: &buf),
detail: FfiConverterString.read(from: &buf),
systemImage: FfiConverterString.read(from: &buf),
kind: FfiConverterTypeMobilePasswordRowKind.read(from: &buf)
)
}
public static func write(_ value: MobilePasswordRow, into buf: inout [UInt8]) {
FfiConverterString.write(value.id, into: &buf)
FfiConverterString.write(value.path, into: &buf)
FfiConverterString.write(value.title, into: &buf)
FfiConverterString.write(value.detail, into: &buf)
FfiConverterString.write(value.systemImage, into: &buf)
FfiConverterTypeMobilePasswordRowKind.write(value.kind, into: &buf)
}
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeMobilePasswordRow_lift(_ buf: RustBuffer) throws -> MobilePasswordRow {
return try FfiConverterTypeMobilePasswordRow.lift(buf)
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeMobilePasswordRow_lower(_ value: MobilePasswordRow) -> RustBuffer {
return FfiConverterTypeMobilePasswordRow.lower(value)
}
public struct MobileShell: Equatable, Hashable {
public var selectedTab: MobileTab
public var pages: [MobilePage]
@@ -2372,6 +2504,238 @@ public func FfiConverterTypeMobileOnboardingPhase_lower(_ value: MobileOnboardin
public enum MobilePasswordErrorKind: Equatable, Hashable {
case missingConfiguration
case configuration
case invalidPath
case directoryMissing
case repository
}
#if compiler(>=6)
extension MobilePasswordErrorKind: Sendable {}
#endif
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public struct FfiConverterTypeMobilePasswordErrorKind: FfiConverterRustBuffer {
typealias SwiftType = MobilePasswordErrorKind
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobilePasswordErrorKind {
let variant: Int32 = try readInt(&buf)
switch variant {
case 1: return .missingConfiguration
case 2: return .configuration
case 3: return .invalidPath
case 4: return .directoryMissing
case 5: return .repository
default: throw UniffiInternalError.unexpectedEnumCase
}
}
public static func write(_ value: MobilePasswordErrorKind, into buf: inout [UInt8]) {
switch value {
case .missingConfiguration:
writeInt(&buf, Int32(1))
case .configuration:
writeInt(&buf, Int32(2))
case .invalidPath:
writeInt(&buf, Int32(3))
case .directoryMissing:
writeInt(&buf, Int32(4))
case .repository:
writeInt(&buf, Int32(5))
}
}
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeMobilePasswordErrorKind_lift(_ buf: RustBuffer) throws -> MobilePasswordErrorKind {
return try FfiConverterTypeMobilePasswordErrorKind.lift(buf)
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeMobilePasswordErrorKind_lower(_ value: MobilePasswordErrorKind) -> RustBuffer {
return FfiConverterTypeMobilePasswordErrorKind.lower(value)
}
public
enum MobilePasswordFfiError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError {
case Failed(kind: MobilePasswordErrorKind, title: String, detail: String
)
public var errorDescription: String? {
String(reflecting: self)
}
}
#if compiler(>=6)
extension MobilePasswordFfiError: Sendable {}
#endif
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public struct FfiConverterTypeMobilePasswordFfiError: FfiConverterRustBuffer {
typealias SwiftType = MobilePasswordFfiError
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobilePasswordFfiError {
let variant: Int32 = try readInt(&buf)
switch variant {
case 1: return .Failed(
kind: try FfiConverterTypeMobilePasswordErrorKind.read(from: &buf),
title: try FfiConverterString.read(from: &buf),
detail: try FfiConverterString.read(from: &buf)
)
default: throw UniffiInternalError.unexpectedEnumCase
}
}
public static func write(_ value: MobilePasswordFfiError, into buf: inout [UInt8]) {
switch value {
case let .Failed(kind,title,detail):
writeInt(&buf, Int32(1))
FfiConverterTypeMobilePasswordErrorKind.write(kind, into: &buf)
FfiConverterString.write(title, into: &buf)
FfiConverterString.write(detail, into: &buf)
}
}
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeMobilePasswordFfiError_lift(_ buf: RustBuffer) throws -> MobilePasswordFfiError {
return try FfiConverterTypeMobilePasswordFfiError.lift(buf)
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeMobilePasswordFfiError_lower(_ value: MobilePasswordFfiError) -> RustBuffer {
return FfiConverterTypeMobilePasswordFfiError.lower(value)
}
public enum MobilePasswordRowKind: Equatable, Hashable {
case directory
case entry
}
#if compiler(>=6)
extension MobilePasswordRowKind: Sendable {}
#endif
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public struct FfiConverterTypeMobilePasswordRowKind: FfiConverterRustBuffer {
typealias SwiftType = MobilePasswordRowKind
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobilePasswordRowKind {
let variant: Int32 = try readInt(&buf)
switch variant {
case 1: return .directory
case 2: return .entry
default: throw UniffiInternalError.unexpectedEnumCase
}
}
public static func write(_ value: MobilePasswordRowKind, into buf: inout [UInt8]) {
switch value {
case .directory:
writeInt(&buf, Int32(1))
case .entry:
writeInt(&buf, Int32(2))
}
}
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeMobilePasswordRowKind_lift(_ buf: RustBuffer) throws -> MobilePasswordRowKind {
return try FfiConverterTypeMobilePasswordRowKind.lift(buf)
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeMobilePasswordRowKind_lower(_ value: MobilePasswordRowKind) -> RustBuffer {
return FfiConverterTypeMobilePasswordRowKind.lower(value)
}
public
enum MobilePreferenceError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError {
@@ -2637,6 +3001,30 @@ fileprivate struct FfiConverterOptionInt64: FfiConverterRustBuffer {
}
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
fileprivate struct FfiConverterOptionString: FfiConverterRustBuffer {
typealias SwiftType = String?
public static func write(_ value: SwiftType, into buf: inout [UInt8]) {
guard let value = value else {
writeInt(&buf, Int8(0))
return
}
writeInt(&buf, Int8(1))
FfiConverterString.write(value, into: &buf)
}
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType {
switch try readInt(&buf) as Int8 {
case 0: return nil
case 1: return try FfiConverterString.read(from: &buf)
default: throw UniffiInternalError.unexpectedOptionalTag
}
}
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
@@ -2785,6 +3173,31 @@ fileprivate struct FfiConverterSequenceTypeMobilePage: FfiConverterRustBuffer {
return seq
}
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
fileprivate struct FfiConverterSequenceTypeMobilePasswordRow: FfiConverterRustBuffer {
typealias SwiftType = [MobilePasswordRow]
public static func write(_ value: [MobilePasswordRow], into buf: inout [UInt8]) {
let len = Int32(value.count)
writeInt(&buf, len)
for item in value {
FfiConverterTypeMobilePasswordRow.write(item, into: &buf)
}
}
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> [MobilePasswordRow] {
let len: Int32 = try readInt(&buf)
var seq = [MobilePasswordRow]()
seq.reserveCapacity(Int(len))
for _ in 0 ..< len {
seq.append(try FfiConverterTypeMobilePasswordRow.read(from: &buf))
}
return seq
}
}
public func mobileHomeOperation() -> MobileHomeOperation {
return try! FfiConverterTypeMobileHomeOperation_lift(try! rustCall() {
uniffiCallStatus in
@@ -2803,6 +3216,14 @@ public func mobileOnboardingOperation(serverUrl: String, account: String, reposi
)
})
}
public func mobilePasswordPage(path: String?)throws -> MobilePasswordPage {
return try FfiConverterTypeMobilePasswordPage_lift(try rustCallWithError(FfiConverterTypeMobilePasswordFfiError_lift) {
uniffiCallStatus in
uniffi_ironstorage_apple_fn_func_mobile_password_page(
FfiConverterOptionString.lower(path),uniffiCallStatus
)
})
}
public func mobileShell() -> MobileShell {
return try! FfiConverterTypeMobileShell_lift(try! rustCall() {
uniffiCallStatus in
@@ -2862,6 +3283,9 @@ private let initializationResult: InitializationResult = {
if (uniffi_ironstorage_apple_checksum_func_mobile_onboarding_operation() != 8354) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_ironstorage_apple_checksum_func_mobile_password_page() != 64312) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_ironstorage_apple_checksum_func_mobile_shell() != 42687) {
return InitializationResult.apiChecksumMismatch
}

View File

@@ -324,6 +324,11 @@ uint64_t uniffi_ironstorage_apple_fn_func_mobile_home_operation(RustCallStatus *
uint64_t uniffi_ironstorage_apple_fn_func_mobile_onboarding_operation(RustBuffer server_url, RustBuffer account, RustBuffer repository_path, RustBuffer application_token, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FUNC_MOBILE_PASSWORD_PAGE
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FUNC_MOBILE_PASSWORD_PAGE
RustBuffer uniffi_ironstorage_apple_fn_func_mobile_password_page(RustBuffer path, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FUNC_MOBILE_SHELL
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FUNC_MOBILE_SHELL
RustBuffer uniffi_ironstorage_apple_fn_func_mobile_shell(RustCallStatus *_Nonnull out_status
@@ -621,6 +626,12 @@ uint16_t uniffi_ironstorage_apple_checksum_func_mobile_home_operation(void
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_FUNC_MOBILE_ONBOARDING_OPERATION
uint16_t uniffi_ironstorage_apple_checksum_func_mobile_onboarding_operation(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_FUNC_MOBILE_PASSWORD_PAGE
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_FUNC_MOBILE_PASSWORD_PAGE
uint16_t uniffi_ironstorage_apple_checksum_func_mobile_password_page(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_FUNC_MOBILE_SHELL

View File

@@ -25,6 +25,11 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
}
}
@MainActor
private protocol MobileTabRoot: AnyObject {
var shellTab: MobileTab { get }
}
@MainActor
private final class AppContext: NSObject, UITabBarControllerDelegate {
private let tabs = UITabBarController()
@@ -34,7 +39,9 @@ private final class AppContext: NSObject, UITabBarControllerDelegate {
func makeRootController() -> UIViewController {
let shell = mobileShellFixture(state: .loading)
navigationControllers = shell.pages.map { page in
let root = ShellViewController(page: page)
let root: UIViewController = page.tab == .passwords
? PasswordDirectoryViewController(shellPage: page)
: ShellViewController(page: page)
let navigation = UINavigationController(rootViewController: root)
navigation.navigationBar.prefersLargeTitles = true
navigation.tabBarItem = UITabBarItem(
@@ -56,7 +63,7 @@ private final class AppContext: NSObject, UITabBarControllerDelegate {
) {
guard
let navigation = viewController as? UINavigationController,
let root = navigation.viewControllers.first as? ShellViewController
let root = navigation.viewControllers.first as? MobileTabRoot
else {
return
}
@@ -86,7 +93,7 @@ private final class AppContext: NSObject, UITabBarControllerDelegate {
}
@MainActor
private final class ShellViewController: UITableViewController {
private final class ShellViewController: UITableViewController, MobileTabRoot {
fileprivate let shellTab: MobileTab
private var page: MobilePage
private var loadTask: Task<Void, Never>?
@@ -607,6 +614,289 @@ private final class ShellViewController: UITableViewController {
}
}
@MainActor
private final class PasswordDirectoryViewController: UITableViewController, MobileTabRoot {
fileprivate let shellTab = MobileTab.passwords
private let path: String?
private var shellPage: MobilePage?
private var directoryPage: MobilePasswordPage?
private var loadTask: Task<Void, Never>?
private var loadGeneration = 0
init(shellPage: MobilePage, path: String? = nil) {
self.shellPage = shellPage
self.path = path
super.init(style: .insetGrouped)
title = shellPage.title
navigationItem.largeTitleDisplayMode = path == nil ? .always : .never
refreshControl = UIRefreshControl()
refreshControl?.addTarget(self, action: #selector(refreshRequested), for: .valueChanged)
NotificationCenter.default.addObserver(
self,
selector: #selector(localStoreDidChange),
name: .ironStorageLocalStoreDidChange,
object: nil
)
}
private init(path: String, title: String) {
self.path = path
shellPage = nil
super.init(style: .insetGrouped)
self.title = title
navigationItem.largeTitleDisplayMode = .never
refreshControl = UIRefreshControl()
refreshControl?.addTarget(self, action: #selector(refreshRequested), for: .valueChanged)
NotificationCenter.default.addObserver(
self,
selector: #selector(localStoreDidChange),
name: .ironStorageLocalStoreDidChange,
object: nil
)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) is not supported")
}
deinit {
loadTask?.cancel()
NotificationCenter.default.removeObserver(self)
}
override func viewDidLoad() {
super.viewDidLoad()
if let shellPage {
apply(shellPage)
} else {
showLoading()
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
reload()
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
loadGeneration += 1
loadTask?.cancel()
refreshControl?.endRefreshing()
}
override func numberOfSections(in tableView: UITableView) -> Int {
directoryPage?.rows.isEmpty == false ? 1 : 0
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
directoryPage?.rows.count ?? 0
}
override func tableView(
_ tableView: UITableView,
cellForRowAt indexPath: IndexPath
) -> UITableViewCell {
let cell = UITableViewCell(style: .subtitle, reuseIdentifier: nil)
guard let row = directoryPage?.rows[indexPath.row] else { return cell }
var content = cell.defaultContentConfiguration()
content.image = UIImage(systemName: row.systemImage)
content.text = row.title
content.secondaryText = row.detail
content.textProperties.numberOfLines = 2
content.secondaryTextProperties.numberOfLines = 1
cell.contentConfiguration = content
cell.accessoryType = .disclosureIndicator
cell.accessibilityLabel = "\(row.title), \(row.detail)"
cell.accessibilityHint = row.kind == .directory
? "Opens this password folder."
: "Opens the locked password viewer."
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let row = directoryPage?.rows[indexPath.row] else { return }
let controller: UIViewController = switch row.kind {
case .directory:
PasswordDirectoryViewController(path: row.path, title: row.title)
case .entry:
LockedPasswordViewController(entry: row)
}
navigationController?.pushViewController(controller, animated: true)
}
@objc private func refreshRequested() {
reload()
}
@objc private func localStoreDidChange() {
guard viewIfLoaded?.window != nil else { return }
reload()
}
private func reload() {
loadGeneration += 1
let generation = loadGeneration
let selectedID = tableView.indexPathForSelectedRow.flatMap {
directoryPage?.rows[$0.row].id
}
loadTask?.cancel()
loadTask = Task { [weak self] in
guard let self else { return }
if path == nil {
let shell = await Task.detached(priority: .userInitiated) { mobileShell() }.value
guard
!Task.isCancelled,
generation == loadGeneration,
let page = shell.pages.first(where: { $0.tab == .passwords })
else { return }
apply(page)
guard page.state == .ready else { return }
} else {
showLoading()
}
let path = self.path
let result = await Task.detached(priority: .userInitiated) {
do {
return Result<MobilePasswordPage, PasswordFailure>.success(
try mobilePasswordPage(path: path)
)
} catch let error as MobilePasswordFfiError {
return .failure(PasswordFailure(error))
} catch {
return .failure(.unexpected)
}
}.value
guard !Task.isCancelled, generation == loadGeneration else { return }
finish(result, selectedID: selectedID)
}
}
private func apply(_ page: MobilePage) {
shellPage = page
title = page.title
guard page.state == .ready else {
directoryPage = nil
tableView.reloadData()
refreshControl?.endRefreshing()
var configuration = page.state == .loading
? UIContentUnavailableConfiguration.loading()
: UIContentUnavailableConfiguration.empty()
configuration.image = page.state == .loading
? nil
: UIImage(systemName: page.state == .error ? "exclamationmark.triangle" : "lock.shield")
configuration.text = page.stateTitle
configuration.secondaryText = page.stateDetail
contentUnavailableConfiguration = configuration
return
}
showLoading()
}
private func finish(
_ result: Result<MobilePasswordPage, PasswordFailure>,
selectedID: String?
) {
refreshControl?.endRefreshing()
switch result {
case let .success(page):
directoryPage = page
title = page.title
contentUnavailableConfiguration = nil
tableView.reloadData()
if page.rows.isEmpty {
var configuration = UIContentUnavailableConfiguration.empty()
configuration.image = UIImage(systemName: "folder")
configuration.text = "Empty Folder"
configuration.secondaryText = "This password folder contains no entries."
contentUnavailableConfiguration = configuration
} else if let selectedID,
let row = page.rows.firstIndex(where: { $0.id == selectedID }) {
tableView.selectRow(at: IndexPath(row: row, section: 0), animated: false, scrollPosition: .none)
}
case let .failure(failure):
if failure.kind == .directoryMissing, path != nil {
UIAccessibility.post(
notification: .announcement,
argument: "The password folder no longer exists."
)
navigationController?.popViewController(animated: true)
return
}
directoryPage = nil
tableView.reloadData()
var configuration = UIContentUnavailableConfiguration.empty()
configuration.image = UIImage(systemName: "exclamationmark.triangle")
configuration.text = failure.title
configuration.secondaryText = failure.detail
contentUnavailableConfiguration = configuration
}
}
private func showLoading() {
var configuration = UIContentUnavailableConfiguration.loading()
configuration.text = "Loading Passwords"
configuration.secondaryText = "Reading the locked folder model from storage."
contentUnavailableConfiguration = configuration
}
}
private struct PasswordFailure: Error, Sendable {
let kind: MobilePasswordErrorKind
let title: String
let detail: String
init(_ error: MobilePasswordFfiError) {
switch error {
case let .Failed(kind, title, detail):
self.kind = kind
self.title = title
self.detail = detail
}
}
static let unexpected = PasswordFailure(
kind: .repository,
title: "Passwords Are Unavailable",
detail: "IronStorage could not load the storage-provided folder."
)
private init(kind: MobilePasswordErrorKind, title: String, detail: String) {
self.kind = kind
self.title = title
self.detail = detail
}
}
@MainActor
private final class LockedPasswordViewController: UIViewController {
private let entry: MobilePasswordRow
init(entry: MobilePasswordRow) {
self.entry = entry
super.init(nibName: nil, bundle: nil)
title = entry.title
navigationItem.largeTitleDisplayMode = .never
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) is not supported")
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemGroupedBackground
var configuration = UIContentUnavailableConfiguration.empty()
configuration.image = UIImage(systemName: "lock.fill")
configuration.text = "Locked Password"
configuration.secondaryText = "Authenticate to reveal this password entry."
contentUnavailableConfiguration = configuration
view.accessibilityIdentifier = entry.id
}
}
private struct HomeFailure: Error, Sendable {
let kind: MobileHomeErrorKind
let title: String