synpad 0.1.0

A full-featured Matrix chat client built with Dioxus
use dioxus::prelude::*;

use crate::app::Route;
use crate::state::app_state::{AppState, AuthStatus};

/// SSO login button component.
#[component]
pub fn SsoLoginButton(homeserver: String) -> Element {
    let mut state = use_context::<Signal<AppState>>();
    let navigator = use_navigator();
    let mut is_loading = use_signal(|| false);
    let mut error_message = use_signal(|| Option::<String>::None);

    let on_click = move |_| {
        is_loading.set(true);
        error_message.set(None);
        let hs = homeserver.clone();

        spawn(async move {
            match start_sso_login(&hs).await {
                Ok(client) => {
                    tracing::info!("SSO login successful");
                    {
                        let mut w = state.write();
                        w.auth_status = AuthStatus::LoggedIn;
                        w.client = Some(client);
                    }
                    navigator.push(Route::Home {});
                }
                Err(e) => {
                    tracing::error!("SSO login failed: {e}");
                    error_message.set(Some(e));
                    is_loading.set(false);
                }
            }
        });
    };

    rsx! {
        div {
            class: "sso-login",

            if let Some(ref err) = *error_message.read() {
                div {
                    class: "sso-login__error",
                    "{err}"
                }
            }

            button {
                class: "sso-login-button",
                onclick: on_click,
                disabled: *is_loading.read(),
                if *is_loading.read() {
                    "Opening browser..."
                } else {
                    "Sign in with SSO"
                }
            }
        }
    }
}

/// Initiate SSO login flow.
async fn start_sso_login(homeserver: &str) -> Result<matrix_sdk::Client, String> {
    let client = crate::client::build_client(homeserver)
        .await
        .map_err(|e| format!("Failed to connect: {e}"))?;

    // Check if SSO login is available
    let login_types = client
        .matrix_auth()
        .get_login_types()
        .await
        .map_err(|e| format!("Failed to get login types: {e}"))?;

    let has_sso = login_types.flows.iter().any(|flow| {
        matches!(
            flow,
            matrix_sdk::ruma::api::client::session::get_login_types::v3::LoginType::Sso(_)
        )
    });

    if !has_sso {
        return Err("This homeserver does not support SSO login".to_string());
    }

    // Use the SDK's built-in SSO login support
    // This opens the user's browser for authentication
    let sso_result = client
        .matrix_auth()
        .login_sso(|url| async move {
            tracing::info!("Opening SSO URL: {url}");
            // Open the URL in the user's default browser
            #[cfg(target_os = "windows")]
            {
                let _ = std::process::Command::new("cmd")
                    .args(["/C", "start", "", &url])
                    .spawn();
            }
            #[cfg(target_os = "macos")]
            {
                let _ = std::process::Command::new("open").arg(&url).spawn();
            }
            #[cfg(target_os = "linux")]
            {
                let _ = std::process::Command::new("xdg-open").arg(&url).spawn();
            }
            Ok(())
        })
        .initial_device_display_name("Netrix")
        .await;

    match sso_result {
        Ok(_) => {
            // Save session after successful SSO login
            if let Some(session) = client.matrix_auth().session() {
                let session_data = crate::persistence::matrix_state::SessionData {
                    homeserver_url: homeserver.to_string(),
                    user_id: session.meta.user_id.to_string(),
                    device_id: session.meta.device_id.to_string(),
                    access_token: session.tokens.access_token.clone(),
                };
                if let Err(e) = crate::persistence::matrix_state::save_session(&session_data).await {
                    tracing::error!("Failed to save SSO session: {e}");
                }
            }

            Ok(client)
        }
        Err(e) => Err(format!("SSO login failed: {e}")),
    }
}