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
3754use makepad_widgets:: * ;
3855use 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 \n Please 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 ) ]
597699pub 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