fractal/components/crypto/
identity_setup_view.rs1use adw::{prelude::*, subclass::prelude::*};
2use gettextrs::gettext;
3use gtk::{
4 glib,
5 glib::{clone, closure_local},
6};
7use tracing::{debug, error};
8
9use super::{CryptoRecoverySetupInitialPage, CryptoRecoverySetupView};
10use crate::{
11 components::{AuthDialog, AuthError, LoadingButton},
12 identity_verification_view::IdentityVerificationView,
13 session::{
14 CryptoIdentityState, IdentityVerification, RecoveryState, Session, SessionVerificationState,
15 },
16 spawn, toast,
17 utils::BoundObjectWeakRef,
18};
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22enum CryptoIdentitySetupPage {
23 ChooseMethod,
25 Verify,
27 Bootstrap,
29 Reset,
31 Recovery,
33}
34
35impl CryptoIdentitySetupPage {
36 const fn tag(self) -> &'static str {
38 match self {
39 Self::ChooseMethod => "choose-method",
40 Self::Verify => "verify",
41 Self::Bootstrap => "bootstrap",
42 Self::Reset => "reset",
43 Self::Recovery => "recovery",
44 }
45 }
46
47 fn from_tag(tag: &str) -> Self {
51 match tag {
52 "choose-method" => Self::ChooseMethod,
53 "verify" => Self::Verify,
54 "bootstrap" => Self::Bootstrap,
55 "reset" => Self::Reset,
56 "recovery" => Self::Recovery,
57 _ => panic!("Unknown CryptoIdentitySetupPage: {tag}"),
58 }
59 }
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq, glib::Enum)]
64#[enum_type(name = "CryptoIdentitySetupNextStep")]
65pub enum CryptoIdentitySetupNextStep {
66 None,
68 EnableRecovery,
70 CompleteRecovery,
72}
73
74mod imp {
75 use std::{
76 cell::{OnceCell, RefCell},
77 sync::LazyLock,
78 };
79
80 use glib::subclass::{InitializingObject, Signal};
81
82 use super::*;
83
84 #[derive(Debug, Default, gtk::CompositeTemplate, glib::Properties)]
85 #[template(resource = "/org/gnome/Fractal/ui/components/crypto/identity_setup_view.ui")]
86 #[properties(wrapper_type = super::CryptoIdentitySetupView)]
87 pub struct CryptoIdentitySetupView {
88 #[template_child]
89 navigation: TemplateChild<adw::NavigationView>,
90 #[template_child]
91 send_request_btn: TemplateChild<LoadingButton>,
92 #[template_child]
93 use_recovery_btn: TemplateChild<gtk::Button>,
94 #[template_child]
95 verification_page: TemplateChild<IdentityVerificationView>,
96 #[template_child]
97 bootstrap_btn: TemplateChild<LoadingButton>,
98 #[template_child]
99 reset_btn: TemplateChild<gtk::Button>,
100 #[property(get, set = Self::set_session, construct_only)]
102 session: glib::WeakRef<Session>,
103 #[property(get)]
105 verification: BoundObjectWeakRef<IdentityVerification>,
106 verification_list_handler: RefCell<Option<glib::SignalHandlerId>>,
107 recovery_view: OnceCell<CryptoRecoverySetupView>,
109 }
110
111 #[glib::object_subclass]
112 impl ObjectSubclass for CryptoIdentitySetupView {
113 const NAME: &'static str = "CryptoIdentitySetupView";
114 type Type = super::CryptoIdentitySetupView;
115 type ParentType = adw::Bin;
116
117 fn class_init(klass: &mut Self::Class) {
118 Self::bind_template(klass);
119 Self::bind_template_callbacks(klass);
120
121 klass.set_css_name("setup-view");
122 }
123
124 fn instance_init(obj: &InitializingObject<Self>) {
125 obj.init_template();
126 }
127 }
128
129 #[glib::derived_properties]
130 impl ObjectImpl for CryptoIdentitySetupView {
131 fn signals() -> &'static [Signal] {
132 static SIGNALS: LazyLock<Vec<Signal>> = LazyLock::new(|| {
133 vec![
134 Signal::builder("completed")
136 .param_types([CryptoIdentitySetupNextStep::static_type()])
137 .build(),
138 ]
139 });
140 SIGNALS.as_ref()
141 }
142
143 fn dispose(&self) {
144 if let Some(verification) = self.verification.obj() {
145 spawn!(clone!(
146 #[strong]
147 verification,
148 async move {
149 let _ = verification.cancel().await;
150 }
151 ));
152 }
153
154 if let Some(session) = self.session.upgrade()
155 && let Some(handler) = self.verification_list_handler.take()
156 {
157 session.verification_list().disconnect(handler);
158 }
159 }
160 }
161
162 impl WidgetImpl for CryptoIdentitySetupView {
163 fn grab_focus(&self) -> bool {
164 match self.visible_page() {
165 CryptoIdentitySetupPage::ChooseMethod => self.send_request_btn.grab_focus(),
166 CryptoIdentitySetupPage::Verify => self.verification_page.grab_focus(),
167 CryptoIdentitySetupPage::Bootstrap => self.bootstrap_btn.grab_focus(),
168 CryptoIdentitySetupPage::Reset => self.reset_btn.grab_focus(),
169 CryptoIdentitySetupPage::Recovery => self.recovery_view().grab_focus(),
170 }
171 }
172 }
173
174 impl BinImpl for CryptoIdentitySetupView {}
175
176 #[gtk::template_callbacks]
177 impl CryptoIdentitySetupView {
178 fn visible_page(&self) -> CryptoIdentitySetupPage {
180 CryptoIdentitySetupPage::from_tag(
181 &self
182 .navigation
183 .visible_page()
184 .expect(
185 "CryptoIdentitySetupView navigation view should always have a visible page",
186 )
187 .tag()
188 .expect("CryptoIdentitySetupView navigation page should always have a tag"),
189 )
190 }
191
192 fn recovery_view(&self) -> &CryptoRecoverySetupView {
194 self.recovery_view.get_or_init(|| {
195 let session = self
196 .session
197 .upgrade()
198 .expect("Session should still have a strong reference");
199 let recovery_view = CryptoRecoverySetupView::new(&session);
200
201 recovery_view.connect_completed(clone!(
202 #[weak(rename_to = imp)]
203 self,
204 move |_| {
205 imp.emit_completed(CryptoIdentitySetupNextStep::None);
206 }
207 ));
208
209 recovery_view
210 })
211 }
212
213 fn set_session(&self, session: &Session) {
215 self.session.set(Some(session));
216
217 let verification_list = session.verification_list();
219 let verification_list_handler = verification_list.connect_items_changed(clone!(
220 #[weak(rename_to = imp)]
221 self,
222 move |verification_list, _, _, _| {
223 if imp.verification.obj().is_some() {
224 return;
226 }
227
228 if let Some(verification) = verification_list.ongoing_session_verification() {
229 imp.set_verification(Some(verification));
230 }
231 }
232 ));
233 self.verification_list_handler
234 .replace(Some(verification_list_handler));
235
236 self.init();
237 }
238
239 fn init(&self) {
241 let Some(session) = self.session.upgrade() else {
242 return;
243 };
244 let security = session.security();
245
246 let verification_state = security.verification_state();
248 if verification_state == SessionVerificationState::Verified {
249 self.navigation
250 .replace_with_tags(&[CryptoIdentitySetupPage::Reset.tag()]);
251 return;
252 }
253
254 let crypto_identity_state = security.crypto_identity_state();
255 let recovery_state = security.recovery_state();
256
257 if crypto_identity_state == CryptoIdentityState::Missing {
259 self.navigation
260 .replace_with_tags(&[CryptoIdentitySetupPage::Bootstrap.tag()]);
261 return;
262 }
263
264 if crypto_identity_state == CryptoIdentityState::LastManStanding {
266 let recovery_view = if recovery_state == RecoveryState::Disabled {
267 self.recovery_page(CryptoRecoverySetupInitialPage::Reset)
269 } else {
270 self.recovery_page(CryptoRecoverySetupInitialPage::Recover)
272 };
273
274 self.navigation.replace(&[recovery_view]);
275
276 return;
277 }
278
279 if let Some(verification) = session.verification_list().ongoing_session_verification() {
280 self.set_verification(Some(verification));
281 }
282
283 self.update_choose_methods();
285 }
286
287 fn update_choose_methods(&self) {
289 let Some(session) = self.session.upgrade() else {
290 return;
291 };
292
293 let can_recover = session.security().recovery_state() != RecoveryState::Disabled;
294 self.use_recovery_btn.set_visible(can_recover);
295 }
296
297 fn set_verification(&self, verification: Option<IdentityVerification>) {
301 let prev_verification = self.verification.obj();
302
303 if prev_verification == verification {
304 return;
305 }
306
307 if let Some(verification) = prev_verification {
308 if !verification.is_finished() {
309 spawn!(clone!(
310 #[strong]
311 verification,
312 async move {
313 let _ = verification.cancel().await;
314 }
315 ));
316 }
317
318 self.verification.disconnect_signals();
319 }
320
321 if let Some(verification) = &verification {
322 let replaced_handler = verification.connect_replaced(clone!(
323 #[weak(rename_to = imp)]
324 self,
325 move |_, new_verification| {
326 imp.set_verification(Some(new_verification.clone()));
327 }
328 ));
329 let done_handler = verification.connect_done(clone!(
330 #[weak(rename_to = imp)]
331 self,
332 #[upgrade_or]
333 glib::Propagation::Stop,
334 move |verification| {
335 imp.emit_completed(CryptoIdentitySetupNextStep::EnableRecovery);
336 imp.set_verification(None);
337 verification.remove_from_list();
338
339 glib::Propagation::Stop
340 }
341 ));
342 let remove_handler = verification.connect_dismiss(clone!(
343 #[weak(rename_to = imp)]
344 self,
345 move |_| {
346 imp.navigation.pop();
347 imp.set_verification(None);
348 }
349 ));
350
351 self.verification.set(
352 verification,
353 vec![replaced_handler, done_handler, remove_handler],
354 );
355 }
356
357 let has_verification = verification.is_some();
358 self.verification_page.set_verification(verification);
359
360 if has_verification
361 && self
362 .navigation
363 .visible_page()
364 .and_then(|p| p.tag())
365 .is_none_or(|t| t != CryptoIdentitySetupPage::Verify.tag())
366 {
367 self.navigation
368 .push_by_tag(CryptoIdentitySetupPage::Verify.tag());
369 }
370
371 self.obj().notify_verification();
372 }
373
374 fn recovery_page(
376 &self,
377 initial_page: CryptoRecoverySetupInitialPage,
378 ) -> adw::NavigationPage {
379 let recovery_view = self.recovery_view();
380 recovery_view.set_initial_page(initial_page);
381
382 let page = adw::NavigationPage::builder()
383 .tag(CryptoIdentitySetupPage::Recovery.tag())
384 .child(recovery_view)
385 .build();
386 page.connect_shown(clone!(
387 #[weak]
388 recovery_view,
389 move |_| {
390 recovery_view.grab_focus();
391 }
392 ));
393
394 page
395 }
396
397 #[template_callback]
399 fn grab_focus(&self) {
400 <Self as WidgetImpl>::grab_focus(self);
401 }
402
403 #[template_callback]
405 async fn send_request(&self) {
406 let Some(session) = self.session.upgrade() else {
407 return;
408 };
409
410 self.send_request_btn.set_is_loading(true);
411
412 if let Err(()) = session.verification_list().create(None).await {
413 toast!(
414 self.obj(),
415 gettext("Could not send a new verification request")
416 );
417 }
418
419 self.send_request_btn.set_is_loading(false);
422 }
423
424 #[template_callback]
426 fn reset(&self) {
427 let Some(session) = self.session.upgrade() else {
428 return;
429 };
430
431 let can_recover = session.security().recovery_state() != RecoveryState::Disabled;
432
433 if can_recover {
434 let recovery_view = self.recovery_page(CryptoRecoverySetupInitialPage::Reset);
435 self.navigation.push(&recovery_view);
436 } else {
437 self.navigation
438 .push_by_tag(CryptoIdentitySetupPage::Bootstrap.tag());
439 }
440 }
441
442 #[template_callback]
444 async fn bootstrap_cross_signing(&self) {
445 let Some(session) = self.session.upgrade() else {
446 return;
447 };
448
449 self.bootstrap_btn.set_is_loading(true);
450
451 let obj = self.obj();
452 let dialog = AuthDialog::new(&session);
453
454 let result = dialog
455 .authenticate(&*obj, move |client, auth| async move {
456 client.encryption().bootstrap_cross_signing(auth).await
457 })
458 .await;
459
460 match result {
461 Ok(()) => self.emit_completed(CryptoIdentitySetupNextStep::CompleteRecovery),
462 Err(AuthError::UserCancelled) => {
463 debug!("User cancelled authentication for cross-signing bootstrap");
464 }
465 Err(error) => {
466 error!("Could not bootstrap cross-signing: {error:?}");
467 toast!(obj, gettext("Could not create the crypto identity"));
468 }
469 }
470
471 self.bootstrap_btn.set_is_loading(false);
472 }
473
474 #[template_callback]
476 fn recover(&self) {
477 let recovery_view = self.recovery_page(CryptoRecoverySetupInitialPage::Recover);
478 self.navigation.push(&recovery_view);
479 }
480
481 #[template_callback]
483 fn emit_completed(&self, next: CryptoIdentitySetupNextStep) {
484 self.obj().emit_by_name::<()>("completed", &[&next]);
485 }
486 }
487}
488
489glib::wrapper! {
490 pub struct CryptoIdentitySetupView(ObjectSubclass<imp::CryptoIdentitySetupView>)
492 @extends gtk::Widget, adw::Bin,
493 @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
494}
495
496impl CryptoIdentitySetupView {
497 pub fn new(session: &Session) -> Self {
498 glib::Object::builder().property("session", session).build()
499 }
500
501 pub fn connect_completed<F: Fn(&Self, CryptoIdentitySetupNextStep) + 'static>(
503 &self,
504 f: F,
505 ) -> glib::SignalHandlerId {
506 self.connect_closure(
507 "completed",
508 true,
509 closure_local!(move |obj: Self, next: CryptoIdentitySetupNextStep| {
510 f(&obj, next);
511 }),
512 )
513 }
514}