Skip to content

Commit 0a0cbfb

Browse files
Fix deep link handling (#903)
* rust side * update ui deep link handler * Update defguard-client.rs
1 parent 174f409 commit 0a0cbfb

4 files changed

Lines changed: 63 additions & 118 deletions

File tree

src-tauri/src/bin/defguard-client.rs

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ use defguard_client::{
2525
DB_POOL,
2626
},
2727
enterprise::provisioning::handle_client_initialization,
28+
events::handle_deep_link,
2829
periodic::run_periodic_tasks,
2930
service,
3031
tray::{configure_tray_icon, setup_tray, show_main_window},
@@ -34,6 +35,7 @@ use defguard_client::{
3435
};
3536
use log::{Level, LevelFilter};
3637
use tauri::{AppHandle, Builder, Manager, RunEvent, WindowEvent};
38+
use tauri_plugin_deep_link::DeepLinkExt;
3739
use tauri_plugin_log::{Target, TargetKind};
3840

3941
#[macro_use]
@@ -274,6 +276,14 @@ fn main() {
274276

275277
let app_handle = app.app_handle();
276278

279+
// Single Rust-side entry point for all deep link events (runtime).
280+
{
281+
let handle = app_handle.clone();
282+
app.deep_link().on_open_url(move |event| {
283+
handle_deep_link(&handle, &event.urls());
284+
});
285+
}
286+
277287
// Prepare `AppConfig`.
278288
let config = AppConfig::new(app_handle);
279289

@@ -369,14 +379,26 @@ fn main() {
369379
}
370380
#[cfg(not(target_os = "linux"))]
371381
{
372-
let has_locations = tauri::async_runtime::block_on(
373-
defguard_client::window_manager::has_non_service_locations()
374-
);
375-
if has_locations {
376-
WindowManager::open_tray(app_handle)?;
377-
} else {
378-
info!("No locations found, showing full view on startup.");
382+
// If the app was cold-launched by a deep link the full view must open, not the tray.
383+
let launched_by_deep_link = app_handle
384+
.deep_link()
385+
.get_current()
386+
.ok()
387+
.flatten()
388+
.is_some();
389+
if launched_by_deep_link {
390+
info!("App launched via deep link, opening full view directly.");
379391
let _ = WindowManager::open_full_view(app_handle);
392+
} else {
393+
let has_locations = tauri::async_runtime::block_on(
394+
defguard_client::window_manager::has_non_service_locations()
395+
);
396+
if has_locations {
397+
WindowManager::open_tray(app_handle)?;
398+
} else {
399+
info!("No locations found, showing full view on startup.");
400+
let _ = WindowManager::open_full_view(app_handle);
401+
}
380402
}
381403
}
382404

@@ -419,6 +441,11 @@ fn main() {
419441
);
420442
tauri::async_runtime::block_on(startup(app_handle));
421443

444+
// Handle a deep link that launched the app (startup case).
445+
if let Ok(Some(urls)) = app_handle.deep_link().get_current() {
446+
handle_deep_link(app_handle, &urls);
447+
}
448+
422449
// Handle Ctrl-C.
423450
debug!("Setting up Ctrl-C handler.");
424451
let app_handle_clone = app_handle.clone();

src-tauri/src/events.rs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
use serde::Serialize;
2-
use tauri::{AppHandle, Emitter, Url};
2+
use tauri::{AppHandle, Emitter, Manager, Url};
33
use tauri_plugin_notification::NotificationExt;
44

5-
use crate::{tray::show_main_window, ConnectionType};
5+
use crate::{
6+
window_manager::{WindowManager, NEW_UI_WINDOW_ID},
7+
ConnectionType,
8+
};
69

710
// Match src/pages/client/types.ts.
811
#[non_exhaustive]
@@ -115,7 +118,16 @@ pub fn handle_deep_link(app_handle: &AppHandle, urls: &[Url]) {
115118
}
116119
}
117120
if let (Some(token), Some(url)) = (token, url) {
118-
show_main_window(app_handle);
121+
info!("Deep link received: token={token}, url={url}");
122+
// If the compact tray window is visible, hide it before opening main view.
123+
if let Some(tray_win) = app_handle.get_webview_window(NEW_UI_WINDOW_ID) {
124+
if tray_win.is_visible().unwrap_or(false) {
125+
let _ = tray_win.hide();
126+
}
127+
}
128+
if let Err(e) = WindowManager::open_full_view(app_handle) {
129+
warn!("Deep link: failed to open main window: {e}");
130+
}
119131
let _ = app_handle.emit(
120132
EventKey::AddInstance.into(),
121133
AddInstancePayload {

src/pages/client/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ export enum TauriEventKey {
113113
DEAD_CONNECTION_DROPPED = 'dead-connection-dropped',
114114
DEAD_CONNECTION_RECONNECTED = 'dead-connection-reconnected',
115115
APPLICATION_CONFIG_CHANGED = 'application-config-changed',
116+
ADD_INSTANCE = 'add-instance',
116117
MFA_TRIGGER = 'mfa-trigger',
117118
VERSION_MISMATCH = 'version-mismatch',
118119
UUID_MISMATCH = 'uuid-mismatch',
Lines changed: 13 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -1,125 +1,30 @@
1-
import { getCurrent, onOpenUrl } from '@tauri-apps/plugin-deep-link';
1+
import { listen } from '@tauri-apps/api/event';
22
import { error } from '@tauri-apps/plugin-log';
3-
import { type PropsWithChildren, useCallback, useEffect, useRef } from 'react';
4-
import z, { string } from 'zod';
3+
import { type PropsWithChildren, useEffect } from 'react';
4+
import { type AddInstancePayload, TauriEventKey } from '../../../pages/client/types';
55
import useAddInstance from '../../hooks/useAddInstance';
66
import { errorDetail } from '../../utils/errorDetail';
77

8-
enum DeepLink {
9-
AddInstance = 'addinstance',
10-
}
11-
128
export const linkStorageKey = 'lastSuccessfullyHandledDeepLink';
139

1410
export const storeLink = (value: string) => {
1511
sessionStorage.setItem(linkStorageKey, value);
1612
};
1713

18-
const readStoreLink = (): string | null => {
19-
return sessionStorage.getItem(linkStorageKey);
20-
};
21-
22-
const addInstanceLinkSchema = z.object({
23-
token: string().trim().min(1),
24-
url: string().trim().min(1).url(),
25-
});
26-
27-
const AddInstanceLink = z.object({
28-
link: z.literal(DeepLink.AddInstance),
29-
data: addInstanceLinkSchema,
30-
});
31-
32-
const validLinkPayload = z.discriminatedUnion('link', [AddInstanceLink]);
33-
34-
type LinkPayload = z.infer<typeof validLinkPayload>;
35-
36-
const linkIntoPayload = (link: URL | null): LinkPayload | null => {
37-
if (link == null) return null;
38-
39-
const searchData = Object.fromEntries(new URLSearchParams(link.search));
40-
const linkKey = [link.hostname, link.pathname]
41-
.map((l) => l.trim().replaceAll('/', ''))
42-
.filter((l) => l !== '')[0] as string;
43-
const payload = {
44-
link: linkKey,
45-
data: searchData,
46-
};
47-
const result = validLinkPayload.safeParse(payload);
48-
if (result.success) {
49-
return result.data;
50-
} else {
51-
error(`Link ${link} was rejected due to schema validation.`);
52-
}
53-
return null;
54-
};
55-
5614
export const DeepLinkProvider = ({ children }: PropsWithChildren) => {
57-
const mounted = useRef(false);
58-
5915
const { handleAddInstance } = useAddInstance();
6016

61-
const handleValidLink = useCallback(
62-
async (payload: LinkPayload, rawLink?: string) => {
63-
const { data, link } = payload;
64-
switch (link) {
65-
case DeepLink.AddInstance:
66-
await handleAddInstance(data, rawLink);
67-
break;
68-
}
69-
if (rawLink) {
70-
storeLink(rawLink);
71-
}
72-
},
73-
[handleAddInstance],
74-
);
75-
76-
// biome-ignore lint/correctness/useExhaustiveDependencies: only on mount
7717
useEffect(() => {
78-
if (!mounted.current) {
79-
mounted.current = true;
80-
81-
let unlisten: (() => void) | undefined;
82-
(async () => {
83-
const start = await getCurrent();
84-
if (start != null) {
85-
const lastLink = readStoreLink();
86-
// if the link is exact as last successfully executed link
87-
// this is only necessary bcs in dev mode window is hot reloaded causing the startup link to be handled multiple times over.
88-
if (lastLink != null && lastLink === start[0]) {
89-
return;
90-
}
91-
const payload = linkIntoPayload(new URL(start[0]));
92-
if (payload != null) {
93-
try {
94-
handleValidLink(payload, start[0]);
95-
} catch (e) {
96-
const detail = errorDetail(e);
97-
error(`Failed to handle startup deep link "${payload.link}": ${detail}`);
98-
}
99-
}
100-
}
101-
unlisten = await onOpenUrl((urls) => {
102-
if (urls?.length) {
103-
const link = urls[0];
104-
const payload = linkIntoPayload(new URL(link));
105-
if (payload != null) {
106-
try {
107-
handleValidLink(payload);
108-
} catch (e) {
109-
const detail = errorDetail(e);
110-
error(
111-
`Failed to handle valid deep link "${payload?.link}" action: ${detail}`,
112-
);
113-
}
114-
}
115-
}
116-
});
117-
})();
118-
return () => {
119-
unlisten?.();
120-
};
121-
}
122-
}, []);
18+
const unlisten = listen<AddInstancePayload>(TauriEventKey.ADD_INSTANCE, (event) => {
19+
handleAddInstance(event.payload).catch((e) => {
20+
error(`Failed to handle add-instance event: ${errorDetail(e)}`);
21+
});
22+
});
23+
24+
return () => {
25+
unlisten.then((fn) => fn());
26+
};
27+
}, [handleAddInstance]);
12328

12429
return <>{children}</>;
12530
};

0 commit comments

Comments
 (0)