Implement Apple Watch TOTP interface (#57)

This commit is contained in:
2026-08-16 14:38:16 +02:00
parent df1d49339c
commit e54d91a83a
6 changed files with 789 additions and 126 deletions

View File

@@ -10,7 +10,8 @@ use std::{
};
use ironstorage::mobile_watch::{
WatchPersistenceAction as StoragePersistenceAction, WatchRuntime as StorageWatchRuntime,
WatchPersistenceAction as StoragePersistenceAction, WatchPresentation as StoragePresentation,
WatchPresentationState as StoragePresentationState, WatchRuntime as StorageWatchRuntime,
WatchSnapshotApply as StorageSnapshotApply, WatchSnapshotError as StorageWatchError,
WatchSnapshotUpdate as StorageSnapshotUpdate, WatchTotpRecord as StorageTotpRecord,
};
@@ -83,10 +84,11 @@ pub struct WatchTotpRecord {
pub code: String,
pub period: u64,
pub valid_until: u64,
pub remaining: u64,
}
impl From<StorageTotpRecord> for WatchTotpRecord {
fn from(value: StorageTotpRecord) -> Self {
impl From<&StorageTotpRecord> for WatchTotpRecord {
fn from(value: &StorageTotpRecord) -> Self {
Self {
path: value.path().to_owned(),
issuer: value.issuer().map(str::to_owned),
@@ -95,6 +97,51 @@ impl From<StorageTotpRecord> for WatchTotpRecord {
.expect("storage-generated TOTP codes are ASCII"),
period: value.period(),
valid_until: value.valid_until(),
remaining: value.remaining(),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
pub enum WatchPresentationState {
Ready,
Empty,
Syncing,
Stale,
Locked,
Unavailable,
Error,
}
impl From<StoragePresentationState> for WatchPresentationState {
fn from(value: StoragePresentationState) -> Self {
match value {
StoragePresentationState::Ready => Self::Ready,
StoragePresentationState::Empty => Self::Empty,
StoragePresentationState::Syncing => Self::Syncing,
StoragePresentationState::Stale => Self::Stale,
StoragePresentationState::Locked => Self::Locked,
StoragePresentationState::Unavailable => Self::Unavailable,
StoragePresentationState::Error => Self::Error,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
pub struct WatchPresentation {
pub state: WatchPresentationState,
pub title: String,
pub detail: String,
pub records: Vec<WatchTotpRecord>,
}
impl From<StoragePresentation> for WatchPresentation {
fn from(value: StoragePresentation) -> Self {
Self {
state: value.state().into(),
title: value.title().to_owned(),
detail: value.detail().to_owned(),
records: value.records().iter().map(WatchTotpRecord::from).collect(),
}
}
}
@@ -138,15 +185,44 @@ impl WatchCore {
.map_err(Into::into)
}
pub fn records_at(&self, unix_seconds: u64) -> Result<Vec<WatchTotpRecord>, WatchFfiError> {
pub fn presentation_at(&self, unix_seconds: u64) -> Result<WatchPresentation, WatchFfiError> {
self.runtime
.lock()
.map_err(|_| lock_error())?
.records_at(unix_seconds)
.map(|records| records.into_iter().map(Into::into).collect())
.presentation_at(unix_seconds)
.map(Into::into)
.map_err(Into::into)
}
pub fn sync_started(&self) -> Result<(), WatchFfiError> {
self.runtime
.lock()
.map_err(|_| lock_error())?
.sync_started();
Ok(())
}
pub fn sync_finished(&self) -> Result<(), WatchFfiError> {
self.runtime
.lock()
.map_err(|_| lock_error())?
.sync_finished();
Ok(())
}
pub fn sync_unavailable(&self) -> Result<(), WatchFfiError> {
self.runtime
.lock()
.map_err(|_| lock_error())?
.sync_unavailable();
Ok(())
}
pub fn sync_failed(&self) -> Result<(), WatchFfiError> {
self.runtime.lock().map_err(|_| lock_error())?.sync_failed();
Ok(())
}
pub fn protected_data_unavailable(&self) -> Result<(), WatchFfiError> {
self.runtime
.lock()
@@ -183,8 +259,16 @@ mod tests {
fn bridge_masks_records_when_protected_data_is_unavailable() {
let core = super::watch_core();
core.no_persisted_snapshot().expect("available Keychain");
assert!(core.records_at(59).expect("empty snapshot").is_empty());
assert!(
core.presentation_at(59)
.expect("empty snapshot")
.records
.is_empty()
);
core.protected_data_unavailable().expect("lock transition");
assert!(core.records_at(59).is_err());
assert_eq!(
core.presentation_at(59).expect("locked presentation").state,
super::WatchPresentationState::Locked
);
}
}