use crate::{AuthContext, AuthSession};
use leptos::prelude::*;
use leptos::tachys::view::any_view::IntoAny;
use leptos_router::hooks::use_navigate;
use url::form_urlencoded;
use crate::components::{EmptyState, EmptyStateCallToAction, EMPTYSTATE_SIGNIN_ILLUSTRATION};
use crate::primitives::{
Button, ButtonAppearance, Dialog, DialogBody, DialogContent, DialogDismissConfig,
DialogSurface, DialogTitle, Material, MaterialCorners, MaterialElevation, MaterialVariant,
OpenBind,
};
pub fn parse_referer_from_search(search: &str) -> Option<String> {
let trimmed = search.trim_start_matches('?');
if trimmed.is_empty() {
return None;
}
for (key, value) in form_urlencoded::parse(trimmed.as_bytes()) {
if key == "referer" {
return Some(value.into_owned());
}
}
None
}
pub fn sanitize_referer_path(referer: Option<String>) -> String {
referer
.filter(|path| {
path.starts_with('/')
&& !path.starts_with("//")
&& !path.starts_with("/auth/")
&& !path.starts_with("/api/")
&& path != "/home"
&& path != "/home/"
})
.unwrap_or_else(|| "/".to_string())
}
pub fn authenticated_route_condition(
auth: AuthContext,
) -> impl Fn() -> Option<bool> + Clone + 'static {
let session = auth.session();
move || match session.get() {
AuthSession::Authenticated(_) => Some(true),
AuthSession::Anonymous(_) => Some(false),
}
}
#[component]
fn AccessGateDialog(
#[prop(into)]
test_id: String,
#[prop(into)]
title: String,
message: &'static str,
description: &'static str,
illustration_src: &'static str,
illustration_alt: &'static str,
#[prop(optional)]
children: Option<Children>,
) -> impl IntoView {
let open = RwSignal::new(true);
let dismissed = RwSignal::new(false);
let navigate = use_navigate();
if let Some(auth_dialog) = crate::use_auth_dialog_controller() {
Effect::new(move |_| {
if dismissed.get() {
return;
}
open.set(!auth_dialog.open().get());
});
}
let (style_sheet, class_names) = turf::inline_style_sheet_values! {
.DialogFrame {
background: transparent;
border: none;
padding: 0;
width: fit-content;
min-width: 280px;
max-width: min(420px, calc(100vw - 48px));
}
.DialogMaterial {
border-radius: var(--orb-radius-xl);
padding: 24px;
box-sizing: border-box;
width: 100%;
}
};
view! {
<style>{style_sheet}</style>
<div data-testid=test_id role="status" aria-live="polite">
<Dialog
open=OpenBind::from(open)
dismiss=DialogDismissConfig {
mask_closeable: Signal::from(false),
close_on_esc: false,
}
>
<DialogSurface class=class_names.dialog_frame>
<Material
class=class_names.dialog_material
variant=MaterialVariant::Frost
elevation=MaterialElevation::Modal
corners=MaterialCorners::Rounded
>
<DialogBody>
<DialogTitle>{title}</DialogTitle>
<DialogContent>
<EmptyState
message=message
description=description
illustration_src=illustration_src
illustration_alt=illustration_alt
>
<EmptyStateCallToAction slot:call_to_action>
{children.map(|c| c())}
<Button
appearance=ButtonAppearance::Subtle
on_click=Callback::new({
let navigate = navigate.clone();
move |_| {
dismissed.set(true);
open.set(false);
crate::nav::navigate_back_or("/", &navigate);
}
})
>
"Take me back"
</Button>
</EmptyStateCallToAction>
</EmptyState>
</DialogContent>
</DialogBody>
</Material>
</DialogSurface>
</Dialog>
</div>
}
}
#[component]
pub fn RequireAuthenticated(
#[prop(optional, default = false)] requires_email_verification: bool,
children: ChildrenFn,
) -> impl IntoView {
let auth = crate::use_auth_state();
let navigate = use_navigate();
let auth_dialog = crate::use_auth_dialog_controller();
view! {
{move || {
match auth.get() {
AuthSession::Anonymous(_) => {
let nav_signin = navigate.clone();
let nav_signup = navigate.clone();
let dialog = auth_dialog;
view! {
<AccessGateDialog
test_id="auth-required-empty-state"
title="Sign in required"
message="Sign in required"
description="Sign in or create an account to continue."
illustration_src=EMPTYSTATE_SIGNIN_ILLUSTRATION
illustration_alt="Sign in required"
>
<Button
appearance=ButtonAppearance::Primary
on_click=Callback::new(move |_| {
if let Some(dialog) = dialog {
dialog.open_signin();
} else {
nav_signin(crate::paths::AUTH_SIGNIN, Default::default());
}
})
>
"Sign In"
</Button>
<Button
appearance=ButtonAppearance::Subtle
on_click=Callback::new(move |_| {
if let Some(dialog) = dialog {
dialog.open_signup();
} else {
nav_signup(crate::paths::AUTH_SIGNUP, Default::default());
}
})
>
"Sign Up"
</Button>
</AccessGateDialog>
}
.into_any()
}
AuthSession::Authenticated(user) => {
if requires_email_verification && !user.email_verified {
let nav_account = navigate.clone();
view! {
<AccessGateDialog
test_id="email-verification-required-empty-state"
title="Email verification required"
message="Email verification required"
description="Verify your email in account settings to continue."
illustration_src=EMPTYSTATE_SIGNIN_ILLUSTRATION
illustration_alt="Email verification required"
>
<Button
appearance=ButtonAppearance::Primary
on_click=Callback::new(move |_| nav_account(crate::paths::USER_ACCOUNT_SETTINGS, Default::default()))
>
"Account Settings"
</Button>
</AccessGateDialog>
}
.into_any()
} else {
children().into_any()
}
}
}
}}
}
}