notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

commit 23b2817320e75791ace9e7aa5d0d6900fbb7c932
parent e6d97deb326add0a025aea2179d2bdf566684b1a
Author: William Casarin <jb55@jb55.com>
Date:   Fri, 13 Feb 2026 13:10:13 -0800

tos: add TOS acceptance screen shown on first launch

Add Route::TosAcceptance with a scrollable EULA view, age verification
and TOS agreement checkboxes, and an accept button. On app init, if TOS
has not been accepted yet, the user is routed to this screen before they
can use the app. Acceptance is persisted via settings.accept_tos().

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Diffstat:
Mcrates/notedeck_columns/src/app.rs | 8++++++++
Mcrates/notedeck_columns/src/nav.rs | 17+++++++++++++++++
Mcrates/notedeck_columns/src/route.rs | 15+++++++++++++++
Mcrates/notedeck_columns/src/ui/column/header.rs | 1+
Mcrates/notedeck_columns/src/ui/mod.rs | 1+
Acrates/notedeck_columns/src/ui/tos.rs | 121+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/notedeck_columns/src/view_state.rs | 4++++
7 files changed, 167 insertions(+), 0 deletions(-)

diff --git a/crates/notedeck_columns/src/app.rs b/crates/notedeck_columns/src/app.rs @@ -255,6 +255,13 @@ fn update_damus(damus: &mut Damus, app_ctx: &mut AppContext<'_>, ctx: &egui::Con ) { warn!("update_damus init: {err}"); } + + if !app_ctx.settings.tos_accepted() { + damus + .columns_mut(app_ctx.i18n, app_ctx.accounts) + .get_selected_router() + .route_to(Route::TosAcceptance); + } } DamusState::Initialized => (), @@ -832,6 +839,7 @@ fn should_show_compose_button(decks: &DecksCache, accounts: &Accounts) -> bool { Route::RepostDecision(_) => false, Route::Following(_) => false, Route::FollowedBy(_) => false, + Route::TosAcceptance => false, } } diff --git a/crates/notedeck_columns/src/nav.rs b/crates/notedeck_columns/src/nav.rs @@ -1025,6 +1025,23 @@ fn render_nav_body( }) } Route::FollowedBy(_pubkey) => DragResponse::none(), + Route::TosAcceptance => { + let resp = ui::tos::TosAcceptanceView::new( + ctx.i18n, + &mut app.view_state.tos_age_confirmed, + &mut app.view_state.tos_confirmed, + ) + .show(ui); + + if let Some(ui::tos::TosAcceptanceResponse::Accept) = resp { + ctx.settings.accept_tos(); + app.view_state.tos_age_confirmed = false; + app.view_state.tos_confirmed = false; + return DragResponse::output(Some(RenderNavAction::Back)); + } + + DragResponse::none() + } Route::Wallet(wallet_type) => { let state = match wallet_type { notedeck::WalletType::Auto => 's: { diff --git a/crates/notedeck_columns/src/route.rs b/crates/notedeck_columns/src/route.rs @@ -37,6 +37,7 @@ pub enum Route { CustomizeZapAmount(NoteZapTargetOwned), Following(Pubkey), FollowedBy(Pubkey), + TosAcceptance, } impl Route { @@ -152,6 +153,9 @@ impl Route { writer.write_token("followed_by"); writer.write_token(&pubkey.hex()); } + Route::TosAcceptance => { + writer.write_token("tos"); + } } } @@ -289,6 +293,12 @@ impl Route { Ok(Route::FollowedBy(pubkey)) }) }, + |p| { + p.parse_all(|p| { + p.parse_token("tos")?; + Ok(Route::TosAcceptance) + }) + }, ], ) } @@ -415,6 +425,11 @@ impl Route { Route::FollowedBy(_) => { ColumnTitle::formatted(tr!(i18n, "Followed by", "Column title for followers")) } + Route::TosAcceptance => ColumnTitle::formatted(tr!( + i18n, + "Terms of Service", + "Column title for TOS acceptance screen" + )), } } } diff --git a/crates/notedeck_columns/src/ui/column/header.rs b/crates/notedeck_columns/src/ui/column/header.rs @@ -548,6 +548,7 @@ impl<'a> NavTitle<'a> { Route::RepostDecision(_) => None, Route::Following(pubkey) => Some(self.show_profile(ui, pubkey, pfp_size)), Route::FollowedBy(pubkey) => Some(self.show_profile(ui, pubkey, pfp_size)), + Route::TosAcceptance => None, } } diff --git a/crates/notedeck_columns/src/ui/mod.rs b/crates/notedeck_columns/src/ui/mod.rs @@ -20,6 +20,7 @@ pub mod support; pub mod thread; pub mod timeline; pub mod toolbar; +pub mod tos; pub mod wallet; pub mod widgets; diff --git a/crates/notedeck_columns/src/ui/tos.rs b/crates/notedeck_columns/src/ui/tos.rs @@ -0,0 +1,121 @@ +use egui::{vec2, Button, Label, Layout, RichText, ScrollArea}; +use notedeck::{tr, Localization, NotedeckTextStyle}; +use notedeck_ui::padding; + +const EULA_TEXT: &str = include_str!("../../../../docs/EULA.md"); + +pub enum TosAcceptanceResponse { + Accept, +} + +pub struct TosAcceptanceView<'a> { + i18n: &'a mut Localization, + age_confirmed: &'a mut bool, + tos_confirmed: &'a mut bool, +} + +impl<'a> TosAcceptanceView<'a> { + pub fn new( + i18n: &'a mut Localization, + age_confirmed: &'a mut bool, + tos_confirmed: &'a mut bool, + ) -> Self { + Self { + i18n, + age_confirmed, + tos_confirmed, + } + } + + pub fn show(&mut self, ui: &mut egui::Ui) -> Option<TosAcceptanceResponse> { + let mut response = None; + + padding(16.0, ui, |ui| { + ui.spacing_mut().item_spacing = vec2(0.0, 12.0); + + ui.add(Label::new( + RichText::new(tr!( + self.i18n, + "Terms of Service", + "TOS acceptance screen title" + )) + .text_style(NotedeckTextStyle::Heading2.text_style()), + )); + + ui.add(Label::new( + RichText::new(tr!( + self.i18n, + "Please read and accept the following terms to continue.", + "TOS acceptance instruction text" + )) + .text_style(NotedeckTextStyle::Body.text_style()), + )); + + let available = ui.available_height() - 120.0; + let scroll_height = available.max(200.0); + + egui::Frame::group(ui.style()) + .fill(ui.style().visuals.extreme_bg_color) + .inner_margin(12.0) + .show(ui, |ui| { + ScrollArea::vertical() + .max_height(scroll_height) + .show(ui, |ui| { + ui.add( + Label::new( + RichText::new(EULA_TEXT) + .text_style(NotedeckTextStyle::Body.text_style()), + ) + .wrap(), + ); + }); + }); + + ui.checkbox( + self.age_confirmed, + tr!( + self.i18n, + "I confirm that I am at least 17 years old", + "Age verification checkbox label" + ), + ); + + ui.checkbox( + self.tos_confirmed, + tr!( + self.i18n, + "I have read and agree to the Terms of Service", + "TOS agreement checkbox label" + ), + ); + + let can_accept = *self.age_confirmed && *self.tos_confirmed; + let button_size = vec2(200.0, 40.0); + + ui.allocate_ui_with_layout(button_size, Layout::top_down(egui::Align::Center), |ui| { + let font_size = notedeck::fonts::get_font_size(ui.ctx(), &NotedeckTextStyle::Body); + let button = Button::new( + RichText::new(tr!( + self.i18n, + "Accept and Continue", + "Button to accept TOS and continue using the app" + )) + .size(font_size), + ) + .min_size(button_size); + + let button = if can_accept { + button.fill(notedeck_ui::colors::PINK) + } else { + button + }; + + if ui.add_enabled(can_accept, button).clicked() { + response = Some(TosAcceptanceResponse::Accept); + } + }); + }); + + response + } +} diff --git a/crates/notedeck_columns/src/view_state.rs b/crates/notedeck_columns/src/view_state.rs @@ -29,6 +29,10 @@ pub struct ViewState { /// Keep track of checkbox state of follow pack onboarding pub follow_packs: Nip51SetUiCache, + + /// TOS acceptance screen checkbox state + pub tos_age_confirmed: bool, + pub tos_confirmed: bool, } impl ViewState {