Skip to content

Commit 6509e20

Browse files
feat: Complete SSO registration implementation with proper modal handling
Major improvements to registration screen: 1. SSO Registration Support: - Added source-aware SSO handling with is_registration flag - Register and login screens now have independent action flows - Proper modal display during SSO authentication - Automatic modal updates with status messages 2. Modal Handling Fixes: - Fixed non-clickable Cancel/Abort buttons in registration modals - Implemented proper was_internal pattern for modal close events - Added button mask state (gray out) during processing - Correct modal text for password vs SSO registration 3. UI/UX Consistency: - Unified modal styles between login and register screens (285px width) - Consistent padding, spacing, colors (#CCC background) - Matching title sections and button styles - Unified outer screen styles (#FFF background) 4. Code Quality: - Simplified RegisterAction enum (removed redundant variants) - Complete decoupling between login and register SSO flows - Clear documentation of SSO action handling architecture - Fixed all layout and z-index issues Technical details: - SpawnSSOServer now includes is_registration flag - sliding_sync.rs sends appropriate actions based on source - Each screen only processes its own action types - Modal shows immediately on SSO button click for better UX
1 parent 0ef2b32 commit 6509e20

File tree

4 files changed

+300
-108
lines changed

4 files changed

+300
-108
lines changed

src/login/login_screen.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,7 @@ impl MatchEvent for LoginScreen {
367367
}
368368

369369
// Handle login-related actions received from background async tasks.
370+
// Skip processing if the login screen is not visible (e.g., user is on register screen)
370371
match action.downcast_ref() {
371372
Some(LoginAction::CliAutoLogin { user_id, homeserver }) => {
372373
user_id_input.set_text(cx, user_id);
@@ -451,7 +452,8 @@ impl MatchEvent for LoginScreen {
451452
submit_async_request(MatrixRequest::SpawnSSOServer{
452453
identity_provider_id: format!("oidc-{}",brand),
453454
brand: brand.to_string(),
454-
homeserver_url: homeserver_input.text()
455+
homeserver_url: homeserver_input.text(),
456+
is_registration: false,
455457
});
456458
}
457459
}

src/register/register_screen.rs

Lines changed: 166 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,27 @@
2929
//! Registration Success Auto Login/Register
3030
//! ```
3131
//!
32+
//! # SSO Action Handling Design
33+
//!
34+
//! The register screen uses source-aware SSO handling:
35+
//!
36+
//! ## How It Works
37+
//! 1. Register screen sends `SpawnSSOServer` with `is_registration: true`
38+
//! 2. `sliding_sync.rs` sends appropriate actions based on this flag:
39+
//! - For registration: `RegisterAction::SsoRegistrationPending/Status/Success/Failure`
40+
//! - For login: `LoginAction::SsoPending/Status/LoginSuccess/LoginFailure`
41+
//! 3. Each screen only receives and handles its own actions
42+
//!
43+
//! ## Benefits
44+
//! - **Zero Coupling:** Login and register screens are completely independent
45+
//! - **Clear Intent:** The SSO flow knows its purpose from the start
46+
//! - **No Action Conversion:** No need to intercept and convert actions
47+
//! - **Maintainable:** Each screen has its own clear action flow
48+
//!
3249
//! # Implementation Notes
50+
//! - SSO at protocol level doesn't distinguish login/register - server decides based on account existence
3351
//! - Registration token support has been intentionally omitted for simplicity
3452
//! - Advanced UIA flows (captcha, email verification) are not supported
35-
//! - SSO state is synchronized with login screen for consistent UX
3653
3754
use makepad_widgets::*;
3855
use crate::sliding_sync::{submit_async_request, MatrixRequest, RegisterRequest};
@@ -50,18 +67,29 @@ live_design! {
5067
use crate::register::register_status_modal::RegisterStatusModal;
5168

5269
IMG_APP_LOGO = dep("crate://self/resources/robrix_logo_alpha.png")
70+
71+
MaskableButton = <RobrixIconButton> {
72+
draw_bg: {
73+
instance mask: 0.0
74+
fn pixel(self) -> vec4 {
75+
let base_color = mix(self.color, mix(self.color, self.color_hover, 0.2), self.hover);
76+
let gray = dot(base_color.rgb, vec3(0.299, 0.587, 0.114));
77+
return mix(base_color, vec4(gray, gray, gray, base_color.a), self.mask);
78+
}
79+
}
80+
}
5381

5482
pub RegisterScreen = {{RegisterScreen}} {
5583
width: Fill, height: Fill,
5684
align: {x: 0.5, y: 0.5}
5785
show_bg: true,
5886
draw_bg: {
59-
color: (COLOR_PRIMARY)
87+
color: #FFF
6088
}
61-
flow: Overlay
6289

6390
<ScrollXYView> {
6491
width: Fit, height: Fill,
92+
// Note: *do NOT* vertically center this, it will break scrolling.
6593
align: {x: 0.5}
6694
show_bg: true,
6795
draw_bg: {
@@ -235,13 +263,14 @@ live_design! {
235263
spacing: 10
236264
visible: true
237265

238-
sso_button = <RobrixIconButton> {
266+
sso_button = <MaskableButton> {
239267
width: Fill, height: 40
240268
padding: 10
241269
margin: {top: 10}
242270
align: {x: 0.5, y: 0.5}
243271
draw_bg: {
244272
color: (COLOR_ACTIVE_PRIMARY)
273+
mask: 0.0
245274
}
246275
draw_text: {
247276
color: (COLOR_PRIMARY)
@@ -280,13 +309,14 @@ live_design! {
280309
is_password: true,
281310
}
282311

283-
register_button = <RobrixIconButton> {
312+
register_button = <MaskableButton> {
284313
width: Fill, height: 40
285314
padding: 10
286315
margin: {top: 5, bottom: 10}
287316
align: {x: 0.5, y: 0.5}
288317
draw_bg: {
289318
color: (COLOR_ACTIVE_PRIMARY)
319+
mask: 0.0
290320
}
291321
draw_text: {
292322
color: (COLOR_PRIMARY)
@@ -337,12 +367,13 @@ live_design! {
337367
text: "Back to Login"
338368
}
339369
}
340-
}
341-
}
342-
343-
status_modal = <Modal> {
344-
content: {
345-
status_modal_inner = <RegisterStatusModal> {}
370+
371+
// Modal for registration status (both password and SSO)
372+
status_modal = <Modal> {
373+
content: {
374+
status_modal_inner = <RegisterStatusModal> {}
375+
}
376+
}
346377
}
347378
}
348379
}
@@ -372,6 +403,20 @@ impl RegisterScreen {
372403
});
373404
}
374405

406+
fn update_button_mask(&self, button: &ButtonRef, cx: &mut Cx, mask: f32) {
407+
button.apply_over(cx, live! {
408+
draw_bg: { mask: (mask) }
409+
});
410+
}
411+
412+
fn reset_modal_state(&mut self, cx: &mut Cx) {
413+
let register_button = self.view.button(id!(register_button));
414+
register_button.set_enabled(cx, true);
415+
register_button.reset_hover(cx);
416+
self.update_button_mask(&register_button, cx, 0.0);
417+
self.redraw(cx);
418+
}
419+
375420
fn update_registration_mode(&mut self, cx: &mut Cx) {
376421
let is_matrix_org = self.selected_homeserver == "matrix.org" || self.selected_homeserver.is_empty();
377422

@@ -421,16 +466,29 @@ impl MatchEvent for RegisterScreen {
421466
if edit_button.clicked(actions) {
422467
self.toggle_homeserver_options(cx);
423468
}
424-
469+
425470
// Handle SSO button click for matrix.org
426471
if sso_button.clicked(actions) && !self.sso_pending {
472+
// Mark SSO as pending for this screen
473+
self.sso_pending = true;
474+
self.update_button_mask(&sso_button, cx, 1.0);
475+
476+
// Show SSO registration modal immediately
477+
let status_label = self.view.label(id!(status_modal_inner.status));
478+
status_label.set_text(cx, "Opening your browser...\n\nPlease complete registration in your browser, then return to Robrix.");
479+
let cancel_button = self.view.button(id!(status_modal_inner.cancel_button));
480+
cancel_button.set_text(cx, "Cancel");
481+
self.view.modal(id!(status_modal)).open(cx);
482+
self.redraw(cx);
483+
427484
// Use the same SSO flow as login screen - spawn SSO server with Google provider
428485
// This follows Element's implementation where SSO login and registration share the same OAuth flow
429486
// The Matrix server will handle whether to create a new account or login existing user
430487
submit_async_request(MatrixRequest::SpawnSSOServer{
431488
identity_provider_id: "oidc-google".to_string(),
432489
brand: "google".to_string(),
433-
homeserver_url: String::new() // Use default matrix.org
490+
homeserver_url: String::new(), // Use default matrix.org
491+
is_registration: true,
434492
});
435493
}
436494

@@ -520,8 +578,15 @@ impl MatchEvent for RegisterScreen {
520578

521579
// Disable register button to prevent duplicate submissions
522580
register_button.set_enabled(cx, false);
523-
524-
// Show registration status modal
581+
self.update_button_mask(&register_button, cx, 1.0);
582+
583+
// Show registration status modal with appropriate text for password registration
584+
let status_label = self.view.label(id!(status_modal_inner.status));
585+
status_label.set_text(cx, "Registering account, please wait...");
586+
let title_label = self.view.label(id!(status_modal_inner.title));
587+
title_label.set_text(cx, "Registration Status");
588+
let cancel_button = self.view.button(id!(status_modal_inner.cancel_button));
589+
cancel_button.set_text(cx, "Cancel");
525590
self.view.modal(id!(status_modal)).open(cx);
526591
self.redraw(cx);
527592

@@ -535,73 +600,116 @@ impl MatchEvent for RegisterScreen {
535600

536601
cx.action(RegisterAction::RegistrationSubmitted);
537602
}
538-
603+
539604
// Handle modal closing for both success and failure in one place
540605
for action in actions {
541606
// Handle RegisterStatusModal close action
542-
if let Some(RegisterStatusModalAction::Close) = action.as_widget_action().cast() {
543-
self.view.modal(id!(status_modal)).close(cx);
544-
// Re-enable register button when modal is closed manually
545-
let register_button = self.view.button(id!(register_button));
546-
register_button.set_enabled(cx, true);
607+
if let Some(RegisterStatusModalAction::Close { was_internal }) = action.downcast_ref::<RegisterStatusModalAction>() {
608+
if *was_internal {
609+
self.view.modal(id!(status_modal)).close(cx);
610+
}
611+
// Reset appropriate button based on registration type
612+
if self.sso_pending {
613+
self.sso_pending = false;
614+
self.update_button_mask(&sso_button, cx, 0.0);
615+
sso_button.set_enabled(cx, true);
616+
sso_button.reset_hover(cx);
617+
} else {
618+
// Password registration - reset register button
619+
self.reset_modal_state(cx);
620+
}
547621
self.redraw(cx);
548622
}
549-
550-
// Handle SSO login actions
551-
match action.downcast_ref::<LoginAction>() {
552-
Some(LoginAction::SsoPending(pending)) => {
553-
self.sso_pending = *pending;
554-
// Update SSO button state
555-
sso_button.set_enabled(cx, !pending);
623+
624+
// Handle SSO completion from login flow
625+
// SSO success ultimately goes through the login flow, so we listen for LoginSuccess
626+
if self.sso_pending {
627+
if let Some(LoginAction::LoginSuccess) = action.downcast_ref::<LoginAction>() {
628+
// SSO registration successful
629+
self.view.modal(id!(status_modal)).close(cx);
630+
self.sso_pending = false;
631+
self.update_button_mask(&sso_button, cx, 0.0);
632+
cx.action(RegisterAction::RegistrationSuccess);
556633
self.redraw(cx);
557634
}
558-
Some(LoginAction::Status { .. }) => {
559-
// Show SSO status in modal
560-
self.view.modal(id!(status_modal)).open(cx);
561-
// Note: We can't easily update the modal text dynamically,
562-
// but the modal will show and user knows something is happening
635+
}
636+
637+
// Handle RegisterAction for SSO (now directly sent from sliding_sync.rs)
638+
match action.downcast_ref::<RegisterAction>() {
639+
Some(RegisterAction::SsoRegistrationPending(pending)) => {
640+
// Update pending state (modal already shown when button clicked)
641+
if !*pending {
642+
// SSO ended
643+
self.sso_pending = false;
644+
self.update_button_mask(&sso_button, cx, 0.0);
645+
self.view.modal(id!(status_modal)).close(cx);
646+
}
563647
self.redraw(cx);
564648
}
565-
Some(LoginAction::LoginSuccess) | Some(LoginAction::LoginFailure(_)) => {
566-
// Handle both success and failure - close modal and reset SSO state
567-
if let Some(LoginAction::LoginFailure(error)) = action.downcast_ref::<LoginAction>() {
568-
self.show_warning(error);
649+
Some(RegisterAction::SsoRegistrationStatus { status }) => {
650+
// Update SSO status in modal (only if our modal is already open)
651+
if self.sso_pending {
652+
let status_label = self.view.label(id!(status_modal_inner.status));
653+
status_label.set_text(cx, status);
654+
let cancel_button = self.view.button(id!(status_modal_inner.cancel_button));
655+
cancel_button.set_text(cx, "Cancel");
656+
self.redraw(cx);
569657
}
570-
self.view.modal(id!(status_modal)).close(cx);
571-
self.sso_pending = false;
572-
sso_button.set_enabled(cx, true);
573-
self.redraw(cx);
574658
}
575659
_ => {}
576660
}
577-
578-
if let Some(RegisterAction::RegistrationSuccess | RegisterAction::RegistrationFailure(_)) = action.downcast_ref::<RegisterAction>() {
579-
// Always hide modal regardless of result
580-
self.view.modal(id!(status_modal)).close(cx);
581-
582-
// Re-enable register button for failure case (success will hide the screen)
583-
if matches!(action.downcast_ref::<RegisterAction>(), Some(RegisterAction::RegistrationFailure(_))) {
584-
let register_button = self.view.button(id!(register_button));
585-
register_button.set_enabled(cx, true);
586-
register_button.reset_hover(cx);
661+
662+
if let Some(reg_action) = action.downcast_ref::<RegisterAction>() {
663+
match reg_action {
664+
RegisterAction::RegistrationSuccess => {
665+
// Close modal and let app.rs handle screen transition
666+
self.view.modal(id!(status_modal)).close(cx);
667+
if self.sso_pending {
668+
self.sso_pending = false;
669+
self.update_button_mask(&sso_button, cx, 0.0);
670+
}
671+
self.redraw(cx);
672+
}
673+
RegisterAction::RegistrationFailure(error) => {
674+
// Show error and reset buttons
675+
if self.sso_pending {
676+
self.show_warning(error);
677+
self.sso_pending = false;
678+
self.update_button_mask(&sso_button, cx, 0.0);
679+
}
680+
self.view.modal(id!(status_modal)).close(cx);
681+
let register_button = self.view.button(id!(register_button));
682+
register_button.set_enabled(cx, true);
683+
register_button.reset_hover(cx);
684+
self.redraw(cx);
685+
}
686+
_ => {}
587687
}
588-
589-
self.redraw(cx);
590688
}
591689
}
592690
}
593691
}
594692

595-
/// Actions related to the register screen
693+
/// Actions for the registration screen.
694+
///
695+
/// These actions handle both password-based and SSO registration flows.
696+
/// SSO actions are completely independent from LoginAction to ensure
697+
/// no interference between login and register screens.
596698
#[derive(Clone, DefaultNone, Debug)]
597699
pub enum RegisterAction {
598-
/// Navigate back to the login screen
700+
/// User requested to go back to the login screen
599701
NavigateToLogin,
600-
/// Registration form was submitted (show loading indicator)
702+
/// Password registration was submitted (internal use)
601703
RegistrationSubmitted,
602-
/// Registration was successful
704+
/// Registration completed successfully (both password and SSO)
603705
RegistrationSuccess,
604-
/// Registration failed with an error message
706+
/// Registration failed with error message (both password and SSO)
605707
RegistrationFailure(String),
708+
/// SSO registration state changed
709+
/// - `true`: SSO flow started, button should be disabled
710+
/// - `false`: SSO flow ended, button should be re-enabled
711+
SsoRegistrationPending(bool),
712+
/// SSO registration progress update (e.g., "Opening browser...")
713+
SsoRegistrationStatus { status: String },
606714
None,
607715
}

0 commit comments

Comments
 (0)