Onboard iPhone password-store clone
This commit is contained in:
@@ -3,11 +3,16 @@
|
||||
|
||||
//! Mechanical UniFFI exports for Apple presentation code.
|
||||
|
||||
use std::{error::Error, fmt};
|
||||
use std::{error::Error, fmt, sync::Arc};
|
||||
|
||||
use ironstorage::{
|
||||
config::ConfigError,
|
||||
mobile::{self, MobileShellState as StorageShellState, MobileTab as StorageTab},
|
||||
mobile_onboarding::{
|
||||
self, MobileOnboardingError as StorageOnboardingError,
|
||||
MobileOnboardingErrorKind as StorageOnboardingErrorKind,
|
||||
MobileOnboardingPhase as StorageOnboardingPhase,
|
||||
},
|
||||
};
|
||||
|
||||
uniffi::setup_scaffolding!();
|
||||
@@ -92,6 +97,146 @@ pub struct MobileShell {
|
||||
pub pages: Vec<MobilePage>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
|
||||
pub enum MobileOnboardingPhase {
|
||||
Validating,
|
||||
Authenticating,
|
||||
Receiving,
|
||||
Integrating,
|
||||
Finishing,
|
||||
}
|
||||
|
||||
impl From<StorageOnboardingPhase> 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<String>,
|
||||
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<StorageOnboardingErrorKind> 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<StorageOnboardingError> 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<MobileOnboardingDiscovery, MobileOnboardingFfiError> {
|
||||
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<MobileOnboardingOutcome, MobileOnboardingFfiError> {
|
||||
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<mobile::MobileShell> for MobileShell {
|
||||
fn from(shell: mobile::MobileShell) -> Self {
|
||||
Self {
|
||||
@@ -156,9 +301,39 @@ pub fn set_selected_mobile_tab(tab: MobileTab) -> Result<(), MobilePreferenceErr
|
||||
mobile::store_selected_tab(tab.into()).map_err(Into::into)
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
pub fn mobile_onboarding_operation(
|
||||
server_url: String,
|
||||
account: String,
|
||||
repository_path: String,
|
||||
application_token: String,
|
||||
) -> Result<Arc<MobileOnboardingOperation>, 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::{MobileShellState, MobileTab};
|
||||
use super::{MobileOnboardingErrorKind, MobileOnboardingFfiError, MobileShellState, MobileTab};
|
||||
|
||||
#[test]
|
||||
fn bridge_reads_product_name_from_storage_crate() {
|
||||
@@ -180,4 +355,23 @@ mod tests {
|
||||
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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user