1use adw::{prelude::*, subclass::prelude::*};
2use gettextrs::gettext;
3use gtk::{
4 gio, glib,
5 glib::{clone, closure_local},
6 CompositeTemplate,
7};
8use matrix_sdk::Client;
9use ruma::{
10 api::client::session::{get_login_types::v3::LoginType, login},
11 OwnedServerName,
12};
13use tracing::warn;
14use url::Url;
15
16mod advanced_dialog;
17mod greeter;
18mod homeserver_page;
19mod in_browser_page;
20mod method_page;
21mod session_setup_view;
22mod sso_idp_button;
23
24use self::{
25 advanced_dialog::LoginAdvancedDialog, greeter::Greeter, homeserver_page::LoginHomeserverPage,
26 in_browser_page::LoginInBrowserPage, method_page::LoginMethodPage,
27 session_setup_view::SessionSetupView,
28};
29use crate::{
30 components::OfflineBanner, prelude::*, secret::Secret, session::model::Session, spawn, toast,
31 Application, Window, RUNTIME, SETTINGS_KEY_CURRENT_SESSION,
32};
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, strum::EnumString, strum::AsRefStr)]
36#[strum(serialize_all = "kebab-case")]
37enum LoginPage {
38 Greeter,
40 Homeserver,
42 Method,
44 InBrowser,
46 Loading,
48 SessionSetup,
50 Completed,
52}
53
54mod imp {
55 use std::{
56 cell::{Cell, RefCell},
57 marker::PhantomData,
58 sync::LazyLock,
59 };
60
61 use glib::subclass::{InitializingObject, Signal};
62
63 use super::*;
64
65 #[derive(Debug, Default, CompositeTemplate, glib::Properties)]
66 #[template(resource = "/org/gnome/Fractal/ui/login/mod.ui")]
67 #[properties(wrapper_type = super::Login)]
68 pub struct Login {
69 #[template_child]
70 navigation: TemplateChild<adw::NavigationView>,
71 #[template_child]
72 greeter: TemplateChild<Greeter>,
73 #[template_child]
74 homeserver_page: TemplateChild<LoginHomeserverPage>,
75 #[template_child]
76 method_page: TemplateChild<LoginMethodPage>,
77 #[template_child]
78 in_browser_page: TemplateChild<LoginInBrowserPage>,
79 #[template_child]
80 done_button: TemplateChild<gtk::Button>,
81 #[property(get, set = Self::set_autodiscovery, construct, explicit_notify, default = true)]
83 autodiscovery: Cell<bool>,
84 login_types: RefCell<Vec<LoginType>>,
86 domain: RefCell<Option<OwnedServerName>>,
88 #[property(get = Self::domain_string)]
90 domain_string: PhantomData<Option<String>>,
91 homeserver_url: RefCell<Option<Url>>,
93 #[property(get = Self::homeserver_url_string)]
95 homeserver_url_string: PhantomData<Option<String>>,
96 client: RefCell<Option<Client>>,
98 session: RefCell<Option<Session>>,
100 }
101
102 #[glib::object_subclass]
103 impl ObjectSubclass for Login {
104 const NAME: &'static str = "Login";
105 type Type = super::Login;
106 type ParentType = adw::Bin;
107
108 fn class_init(klass: &mut Self::Class) {
109 OfflineBanner::ensure_type();
110
111 Self::bind_template(klass);
112 Self::bind_template_callbacks(klass);
113
114 klass.set_css_name("login");
115 klass.set_accessible_role(gtk::AccessibleRole::Group);
116
117 klass.install_action_async(
118 "login.sso",
119 Some(&Option::<String>::static_variant_type()),
120 |obj, _, variant| async move {
121 let sso_idp_id = variant.and_then(|v| v.get::<Option<String>>()).flatten();
122 obj.imp().show_in_browser_page(sso_idp_id, false);
123 },
124 );
125
126 klass.install_action_async("login.open-advanced", None, |obj, _, _| async move {
127 obj.imp().open_advanced_dialog().await;
128 });
129 }
130
131 fn instance_init(obj: &InitializingObject<Self>) {
132 obj.init_template();
133 }
134 }
135
136 #[glib::derived_properties]
137 impl ObjectImpl for Login {
138 fn signals() -> &'static [Signal] {
139 static SIGNALS: LazyLock<Vec<Signal>> = LazyLock::new(|| {
140 vec![
141 Signal::builder("login-types-changed").build(),
143 ]
144 });
145 SIGNALS.as_ref()
146 }
147
148 fn constructed(&self) {
149 let obj = self.obj();
150 obj.action_set_enabled("login.next", false);
151
152 self.parent_constructed();
153
154 let monitor = gio::NetworkMonitor::default();
155 monitor.connect_network_changed(clone!(
156 #[weak]
157 obj,
158 move |_, available| {
159 obj.action_set_enabled("login.sso", available);
160 }
161 ));
162 obj.action_set_enabled("login.sso", monitor.is_network_available());
163
164 self.navigation.connect_visible_page_notify(clone!(
165 #[weak(rename_to = imp)]
166 self,
167 move |_| {
168 imp.visible_page_changed();
169 }
170 ));
171 }
172
173 fn dispose(&self) {
174 self.drop_client();
175 self.drop_session();
176 }
177 }
178
179 impl WidgetImpl for Login {
180 fn grab_focus(&self) -> bool {
181 match self.visible_page() {
182 LoginPage::Greeter => self.greeter.grab_focus(),
183 LoginPage::Homeserver => self.homeserver_page.grab_focus(),
184 LoginPage::Method => self.method_page.grab_focus(),
185 LoginPage::InBrowser => self.in_browser_page.grab_focus(),
186 LoginPage::Loading => false,
187 LoginPage::SessionSetup => {
188 if let Some(session_setup) = self.session_setup() {
189 session_setup.grab_focus()
190 } else {
191 false
192 }
193 }
194 LoginPage::Completed => self.done_button.grab_focus(),
195 }
196 }
197 }
198
199 impl BinImpl for Login {}
200 impl AccessibleImpl for Login {}
201
202 #[gtk::template_callbacks]
203 impl Login {
204 pub(super) fn visible_page(&self) -> LoginPage {
206 self.navigation
207 .visible_page()
208 .and_then(|p| p.tag())
209 .and_then(|s| s.as_str().try_into().ok())
210 .unwrap()
211 }
212
213 pub fn set_autodiscovery(&self, autodiscovery: bool) {
215 if self.autodiscovery.get() == autodiscovery {
216 return;
217 }
218
219 self.autodiscovery.set(autodiscovery);
220 self.obj().notify_autodiscovery();
221 }
222
223 pub(super) fn session_setup(&self) -> Option<SessionSetupView> {
225 self.navigation
226 .find_page(LoginPage::SessionSetup.as_ref())
227 .and_downcast()
228 }
229
230 fn visible_page_changed(&self) {
232 match self.visible_page() {
233 LoginPage::Greeter => {
234 self.clean();
235 }
236 LoginPage::Homeserver => {
237 self.drop_client();
239 self.drop_session();
241 self.method_page.clean();
242 }
243 LoginPage::Method => {
244 self.drop_session();
246 }
247 _ => {}
248 }
249 }
250
251 pub(super) async fn client(&self) -> Option<Client> {
253 if let Some(client) = self.client.borrow().clone() {
254 return Some(client);
255 }
256
257 self.homeserver_page.check_homeserver().await;
259 if let Some(client) = self.client.borrow().clone() {
260 return Some(client);
261 }
262
263 None
264 }
265
266 pub(super) fn set_client(&self, client: Option<Client>) {
268 let homeserver = client.as_ref().map(Client::homeserver);
269
270 self.set_homeserver_url(homeserver);
271 self.client.replace(client);
272 }
273
274 pub(super) fn drop_client(&self) {
276 if let Some(client) = self.client.take() {
277 let _guard = RUNTIME.enter();
279 drop(client);
280 }
281 }
282
283 fn drop_session(&self) {
285 if let Some(session) = self.session.take() {
286 spawn!(async move {
287 let _ = session.log_out().await;
288 });
289 }
290 }
291
292 pub(super) fn set_domain(&self, domain: Option<OwnedServerName>) {
294 if *self.domain.borrow() == domain {
295 return;
296 }
297
298 self.domain.replace(domain);
299 self.obj().notify_domain_string();
300 }
301
302 fn domain_string(&self) -> Option<String> {
307 if self.autodiscovery.get() {
308 self.domain.borrow().clone().map(Into::into)
309 } else {
310 self.homeserver_url_string()
311 }
312 }
313
314 fn homeserver_url_string(&self) -> Option<String> {
316 self.homeserver_url
317 .borrow()
318 .as_ref()
319 .map(|url| url.as_ref().trim_end_matches('/').to_owned())
320 }
321
322 fn set_homeserver_url(&self, homeserver: Option<Url>) {
324 if *self.homeserver_url.borrow() == homeserver {
325 return;
326 }
327
328 self.homeserver_url.replace(homeserver);
329
330 let obj = self.obj();
331 obj.notify_homeserver_url_string();
332
333 if !self.autodiscovery.get() {
334 obj.notify_domain_string();
335 }
336 }
337
338 pub(super) fn set_login_types(&self, types: Vec<LoginType>) {
340 self.login_types.replace(types);
341 self.obj().emit_by_name::<()>("login-types-changed", &[]);
342 }
343
344 pub(super) fn login_types(&self) -> Vec<LoginType> {
346 self.login_types.borrow().clone()
347 }
348
349 async fn open_advanced_dialog(&self) {
351 let obj = self.obj();
352 let dialog = LoginAdvancedDialog::new();
353 obj.bind_property("autodiscovery", &dialog, "autodiscovery")
354 .sync_create()
355 .bidirectional()
356 .build();
357 dialog.run_future(&*obj).await;
358 }
359
360 pub(super) fn show_login_page(&self) {
362 let mut oidc_compatibility = false;
363 let mut supports_password = false;
364
365 for login_type in self.login_types.borrow().iter() {
366 match login_type {
367 LoginType::Sso(sso) if sso.delegated_oidc_compatibility => {
368 oidc_compatibility = true;
369 break;
371 }
372 LoginType::Password(_) => {
373 supports_password = true;
374 }
375 _ => {}
376 }
377 }
378
379 if oidc_compatibility || !supports_password {
380 self.show_in_browser_page(None, oidc_compatibility);
381 } else {
382 self.navigation.push_by_tag(LoginPage::Method.as_ref());
383 }
384 }
385
386 fn show_in_browser_page(&self, sso_idp_id: Option<String>, oidc_compatibility: bool) {
388 self.in_browser_page.set_sso_idp_id(sso_idp_id);
389 self.in_browser_page
390 .set_oidc_compatibility(oidc_compatibility);
391
392 self.navigation.push_by_tag(LoginPage::InBrowser.as_ref());
393 }
394
395 pub(super) async fn handle_login_response(&self, response: login::v3::Response) {
397 let client = self.client().await.expect("client was constructed");
398 let homeserver = client.homeserver();
401
402 match Session::create(homeserver, (&response).into()).await {
403 Ok(session) => {
404 self.init_session(session).await;
405 }
406 Err(error) => {
407 warn!("Could not create session: {error}");
408 let obj = self.obj();
409 toast!(obj, error.to_user_facing());
410
411 self.navigation.pop();
412 }
413 }
414 }
415
416 async fn init_session(&self, session: Session) {
418 let setup_view = SessionSetupView::new(&session);
419 setup_view.connect_completed(clone!(
420 #[weak(rename_to = imp)]
421 self,
422 move |_| {
423 imp.navigation.push_by_tag(LoginPage::Completed.as_ref());
424 }
425 ));
426 self.navigation.push(&setup_view);
427
428 self.drop_client();
429 self.session.replace(Some(session.clone()));
430
431 let settings = Application::default().settings();
433 if let Err(err) =
434 settings.set_string(SETTINGS_KEY_CURRENT_SESSION, session.session_id())
435 {
436 warn!("Could not save current session: {err}");
437 }
438
439 let session_info = session.info().clone();
440
441 if Secret::store_session(session_info).await.is_err() {
442 let obj = self.obj();
443 toast!(obj, gettext("Could not store session"));
444 }
445
446 session.prepare().await;
447 }
448
449 #[template_callback]
451 fn finish_login(&self) {
452 let Some(window) = self.obj().root().and_downcast::<Window>() else {
453 return;
454 };
455
456 if let Some(session) = self.session.take() {
457 window.add_session(session);
458 }
459
460 self.clean();
461 }
462
463 pub(super) fn clean(&self) {
465 self.homeserver_page.clean();
467 self.method_page.clean();
468
469 self.set_autodiscovery(true);
471 self.set_login_types(vec![]);
472 self.set_domain(None);
473 self.set_homeserver_url(None);
474 self.drop_client();
475 self.drop_session();
476
477 self.navigation.pop_to_tag(LoginPage::Greeter.as_ref());
479 self.unfreeze();
480 }
481
482 pub(super) fn freeze(&self) {
484 self.navigation.set_sensitive(false);
485 }
486
487 pub(super) fn unfreeze(&self) {
489 self.navigation.set_sensitive(true);
490 }
491 }
492}
493
494glib::wrapper! {
495 pub struct Login(ObjectSubclass<imp::Login>)
497 @extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
498}
499
500impl Login {
501 pub fn new() -> Self {
502 glib::Object::new()
503 }
504
505 fn set_client(&self, client: Option<Client>) {
507 self.imp().set_client(client);
508 }
509
510 async fn client(&self) -> Option<Client> {
512 self.imp().client().await
513 }
514
515 fn drop_client(&self) {
517 self.imp().drop_client();
518 }
519
520 fn set_domain(&self, domain: Option<OwnedServerName>) {
522 self.imp().set_domain(domain);
523 }
524
525 fn set_login_types(&self, types: Vec<LoginType>) {
527 self.imp().set_login_types(types);
528 }
529
530 fn login_types(&self) -> Vec<LoginType> {
532 self.imp().login_types()
533 }
534
535 async fn handle_login_response(&self, response: login::v3::Response) {
537 self.imp().handle_login_response(response).await;
538 }
539
540 fn show_login_page(&self) {
542 self.imp().show_login_page();
543 }
544
545 fn freeze(&self) {
547 self.imp().freeze();
548 }
549
550 fn unfreeze(&self) {
552 self.imp().unfreeze();
553 }
554
555 pub fn connect_login_types_changed<F: Fn(&Self) + 'static>(
557 &self,
558 f: F,
559 ) -> glib::SignalHandlerId {
560 self.connect_closure(
561 "login-types-changed",
562 true,
563 closure_local!(move |obj: Self| {
564 f(&obj);
565 }),
566 )
567 }
568}