Add cached TOTP search (#77)

This commit is contained in:
2026-08-12 20:36:03 +02:00
parent 1ce7b7814b
commit 87f9e7be98
7 changed files with 156 additions and 7 deletions

View File

@@ -643,6 +643,8 @@ public protocol MobileAuthenticationProtocol: AnyObject, Sendable {
func saveEntryEditor(editor: UInt64, fields: [MobileEntryEditorInput]) throws -> MobileEntryPage
func searchCachedTotpPage(query: String) throws -> MobileTotpPage?
func setBiometricUnlock(enabled: Bool) throws -> MobileAuthenticationState
func setGitIdentity(name: String, email: String) throws -> MobileGitIdentity
@@ -928,6 +930,16 @@ open func saveEntryEditor(editor: UInt64, fields: [MobileEntryEditorInput])throw
})
}
open func searchCachedTotpPage(query: String)throws -> MobileTotpPage? {
return try FfiConverterOptionTypeMobileTotpPage.lift(try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
uniffiCallStatus in
uniffi_ironstorage_apple_fn_method_mobileauthentication_search_cached_totp_page(
self.uniffiCloneHandle(),
FfiConverterString.lower(query),uniffiCallStatus
)
})
}
open func setBiometricUnlock(enabled: Bool)throws -> MobileAuthenticationState {
return try FfiConverterTypeMobileAuthenticationState_lift(try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
uniffiCallStatus in
@@ -6969,6 +6981,9 @@ private let initializationResult: InitializationResult = {
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_save_entry_editor() != 30971) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_search_cached_totp_page() != 13608) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_set_biometric_unlock() != 9486) {
return InitializationResult.apiChecksumMismatch
}
@@ -7067,4 +7082,4 @@ public func uniffiEnsureIronstorageAppleInitialized() {
}
}
// swiftlint:enable all
// swiftlint:enable all

View File

@@ -353,6 +353,11 @@ RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_reveal_entry_
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_save_entry_editor(uint64_t ptr, uint64_t editor, RustBuffer fields, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_SEARCH_CACHED_TOTP_PAGE
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_SEARCH_CACHED_TOTP_PAGE
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_search_cached_totp_page(uint64_t ptr, RustBuffer query, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_SET_BIOMETRIC_UNLOCK
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_SET_BIOMETRIC_UNLOCK
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_set_biometric_unlock(uint64_t ptr, int8_t enabled, RustCallStatus *_Nonnull out_status
@@ -1054,6 +1059,12 @@ uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_reveal_en
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_SAVE_ENTRY_EDITOR
uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_save_entry_editor(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_SEARCH_CACHED_TOTP_PAGE
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_SEARCH_CACHED_TOTP_PAGE
uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_search_cached_totp_page(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_SET_BIOMETRIC_UNLOCK

View File

@@ -1744,17 +1744,23 @@ private final class TotpDiscoveryView: UIView {
}
@MainActor
private final class TotpListViewController: UITableViewController, MobileTabRoot {
private final class TotpListViewController: UITableViewController, MobileTabRoot,
UISearchResultsUpdating
{
fileprivate let shellTab = MobileTab.totp
private let authentication: MobileAuthentication?
private let searchController = UISearchController(searchResultsController: nil)
private var shellPage: MobilePage
private var page: MobileTotpPage?
private var unfilteredPage: MobileTotpPage?
private var loadTask: Task<Void, Never>?
private var searchTask: Task<Void, Never>?
private var unlockTask: Task<Void, Never>?
private var shellTask: Task<Void, Never>?
private var progressTask: Task<Void, Never>?
private var operation: MobileTotpOperation?
private var loadGeneration = 0
private var searchGeneration = 0
private var refreshAfterUnlock = false
init(shellPage: MobilePage, authentication: MobileAuthentication?) {
@@ -1763,6 +1769,15 @@ private final class TotpListViewController: UITableViewController, MobileTabRoot
super.init(style: .insetGrouped)
title = shellPage.title
navigationItem.largeTitleDisplayMode = .always
searchController.searchResultsUpdater = self
searchController.obscuresBackgroundDuringPresentation = false
searchController.searchBar.placeholder = "TOTP entry names and folders"
searchController.searchBar.autocapitalizationType = .none
searchController.searchBar.autocorrectionType = .no
searchController.searchBar.spellCheckingType = .no
navigationItem.searchController = searchController
navigationItem.hidesSearchBarWhenScrolling = false
definesPresentationContext = true
refreshControl = UIRefreshControl()
refreshControl?.addTarget(self, action: #selector(refreshRequested), for: .valueChanged)
}
@@ -1775,6 +1790,7 @@ private final class TotpListViewController: UITableViewController, MobileTabRoot
deinit {
operation?.cancel()
loadTask?.cancel()
searchTask?.cancel()
unlockTask?.cancel()
shellTask?.cancel()
progressTask?.cancel()
@@ -1809,6 +1825,10 @@ private final class TotpListViewController: UITableViewController, MobileTabRoot
reloadShell()
}
func updateSearchResults(for searchController: UISearchController) {
search(searchController.searchBar.text ?? "")
}
override func numberOfSections(in tableView: UITableView) -> Int { 1 }
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
@@ -1912,12 +1932,15 @@ private final class TotpListViewController: UITableViewController, MobileTabRoot
@objc private func localStoreDidChange() {
operation?.cancel()
loadTask?.cancel()
searchTask?.cancel()
progressTask?.cancel()
loadGeneration += 1
searchGeneration += 1
operation = nil
progressTask = nil
tableView.tableHeaderView = nil
page = nil
unfilteredPage = nil
loadCachedPage()
}
@@ -1942,7 +1965,10 @@ private final class TotpListViewController: UITableViewController, MobileTabRoot
private func refreshState(force: Bool = false) {
guard shellPage.state == .ready else {
searchTask?.cancel()
searchGeneration += 1
page = nil
unfilteredPage = nil
showUnavailable(
title: shellPage.stateTitle,
detail: shellPage.stateDetail,
@@ -2146,21 +2172,66 @@ private final class TotpListViewController: UITableViewController, MobileTabRoot
}
}
private func apply(_ page: MobileTotpPage) {
private func apply(_ page: MobileTotpPage, remember: Bool = true) {
if remember {
unfilteredPage = page
let query = searchController.searchBar.text ?? ""
if !query.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
search(query)
return
}
}
self.page = page
configureLockButton()
tableView.reloadData()
if page.rows.isEmpty {
let query = searchController.searchBar.text ?? ""
let searching = !query.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
showUnavailable(
title: "No TOTP Codes",
detail: "No valid time-based OTP entries were found in the password store.",
image: "timer"
title: searching ? "No Results" : "No TOTP Codes",
detail: searching
? "No cached TOTP entries match “\(query)”."
: "No valid time-based OTP entries were found in the password store.",
image: searching ? "magnifyingglass" : "timer"
)
} else {
contentUnavailableConfiguration = nil
}
}
private func search(_ query: String) {
searchGeneration += 1
let generation = searchGeneration
searchTask?.cancel()
guard !query.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
if let unfilteredPage { apply(unfilteredPage, remember: false) }
return
}
guard let authentication else { return }
searchTask = Task { [weak self] in
let result = await Task.detached(priority: .userInitiated) {
do {
return Result<MobileTotpPage?, AuthenticationFailure>.success(
try authentication.searchCachedTotpPage(query: query)
)
} catch let error as MobileAuthenticationFfiError {
return .failure(AuthenticationFailure(error))
} catch {
return .failure(.unexpected)
}
}.value
guard !Task.isCancelled, let self, generation == searchGeneration else { return }
switch result {
case let .success(.some(page)):
apply(page, remember: false)
case .success(.none):
break
case let .failure(failure):
handle(failure)
}
}
}
private func configureLockButton() {
let unlocked = (try? authentication?.state().unlocked) == true
navigationItem.rightBarButtonItem = UIBarButtonItem(
@@ -2175,6 +2246,7 @@ private final class TotpListViewController: UITableViewController, MobileTabRoot
private func showLocked() {
page = nil
unfilteredPage = nil
tableView.reloadData()
navigationItem.rightBarButtonItem = nil
var configuration = UIContentUnavailableConfiguration.empty()