diff --git a/packages/oneclient_app/src/hooks/active_cluster.rs b/packages/oneclient_app/src/hooks/active_cluster.rs index df24a08d..0b7fe84a 100644 --- a/packages/oneclient_app/src/hooks/active_cluster.rs +++ b/packages/oneclient_app/src/hooks/active_cluster.rs @@ -8,7 +8,7 @@ use oneclient_db::models::ClusterId; pub struct ActiveClusterState(pub State>); pub fn use_provide_active_cluster(active: ActiveClusterState) { - use_provide_root_context(move || active.clone()); + use_hook(move || provide_root_context(active)); } pub fn use_active_cluster_id() -> State> { @@ -48,7 +48,7 @@ pub fn use_start_maximized() -> bool { pub struct BrowserCompatState(pub State); pub fn use_provide_browser_compat(state: BrowserCompatState) { - use_provide_root_context(move || state.clone()); + use_hook(move || provide_root_context(state)); } pub fn use_browser_compat() -> State { @@ -89,7 +89,7 @@ impl Default for BrowserUiState { pub struct BrowserStateStore(pub State>); pub fn use_provide_browser_state(store: BrowserStateStore) { - use_provide_root_context(move || store.clone()); + use_hook(move || provide_root_context(store)); } pub fn use_browser_state_store() -> State> { @@ -114,9 +114,63 @@ pub struct OnboardingSelectionState { } pub fn use_provide_onboarding_selection(state: OnboardingSelectionState) { - use_provide_root_context(move || state.clone()); + use_hook(move || provide_root_context(state)); } pub fn use_onboarding_selection() -> OnboardingSelectionState { consume_root_context::() } + +#[cfg(test)] +mod tests { + use freya_testing::TestingRunner; + + use super::*; + + /// Stands in for a route layout: it owns the state and publishes it as a root + /// context, so unmounting it drops the value the context still points at + #[derive(PartialEq)] + struct Shell; + + impl Component for Shell { + fn render(&self) -> impl IntoElement { + let active = use_state(|| None::); + use_provide_active_cluster(ActiveClusterState(active)); + rect().child(Reader) + } + } + + #[derive(PartialEq)] + struct Reader; + + impl Component for Reader { + fn render(&self) -> impl IntoElement { + let active = *use_active_cluster_id().read(); + label().text(format!("{active:?}")) + } + } + + fn app() -> impl IntoElement { + let mounted = use_consume::>(); + rect().maybe_child(mounted().then(|| Shell.into_element())) + } + + #[test] + fn a_remounted_shell_replaces_the_dropped_root_state() { + let (mut test, mut mounted) = TestingRunner::new( + app, + Size2D::new(300., 300.), + |runner| runner.provide_root_context(|| State::create(true)), + 1., + ); + test.sync_and_update(); + + // Leaving the shell drops the state the root context holds + test.run_in(|| mounted.set(false)); + test.sync_and_update(); + + // Coming back must not hand the reader that dropped state + test.run_in(|| mounted.set(true)); + test.sync_and_update(); + } +} diff --git a/packages/oneclient_app/src/view/onboarding/terms.rs b/packages/oneclient_app/src/view/onboarding/terms.rs index b01ef51d..490bd24c 100644 --- a/packages/oneclient_app/src/view/onboarding/terms.rs +++ b/packages/oneclient_app/src/view/onboarding/terms.rs @@ -4,7 +4,7 @@ use freya::router::RouterContext; use crate::components::{ Button, Icon, IconType, Markdown, MarkdownStyle, OverlayPopup, ScrollArea, Segment, - SegmentedControl, toggle, + SegmentedControl, }; use crate::hooks::{ TermsQuery, has_migration_data, terms_document, terms_error, terms_is_loading, use_dispatch, @@ -38,7 +38,6 @@ impl Component for OnboardingTerms { let error = terms_error(&query); let loading = terms_is_loading(&query); - let accepted = use_state(|| false); let tab = use_state(|| LegalTab::Terms); let confirming_decline = use_state(|| false); @@ -100,19 +99,21 @@ impl Component for OnboardingTerms { .maybe_child(tabs) .child(body) .child(link_row(terms_url, privacy_url)) - .child(accept_row(accepted)) .into_element(); let accept_dispatch = dispatch.clone(); - let nav = terms_nav(back, !loading, move || { - if *accepted.read() { + let nav = terms_nav( + back, + !loading, + move || { accept_dispatch.accept_tos(terms_version, privacy_version); let _ = RouterContext::get().replace(next.clone()); - } else { + }, + move || { let mut confirming = confirming_decline; confirming.set(true); - } - }); + }, + ); let modal = confirming_decline.read().then(|| { decline_modal(confirming_decline, move || { @@ -266,46 +267,14 @@ fn external_link_button(text: &'static str, url: String) -> impl IntoElement { .child(Icon::new(IconType::LinkExternal01).size(14.)) } -fn accept_row(accepted: State) -> Element { - rect() - .horizontal() - .width(Size::fill()) - .cross_align(Alignment::Center) - .spacing(16.) - .content(Content::Flex) - .padding(Gaps::new_symmetric(12., 16.)) - .corner_radius(CornerRadius::new_all(12.)) - .background(colors::page_elevated()) - .border(border_all_color(1., colors::component_border())) - .child( - rect() - .vertical() - .width(Size::flex(1.0)) - .spacing(3.) - .child( - label() - .text("I accept the Terms of Service and Privacy Policy") - .font_size(14.) - .font_weight(FontWeight::MEDIUM) - .color(colors::fg_primary()), - ) - .child( - label() - .text("Required for world hosting, socials, nametag indicator, and more.") - .font_size(11.) - .color(colors::fg_secondary()), - ), - ) - .child(toggle(accepted)) - .into_element() -} - fn terms_nav( back: Option, - next_enabled: bool, - on_next: impl FnMut() + 'static, + accept_enabled: bool, + on_accept: impl FnMut() + 'static, + on_decline: impl FnMut() + 'static, ) -> Element { - let mut on_next = on_next; + let mut on_accept = on_accept; + let mut on_decline = on_decline; rect() .horizontal() .width(Size::fill()) @@ -323,14 +292,22 @@ fn terms_nav( .text("Back") .into_element() })) + .child( + Button::new() + .secondary() + .width(Size::px(140.)) + .on_press(move |_| on_decline()) + .text("Decline") + .child(Icon::new(IconType::X).size(16.)), + ) .child( Button::new() .primary() .width(Size::px(140.)) - .enabled(next_enabled) - .on_press(move |_| on_next()) - .text("Next") - .child(Icon::new(IconType::ArrowRight).size(16.)), + .enabled(accept_enabled) + .on_press(move |_| on_accept()) + .text("Accept") + .child(Icon::new(IconType::Check).size(16.)), ) .into_element() } @@ -407,7 +384,7 @@ fn decline_modal(confirming: State, on_confirm: impl FnMut() + 'static) -> Button::new() .secondary() .on_press(move |_| confirming.set(false)) - .text("Close"), + .text("Cancel"), ) .child( Button::new()