#![forbid(unsafe_code)] #![deny(clippy::disallowed_types)] //! Mechanical UniFFI exports for Apple presentation code. use std::{error::Error, fmt, sync::Arc}; use ironstorage::{ config::ConfigError, mobile::{self, MobileShellState as StorageShellState, MobileTab as StorageTab}, mobile_authentication::{ MobileAuthenticationError as StorageAuthenticationError, MobileAuthenticationErrorKind as StorageAuthenticationErrorKind, MobileAuthenticationState as StorageAuthenticationState, MobileEntryCopy as StorageEntryCopy, }, mobile_entry::{ MobileEntryEditorFieldKind as StorageEntryEditorFieldKind, MobileEntryEditorInput as StorageEntryEditorInput, MobileEntryEditorPage as StorageEntryEditorPage, MobileEntryEditorSession as StorageEntryEditorSession, MobileEntryPage as StorageEntryPage, MobileEntrySectionKind as StorageEntrySectionKind, }, mobile_home::{ self, MobileHomeChangeKind as StorageHomeChangeKind, MobileHomeChangeStatus as StorageHomeChangeStatus, MobileHomeError as StorageHomeError, MobileHomeErrorKind as StorageHomeErrorKind, MobileHomeFreshness as StorageHomeFreshness, MobileHomePhase as StorageHomePhase, }, mobile_onboarding::{ self, MobileOnboardingError as StorageOnboardingError, MobileOnboardingErrorKind as StorageOnboardingErrorKind, MobileOnboardingPhase as StorageOnboardingPhase, }, mobile_passwords::{ MobilePasswordError as StoragePasswordError, MobilePasswordErrorKind as StoragePasswordErrorKind, MobilePasswordRowKind as StoragePasswordRowKind, }, }; uniffi::setup_scaffolding!(); #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)] pub enum MobileTab { Home, Passwords, Totp, Preferences, } impl From for MobileTab { fn from(tab: StorageTab) -> Self { match tab { StorageTab::Home => Self::Home, StorageTab::Passwords => Self::Passwords, StorageTab::Totp => Self::Totp, StorageTab::Preferences => Self::Preferences, } } } impl From for StorageTab { fn from(tab: MobileTab) -> Self { match tab { MobileTab::Home => Self::Home, MobileTab::Passwords => Self::Passwords, MobileTab::Totp => Self::Totp, MobileTab::Preferences => Self::Preferences, } } } #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)] pub enum MobileShellState { Loading, Empty, Ready, Locked, Error, } impl From for MobileShellState { fn from(state: StorageShellState) -> Self { match state { StorageShellState::Loading => Self::Loading, StorageShellState::Empty => Self::Empty, StorageShellState::Ready => Self::Ready, StorageShellState::Locked => Self::Locked, StorageShellState::Error => Self::Error, } } } impl From for StorageShellState { fn from(state: MobileShellState) -> Self { match state { MobileShellState::Loading => Self::Loading, MobileShellState::Empty => Self::Empty, MobileShellState::Ready => Self::Ready, MobileShellState::Locked => Self::Locked, MobileShellState::Error => Self::Error, } } } #[derive(Clone, uniffi::Record)] pub struct MobilePage { pub tab: MobileTab, pub title: String, pub system_image: String, pub selected_system_image: String, pub state: MobileShellState, pub state_title: String, pub state_detail: String, } #[derive(Clone, uniffi::Record)] pub struct MobileShell { pub selected_tab: MobileTab, pub pages: Vec, } #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)] pub enum MobileHomeFreshness { NeverRefreshed, Cached, Current, } impl From for MobileHomeFreshness { fn from(freshness: StorageHomeFreshness) -> Self { match freshness { StorageHomeFreshness::NeverRefreshed => Self::NeverRefreshed, StorageHomeFreshness::Cached => Self::Cached, StorageHomeFreshness::Current => Self::Current, } } } #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)] pub enum MobileHomeChangeKind { PasswordEntry, RecipientPolicy, RecipientSignature, RepositoryFile, } impl From for MobileHomeChangeKind { fn from(kind: StorageHomeChangeKind) -> Self { match kind { StorageHomeChangeKind::PasswordEntry => Self::PasswordEntry, StorageHomeChangeKind::RecipientPolicy => Self::RecipientPolicy, StorageHomeChangeKind::RecipientSignature => Self::RecipientSignature, StorageHomeChangeKind::RepositoryFile => Self::RepositoryFile, } } } #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)] pub enum MobileHomeChangeStatus { Added, Modified, Deleted, } impl From for MobileHomeChangeStatus { fn from(status: StorageHomeChangeStatus) -> Self { match status { StorageHomeChangeStatus::Added => Self::Added, StorageHomeChangeStatus::Modified => Self::Modified, StorageHomeChangeStatus::Deleted => Self::Deleted, } } } #[derive(Clone, uniffi::Record)] pub struct MobileHomeSummaryRow { pub id: String, pub title: String, pub detail: String, pub system_image: String, } #[derive(Clone, uniffi::Record)] pub struct MobileHomeChange { pub id: String, pub title: String, pub detail: String, pub system_image: String, pub kind: MobileHomeChangeKind, pub status: MobileHomeChangeStatus, } #[derive(Clone, uniffi::Record)] pub struct MobileHomeCommit { pub id: String, pub title: String, pub detail: String, pub system_image: String, pub timestamp: i64, pub changes: Vec, } #[derive(Clone, uniffi::Record)] pub struct MobileHomeNotice { pub title: String, pub detail: String, pub system_image: String, } #[derive(Clone, uniffi::Record)] pub struct MobileHomePage { pub freshness: MobileHomeFreshness, pub refreshed_at: Option, pub summaries: Vec, pub incoming: Vec, pub outgoing: Vec, pub incoming_total: u32, pub outgoing_total: u32, pub notice: Option, } impl From for MobileHomePage { fn from(page: mobile_home::MobileHomePage) -> Self { Self { freshness: page.freshness().into(), refreshed_at: page.refreshed_at(), summaries: page .summaries() .iter() .map(|row| MobileHomeSummaryRow { id: row.id().to_owned(), title: row.title().to_owned(), detail: row.detail().to_owned(), system_image: row.system_image().to_owned(), }) .collect(), incoming: page.incoming().iter().map(mobile_home_commit).collect(), outgoing: page.outgoing().iter().map(mobile_home_commit).collect(), incoming_total: u32::try_from(page.incoming_total()).unwrap_or(u32::MAX), outgoing_total: u32::try_from(page.outgoing_total()).unwrap_or(u32::MAX), notice: page.notice().map(|notice| MobileHomeNotice { title: notice.title().to_owned(), detail: notice.detail().to_owned(), system_image: notice.system_image().to_owned(), }), } } } fn mobile_home_commit(commit: &mobile_home::MobileHomeCommit) -> MobileHomeCommit { MobileHomeCommit { id: commit.id().to_owned(), title: commit.title().to_owned(), detail: commit.detail().to_owned(), system_image: commit.system_image().to_owned(), timestamp: commit.timestamp(), changes: commit .changes() .iter() .map(|change| MobileHomeChange { id: change.id().to_owned(), title: change.title().to_owned(), detail: change.detail().to_owned(), system_image: change.system_image().to_owned(), kind: change.kind().into(), status: change.status().into(), }) .collect(), } } #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)] pub enum MobileHomePhase { Validating, Authenticating, Receiving, Integrating, Finishing, } impl From for MobileHomePhase { fn from(phase: StorageHomePhase) -> Self { match phase { StorageHomePhase::Validating => Self::Validating, StorageHomePhase::Authenticating => Self::Authenticating, StorageHomePhase::Receiving => Self::Receiving, StorageHomePhase::Integrating => Self::Integrating, StorageHomePhase::Finishing => Self::Finishing, } } } #[derive(Clone, uniffi::Record)] pub struct MobileHomeProgress { pub phase: MobileHomePhase, pub title: String, pub detail: String, } #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)] pub enum MobileHomeErrorKind { MissingConfiguration, Configuration, Authentication, Conflict, DirtyLocalChanges, Offline, Interrupted, SecureStorage, Repository, PartialProgress, } impl From for MobileHomeErrorKind { fn from(kind: StorageHomeErrorKind) -> Self { match kind { StorageHomeErrorKind::MissingConfiguration => Self::MissingConfiguration, StorageHomeErrorKind::Configuration => Self::Configuration, StorageHomeErrorKind::Authentication => Self::Authentication, StorageHomeErrorKind::Conflict => Self::Conflict, StorageHomeErrorKind::DirtyLocalChanges => Self::DirtyLocalChanges, StorageHomeErrorKind::Offline => Self::Offline, StorageHomeErrorKind::Interrupted => Self::Interrupted, StorageHomeErrorKind::SecureStorage => Self::SecureStorage, StorageHomeErrorKind::Repository => Self::Repository, StorageHomeErrorKind::PartialProgress => Self::PartialProgress, } } } #[derive(Debug, uniffi::Error)] pub enum MobileHomeFfiError { Failed { kind: MobileHomeErrorKind, title: String, detail: String, }, } impl fmt::Display for MobileHomeFfiError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Failed { title, detail, .. } => write!(formatter, "{title}: {detail}"), } } } impl Error for MobileHomeFfiError {} impl From for MobileHomeFfiError { fn from(error: StorageHomeError) -> Self { Self::Failed { kind: error.kind().into(), title: error.title().to_owned(), detail: error.detail().to_owned(), } } } #[derive(uniffi::Object)] pub struct MobileHomeOperation { operation: mobile_home::MobileHomeOperation, } #[uniffi::export] impl MobileHomeOperation { pub fn progress(&self) -> MobileHomeProgress { let progress = self.operation.progress(); MobileHomeProgress { phase: progress.phase().into(), title: progress.title().to_owned(), detail: progress.detail().to_owned(), } } pub fn cached(&self) -> Result { self.operation.cached().map(Into::into).map_err(Into::into) } pub fn refresh_if_stale(&self) -> Result { self.operation .refresh_if_stale() .map(Into::into) .map_err(Into::into) } pub fn refresh(&self) -> Result { self.operation.refresh().map(Into::into).map_err(Into::into) } pub fn pull(&self) -> Result { self.operation.pull().map(Into::into).map_err(Into::into) } pub fn cancel(&self) { self.operation.cancel(); } } #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)] pub enum MobilePasswordRowKind { Directory, Entry, } impl From for MobilePasswordRowKind { fn from(kind: StoragePasswordRowKind) -> Self { match kind { StoragePasswordRowKind::Directory => Self::Directory, StoragePasswordRowKind::Entry => Self::Entry, } } } #[derive(Clone, uniffi::Record)] pub struct MobilePasswordRow { pub id: String, pub path: String, pub title: String, pub detail: String, pub system_image: String, pub kind: MobilePasswordRowKind, } #[derive(Clone, uniffi::Record)] pub struct MobilePasswordPage { pub id: String, pub path: String, pub title: String, pub rows: Vec, } impl From for MobilePasswordPage { fn from(page: ironstorage::mobile_passwords::MobilePasswordPage) -> Self { Self { id: page.id().to_owned(), path: page.path().to_owned(), title: page.title().to_owned(), rows: page .rows() .iter() .map(|row| MobilePasswordRow { id: row.id().to_owned(), path: row.path().to_owned(), title: row.title().to_owned(), detail: row.detail().to_owned(), system_image: row.system_image().to_owned(), kind: row.kind().into(), }) .collect(), } } } #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)] pub enum MobilePasswordErrorKind { MissingConfiguration, Configuration, InvalidPath, DirectoryMissing, Repository, } impl From for MobilePasswordErrorKind { fn from(kind: StoragePasswordErrorKind) -> Self { match kind { StoragePasswordErrorKind::MissingConfiguration => Self::MissingConfiguration, StoragePasswordErrorKind::Configuration => Self::Configuration, StoragePasswordErrorKind::InvalidPath => Self::InvalidPath, StoragePasswordErrorKind::DirectoryMissing => Self::DirectoryMissing, StoragePasswordErrorKind::Repository => Self::Repository, } } } #[derive(Debug, uniffi::Error)] pub enum MobilePasswordFfiError { Failed { kind: MobilePasswordErrorKind, title: String, detail: String, }, } impl fmt::Display for MobilePasswordFfiError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Failed { title, detail, .. } => write!(formatter, "{title}: {detail}"), } } } impl Error for MobilePasswordFfiError {} impl From for MobilePasswordFfiError { fn from(error: StoragePasswordError) -> Self { Self::Failed { kind: error.kind().into(), title: error.title().to_owned(), detail: error.detail().to_owned(), } } } #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)] pub enum MobileAuthenticationErrorKind { PassphraseRequired, InvalidPassphrase, Cancelled, BiometryUnavailable, Configuration, KeyMaterial, Entry, SecureStorage, Expired, Conflict, } impl From for MobileAuthenticationErrorKind { fn from(kind: StorageAuthenticationErrorKind) -> Self { match kind { StorageAuthenticationErrorKind::PassphraseRequired => Self::PassphraseRequired, StorageAuthenticationErrorKind::InvalidPassphrase => Self::InvalidPassphrase, StorageAuthenticationErrorKind::Cancelled => Self::Cancelled, StorageAuthenticationErrorKind::BiometryUnavailable => Self::BiometryUnavailable, StorageAuthenticationErrorKind::Configuration => Self::Configuration, StorageAuthenticationErrorKind::KeyMaterial => Self::KeyMaterial, StorageAuthenticationErrorKind::Entry => Self::Entry, StorageAuthenticationErrorKind::SecureStorage => Self::SecureStorage, StorageAuthenticationErrorKind::Expired => Self::Expired, StorageAuthenticationErrorKind::Conflict => Self::Conflict, } } } #[derive(Clone, uniffi::Record)] pub struct MobileAuthenticationState { pub unlocked: bool, pub biometric_unlock_enabled: bool, pub remaining_seconds: u64, } #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)] pub enum MobileEntrySectionKind { Password, Details, OneTimePassword, Notes, } impl From for MobileEntrySectionKind { fn from(kind: StorageEntrySectionKind) -> Self { match kind { StorageEntrySectionKind::Password => Self::Password, StorageEntrySectionKind::Details => Self::Details, StorageEntrySectionKind::OneTimePassword => Self::OneTimePassword, StorageEntrySectionKind::Notes => Self::Notes, } } } #[derive(Clone, uniffi::Record)] pub struct MobileEntryField { pub id: u64, pub label: String, pub system_image: String, pub value: Option, pub masked_value: String, pub detail: Option, pub diagnostic: Option, pub sensitive: bool, pub multiline: bool, pub selectable: bool, pub editable: bool, } #[derive(Clone, uniffi::Record)] pub struct MobileEntrySection { pub kind: MobileEntrySectionKind, pub title: String, pub fields: Vec, } #[derive(Clone, uniffi::Record)] pub struct MobileEntryPage { pub id: String, pub title: String, pub sections: Vec, } impl From for MobileEntryPage { fn from(page: StorageEntryPage) -> Self { Self { id: page.id().to_owned(), title: page.title().to_owned(), sections: page .sections() .iter() .map(|section| MobileEntrySection { kind: section.kind().into(), title: section.title().to_owned(), fields: section .fields() .iter() .map(|field| MobileEntryField { id: field.id(), label: field.label().to_owned(), system_image: field.system_image().to_owned(), value: field.value().map(str::to_owned), masked_value: field.masked_value().to_owned(), detail: field.detail().map(str::to_owned), diagnostic: field.diagnostic().map(str::to_owned), sensitive: field.sensitive(), multiline: field.multiline(), selectable: field.selectable(), editable: field.editable(), }) .collect(), }) .collect(), } } } #[derive(Clone, uniffi::Record)] pub struct MobileEntryCopy { pub value: String, pub timeout_seconds: u64, } #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)] pub enum MobileEntryEditorFieldKind { Password, Field, Note, OtpUri, } impl From for MobileEntryEditorFieldKind { fn from(kind: StorageEntryEditorFieldKind) -> Self { match kind { StorageEntryEditorFieldKind::Password => Self::Password, StorageEntryEditorFieldKind::Field => Self::Field, StorageEntryEditorFieldKind::Note => Self::Note, StorageEntryEditorFieldKind::OtpUri => Self::OtpUri, } } } impl From for StorageEntryEditorFieldKind { fn from(kind: MobileEntryEditorFieldKind) -> Self { match kind { MobileEntryEditorFieldKind::Password => Self::Password, MobileEntryEditorFieldKind::Field => Self::Field, MobileEntryEditorFieldKind::Note => Self::Note, MobileEntryEditorFieldKind::OtpUri => Self::OtpUri, } } } #[derive(Clone, uniffi::Record)] pub struct MobileEntryEditorField { pub id: u64, pub kind: MobileEntryEditorFieldKind, pub name: Option, pub label: String, pub system_image: String, pub value: Option, pub masked_value: String, pub diagnostic: Option, pub sensitive: bool, pub multiline: bool, pub name_editable: bool, pub removable: bool, pub reorderable: bool, } #[derive(Clone, uniffi::Record)] pub struct MobileEntryEditorPage { pub path: String, pub title: String, pub fields: Vec, pub dirty: bool, pub creating: bool, pub default_password_length: u32, pub maximum_password_length: u32, } impl From<&StorageEntryEditorPage> for MobileEntryEditorPage { fn from(page: &StorageEntryEditorPage) -> Self { Self { path: page.path().to_owned(), title: page.title().to_owned(), fields: page .fields() .iter() .map(|field| MobileEntryEditorField { id: field.id(), kind: field.kind().into(), name: field.name().map(str::to_owned), label: field.label().to_owned(), system_image: field.system_image().to_owned(), value: field.value().map(str::to_owned), masked_value: field.masked_value().to_owned(), diagnostic: field.diagnostic().map(str::to_owned), sensitive: field.sensitive(), multiline: field.multiline(), name_editable: field.name_editable(), removable: field.removable(), reorderable: field.reorderable(), }) .collect(), dirty: page.dirty(), creating: page.creating(), default_password_length: page.default_password_length(), maximum_password_length: page.maximum_password_length(), } } } #[derive(Clone, uniffi::Record)] pub struct MobileEntryEditorSession { pub id: u64, pub page: MobileEntryEditorPage, } impl From for MobileEntryEditorSession { fn from(session: StorageEntryEditorSession) -> Self { Self { id: session.id(), page: session.page().into(), } } } #[derive(Clone, uniffi::Record)] pub struct MobileEntryEditorInput { pub id: u64, pub name: Option, pub value: Option, } impl From for StorageEntryEditorInput { fn from(input: MobileEntryEditorInput) -> Self { Self::new(input.id, input.name, input.value) } } impl From for MobileEntryCopy { fn from(copy: StorageEntryCopy) -> Self { Self { value: copy.value().to_owned(), timeout_seconds: copy.timeout_seconds(), } } } impl From for MobileAuthenticationState { fn from(state: StorageAuthenticationState) -> Self { Self { unlocked: state.unlocked(), biometric_unlock_enabled: state.biometric_unlock_enabled(), remaining_seconds: state.remaining_seconds(), } } } #[derive(Debug, uniffi::Error)] pub enum MobileAuthenticationFfiError { Failed { kind: MobileAuthenticationErrorKind, title: String, detail: String, }, } impl fmt::Display for MobileAuthenticationFfiError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Failed { title, detail, .. } => write!(formatter, "{title}: {detail}"), } } } impl Error for MobileAuthenticationFfiError {} impl From for MobileAuthenticationFfiError { fn from(error: StorageAuthenticationError) -> Self { Self::Failed { kind: error.kind().into(), title: error.title().to_owned(), detail: error.detail().to_owned(), } } } #[derive(uniffi::Object)] pub struct MobileAuthentication { authentication: ironstorage::mobile_authentication::MobileAuthentication, } #[uniffi::export] impl MobileAuthentication { pub fn state(&self) -> Result { self.authentication .state() .map(Into::into) .map_err(Into::into) } pub fn unlock_entry( &self, path: String, passphrase: Option, ) -> Result { self.authentication .unlock_entry( &path, passphrase .map(|value| ironstorage::repository::SecretBytes::new(value.into_bytes())), ) .map(Into::into) .map_err(Into::into) } pub fn set_biometric_unlock( &self, enabled: bool, ) -> Result { self.authentication .set_biometric_unlock(enabled) .map(Into::into) .map_err(Into::into) } pub fn touch_user_activity(&self) -> Result<(), MobileAuthenticationFfiError> { self.authentication .touch_user_activity() .map_err(Into::into) } pub fn entry_page( &self, path: String, ) -> Result { self.authentication .entry_page(&path) .map(Into::into) .map_err(Into::into) } pub fn reveal_entry_field( &self, path: String, field: u64, ) -> Result { self.authentication .reveal_entry_field(&path, field) .map_err(Into::into) } pub fn copy_entry_field( &self, path: String, field: u64, ) -> Result { self.authentication .copy_entry_field(&path, field) .map(Into::into) .map_err(Into::into) } pub fn replace_entry_field( &self, path: String, field: u64, value: String, ) -> Result { self.authentication .replace_entry_field(&path, field, value) .map(Into::into) .map_err(Into::into) } pub fn begin_entry_editor( &self, path: String, ) -> Result { self.authentication .begin_entry_editor(&path) .map(Into::into) .map_err(Into::into) } pub fn begin_create_entry( &self, directory: String, name: String, passphrase: Option, ) -> Result { self.authentication .begin_create_entry( &directory, &name, passphrase .map(|value| ironstorage::repository::SecretBytes::new(value.into_bytes())), ) .map(Into::into) .map_err(Into::into) } pub fn entry_editor( &self, editor: u64, ) -> Result { self.authentication .entry_editor(editor) .map(|page| (&page).into()) .map_err(Into::into) } pub fn update_entry_editor( &self, editor: u64, fields: Vec, ) -> Result { self.authentication .update_entry_editor(editor, fields.into_iter().map(Into::into).collect()) .map(|page| (&page).into()) .map_err(Into::into) } pub fn add_entry_editor_field( &self, editor: u64, kind: MobileEntryEditorFieldKind, name: Option, value: String, ) -> Result { self.authentication .add_entry_editor_field(editor, kind.into(), name, value) .map(|page| (&page).into()) .map_err(Into::into) } pub fn remove_entry_editor_field( &self, editor: u64, field: u64, ) -> Result { self.authentication .remove_entry_editor_field(editor, field) .map(|page| (&page).into()) .map_err(Into::into) } pub fn reorder_entry_editor_field( &self, editor: u64, field: u64, index: u32, ) -> Result { self.authentication .reorder_entry_editor_field(editor, field, index as usize) .map(|page| (&page).into()) .map_err(Into::into) } pub fn generate_entry_editor_password( &self, editor: u64, length: Option, no_symbols: bool, ) -> Result { self.authentication .generate_entry_editor_password(editor, length, no_symbols) .map(|page| (&page).into()) .map_err(Into::into) } pub fn save_entry_editor( &self, editor: u64, fields: Vec, ) -> Result { self.authentication .save_entry_editor(editor, fields.into_iter().map(Into::into).collect()) .map(Into::into) .map_err(Into::into) } pub fn discard_entry_editor(&self, editor: u64) -> Result<(), MobileAuthenticationFfiError> { self.authentication .discard_entry_editor(editor) .map_err(Into::into) } pub fn manual_lock(&self) -> Result<(), MobileAuthenticationFfiError> { self.authentication.manual_lock().map_err(Into::into) } pub fn cancel(&self) -> Result<(), MobileAuthenticationFfiError> { self.authentication.cancel().map_err(Into::into) } } #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)] pub enum MobileOnboardingPhase { Validating, Authenticating, Receiving, Integrating, Finishing, } impl From for MobileOnboardingPhase { fn from(phase: StorageOnboardingPhase) -> Self { match phase { StorageOnboardingPhase::Validating => Self::Validating, StorageOnboardingPhase::Authenticating => Self::Authenticating, StorageOnboardingPhase::Receiving => Self::Receiving, StorageOnboardingPhase::Integrating => Self::Integrating, StorageOnboardingPhase::Finishing => Self::Finishing, } } } #[derive(Clone, uniffi::Record)] pub struct MobileOnboardingProgress { pub phase: MobileOnboardingPhase, pub title: String, pub detail: String, } #[derive(Clone, uniffi::Record)] pub struct MobileOnboardingDiscovery { pub branches: Vec, pub selected_branch: u32, } #[derive(Clone, uniffi::Record)] pub struct MobileOnboardingOutcome { pub title: String, pub detail: String, } #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)] pub enum MobileOnboardingErrorKind { InvalidInput, UnsupportedRemote, Authentication, Repository, ExistingClone, Interrupted, SecureStorage, Configuration, AlreadyConfigured, } impl From for MobileOnboardingErrorKind { fn from(kind: StorageOnboardingErrorKind) -> Self { match kind { StorageOnboardingErrorKind::InvalidInput => Self::InvalidInput, StorageOnboardingErrorKind::UnsupportedRemote => Self::UnsupportedRemote, StorageOnboardingErrorKind::Authentication => Self::Authentication, StorageOnboardingErrorKind::Repository => Self::Repository, StorageOnboardingErrorKind::ExistingClone => Self::ExistingClone, StorageOnboardingErrorKind::Interrupted => Self::Interrupted, StorageOnboardingErrorKind::SecureStorage => Self::SecureStorage, StorageOnboardingErrorKind::Configuration => Self::Configuration, StorageOnboardingErrorKind::AlreadyConfigured => Self::AlreadyConfigured, } } } #[derive(Debug, uniffi::Error)] pub enum MobileOnboardingFfiError { Failed { kind: MobileOnboardingErrorKind, title: String, detail: String, }, } impl fmt::Display for MobileOnboardingFfiError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Failed { title, detail, .. } => write!(formatter, "{title}: {detail}"), } } } impl Error for MobileOnboardingFfiError {} impl From for MobileOnboardingFfiError { fn from(error: StorageOnboardingError) -> Self { Self::Failed { kind: error.kind().into(), title: error.title().to_owned(), detail: error.detail().to_owned(), } } } #[derive(uniffi::Object)] pub struct MobileOnboardingOperation { operation: mobile_onboarding::MobileOnboardingOperation, request: mobile_onboarding::MobileOnboardingRequest, } #[uniffi::export] impl MobileOnboardingOperation { pub fn progress(&self) -> MobileOnboardingProgress { let progress = self.operation.progress(); MobileOnboardingProgress { phase: progress.phase().into(), title: progress.title().to_owned(), detail: progress.detail().to_owned(), } } pub fn discover(&self) -> Result { let discovery = self.operation.discover(&self.request)?; Ok(MobileOnboardingDiscovery { branches: discovery.branches().to_vec(), selected_branch: u32::try_from(discovery.selected_branch()).unwrap_or_default(), }) } pub fn setup( &self, branch: String, use_existing: bool, ) -> Result { let outcome = self.operation.setup(&self.request, &branch, use_existing)?; Ok(MobileOnboardingOutcome { title: outcome.title().to_owned(), detail: outcome.detail().to_owned(), }) } pub fn cancel(&self) { self.operation.cancel(); } } impl From for MobileShell { fn from(shell: mobile::MobileShell) -> Self { Self { selected_tab: shell.selected_tab().into(), pages: shell .pages() .iter() .map(|page| MobilePage { tab: page.tab().into(), title: page.title().to_owned(), system_image: page.system_image().to_owned(), selected_system_image: page.selected_system_image().to_owned(), state: page.state().into(), state_title: page.state_title().to_owned(), state_detail: page.state_detail().to_owned(), }) .collect(), } } } #[derive(Debug, uniffi::Error)] pub enum MobilePreferenceError { Configuration { message: String }, } impl fmt::Display for MobilePreferenceError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Configuration { message } => formatter.write_str(message), } } } impl Error for MobilePreferenceError {} impl From for MobilePreferenceError { fn from(error: ConfigError) -> Self { Self::Configuration { message: error.to_string(), } } } #[uniffi::export] pub fn product_name() -> String { ironstorage::PRODUCT_NAME.to_owned() } #[uniffi::export] pub fn mobile_shell() -> MobileShell { mobile::MobileShell::load().into() } #[uniffi::export] pub fn mobile_shell_fixture(state: MobileShellState) -> MobileShell { mobile::MobileShell::fixture(state.into()).into() } #[uniffi::export] pub fn set_selected_mobile_tab(tab: MobileTab) -> Result<(), MobilePreferenceError> { mobile::store_selected_tab(tab.into()).map_err(Into::into) } #[uniffi::export] pub fn mobile_home_operation() -> Arc { Arc::new(MobileHomeOperation { operation: mobile_home::MobileHomeOperation::default(), }) } #[uniffi::export] pub fn mobile_password_page( path: Option, ) -> Result { ironstorage::mobile_passwords::MobilePasswordPage::load(path.as_deref()) .map(Into::into) .map_err(Into::into) } #[uniffi::export] pub fn mobile_authentication() -> Result, MobileAuthenticationFfiError> { Ok(Arc::new(MobileAuthentication { authentication: ironstorage::mobile_authentication::MobileAuthentication::load()?, })) } #[uniffi::export] pub fn mobile_onboarding_operation( server_url: String, account: String, repository_path: String, application_token: String, ) -> Result, MobileOnboardingFfiError> { Ok(Arc::new(MobileOnboardingOperation { operation: mobile_onboarding::MobileOnboardingOperation::default(), request: mobile_onboarding::MobileOnboardingRequest::new( server_url, account, repository_path, application_token.into_bytes(), )?, })) } #[uniffi::export] pub fn replace_configured_mobile_application_token( account: String, application_token: String, ) -> Result<(), MobileOnboardingFfiError> { mobile_onboarding::replace_configured_application_token( account, application_token.into_bytes(), )?; Ok(()) } #[cfg(test)] mod tests { use super::{ MobileHomePhase, MobileOnboardingErrorKind, MobileOnboardingFfiError, MobileShellState, MobileTab, }; #[test] fn bridge_reads_product_name_from_storage_crate() { assert_eq!(super::product_name(), ironstorage::PRODUCT_NAME); } #[test] fn bridge_exposes_every_view_ready_shell_fixture() { for state in [ MobileShellState::Loading, MobileShellState::Empty, MobileShellState::Ready, MobileShellState::Locked, MobileShellState::Error, ] { let shell = super::mobile_shell_fixture(state); assert_eq!(shell.selected_tab, MobileTab::Home); assert_eq!(shell.pages.len(), 4); assert!(shell.pages.iter().all(|page| page.state == state)); } } #[test] fn bridge_rejects_non_https_before_creating_an_operation() { let error = match super::mobile_onboarding_operation( "ssh://example.test".to_owned(), "alice".to_owned(), "team/passwords".to_owned(), "DO-NOT-RENDER".to_owned(), ) { Ok(_) => panic!("SSH must be rejected"), Err(error) => error, }; match error { MobileOnboardingFfiError::Failed { kind, detail, .. } => { assert_eq!(kind, MobileOnboardingErrorKind::UnsupportedRemote); assert!(!detail.contains("DO-NOT-RENDER")); } } } #[test] fn home_operation_exposes_typed_progress_and_cancellation() { let operation = super::mobile_home_operation(); assert_eq!(operation.progress().phase, MobileHomePhase::Validating); operation.cancel(); assert_eq!(operation.progress().phase, MobileHomePhase::Validating); } }