use crate::context::{ClerkContext, use_clerk_context};
use crate::core::{ClerkError, ReverificationLevel};
use dioxus::prelude::*;
use std::future::Future;
#[cfg_attr(not(clerk_client), allow(dead_code))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ReverificationOutcome {
Completed,
Cancelled,
}
pub(crate) async fn run_with_reverification<T, Fetch, FetchFut, Prompt, PromptFut>(
fetcher: Fetch,
prompt: Prompt,
) -> Result<T, ClerkError>
where
Fetch: Fn() -> FetchFut,
FetchFut: Future<Output = Result<T, ClerkError>>,
Prompt: FnOnce(Option<ReverificationLevel>) -> PromptFut,
PromptFut: Future<Output = Result<ReverificationOutcome, ClerkError>>,
{
match fetcher().await {
Err(ClerkError::NeedsReverification { level }) => match prompt(level).await? {
ReverificationOutcome::Completed => fetcher().await,
ReverificationOutcome::Cancelled => Err(ClerkError::ReverificationCancelled),
},
other => other,
}
}
#[cfg(clerk_client)]
async fn prompt_reverification(
ctx: ClerkContext,
level: Option<ReverificationLevel>,
) -> Result<ReverificationOutcome, ClerkError> {
crate::lifecycle::run_async_bridge_action_after_loaded(ctx, move |bridge| async move {
bridge.open_reverification(level).await
})
.await
}
#[cfg(not(clerk_client))]
async fn prompt_reverification(
_ctx: ClerkContext,
_level: Option<ReverificationLevel>,
) -> Result<ReverificationOutcome, ClerkError> {
Err(ClerkError::UnsupportedTarget)
}
#[derive(Clone, Copy)]
pub struct UseReverification {
ctx: ClerkContext,
}
impl std::fmt::Debug for UseReverification {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("UseReverification").finish_non_exhaustive()
}
}
impl UseReverification {
pub async fn guard<T, F, Fut>(&self, action: F) -> Result<T, ClerkError>
where
F: Fn() -> Fut,
Fut: Future<Output = Result<T, ClerkError>>,
{
let ctx = self.ctx;
run_with_reverification(action, move |level| prompt_reverification(ctx, level)).await
}
}
pub fn use_reverification() -> UseReverification {
UseReverification {
ctx: use_clerk_context(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::ClerkError;
use futures_util::FutureExt;
use std::cell::Cell;
#[test]
fn success_returns_value_and_never_prompts() {
let prompted = Cell::new(false);
let result = run_with_reverification(
|| async { Ok::<_, ClerkError>(7) },
|_level| async {
prompted.set(true);
Ok(ReverificationOutcome::Completed)
},
)
.now_or_never()
.expect("all futures resolve immediately");
assert_eq!(result, Ok(7));
assert!(!prompted.get(), "a successful action must not prompt");
}
#[test]
fn completed_prompt_retries_the_action() {
let calls = Cell::new(0u32);
let seen_level = Cell::new(None);
let result = run_with_reverification(
|| async {
let n = calls.get();
calls.set(n + 1);
if n == 0 {
Err(ClerkError::NeedsReverification {
level: Some(ReverificationLevel::SecondFactor),
})
} else {
Ok(42)
}
},
|level| async {
seen_level.set(Some(level));
Ok(ReverificationOutcome::Completed)
},
)
.now_or_never()
.expect("all futures resolve immediately");
assert_eq!(result, Ok(42));
assert_eq!(calls.get(), 2, "the action retries after reverification");
assert_eq!(
seen_level.take(),
Some(Some(ReverificationLevel::SecondFactor)),
"the prompt receives the required level"
);
}
#[test]
fn cancelled_prompt_yields_cancelled_and_does_not_retry() {
let calls = Cell::new(0u32);
let result = run_with_reverification(
|| async {
calls.set(calls.get() + 1);
Err::<i32, _>(ClerkError::NeedsReverification { level: None })
},
|_level| async { Ok(ReverificationOutcome::Cancelled) },
)
.now_or_never()
.expect("all futures resolve immediately");
assert_eq!(result, Err(ClerkError::ReverificationCancelled));
assert_eq!(calls.get(), 1, "a cancelled prompt must not retry");
}
#[test]
fn other_errors_pass_through_without_prompting() {
let prompted = Cell::new(false);
let result = run_with_reverification(
|| async { Err::<i32, _>(ClerkError::Unauthenticated) },
|_level| async {
prompted.set(true);
Ok(ReverificationOutcome::Completed)
},
)
.now_or_never()
.expect("all futures resolve immediately");
assert_eq!(result, Err(ClerkError::Unauthenticated));
assert!(
!prompted.get(),
"a non-reverification error must not prompt"
);
}
#[test]
fn prompt_error_propagates_without_retry() {
let calls = Cell::new(0u32);
let result = run_with_reverification(
|| async {
calls.set(calls.get() + 1);
Err::<i32, _>(ClerkError::NeedsReverification { level: None })
},
|_level| async { Err(ClerkError::NotLoaded) },
)
.now_or_never()
.expect("all futures resolve immediately");
assert_eq!(result, Err(ClerkError::NotLoaded));
assert_eq!(calls.get(), 1, "a failed prompt must not retry the action");
}
}