fix: show oauth url and code properly on device auth popup

- Auth flow now waits for user's Enter before starting
- Start auth only when user presses Enter on DeviceAuth popup
- Proper error handling: missing GOOGLE_CLIENT_ID shows clear message
- Error messages displayed in popup with Retry option
- Popup shows instructions before auth, URL+code during auth
- Handle both verification_url and verification_uri field names from Google
- Check HTTP status code and show error_description on failures
- AuthError propagated to render function for display
- Popup border turns green when URL+code are ready
This commit is contained in:
Ruben Rosario
2026-06-20 19:56:41 +01:00
parent 320a9c2572
commit 985e8c9bc9
5 changed files with 138 additions and 61 deletions
+55 -39
View File
@@ -28,10 +28,18 @@ pub struct App {
#[allow(dead_code)]
pub api_client: Arc<ApiClient>,
pub needs_auth: bool,
pub auth_error: Option<String>,
auth_tx: std_mpsc::Sender<AuthEvent>,
auth_rx: std_mpsc::Receiver<AuthEvent>,
sync_tx: mpsc::Sender<SyncCommand>,
}
enum AuthEvent {
DeviceCode(String, String),
Complete,
Error(String),
}
#[allow(dead_code)]
pub enum SyncCommand {
TriggerSync,
@@ -39,42 +47,11 @@ pub enum SyncCommand {
Shutdown,
}
enum AuthEvent {
DeviceCode(String, String),
Complete,
Error(String),
}
impl App {
pub fn new(db: Arc<Db>, api_client: Arc<ApiClient>, sync_tx: mpsc::Sender<SyncCommand>) -> Self {
let has_token = api_client.token_file_exists();
let (auth_tx, auth_rx) = std_mpsc::channel();
if !has_token {
let api = api_client.clone();
let tx = auth_tx.clone();
std::thread::spawn(move || {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async move {
match api.authenticate().await {
Ok((url, code)) => {
let _ = tx.send(AuthEvent::DeviceCode(url, code));
loop {
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
if api.token_file_exists() {
let _ = tx.send(AuthEvent::Complete);
break;
}
}
}
Err(_) => {
let _ = tx.send(AuthEvent::Error("Auth failed".to_string()));
}
}
});
});
}
let show_popup = if has_token {
None
} else {
@@ -109,11 +86,45 @@ impl App {
db,
api_client,
needs_auth: !has_token,
auth_error: None,
auth_tx,
auth_rx,
sync_tx,
}
}
pub fn start_auth_process(&mut self) {
let api = self.api_client.clone();
let tx = self.auth_tx.clone();
self.auth_error = None;
std::thread::spawn(move || {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async move {
match api.authenticate().await {
Ok((url, code)) => {
let _ = tx.send(AuthEvent::DeviceCode(url, code));
loop {
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
if api.token_file_exists() {
let _ = tx.send(AuthEvent::Complete);
break;
}
}
}
Err(e) => {
let msg = match &e {
crate::infrastructure::api::ApiError::Network(s) => format!("Network: {}", s),
crate::infrastructure::api::ApiError::Auth(s) => format!("Auth: {}", s),
crate::infrastructure::api::ApiError::Api(s) => format!("API: {}", s),
};
let _ = tx.send(AuthEvent::Error(msg));
}
}
});
});
}
pub fn poll_auth(&mut self) {
if !self.needs_auth {
return;
@@ -121,20 +132,20 @@ impl App {
while let Ok(event) = self.auth_rx.try_recv() {
match event {
AuthEvent::DeviceCode(url, code) => {
self.show_popup = Some(Popup::DeviceAuth {
url,
code,
});
self.show_popup = Some(Popup::DeviceAuth { url, code });
}
AuthEvent::Complete => {
self.needs_auth = false;
self.auth_error = None;
self.show_popup = None;
let _ = self.sync_tx.try_send(SyncCommand::InitialSync);
}
AuthEvent::Error(msg) => {
self.needs_auth = false;
self.show_popup = None;
eprintln!("Auth error: {}", msg);
self.auth_error = Some(msg.clone());
self.show_popup = Some(Popup::DeviceAuth {
url: String::new(),
code: String::new(),
});
}
}
}
@@ -255,7 +266,12 @@ impl App {
fn handle_popup_key(&mut self, key: KeyEvent, popup: &Popup) {
match popup {
Popup::DeviceAuth { .. } => match key.code {
Popup::DeviceAuth { url, code: _ } => match key.code {
KeyCode::Enter => {
if url.is_empty() && self.auth_error.is_none() {
self.start_auth_process();
}
}
KeyCode::Esc => {
self.show_popup = None;
}