fix: use on_open_url for OAuth deep-link callback
All checks were successful
Release / build-and-release (push) Successful in 27m50s
All checks were successful
Release / build-and-release (push) Successful in 27m50s
The listener `app.listen("deep-link://new-url", ...)` did not reliably
fire when tauri-plugin-single-instance (deep-link feature) forwarded a
simpl-resultat://auth/callback URL to the running instance. The user
saw the browser complete the OAuth flow, the app regain focus, and
then sit in "loading" forever because the listener never received the
URL.
Switch to the canonical Tauri v2 API — `app.deep_link().on_open_url()`
via DeepLinkExt — which is directly coupled to the deep-link plugin
and catches URLs from both initial launch and single-instance forwards.
Also surface OAuth error responses: if the callback URL contains an
`error` parameter instead of a `code`, emit `auth-callback-error` so
the UI can show the error instead of staying stuck in "loading".
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f14ac3c6f8
commit
f5d74b4664
7 changed files with 65 additions and 24 deletions
|
|
@ -2,6 +2,12 @@
|
||||||
|
|
||||||
## [Non publié]
|
## [Non publié]
|
||||||
|
|
||||||
|
## [0.7.3] - 2026-04-13
|
||||||
|
|
||||||
|
### Corrigé
|
||||||
|
- Connexion Compte Maximus : le callback deep-link utilise maintenant l'API canonique Tauri v2 `on_open_url`, donc le code d'autorisation parvient bien à l'app en cours d'exécution au lieu de laisser l'interface bloquée en « chargement » (#51, #65)
|
||||||
|
- Les callbacks OAuth contenant un paramètre `error` remontent maintenant l'erreur à l'interface au lieu d'être ignorés silencieusement (#51)
|
||||||
|
|
||||||
## [0.7.2] - 2026-04-13
|
## [0.7.2] - 2026-04-13
|
||||||
|
|
||||||
### Modifié
|
### Modifié
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,12 @@
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.7.3] - 2026-04-13
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Maximus Account sign-in: the deep-link callback now uses the canonical Tauri v2 `on_open_url` API, so the auth code is properly received by the running app instead of leaving the UI stuck in "loading" (#51, #65)
|
||||||
|
- OAuth callbacks containing an `error` parameter now surface the error to the UI instead of being silently ignored (#51)
|
||||||
|
|
||||||
## [0.7.2] - 2026-04-13
|
## [0.7.2] - 2026-04-13
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "simpl_result_scaffold",
|
"name": "simpl_result_scaffold",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.7.2",
|
"version": "0.7.3",
|
||||||
"license": "GPL-3.0-only",
|
"license": "GPL-3.0-only",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
||||||
2
src-tauri/Cargo.lock
generated
2
src-tauri/Cargo.lock
generated
|
|
@ -4280,7 +4280,7 @@ checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "simpl-result"
|
name = "simpl-result"
|
||||||
version = "0.7.2"
|
version = "0.7.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes-gcm",
|
"aes-gcm",
|
||||||
"argon2",
|
"argon2",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "simpl-result"
|
name = "simpl-result"
|
||||||
version = "0.7.2"
|
version = "0.7.3"
|
||||||
description = "Personal finance management app"
|
description = "Personal finance management app"
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
authors = ["you"]
|
authors = ["you"]
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,8 @@ mod commands;
|
||||||
mod database;
|
mod database;
|
||||||
|
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
use tauri::{Emitter, Listener, Manager};
|
use tauri::{Emitter, Manager};
|
||||||
|
use tauri_plugin_deep_link::DeepLinkExt;
|
||||||
use tauri_plugin_sql::{Migration, MigrationKind};
|
use tauri_plugin_sql::{Migration, MigrationKind};
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
|
|
@ -103,26 +104,40 @@ pub fn run() {
|
||||||
#[cfg(desktop)]
|
#[cfg(desktop)]
|
||||||
app.handle().plugin(tauri_plugin_updater::Builder::new().build())?;
|
app.handle().plugin(tauri_plugin_updater::Builder::new().build())?;
|
||||||
|
|
||||||
// Listen for deep-link events (simpl-resultat://auth/callback?code=...)
|
// Register the custom scheme at runtime on Linux (the .desktop file
|
||||||
|
// handles it in prod, but register_all is a no-op there and required
|
||||||
|
// for AppImage/dev builds).
|
||||||
|
#[cfg(any(target_os = "linux", all(debug_assertions, windows)))]
|
||||||
|
{
|
||||||
|
let _ = app.deep_link().register_all();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Canonical Tauri v2 pattern: on_open_url fires for both initial-launch
|
||||||
|
// URLs and subsequent URLs forwarded by tauri-plugin-single-instance
|
||||||
|
// (with the `deep-link` feature).
|
||||||
let handle = app.handle().clone();
|
let handle = app.handle().clone();
|
||||||
app.listen("deep-link://new-url", move |event| {
|
app.deep_link().on_open_url(move |event| {
|
||||||
let payload = event.payload();
|
for url in event.urls() {
|
||||||
// payload is a JSON-serialized array of URL strings
|
let url_str = url.as_str();
|
||||||
if let Ok(urls) = serde_json::from_str::<Vec<String>>(payload) {
|
let h = handle.clone();
|
||||||
for url in urls {
|
if let Some(code) = extract_auth_code(url_str) {
|
||||||
if let Some(code) = extract_auth_code(&url) {
|
tauri::async_runtime::spawn(async move {
|
||||||
let h = handle.clone();
|
match commands::handle_auth_callback(h.clone(), code).await {
|
||||||
tauri::async_runtime::spawn(async move {
|
Ok(account) => {
|
||||||
match commands::handle_auth_callback(h.clone(), code).await {
|
let _ = h.emit("auth-callback-success", &account);
|
||||||
Ok(account) => {
|
|
||||||
let _ = h.emit("auth-callback-success", &account);
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
let _ = h.emit("auth-callback-error", &err);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
Err(err) => {
|
||||||
}
|
let _ = h.emit("auth-callback-error", &err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// No `code` param — likely an OAuth error response. Surface
|
||||||
|
// it to the frontend instead of leaving the UI stuck in
|
||||||
|
// "loading" forever.
|
||||||
|
let err_msg = extract_auth_error(url_str)
|
||||||
|
.unwrap_or_else(|| "OAuth callback did not include a code".to_string());
|
||||||
|
let _ = h.emit("auth-callback-error", &err_msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -177,6 +192,20 @@ pub fn run() {
|
||||||
/// Extract the `code` query parameter from a deep-link callback URL.
|
/// Extract the `code` query parameter from a deep-link callback URL.
|
||||||
/// e.g. "simpl-resultat://auth/callback?code=abc123&state=xyz" → Some("abc123")
|
/// e.g. "simpl-resultat://auth/callback?code=abc123&state=xyz" → Some("abc123")
|
||||||
fn extract_auth_code(url: &str) -> Option<String> {
|
fn extract_auth_code(url: &str) -> Option<String> {
|
||||||
|
extract_query_param(url, "code")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract an OAuth error description from a callback URL. Returns a
|
||||||
|
/// formatted string combining `error` and `error_description` when present.
|
||||||
|
fn extract_auth_error(url: &str) -> Option<String> {
|
||||||
|
let error = extract_query_param(url, "error")?;
|
||||||
|
match extract_query_param(url, "error_description") {
|
||||||
|
Some(desc) => Some(format!("{}: {}", error, desc)),
|
||||||
|
None => Some(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_query_param(url: &str, key: &str) -> Option<String> {
|
||||||
let url = url.trim();
|
let url = url.trim();
|
||||||
if !url.starts_with("simpl-resultat://auth/callback") {
|
if !url.starts_with("simpl-resultat://auth/callback") {
|
||||||
return None;
|
return None;
|
||||||
|
|
@ -184,7 +213,7 @@ fn extract_auth_code(url: &str) -> Option<String> {
|
||||||
let query = url.split('?').nth(1)?;
|
let query = url.split('?').nth(1)?;
|
||||||
for pair in query.split('&') {
|
for pair in query.split('&') {
|
||||||
let mut kv = pair.splitn(2, '=');
|
let mut kv = pair.splitn(2, '=');
|
||||||
if kv.next()? == "code" {
|
if kv.next()? == key {
|
||||||
return kv.next().map(|v| {
|
return kv.next().map(|v| {
|
||||||
urlencoding::decode(v).map(|s| s.into_owned()).unwrap_or_else(|_| v.to_string())
|
urlencoding::decode(v).map(|s| s.into_owned()).unwrap_or_else(|_| v.to_string())
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "Simpl Resultat",
|
"productName": "Simpl Resultat",
|
||||||
"version": "0.7.2",
|
"version": "0.7.3",
|
||||||
"identifier": "com.simpl.resultat",
|
"identifier": "com.simpl.resultat",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "npm run dev",
|
"beforeDevCommand": "npm run dev",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue