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