diff --git a/package-lock.json b/package-lock.json index ece46e1c3..958bec733 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "@tauri-apps/api": "^2", + "@tauri-apps/plugin-deep-link": "^2", "@tauri-apps/plugin-opener": "^2", "@types/react-router-dom": "^5.3.3", "react": "^19.1.0", @@ -1464,6 +1465,15 @@ "node": ">= 10" } }, + "node_modules/@tauri-apps/plugin-deep-link": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-deep-link/-/plugin-deep-link-2.4.6.tgz", + "integrity": "sha512-UUOSt0U5juK20uhO2MoHZX/IPblkrhUh+VPtIeu3RwtzI0R9Em3Auzfg/PwcZ9Pv8mLne3cQ4p9CFXD6WxqCZA==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.8.0" + } + }, "node_modules/@tauri-apps/plugin-opener": { "version": "2.5.3", "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.5.3.tgz", @@ -1876,18 +1886,6 @@ } } }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "peer": true, - "engines": { - "node": ">=8" - } - }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -2169,18 +2167,6 @@ "node": ">=0.12.0" } }, - "node_modules/jiti": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", - "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -2214,280 +2200,6 @@ "node": ">=6" } }, - "node_modules/lightningcss": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", - "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", - "dev": true, - "license": "MPL-2.0", - "optional": true, - "peer": true, - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-android-arm64": "1.30.2", - "lightningcss-darwin-arm64": "1.30.2", - "lightningcss-darwin-x64": "1.30.2", - "lightningcss-freebsd-x64": "1.30.2", - "lightningcss-linux-arm-gnueabihf": "1.30.2", - "lightningcss-linux-arm64-gnu": "1.30.2", - "lightningcss-linux-arm64-musl": "1.30.2", - "lightningcss-linux-x64-gnu": "1.30.2", - "lightningcss-linux-x64-musl": "1.30.2", - "lightningcss-win32-arm64-msvc": "1.30.2", - "lightningcss-win32-x64-msvc": "1.30.2" - } - }, - "node_modules/lightningcss-android-arm64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", - "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", - "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", - "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", - "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", - "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", - "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", - "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", - "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", - "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", - "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", - "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", diff --git a/package.json b/package.json index cce3b4b78..fc14022dc 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "dependencies": { "@tauri-apps/api": "^2", "@tauri-apps/plugin-opener": "^2", + "@tauri-apps/plugin-deep-link": "^2", "@types/react-router-dom": "^5.3.3", "react": "^19.1.0", "react-dom": "^19.1.0", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 760d4b66d..188399b65 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -460,6 +460,26 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "tiny-keccak", +] + [[package]] name = "convert_case" version = "0.4.0" @@ -549,6 +569,12 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-common" version = "0.1.7" @@ -735,6 +761,15 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "dlv-list" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] + [[package]] name = "dpi" version = "0.1.2" @@ -1350,6 +1385,12 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "hashbrown" version = "0.16.1" @@ -2289,6 +2330,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ordered-multimap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" +dependencies = [ + "dlv-list", + "hashbrown 0.14.5", +] + [[package]] name = "ordered-stream" version = "0.2.0" @@ -2881,6 +2932,16 @@ dependencies = [ "web-sys", ] +[[package]] +name = "rust-ini" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + [[package]] name = "rustc_version" version = "0.4.1" @@ -3513,6 +3574,7 @@ dependencies = [ "serde_json", "tauri", "tauri-build", + "tauri-plugin-deep-link", "tauri-plugin-opener", ] @@ -3596,6 +3658,27 @@ dependencies = [ "walkdir", ] +[[package]] +name = "tauri-plugin-deep-link" +version = "2.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "444b091f24f2f6bdb4a305b54d3961f629c11861c685aceeea9a1972f89e43d5" +dependencies = [ + "dunce", + "plist", + "rust-ini", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.18", + "tracing", + "url", + "windows-registry", + "windows-result 0.3.4", +] + [[package]] name = "tauri-plugin-opener" version = "2.5.3" @@ -3814,6 +3897,15 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinystr" version = "0.8.2" @@ -4559,6 +4651,17 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-registry" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +dependencies = [ + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + [[package]] name = "windows-result" version = "0.3.4" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 6875383ea..22074cbd8 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -20,6 +20,7 @@ tauri-build = { version = "2", features = [] } [dependencies] tauri = { version = "2", features = [] } tauri-plugin-opener = "2" +tauri-plugin-deep-link = "2.0.0" serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 4cdbf49a7..ec2e275b3 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -5,6 +5,7 @@ "windows": ["main"], "permissions": [ "core:default", - "opener:default" + "opener:default", + "deep-link:default" ] } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 4a277ef35..bc85cc65a 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -4,10 +4,23 @@ fn greet(name: &str) -> String { format!("Hello, {}! You've been greeted from Rust!", name) } +use tauri_plugin_deep_link::DeepLinkExt; + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_opener::init()) + .plugin(tauri_plugin_deep_link::init()) + .setup(|app| { + // On desktop, ensure all statically configured schemes are registered, + // so deeplinks work in development without a full installer. + #[cfg(any(windows, target_os = "linux"))] + { + app.deep_link().register_all()?; + } + + Ok(()) + }) .invoke_handler(tauri::generate_handler![greet]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 44a3c3fab..d42e8e455 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -31,5 +31,12 @@ "icons/icon.icns", "icons/icon.ico" ] + }, + "plugins": { + "deep-link": { + "desktop": { + "schemes": ["outsourced"] + } + } } } diff --git a/src/main.tsx b/src/main.tsx index 8b1ddb971..a05a0292c 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -2,6 +2,10 @@ import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App"; import "./index.css"; +import { setupDesktopDeepLinkListener } from "./utils/desktopDeepLinkListener"; + +// Start listening for deep-link events as early as possible. +void setupDesktopDeepLinkListener(); ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index c2eca553c..bbc74a653 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -1,6 +1,8 @@ import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; +import { openUrl } from '@tauri-apps/plugin-opener'; import { countries } from '../data/countries'; +import { BACKEND_URL } from '../utils/config'; const Login = () => { const navigate = useNavigate(); @@ -35,9 +37,12 @@ const Login = () => { } }; - const handleTelegramAuth = () => { - // In a real app, this would handle Telegram OAuth - navigate('/onboarding/step1'); + const handleTelegramAuth = async () => { + try { + await openUrl(`${BACKEND_URL}/auth/telegram?platform=desktop`); + } catch (error) { + console.error('Failed to open Telegram auth page:', error); + } }; return ( diff --git a/src/utils/config.ts b/src/utils/config.ts new file mode 100644 index 000000000..f8ebc49b2 --- /dev/null +++ b/src/utils/config.ts @@ -0,0 +1,2 @@ +export const BACKEND_URL = + import.meta.env.VITE_BACKEND_URL || 'https://api.yourapp.com'; diff --git a/src/utils/deeplink.ts b/src/utils/deeplink.ts new file mode 100644 index 000000000..96ef6216e --- /dev/null +++ b/src/utils/deeplink.ts @@ -0,0 +1,56 @@ +export type WebLoginMethod = 'phone' | 'telegram'; + +export interface PhoneLoginContext { + method: 'phone'; + phoneNumber: string; + countryCode: string; +} + +// The shape of the Telegram user object is defined by Telegram. +// We keep it as unknown here and let the backend interpret it. +export interface TelegramLoginContext { + method: 'telegram'; + telegramUser: unknown; +} + +export type WebLoginContext = PhoneLoginContext | TelegramLoginContext; + +const DESKTOP_SCHEME = 'outsourced'; + +export const buildDesktopDeeplink = (token: string): string => { + const encoded = encodeURIComponent(token); + return `${DESKTOP_SCHEME}://auth?token=${encoded}`; +}; + +/** + * Call the backend to complete the web login and obtain a short-lived token + * that can be handed off to the desktop app via deeplink. + * + * This expects a backend endpoint at POST /api/auth/web-complete that + * validates the current web session and returns `{ loginToken: string }`. + */ +export const completeWebLoginAndOpenDesktop = async ( + context: WebLoginContext, +): Promise => { + const response = await fetch('/api/auth/web-complete', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(context), + credentials: 'include', + }); + + if (!response.ok) { + throw new Error('Failed to complete web login for desktop handoff'); + } + + const data = (await response.json()) as { loginToken?: string }; + if (!data.loginToken) { + throw new Error('Backend response did not include a loginToken'); + } + + const deeplink = buildDesktopDeeplink(data.loginToken); + window.location.href = deeplink; +}; + diff --git a/src/utils/desktopDeepLinkListener.ts b/src/utils/desktopDeepLinkListener.ts new file mode 100644 index 000000000..61b306506 --- /dev/null +++ b/src/utils/desktopDeepLinkListener.ts @@ -0,0 +1,82 @@ +import { getCurrent, onOpenUrl } from '@tauri-apps/plugin-deep-link'; +import { BACKEND_URL } from './config'; + +/** + * Handle a list of deep link URLs delivered by the Tauri deep-link plugin. + * Parses `outsourced://auth?token=...` URLs and exchanges the token for a + * desktop session via the backend. + */ +const handleDeepLinkUrls = async (urls: string[] | null | undefined) => { + if (!urls || urls.length === 0) { + return; + } + + const url = urls[0]; + + try { + const parsed = new URL(url); + if (parsed.protocol !== 'outsourced:') { + return; + } + + const token = parsed.searchParams.get('token'); + if (!token) { + console.warn('Deep link URL did not contain a token query parameter'); + return; + } + + const response = await fetch(`${BACKEND_URL}/api/auth/desktop-exchange`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ token }), + }); + + if (!response.ok) { + console.error('Token exchange failed:', response.status); + return; + } + + const data = (await response.json()) as { + sessionToken?: string; + user?: { id: string; username: string; firstName?: string }; + }; + + if (!data.sessionToken) { + console.error('Backend did not return a sessionToken'); + return; + } + + // Persist session so the app can use it after navigation. + localStorage.setItem('sessionToken', data.sessionToken); + if (data.user) { + localStorage.setItem('user', JSON.stringify(data.user)); + } + + // Navigate to post-login flow. This listener runs outside the React + // router context, so we assign the path directly and reload. + window.location.replace('/onboarding/step1'); + } catch (error) { + console.error('Failed to handle deep link URL:', url, error); + } +}; + +/** + * Set up listeners for deep links so that when the desktop app is opened + * via a URL like `outsourced://auth?token=...`, we can react to it. + */ +export const setupDesktopDeepLinkListener = async () => { + // Guard against running in plain web browser without Tauri. + if (!(window as any).__TAURI__) { + return; + } + + const startUrls = await getCurrent(); + if (startUrls) { + await handleDeepLinkUrls(startUrls); + } + + await onOpenUrl(event => { + void handleDeepLinkUrls(event); + }); +}; +