From e92d8b6921f9ea01776defcc432d5adcd5023bad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20=C5=9Al=C4=99zak?= Date: Mon, 25 May 2026 11:19:41 +0200 Subject: [PATCH 1/3] rust side --- src-tauri/src/bin/defguard-client.rs | 42 +++++++++++++++++++++++----- src-tauri/src/events.rs | 18 ++++++++++-- 2 files changed, 50 insertions(+), 10 deletions(-) diff --git a/src-tauri/src/bin/defguard-client.rs b/src-tauri/src/bin/defguard-client.rs index 15c10029..da0564aa 100644 --- a/src-tauri/src/bin/defguard-client.rs +++ b/src-tauri/src/bin/defguard-client.rs @@ -25,6 +25,7 @@ use defguard_client::{ DB_POOL, }, enterprise::provisioning::handle_client_initialization, + events::handle_deep_link, periodic::run_periodic_tasks, service, tray::{configure_tray_icon, setup_tray, show_main_window}, @@ -34,6 +35,7 @@ use defguard_client::{ }; use log::{Level, LevelFilter}; use tauri::{AppHandle, Builder, Manager, RunEvent, WindowEvent}; +use tauri_plugin_deep_link::DeepLinkExt; use tauri_plugin_log::{Target, TargetKind}; #[macro_use] @@ -272,6 +274,14 @@ fn main() { let app_handle = app.app_handle(); + // Single Rust-side entry point for all deep link events (runtime). + { + let handle = app_handle.clone(); + app.deep_link().on_open_url(move |event| { + handle_deep_link(&handle, &event.urls()); + }); + } + // Prepare `AppConfig`. let config = AppConfig::new(app_handle); @@ -360,6 +370,14 @@ fn main() { warn!("Failed to pre-build full window: {e}"); } + // If the app was cold-launched by a deep link the full view must open, not the tray. + let launched_by_deep_link = app_handle + .deep_link() + .get_current() + .ok() + .flatten() + .is_some(); + // Decide which window to show based on platform and available locations. #[cfg(target_os = "linux")] { @@ -367,14 +385,19 @@ fn main() { } #[cfg(not(target_os = "linux"))] { - let has_locations = tauri::async_runtime::block_on( - defguard_client::window_manager::has_non_service_locations() - ); - if has_locations { - WindowManager::open_tray(app_handle)?; - } else { - info!("No locations found, showing full view on startup."); + if launched_by_deep_link { + info!("App launched via deep link, opening full view directly."); let _ = WindowManager::open_full_view(app_handle); + } else { + let has_locations = tauri::async_runtime::block_on( + defguard_client::window_manager::has_non_service_locations() + ); + if has_locations { + WindowManager::open_tray(app_handle)?; + } else { + info!("No locations found, showing full view on startup."); + let _ = WindowManager::open_full_view(app_handle); + } } } @@ -417,6 +440,11 @@ fn main() { ); tauri::async_runtime::block_on(startup(app_handle)); + // Handle a deep link that launched the app (startup case). + if let Ok(Some(urls)) = app_handle.deep_link().get_current() { + handle_deep_link(app_handle, &urls); + } + // Handle Ctrl-C. debug!("Setting up Ctrl-C handler."); let app_handle_clone = app_handle.clone(); diff --git a/src-tauri/src/events.rs b/src-tauri/src/events.rs index e7267d08..b1438974 100644 --- a/src-tauri/src/events.rs +++ b/src-tauri/src/events.rs @@ -1,8 +1,11 @@ use serde::Serialize; -use tauri::{AppHandle, Emitter, Url}; +use tauri::{AppHandle, Emitter, Manager, Url}; use tauri_plugin_notification::NotificationExt; -use crate::{tray::show_main_window, ConnectionType}; +use crate::{ + window_manager::{WindowManager, NEW_UI_WINDOW_ID}, + ConnectionType, +}; // Match src/pages/client/types.ts. #[non_exhaustive] @@ -115,7 +118,16 @@ pub fn handle_deep_link(app_handle: &AppHandle, urls: &[Url]) { } } if let (Some(token), Some(url)) = (token, url) { - show_main_window(app_handle); + info!("Deep link received: token={token}, url={url}"); + // If the compact tray window is visible, hide it before opening main view. + if let Some(tray_win) = app_handle.get_webview_window(NEW_UI_WINDOW_ID) { + if tray_win.is_visible().unwrap_or(false) { + let _ = tray_win.hide(); + } + } + if let Err(e) = WindowManager::open_full_view(app_handle) { + warn!("Deep link: failed to open main window: {e}"); + } let _ = app_handle.emit( EventKey::AddInstance.into(), AddInstancePayload { From e08c416be2684d16817984d109369a880ebacf72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20=C5=9Al=C4=99zak?= Date: Mon, 25 May 2026 11:27:18 +0200 Subject: [PATCH 2/3] update ui deep link handler --- src/pages/client/types.ts | 1 + .../components/providers/DeepLinkProvider.tsx | 121 ++---------------- 2 files changed, 14 insertions(+), 108 deletions(-) diff --git a/src/pages/client/types.ts b/src/pages/client/types.ts index 9954bb26..7f25184d 100644 --- a/src/pages/client/types.ts +++ b/src/pages/client/types.ts @@ -113,6 +113,7 @@ export enum TauriEventKey { DEAD_CONNECTION_DROPPED = 'dead-connection-dropped', DEAD_CONNECTION_RECONNECTED = 'dead-connection-reconnected', APPLICATION_CONFIG_CHANGED = 'application-config-changed', + ADD_INSTANCE = 'add-instance', MFA_TRIGGER = 'mfa-trigger', VERSION_MISMATCH = 'version-mismatch', UUID_MISMATCH = 'uuid-mismatch', diff --git a/src/shared/components/providers/DeepLinkProvider.tsx b/src/shared/components/providers/DeepLinkProvider.tsx index 2f0e9024..306ab38c 100644 --- a/src/shared/components/providers/DeepLinkProvider.tsx +++ b/src/shared/components/providers/DeepLinkProvider.tsx @@ -1,125 +1,30 @@ -import { getCurrent, onOpenUrl } from '@tauri-apps/plugin-deep-link'; +import { listen } from '@tauri-apps/api/event'; import { error } from '@tauri-apps/plugin-log'; -import { type PropsWithChildren, useCallback, useEffect, useRef } from 'react'; -import z, { string } from 'zod'; +import { type PropsWithChildren, useEffect } from 'react'; +import { type AddInstancePayload, TauriEventKey } from '../../../pages/client/types'; import useAddInstance from '../../hooks/useAddInstance'; import { errorDetail } from '../../utils/errorDetail'; -enum DeepLink { - AddInstance = 'addinstance', -} - export const linkStorageKey = 'lastSuccessfullyHandledDeepLink'; export const storeLink = (value: string) => { sessionStorage.setItem(linkStorageKey, value); }; -const readStoreLink = (): string | null => { - return sessionStorage.getItem(linkStorageKey); -}; - -const addInstanceLinkSchema = z.object({ - token: string().trim().min(1), - url: string().trim().min(1).url(), -}); - -const AddInstanceLink = z.object({ - link: z.literal(DeepLink.AddInstance), - data: addInstanceLinkSchema, -}); - -const validLinkPayload = z.discriminatedUnion('link', [AddInstanceLink]); - -type LinkPayload = z.infer; - -const linkIntoPayload = (link: URL | null): LinkPayload | null => { - if (link == null) return null; - - const searchData = Object.fromEntries(new URLSearchParams(link.search)); - const linkKey = [link.hostname, link.pathname] - .map((l) => l.trim().replaceAll('/', '')) - .filter((l) => l !== '')[0] as string; - const payload = { - link: linkKey, - data: searchData, - }; - const result = validLinkPayload.safeParse(payload); - if (result.success) { - return result.data; - } else { - error(`Link ${link} was rejected due to schema validation.`); - } - return null; -}; - export const DeepLinkProvider = ({ children }: PropsWithChildren) => { - const mounted = useRef(false); - const { handleAddInstance } = useAddInstance(); - const handleValidLink = useCallback( - async (payload: LinkPayload, rawLink?: string) => { - const { data, link } = payload; - switch (link) { - case DeepLink.AddInstance: - await handleAddInstance(data, rawLink); - break; - } - if (rawLink) { - storeLink(rawLink); - } - }, - [handleAddInstance], - ); - - // biome-ignore lint/correctness/useExhaustiveDependencies: only on mount useEffect(() => { - if (!mounted.current) { - mounted.current = true; - - let unlisten: (() => void) | undefined; - (async () => { - const start = await getCurrent(); - if (start != null) { - const lastLink = readStoreLink(); - // if the link is exact as last successfully executed link - // this is only necessary bcs in dev mode window is hot reloaded causing the startup link to be handled multiple times over. - if (lastLink != null && lastLink === start[0]) { - return; - } - const payload = linkIntoPayload(new URL(start[0])); - if (payload != null) { - try { - handleValidLink(payload, start[0]); - } catch (e) { - const detail = errorDetail(e); - error(`Failed to handle startup deep link "${payload.link}": ${detail}`); - } - } - } - unlisten = await onOpenUrl((urls) => { - if (urls?.length) { - const link = urls[0]; - const payload = linkIntoPayload(new URL(link)); - if (payload != null) { - try { - handleValidLink(payload); - } catch (e) { - const detail = errorDetail(e); - error( - `Failed to handle valid deep link "${payload?.link}" action: ${detail}`, - ); - } - } - } - }); - })(); - return () => { - unlisten?.(); - }; - } - }, []); + const unlisten = listen(TauriEventKey.ADD_INSTANCE, (event) => { + handleAddInstance(event.payload).catch((e) => { + error(`Failed to handle add-instance event: ${errorDetail(e)}`); + }); + }); + + return () => { + unlisten.then((fn) => fn()); + }; + }, [handleAddInstance]); return <>{children}; }; From aee90c6ad7eca8892f4821f558073c3f81e67ae0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20=C5=9Al=C4=99zak?= Date: Mon, 25 May 2026 11:34:24 +0200 Subject: [PATCH 3/3] Update defguard-client.rs --- src-tauri/src/bin/defguard-client.rs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src-tauri/src/bin/defguard-client.rs b/src-tauri/src/bin/defguard-client.rs index da0564aa..65642229 100644 --- a/src-tauri/src/bin/defguard-client.rs +++ b/src-tauri/src/bin/defguard-client.rs @@ -370,14 +370,6 @@ fn main() { warn!("Failed to pre-build full window: {e}"); } - // If the app was cold-launched by a deep link the full view must open, not the tray. - let launched_by_deep_link = app_handle - .deep_link() - .get_current() - .ok() - .flatten() - .is_some(); - // Decide which window to show based on platform and available locations. #[cfg(target_os = "linux")] { @@ -385,6 +377,13 @@ fn main() { } #[cfg(not(target_os = "linux"))] { + // If the app was cold-launched by a deep link the full view must open, not the tray. + let launched_by_deep_link = app_handle + .deep_link() + .get_current() + .ok() + .flatten() + .is_some(); if launched_by_deep_link { info!("App launched via deep link, opening full view directly."); let _ = WindowManager::open_full_view(app_handle);