1use super::button::Button;
12use super::filler::Filler;
13use super::label::Label;
14use super::{Align, Bus, Event, Hub, Id, RenderQueue, View, ViewId, ID_FEEDER};
15use crate::color::WHITE;
16use crate::context::Context;
17use crate::device::CURRENT_DEVICE;
18use crate::font::{font_from_style, Fonts, NORMAL_STYLE};
19use crate::framebuffer::Framebuffer;
20use crate::geom::Rectangle;
21use crate::gesture::GestureEvent;
22use crate::github::{GithubClient, GithubError, TokenPollResult};
23use crate::unit::scale_by_dpi;
24use crate::view::github::GithubEvent;
25use crate::view::ota::OtaViewId;
26use std::sync::atomic::{AtomicBool, Ordering};
27use std::sync::Arc;
28use std::thread;
29use std::time::Duration;
30
31pub struct DeviceAuthView {
41 id: Id,
42 rect: Rectangle,
43 children: Vec<Box<dyn View>>,
44 view_id: ViewId,
45 cancelled: Arc<AtomicBool>,
47}
48
49impl DeviceAuthView {
50 #[cfg_attr(feature = "otel", tracing::instrument(skip_all))]
65 pub fn new(hub: &Hub, context: &mut Context) -> Self {
66 let id = ID_FEEDER.next();
67 let view_id = ViewId::Ota(OtaViewId::DeviceAuth);
68 let (width, height) = CURRENT_DEVICE.dims;
69 let full_rect = rect![0, 0, width as i32, height as i32];
70 let cancelled = Arc::new(AtomicBool::new(false));
71
72 let mut children: Vec<Box<dyn View>> = Vec::new();
73 children.push(Box::new(Filler::new(full_rect, WHITE)));
74
75 let (url_text, code_text) = match Self::initiate_and_spawn(hub, Arc::clone(&cancelled)) {
76 Ok((url, code)) => (format!("Go to: {}", url), format!("Enter code: {}", code)),
77 Err(e) => {
78 tracing::error!(error = %e, "Device flow initiation failed");
79 hub.send(Event::Github(GithubEvent::DeviceAuthError(e.to_string())))
80 .ok();
81 (
82 "GitHub auth failed".to_owned(),
83 "Check logs for details".to_owned(),
84 )
85 }
86 };
87
88 let dpi = CURRENT_DEVICE.dpi;
89 let font = font_from_style(&mut context.fonts, &NORMAL_STYLE, dpi);
90 let x_height = font.x_heights.0 as i32;
91 let padding = font.em() as i32;
92
93 let center_y = height as i32 / 2;
94 let line_height = 3 * x_height;
95
96 let url_rect = rect![
97 padding,
98 center_y - line_height - padding / 2,
99 width as i32 - padding,
100 center_y - padding / 2
101 ];
102 children.push(Box::new(Label::new(url_rect, url_text, Align::Center)));
103
104 let code_rect = rect![
105 padding,
106 center_y + padding / 2,
107 width as i32 - padding,
108 center_y + line_height + padding / 2
109 ];
110 children.push(Box::new(Label::new(code_rect, code_text, Align::Center)));
111
112 let button_width = scale_by_dpi(200.0, dpi) as i32;
113 let button_height = scale_by_dpi(40.0, dpi) as i32;
114 let button_x = (width as i32 - button_width) / 2;
115 let button_y = center_y + line_height + 2 * padding;
116 let cancel_rect = rect![
117 button_x,
118 button_y,
119 button_x + button_width,
120 button_y + button_height
121 ];
122 children.push(Box::new(Button::new(
123 cancel_rect,
124 Event::Close(view_id),
125 "Cancel".to_owned(),
126 )));
127
128 Self {
129 id,
130 rect: full_rect,
131 children,
132 view_id,
133 cancelled,
134 }
135 }
136
137 fn initiate_and_spawn(
143 hub: &Hub,
144 cancelled: Arc<AtomicBool>,
145 ) -> Result<(String, String), crate::github::GithubError> {
146 let client = GithubClient::new(None)?;
147 let device_code_response = client.initiate_device_flow().map_err(GithubError::Api)?;
148
149 let verification_uri = device_code_response.verification_uri.clone();
150 let user_code = device_code_response.user_code.clone();
151 let device_code = device_code_response.device_code.clone();
152 let interval_secs = device_code_response.interval;
153
154 tracing::info!(
155 user_code = %user_code,
156 verification_uri = %verification_uri,
157 "Device flow initiated"
158 );
159
160 let hub2 = hub.clone();
161 let parent_span = tracing::Span::current();
162
163 thread::spawn(move || {
164 let _span = tracing::info_span!(parent: &parent_span, "device_flow_poll").entered();
165
166 let poll_client = match GithubClient::new(None) {
167 Ok(c) => c,
168 Err(e) => {
169 tracing::error!(error = %e, "Failed to create poll client");
170 hub2.send(Event::Github(GithubEvent::DeviceAuthError(e.to_string())))
171 .ok();
172 return;
173 }
174 };
175
176 let mut interval = Duration::from_secs(interval_secs);
177
178 loop {
179 thread::sleep(interval);
180
181 if cancelled.load(Ordering::Relaxed) {
182 tracing::info!("Device flow polling cancelled");
183 return;
184 }
185
186 match poll_client.poll_device_token(&device_code) {
187 Ok(TokenPollResult::Pending) => {
188 tracing::debug!("Authorization pending, continuing to poll");
189 }
190 Ok(TokenPollResult::SlowDown) => {
191 interval += Duration::from_secs(5);
192 tracing::debug!(interval_secs = interval.as_secs(), "Slowing down poll");
193 }
194 Ok(TokenPollResult::Complete(token)) => {
195 tracing::info!("Device flow authorization complete");
196 hub2.send(Event::Github(GithubEvent::DeviceAuthComplete(token)))
197 .ok();
198 return;
199 }
200 Ok(TokenPollResult::Expired) => {
201 tracing::warn!("Device flow code expired");
202 hub2.send(Event::Github(GithubEvent::DeviceAuthExpired))
203 .ok();
204 return;
205 }
206 Ok(TokenPollResult::Cancelled) => {
207 tracing::info!("Device flow cancelled by user on GitHub");
208 hub2.send(Event::Github(GithubEvent::DeviceAuthError(
209 "Authorization cancelled".to_owned(),
210 )))
211 .ok();
212 return;
213 }
214 Err(e) => {
215 tracing::error!(error = %e, "Device flow poll error");
216 hub2.send(Event::Github(GithubEvent::DeviceAuthError(e)))
217 .ok();
218 return;
219 }
220 }
221 }
222 });
223
224 Ok((verification_uri, user_code))
225 }
226
227 fn cancel_polling(&self) {
229 self.cancelled.store(true, Ordering::Relaxed);
230 }
231}
232
233impl View for DeviceAuthView {
234 #[cfg_attr(
240 feature = "otel",
241 tracing::instrument(
242 skip(self, _hub, bus, _rq, _context),
243 fields(event = ?evt),
244 ret(level = tracing::Level::TRACE)
245 )
246 )]
247 fn handle_event(
248 &mut self,
249 evt: &Event,
250 _hub: &Hub,
251 bus: &mut Bus,
252 _rq: &mut RenderQueue,
253 _context: &mut Context,
254 ) -> bool {
255 match evt {
256 Event::Close(id) if *id == self.view_id => {
257 self.cancel_polling();
258 bus.push_back(Event::Close(ViewId::Ota(OtaViewId::Main)));
259 true
260 }
261 Event::Gesture(GestureEvent::Tap(center)) if self.rect.includes(*center) => true,
262 _ => false,
263 }
264 }
265
266 #[cfg_attr(
267 feature = "otel",
268 tracing::instrument(skip(self, _fb, _fonts, _rect), fields(rect = ?_rect))
269 )]
270 fn render(&self, _fb: &mut dyn Framebuffer, _rect: Rectangle, _fonts: &mut Fonts) {}
271
272 fn rect(&self) -> &Rectangle {
273 &self.rect
274 }
275
276 fn rect_mut(&mut self) -> &mut Rectangle {
277 &mut self.rect
278 }
279
280 fn children(&self) -> &Vec<Box<dyn View>> {
281 &self.children
282 }
283
284 fn children_mut(&mut self) -> &mut Vec<Box<dyn View>> {
285 &mut self.children
286 }
287
288 fn id(&self) -> Id {
289 self.id
290 }
291
292 fn view_id(&self) -> Option<ViewId> {
293 Some(self.view_id)
294 }
295
296 fn resize(
297 &mut self,
298 _rect: Rectangle,
299 _hub: &Hub,
300 _rq: &mut RenderQueue,
301 _context: &mut Context,
302 ) {
303 }
304}