diff --git a/src/app.rs b/src/app.rs index 22476ba1..dc2d81e2 100644 --- a/src/app.rs +++ b/src/app.rs @@ -7,19 +7,11 @@ use makepad_widgets::*; use matrix_sdk::ruma::{OwnedRoomId, RoomId}; use serde::{Deserialize, Serialize}; use crate::{ - avatar_cache::clear_avatar_cache, - home::{ + avatar_cache::clear_avatar_cache, home::{ main_desktop_ui::MainDesktopUiAction, new_message_context_menu::NewMessageContextMenuWidgetRefExt, room_screen::{MessageAction, clear_timeline_states}, rooms_list::{RoomsListAction, RoomsListRef, RoomsListUpdate, clear_all_invited_rooms, enqueue_rooms_list_update} - }, - join_leave_room_modal::{ + }, join_leave_room_modal::{ JoinLeaveModalKind, JoinLeaveRoomModalAction, JoinLeaveRoomModalWidgetRefExt - }, - login::login_screen::LoginAction, - logout::logout_confirm_modal::{LogoutAction, LogoutConfirmModalAction, LogoutConfirmModalWidgetRefExt}, - persistence, - profile::user_profile_cache::clear_user_profile_cache, - room::BasicRoomDetails, - shared::{callout_tooltip::{ + }, login::login_screen::LoginAction, logout::logout_confirm_modal::{LogoutAction, LogoutConfirmModalAction, LogoutConfirmModalWidgetRefExt}, persistence, profile::user_profile_cache::clear_user_profile_cache, register::register_screen::RegisterAction, room::BasicRoomDetails, shared::{callout_tooltip::{ CalloutTooltipWidgetRefExt, TooltipAction, }, image_viewer::{ImageViewerAction, LoadState}}, sliding_sync::current_user_id, utils::RoomNameId, verification::VerificationAction, verification_modal::{ @@ -38,6 +30,7 @@ live_design! { use crate::verification_modal::VerificationModal; use crate::join_leave_room_modal::JoinLeaveRoomModal; use crate::login::login_screen::LoginScreen; + use crate::register::register_screen::RegisterScreen; use crate::logout::logout_confirm_modal::LogoutConfirmModal; use crate::shared::popup_list::*; use crate::home::new_message_context_menu::*; @@ -98,7 +91,10 @@ live_design! { visible: true login_screen = {} } - + register_screen_view = { + visible: false + register_screen = {} + } image_viewer_modal = { content: { width: Fill, height: Fill, @@ -183,6 +179,7 @@ impl LiveRegister for App { crate::home::live_design(cx); crate::profile::live_design(cx); crate::login::live_design(cx); + crate::register::live_design(cx); crate::logout::live_design(cx); } } @@ -260,11 +257,59 @@ impl MatchEvent for App { continue; } - if let Some(LoginAction::LoginSuccess) = action.downcast_ref() { - log!("Received LoginAction::LoginSuccess, hiding login view."); - self.app_state.logged_in = true; - self.update_login_visibility(cx); - self.ui.redraw(cx); + // Handle login-related actions + if let Some(login_action) = action.downcast_ref::() { + match login_action { + LoginAction::LoginSuccess => { + log!("Received LoginAction::LoginSuccess, hiding login view."); + self.app_state.logged_in = true; + self.update_login_visibility(cx); + self.ui.redraw(cx); + } + LoginAction::NavigateToRegister => { + log!("Navigating from login to register screen"); + // Start from a clean register screen state + if let Some(mut register_screen_ref) = self + .ui + .widget(ids!(register_screen_view.register_screen)) + .borrow_mut::() + { + register_screen_ref.reset_screen_state(cx); + } + self.ui.view(ids!(login_screen_view)).set_visible(cx, false); + self.ui.view(ids!(register_screen_view)).set_visible(cx, true); + self.ui.redraw(cx); + } + _ => {} + } + continue; + } + + // Handle register-related actions + if let Some(register_action) = action.downcast_ref::() { + match register_action { + RegisterAction::NavigateToLogin => { + log!("Navigating from register to login screen"); + // Reset the register screen state before hiding it + if let Some(mut register_screen_ref) = self.ui.widget(ids!(register_screen_view.register_screen)).borrow_mut::() { + register_screen_ref.reset_screen_state(cx); + } + self.ui.view(ids!(register_screen_view)).set_visible(cx, false); + self.ui.view(ids!(login_screen_view)).set_visible(cx, true); + self.ui.redraw(cx); + } + RegisterAction::RegistrationSuccess => { + log!("Registration successful, transitioning to logged in state"); + // Clear register screen state after successful registration + if let Some(mut register_screen_ref) = self.ui.widget(ids!(register_screen_view.register_screen)).borrow_mut::() { + register_screen_ref.reset_screen_state(cx); + } + self.app_state.logged_in = true; + self.update_login_visibility(cx); + self.ui.redraw(cx); + } + _ => {} + } continue; } @@ -545,6 +590,7 @@ impl App { .close(cx); } self.ui.view(ids!(login_screen_view)).set_visible(cx, show_login); + self.ui.view(ids!(register_screen_view)).set_visible(cx, false); self.ui.view(ids!(home_screen_view)).set_visible(cx, !show_login); } diff --git a/src/lib.rs b/src/lib.rs index 9b436777..0c677541 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,6 +13,8 @@ pub mod settings; /// Login screen pub mod login; +/// Register screen +pub mod register; /// Logout confirmation and state management pub mod logout; /// Core UI content: the main home screen (rooms list), room screen. diff --git a/src/login/login_screen.rs b/src/login/login_screen.rs index 208c24eb..7650ff25 100644 --- a/src/login/login_screen.rs +++ b/src/login/login_screen.rs @@ -286,8 +286,6 @@ live_design! { } } -static MATRIX_SIGN_UP_URL: &str = "https://matrix.org/docs/chat_basics/matrix-for-im/#creating-a-matrix-account"; - #[derive(Live, LiveHook, Widget)] pub struct LoginScreen { #[deref] view: View, @@ -321,8 +319,7 @@ impl MatchEvent for LoginScreen { let login_status_modal_inner = self.view.login_status_modal(ids!(login_status_modal_inner)); if signup_button.clicked(actions) { - log!("Opening URL \"{}\"", MATRIX_SIGN_UP_URL); - let _ = robius_open::Uri::new(MATRIX_SIGN_UP_URL).open(); + cx.action(LoginAction::NavigateToRegister); } if login_button.clicked(actions) @@ -370,6 +367,7 @@ impl MatchEvent for LoginScreen { } // Handle login-related actions received from background async tasks. + // Skip processing if the login screen is not visible (e.g., user is on register screen) match action.downcast_ref() { Some(LoginAction::CliAutoLogin { user_id, homeserver }) => { user_id_input.set_text(cx, user_id); @@ -454,7 +452,8 @@ impl MatchEvent for LoginScreen { submit_async_request(MatrixRequest::SpawnSSOServer{ identity_provider_id: format!("oidc-{}",brand), brand: brand.to_string(), - homeserver_url: homeserver_input.text() + homeserver_url: homeserver_input.text(), + is_registration: false, }); } } @@ -493,5 +492,7 @@ pub enum LoginAction { /// When an SSO-based login is pendng, pressing the cancel button will send /// an HTTP request to this SSO server URL to gracefully shut it down. SsoSetRedirectUrl(Url), + /// Navigate to the register screen. + NavigateToRegister, None, } diff --git a/src/register/mod.rs b/src/register/mod.rs new file mode 100644 index 00000000..5a867458 --- /dev/null +++ b/src/register/mod.rs @@ -0,0 +1,9 @@ +use makepad_widgets::*; + +pub mod register_screen; +pub mod register_status_modal; + +pub fn live_design(cx: &mut Cx) { + register_screen::live_design(cx); + register_status_modal::live_design(cx); +} \ No newline at end of file diff --git a/src/register/register_screen.rs b/src/register/register_screen.rs new file mode 100644 index 00000000..a726d7b0 --- /dev/null +++ b/src/register/register_screen.rs @@ -0,0 +1,834 @@ +//! Registration screen implementation with support for both password-based and SSO registration. +//! +//! # Supported Registration Methods +//! +//! ## 1. Password-based Registration (Custom Homeservers) +//! For custom Matrix homeservers, users can register with username/password: +//! - Minimum password length: 8 characters +//! - Automatic UIA handling for `m.login.dummy` and `m.login.registration_token` +//! - Basic URL validation for custom homeserver addresses +//! +//! ## 2. SSO Registration (matrix.org) +//! For matrix.org, registration uses Google SSO by default: +//! - **Why Google SSO?** Following Element's implementation, matrix.org primarily uses +//! Google OAuth as the main SSO provider for public registrations +//! - The SSO flow is shared with login - Matrix server automatically determines whether +//! to create a new account or login an existing user based on the OAuth identity +//! - UI provides clear feedback during SSO process (button disabled, status modal) +//! +//! # Registration Flow +//! +//! ```text +//! Password Registration: SSO Registration: +//! User → Username/Password → Server User → Continue with SSO → Browser OAuth +//! ↓ ↓ +//! UIA Challenge (if needed) Google Authentication +//! ↓ ↓ +//! Auto-handle m.login.dummy OAuth Callback +//! ↓ ↓ +//! Registration Success Auto Login/Register +//! ``` +//! +//! # SSO Action Handling Design +//! +//! The register screen uses source-aware SSO handling: +//! +//! ## How It Works +//! 1. Register screen sends `SpawnSSOServer` with `is_registration: true` +//! 2. `sliding_sync.rs` sends appropriate actions based on this flag: +//! - For registration: `RegisterAction::SsoRegistrationPending/Status/Success/Failure` +//! - For login: `LoginAction::SsoPending/Status/LoginSuccess/LoginFailure` +//! 3. Each screen only receives and handles its own actions +//! +//! ## Benefits +//! - **Zero Coupling:** Login and register screens are completely independent +//! - **Clear Intent:** The SSO flow knows its purpose from the start +//! - **No Action Conversion:** No need to intercept and convert actions +//! - **Maintainable:** Each screen has its own clear action flow +//! +//! # Implementation Notes +//! - SSO at protocol level doesn't distinguish login/register - server decides based on account existence +//! - Registration currently supports the dummy + registration token UIA stages; more complex flows +//! (captcha, email verification, etc.) are not yet implemented and will prompt users to register via web + +use makepad_widgets::*; +use url::Url; +use crate::sliding_sync::{submit_async_request, MatrixRequest, RegisterRequest}; +use crate::login::login_screen::LoginAction; +use super::register_status_modal::RegisterStatusModalAction; + +live_design! { + use link::theme::*; + use link::shaders::*; + use link::widgets::*; + + use crate::shared::helpers::*; + use crate::shared::styles::*; + use crate::shared::icon_button::*; + use crate::register::register_status_modal::RegisterStatusModal; + + IMG_APP_LOGO = dep("crate://self/resources/robrix_logo_alpha.png") + + MaskableButton = { + draw_bg: { + instance mask: 0.0 + fn pixel(self) -> vec4 { + let base_color = mix(self.color, mix(self.color, self.color_hover, 0.2), self.hover); + let gray = dot(base_color.rgb, vec3(0.299, 0.587, 0.114)); + return mix(base_color, vec4(gray, gray, gray, base_color.a), self.mask); + } + } + } + + pub RegisterScreen = {{RegisterScreen}} { + width: Fill, height: Fill, + align: {x: 0.5, y: 0.5} + show_bg: true, + draw_bg: { + color: #FFF + } + + { + width: Fit, height: Fill, + // Note: *do NOT* vertically center this, it will break scrolling. + align: {x: 0.5} + show_bg: true, + draw_bg: { + color: (COLOR_PRIMARY) + } + + { + margin: 40 + width: Fit, height: Fit + align: {x: 0.5, y: 0.5} + flow: Overlay, + + show_bg: true, + draw_bg: { + color: (COLOR_SECONDARY) + border_radius: 6.0 + } + + { + width: Fit, height: Fit + flow: Down + align: {x: 0.5, y: 0.5} + padding: 30 + margin: 40 + spacing: 15.0 + + logo_image = { + fit: Smallest, + width: 80 + source: (IMG_APP_LOGO), + } + + title =