Skip to content

Commit 1de9b67

Browse files
committed
feat: improve OAuth flow with browser failure handling and UI cleanup
- Refactor OAuth URL sending into perform_oauth_flow for cleaner architecture - Add browser failure detection and TUI notifications - Remove emojis from auth popup, improve URL display alignment - Add detailed logging for Drive folder operations - Update demo screenshot in README
1 parent fd80ca2 commit 1de9b67

File tree

12 files changed

+84
-28
lines changed

12 files changed

+84
-28
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ IMPLEMENTATION_PLAN.md
1717
/client/dist
1818
/src/data/logs/
1919
commands.md
20+
src/plans
2021

2122
init.sql/
2223

CHANGELOG.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.1.23] - 2025-12-18
9+
10+
### Changed
11+
- **OAuth Flow Refactoring**: Consolidated auth URL sending into `perform_oauth_flow` function for cleaner code architecture
12+
- **Browser Failure Handling**: OAuth flow now detects when browser fails to open and sends notification to TUI
13+
- **Auth Popup UI**: Removed emojis from authentication popup for cleaner display, changed text alignment to left for better URL readability
14+
- **Demo Screenshot**: Updated README to use new static demo.png instead of animated GIF
15+
16+
### Added
17+
- **Browser Failure Notifications**: New `__GMAIL_BROWSER_FAILED__` and `__DRIVE_BROWSER_FAILED__` message handlers in TUI to inform users when automatic browser opening fails
18+
- **Enhanced Drive Logging**: Added detailed logging for folder search and creation operations in drive/folder.rs
19+
20+
### Fixed
21+
- **Auth URL Display**: URLs now display with left alignment and no trimming for easier copying
22+
- **OAuth URL Timing**: Auth URLs are now sent to TUI immediately from the OAuth flow rather than after authorization completes
23+
24+
### Technical Improvements
25+
- **Code Cleanup**: Refactored `authorize_gmail` and `authorize_drive` to accept optional channel parameter for URL sending
26+
- **Unified OAuth Handling**: Both Gmail and Drive auth now use consistent pattern with service-specific prefixes (GMAIL_, DRIVE_)
27+
828
## [0.1.22] - 2025-11-07
929

1030
### Changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "invoice-pilot"
3-
version = "0.1.22"
3+
version = "0.1.23"
44
edition = "2024"
55

66
[dependencies]

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Invoice Pilot is a fully automated invoice and bank statement management tool bu
1212

1313
## Demo
1414

15-
![Demo](src/screenshots/invoice-pilot-demo.gif)
15+
![Demo](src/screenshots/demo.png)
1616

1717
## Table of Contents
1818

src/auth/drive_auth.rs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ pub async fn get_drive_token(client_id: String, client_secret: String) -> Result
4747
}
4848

4949
// Need new authorization
50-
let (token, _) = authorize_drive(client_id, client_secret).await?;
50+
let (token, _) = authorize_drive(client_id, client_secret, None).await?;
5151
Ok(token)
5252
}
5353

@@ -92,19 +92,18 @@ pub async fn get_drive_token_with_url(client_id: String, client_secret: String,
9292
}
9393
}
9494

95-
// Need new authorization - send URL first, then proceed
96-
let (token, auth_url) = authorize_drive(client_id, client_secret).await?;
97-
// Send the auth URL to the TUI
98-
let _ = tx.send(format!("__DRIVE_AUTH_URL__:{}", auth_url));
95+
// Need new authorization - URL will be sent via channel from perform_oauth_flow
96+
let (token, _auth_url) = authorize_drive(client_id, client_secret, Some(tx)).await?;
9997
Ok(token)
10098
}
10199

102100
/// Perform full Drive authorization flow
103-
async fn authorize_drive(client_id: String, client_secret: String) -> Result<(String, String)> {
101+
async fn authorize_drive(client_id: String, client_secret: String, tx: Option<tokio::sync::mpsc::UnboundedSender<String>>) -> Result<(String, String)> {
104102
let client = create_oauth_client(client_id, client_secret)?;
105103
let scopes = vec![DRIVE_SCOPE.to_string()];
106104

107-
let (token, auth_url) = perform_oauth_flow(&client, scopes).await?;
105+
let sender_with_prefix = tx.map(|sender| (sender, "DRIVE_"));
106+
let (token, auth_url) = perform_oauth_flow(&client, scopes, sender_with_prefix).await?;
108107

109108
let expires_at = token.expires_in()
110109
.map(|d| chrono::Utc::now().timestamp() + d.as_secs() as i64);

src/auth/gmail_auth.rs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ pub async fn get_gmail_token(client_id: String, client_secret: String) -> Result
4747
}
4848

4949
// Need new authorization
50-
let (token, _) = authorize_gmail(client_id, client_secret).await?;
50+
let (token, _) = authorize_gmail(client_id, client_secret, None).await?;
5151
Ok(token)
5252
}
5353

@@ -92,19 +92,18 @@ pub async fn get_gmail_token_with_url(client_id: String, client_secret: String,
9292
}
9393
}
9494

95-
// Need new authorization - send URL first, then proceed
96-
let (token, auth_url) = authorize_gmail(client_id, client_secret).await?;
97-
// Send the auth URL to the TUI
98-
let _ = tx.send(format!("__GMAIL_AUTH_URL__:{}", auth_url));
95+
// Need new authorization - URL will be sent via channel from perform_oauth_flow
96+
let (token, _auth_url) = authorize_gmail(client_id, client_secret, Some(tx)).await?;
9997
Ok(token)
10098
}
10199

102100
/// Perform full Gmail authorization flow
103-
async fn authorize_gmail(client_id: String, client_secret: String) -> Result<(String, String)> {
101+
async fn authorize_gmail(client_id: String, client_secret: String, tx: Option<tokio::sync::mpsc::UnboundedSender<String>>) -> Result<(String, String)> {
104102
let client = create_oauth_client(client_id, client_secret)?;
105103
let scopes = vec![GMAIL_SCOPE.to_string()];
106104

107-
let (token, auth_url) = perform_oauth_flow(&client, scopes).await?;
105+
let sender_with_prefix = tx.map(|sender| (sender, "GMAIL_"));
106+
let (token, auth_url) = perform_oauth_flow(&client, scopes, sender_with_prefix).await?;
108107

109108
let expires_at = token.expires_in()
110109
.map(|d| chrono::Utc::now().timestamp() + d.as_secs() as i64);

src/auth/oauth.rs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ pub fn create_oauth_client(
9292
pub async fn perform_oauth_flow(
9393
client: &BasicClient,
9494
scopes: Vec<String>,
95+
url_sender: Option<(tokio::sync::mpsc::UnboundedSender<String>, &str)>,
9596
) -> Result<(StandardTokenResponse<oauth2::EmptyExtraTokenFields, BasicTokenType>, String)> {
9697
let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
9798

@@ -110,8 +111,23 @@ pub async fn perform_oauth_flow(
110111
let auth_url_str = auth_url.to_string();
111112

112113
// Try to open the URL in the default browser
113-
if let Err(e) = webbrowser::open(&auth_url_str) {
114-
warn!("Failed to open browser automatically: {}. Please manually open: {}", e, auth_url_str);
114+
let browser_opened = webbrowser::open(&auth_url_str).is_ok();
115+
116+
if !browser_opened {
117+
warn!("Failed to open browser automatically. Please manually open: {}", auth_url_str);
118+
}
119+
120+
info!("Authorization URL ready: {}", auth_url_str);
121+
122+
// Send URL and browser status to TUI if sender is provided
123+
if let Some((sender, prefix)) = url_sender {
124+
// Always send the URL first so user can manually open it
125+
let _ = sender.send(format!("__{}AUTH_URL__:{}", prefix, auth_url_str));
126+
127+
// If browser didn't open, send a notification
128+
if !browser_opened {
129+
let _ = sender.send(format!("__{}BROWSER_FAILED__:Failed to open browser automatically. Please copy and open the URL above manually.", prefix));
130+
}
115131
}
116132

117133
// Start local server to receive callback

src/drive/folder.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,16 @@ async fn find_or_create_single_folder(
2929
folder_name: &str,
3030
parent_id: &str,
3131
) -> Result<String> {
32+
log::info!("Looking for folder '{}' in parent '{}'", folder_name, parent_id);
33+
3234
// Try to find existing folder
3335
if let Some(folder_id) = find_folder(client, folder_name, parent_id).await? {
36+
log::info!("Found existing folder '{}' with ID: {}", folder_name, folder_id);
3437
return Ok(folder_id);
3538
}
3639

3740
// Create new folder
41+
log::info!("Folder '{}' not found, creating new one", folder_name);
3842
create_folder(client, folder_name, parent_id).await
3943
}
4044

@@ -51,6 +55,8 @@ async fn find_folder(
5155
FOLDER_MIME_TYPE
5256
);
5357

58+
log::debug!("Drive search query: {}", query);
59+
5460
let url = format!("{}/files", DRIVE_API_BASE);
5561

5662
let response = client.client()
@@ -70,7 +76,15 @@ async fn find_folder(
7076
let result: FileListResponse = response.json().await
7177
.context("Failed to parse folder search response")?;
7278

73-
Ok(result.files.and_then(|files| files.first().map(|f| f.id.clone())))
79+
let folder_id = result.files.and_then(|files| {
80+
log::debug!("Found {} folders matching search", files.len());
81+
files.first().map(|f| {
82+
log::debug!("Using folder: name='{}', id='{}'", f.name, f.id);
83+
f.id.clone()
84+
})
85+
});
86+
87+
Ok(folder_id)
7488
}
7589

7690
/// Create a new folder

src/interfaces/tui.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,12 @@ async fn run_app<B: Backend>(
137137
} else if message.starts_with("__DRIVE_AUTH_URL__:") {
138138
let url = message.strip_prefix("__DRIVE_AUTH_URL__:").unwrap_or("");
139139
app.auth_url = Some(url.to_string());
140+
} else if message.starts_with("__GMAIL_BROWSER_FAILED__:") {
141+
let error = message.strip_prefix("__GMAIL_BROWSER_FAILED__:").unwrap_or("Browser failed to open");
142+
app.add_progress_message(format!("Gmail Auth: {}", error));
143+
} else if message.starts_with("__DRIVE_BROWSER_FAILED__:") {
144+
let error = message.strip_prefix("__DRIVE_BROWSER_FAILED__:").unwrap_or("Browser failed to open");
145+
app.add_progress_message(format!("Drive Auth: {}", error));
140146
} else if message.starts_with("__RESULTS__:") {
141147
// Parse results: processed=5,uploaded=4,failed=1,month=October,folder=Invoices/October
142148
let results_str = message.strip_prefix("__RESULTS__:").unwrap_or("");

src/interfaces/ui.rs

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -749,36 +749,37 @@ fn draw_specific_auth_url_popup(frame: &mut Frame, app: &mut App, service_name:
749749

750750
// Title
751751
let title = if app.auth_popup_success {
752-
Paragraph::new(format!("{} Authenticated", service_name))
752+
Paragraph::new(format!("{} Authenticated", service_name))
753753
.style(Style::default().fg(Color::Green).add_modifier(Modifier::BOLD))
754754
.alignment(Alignment::Center)
755755
} else {
756-
Paragraph::new(format!("🔐 Authenticate {}", service_name))
756+
Paragraph::new(format!("Authenticate {}", service_name))
757757
.style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD))
758758
.alignment(Alignment::Center)
759759
};
760760
frame.render_widget(title, chunks[0]);
761761

762762
// Content display
763763
let content_text = if app.auth_popup_success {
764-
format!("🎉 {} authentication completed successfully!\n\nYour tokens are cached and ready to use.", service_name)
764+
format!("{} authentication completed successfully!\n\nYour tokens are cached and ready to use.", service_name)
765765
} else if let Some(url) = &app.auth_url {
766-
format!("🌐 {} Authorization URL:\n\n{}", service_name, url)
766+
format!("{} Authorization URL:\n\n{}", service_name, url)
767767
} else {
768-
format!("🔄 Preparing {} authorization URL...\n\nPlease wait while we set up the OAuth flow.", service_name)
768+
format!("Preparing {} authorization URL...\n\nPlease wait while we set up the OAuth flow.", service_name)
769769
};
770770

771771
let content_display = Paragraph::new(content_text)
772772
.style(Style::default().fg(Color::White))
773-
.alignment(Alignment::Center)
774-
.wrap(Wrap { trim: true });
773+
.alignment(Alignment::Left)
774+
.wrap(Wrap { trim: false })
775+
.scroll((0, 0));
775776
frame.render_widget(content_display, chunks[1]);
776777

777778
// Instructions
778779
let instructions = if app.auth_popup_success {
779-
format!("📋 Your {} authentication is active.\nYou can now close this popup or clear tokens if needed.", service_name)
780+
format!("Your {} authentication is active.\nYou can now close this popup or clear tokens if needed.", service_name)
780781
} else {
781-
format!("📋 INSTRUCTIONS:\n Copy the URL above\n Open it in your web browser\n Complete the Google OAuth flow for {}\n Return here when done\n The app will detect completion automatically", service_name)
782+
format!("INSTRUCTIONS:\n- Copy the URL above\n- Open it in your web browser\n- Complete the Google OAuth flow for {}\n- Return here when done\n- The app will detect completion automatically", service_name)
782783
};
783784

784785
let instructions_widget = Paragraph::new(instructions)

0 commit comments

Comments
 (0)