From 021d23bef330de4ce001993e0ef2c7ab7815f044 Mon Sep 17 00:00:00 2001 From: Fabian-Lars Date: Thu, 9 May 2024 18:58:15 +0200 Subject: [PATCH] feat(deep-link): Add deep link support for desktop (#916) --- .changes/deep-link-desktop.md | 6 + .github/workflows/test-rust.yml | 3 + Cargo.lock | 82 +++++- Cargo.toml | 1 + plugins/deep-link/Cargo.toml | 10 + plugins/deep-link/README.md | 13 +- plugins/deep-link/api-iife.js | 2 +- plugins/deep-link/build.rs | 6 +- .../examples/app/src-tauri/tauri.conf.json | 16 +- plugins/deep-link/examples/app/tsconfig.json | 2 +- plugins/deep-link/guest-js/index.ts | 87 +++++- .../autogenerated/commands/is_registered.toml | 13 + .../autogenerated/commands/register.toml | 13 + .../autogenerated/commands/unregister.toml | 13 + .../permissions/autogenerated/reference.md | 6 + .../deep-link/permissions/schemas/schema.json | 42 +++ plugins/deep-link/src/commands.rs | 30 ++ plugins/deep-link/src/config.rs | 16 +- plugins/deep-link/src/error.rs | 13 + plugins/deep-link/src/lib.rs | 260 +++++++++++++++++- plugins/deep-link/src/template.desktop | 7 + 21 files changed, 613 insertions(+), 28 deletions(-) create mode 100644 .changes/deep-link-desktop.md create mode 100644 plugins/deep-link/permissions/autogenerated/commands/is_registered.toml create mode 100644 plugins/deep-link/permissions/autogenerated/commands/register.toml create mode 100644 plugins/deep-link/permissions/autogenerated/commands/unregister.toml create mode 100644 plugins/deep-link/src/template.desktop diff --git a/.changes/deep-link-desktop.md b/.changes/deep-link-desktop.md new file mode 100644 index 00000000..a695648c --- /dev/null +++ b/.changes/deep-link-desktop.md @@ -0,0 +1,6 @@ +--- +"deep-link": patch +"deep-link-js": patch +--- + +Added desktop support. diff --git a/.github/workflows/test-rust.yml b/.github/workflows/test-rust.yml index cca4bba0..21c5ec8c 100644 --- a/.github/workflows/test-rust.yml +++ b/.github/workflows/test-rust.yml @@ -56,6 +56,9 @@ jobs: tauri-plugin-clipboard-manager: - .github/workflows/test-rust.yml - plugins/clipboard-manager/** + tauri-plugin-deep-link: + - .github/workflows/test-rust.yml + - plugins/deep-link/** tauri-plugin-dialog: - .github/workflows/test-rust.yml - plugins/dialog/** diff --git a/Cargo.lock b/Cargo.lock index f6cc3a19..d15d2014 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1161,6 +1161,26 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[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.12", + "once_cell", + "tiny-keccak", +] + [[package]] name = "constant_time_eq" version = "0.1.5" @@ -1621,6 +1641,15 @@ dependencies = [ "syn 2.0.52", ] +[[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 = "dotenvy" version = "0.15.7" @@ -3199,7 +3228,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.52.5", ] [[package]] @@ -3830,6 +3859,16 @@ dependencies = [ "vcpkg", ] +[[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.3", +] + [[package]] name = "ordered-stream" version = "0.2.0" @@ -4779,6 +4818,17 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rust-ini" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d625ed57d8f49af6cfa514c42e1a71fadcff60eb0b1c517ff82fe41aa025b41" +dependencies = [ + "cfg-if", + "ordered-multimap", + "trim-in-place", +] + [[package]] name = "rust_decimal" version = "1.34.3" @@ -6132,13 +6182,18 @@ dependencies = [ name = "tauri-plugin-deep-link" version = "2.0.0-beta.4" dependencies = [ + "dunce", "log", + "rust-ini", "serde", "serde_json", "tauri", "tauri-plugin", + "tauri-utils", "thiserror", "url", + "windows-registry", + "windows-result", ] [[package]] @@ -6702,6 +6757,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 = "tiny_http" version = "0.12.0" @@ -7024,6 +7088,12 @@ dependencies = [ "serde_json", ] +[[package]] +name = "trim-in-place" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc" + [[package]] name = "try-lock" version = "0.2.5" @@ -7742,6 +7812,16 @@ dependencies = [ "syn 2.0.52", ] +[[package]] +name = "windows-registry" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f721bc2e55efb506a1a395a545cb76c2481fb023d33b51f0050e7888716281cf" +dependencies = [ + "windows-result", + "windows-targets 0.52.5", +] + [[package]] name = "windows-result" version = "0.1.1" diff --git a/Cargo.toml b/Cargo.toml index 72db49c7..f75db854 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ log = "0.4" tauri = "2.0.0-beta.17" tauri-build = "2.0.0-beta.13" tauri-plugin = "2.0.0-beta.13" +tauri-utils = "2.0.0-beta.13" serde_json = "1" thiserror = "1" url = "2" diff --git a/plugins/deep-link/Cargo.toml b/plugins/deep-link/Cargo.toml index 9f393d29..e76fda41 100644 --- a/plugins/deep-link/Cargo.toml +++ b/plugins/deep-link/Cargo.toml @@ -17,12 +17,22 @@ targets = [ "x86_64-linux-android" ] [build-dependencies] serde = { workspace = true } serde_json = { workspace = true } +tauri-utils = { workspace = true } tauri-plugin = { workspace = true, features = [ "build" ] } [dependencies] serde = { workspace = true } serde_json = { workspace = true } tauri = { workspace = true } +tauri-utils = { workspace = true } log = { workspace = true } thiserror = { workspace = true } url = { workspace = true } + +[target."cfg(windows)".dependencies] +dunce = "1" +windows-registry = "0.1" +windows-result = "0.1" + +[target.'cfg(target_os = "linux")'.dependencies] +rust-ini = "0.21" diff --git a/plugins/deep-link/README.md b/plugins/deep-link/README.md index d014040c..ebdcb465 100644 --- a/plugins/deep-link/README.md +++ b/plugins/deep-link/README.md @@ -93,16 +93,19 @@ See [supporting associated domains](https://developer.apple.com/documentation/xc ## Configuration -Under `tauri.conf.json > plugins > deep-link`, configure the domains you want to associate with your application: +Under `tauri.conf.json > plugins > deep-link`, configure the domains (mobile) and schemes (desktop) you want to associate with your application: ```json { "plugins": { "deep-link": { - "domains": [ + "mobile": [ { "host": "your.website.com", "pathPrefix": ["/open"] }, { "host": "another.site.br" } - ] + ], + "desktop": { + "schemes": ["something", "my-tauri-app"] + } } } } @@ -128,10 +131,12 @@ Afterwards all the plugin's APIs are available through the JavaScript guest bind ```javascript import { onOpenUrl } from "@tauri-apps/plugin-deep-link"; await onOpenUrl((urls) => { - console.log('deep link:', urls); + console.log("deep link:", urls); }); ``` +Note that the Plugin will only emit events on macOS, iOS and Android. On Windows and Linux the OS will spawn a new instance of your app with the URL as a CLI argument. If you want your app to behave on Windows & Linux similar to the other platforms you can use the [single-instance](../single-instance/) plugin. + ## Contributing PRs accepted. Please make sure to read the Contributing Guide before making a pull request. diff --git a/plugins/deep-link/api-iife.js b/plugins/deep-link/api-iife.js index 9f139a80..74922bb2 100644 --- a/plugins/deep-link/api-iife.js +++ b/plugins/deep-link/api-iife.js @@ -1 +1 @@ -if("__TAURI__"in window){var __TAURI_PLUGIN_DEEPLINK__=function(e){"use strict";function n(e,n=!1){return window.__TAURI_INTERNALS__.transformCallback(e,n)}async function r(e,n={},r){return window.__TAURI_INTERNALS__.invoke(e,n,r)}var t;async function i(e,t,i){const _=(void 0,{kind:"Any"});return r("plugin:event|listen",{event:e,target:_,handler:n(t)}).then((n=>async()=>async function(e,n){await r("plugin:event|unlisten",{event:e,eventId:n})}(e,n)))}async function _(){return await r("plugin:deep-link|get_current")}return"function"==typeof SuppressedError&&SuppressedError,function(e){e.WINDOW_RESIZED="tauri://resize",e.WINDOW_MOVED="tauri://move",e.WINDOW_CLOSE_REQUESTED="tauri://close-requested",e.WINDOW_DESTROYED="tauri://destroyed",e.WINDOW_FOCUS="tauri://focus",e.WINDOW_BLUR="tauri://blur",e.WINDOW_SCALE_FACTOR_CHANGED="tauri://scale-change",e.WINDOW_THEME_CHANGED="tauri://theme-changed",e.WINDOW_CREATED="tauri://window-created",e.WEBVIEW_CREATED="tauri://webview-created",e.DRAG="tauri://drag",e.DROP="tauri://drop",e.DROP_OVER="tauri://drop-over",e.DROP_CANCELLED="tauri://drag-cancelled"}(t||(t={})),e.getCurrent=_,e.onOpenUrl=async function(e){const n=await _();return null!=n&&e(n),await i("deep-link://new-url",(n=>{e(n.payload)}))},e}({});Object.defineProperty(window.__TAURI__,"deepLink",{value:__TAURI_PLUGIN_DEEPLINK__})} +if("__TAURI__"in window){var __TAURI_PLUGIN_DEEPLINK__=function(e){"use strict";function n(e,n=!1){return window.__TAURI_INTERNALS__.transformCallback(e,n)}async function r(e,n={},r){return window.__TAURI_INTERNALS__.invoke(e,n,r)}var t;async function i(e,t,i){const a=(void 0,{kind:"Any"});return r("plugin:event|listen",{event:e,target:a,handler:n(t)}).then((n=>async()=>async function(e,n){await r("plugin:event|unlisten",{event:e,eventId:n})}(e,n)))}async function a(){return await r("plugin:deep-link|get_current")}return"function"==typeof SuppressedError&&SuppressedError,function(e){e.WINDOW_RESIZED="tauri://resize",e.WINDOW_MOVED="tauri://move",e.WINDOW_CLOSE_REQUESTED="tauri://close-requested",e.WINDOW_DESTROYED="tauri://destroyed",e.WINDOW_FOCUS="tauri://focus",e.WINDOW_BLUR="tauri://blur",e.WINDOW_SCALE_FACTOR_CHANGED="tauri://scale-change",e.WINDOW_THEME_CHANGED="tauri://theme-changed",e.WINDOW_CREATED="tauri://window-created",e.WEBVIEW_CREATED="tauri://webview-created",e.DRAG="tauri://drag",e.DROP="tauri://drop",e.DROP_OVER="tauri://drop-over",e.DROP_CANCELLED="tauri://drag-cancelled"}(t||(t={})),e.getCurrent=a,e.isRegistered=async function(e){return await r("plugin:deep-link|i_registered",{protocol:e})},e.onOpenUrl=async function(e){const n=await a();return null!=n&&e(n),await i("deep-link://new-url",(n=>{e(n.payload)}))},e.register=async function(e){return await r("plugin:deep-link|register",{protocol:e})},e.unregister=async function(e){return await r("plugin:deep-link|unregister",{protocol:e})},e}({});Object.defineProperty(window.__TAURI__,"deepLink",{value:__TAURI_PLUGIN_DEEPLINK__})} diff --git a/plugins/deep-link/build.rs b/plugins/deep-link/build.rs index 4e66abe6..4abbd2d7 100644 --- a/plugins/deep-link/build.rs +++ b/plugins/deep-link/build.rs @@ -6,7 +6,7 @@ mod config; use config::{AssociatedDomain, Config}; -const COMMANDS: &[&str] = &["get_current"]; +const COMMANDS: &[&str] = &["get_current", "register", "unregister", "is_registered"]; // TODO: Consider using activity-alias in case users may have multiple activities in their app. // TODO: Do we want to support the other path* configs too? @@ -48,7 +48,7 @@ fn main() { "DEEP LINK PLUGIN", "activity", config - .domains + .mobile .iter() .map(intent_filter) .collect::>() @@ -62,7 +62,7 @@ fn main() { entitlements.insert( "com.apple.developer.associated-domains".into(), config - .domains + .mobile .into_iter() .map(|d| format!("applinks:{}", d.host).into()) .collect::>() diff --git a/plugins/deep-link/examples/app/src-tauri/tauri.conf.json b/plugins/deep-link/examples/app/src-tauri/tauri.conf.json index bec5fb76..fab85cb7 100644 --- a/plugins/deep-link/examples/app/src-tauri/tauri.conf.json +++ b/plugins/deep-link/examples/app/src-tauri/tauri.conf.json @@ -28,15 +28,13 @@ "hello": "world" }, "deep-link": { - "domains": [ - { - "host": "fabianlars.de", - "pathPrefix": ["/intent"] - }, - { - "host": "tauri.app" - } - ] + "mobile": [ + { "host": "fabianlars.de", "pathPrefix": ["/intent"] }, + { "host": "tauri.app" } + ], + "desktop": { + "schemes": ["fabianlars", "my-tauri-app"] + } } }, "bundle": { diff --git a/plugins/deep-link/examples/app/tsconfig.json b/plugins/deep-link/examples/app/tsconfig.json index 43d4bc70..c0f337a2 100644 --- a/plugins/deep-link/examples/app/tsconfig.json +++ b/plugins/deep-link/examples/app/tsconfig.json @@ -3,7 +3,7 @@ "target": "ESNext", "module": "ESNext", "lib": ["ESNext", "DOM"], - "moduleResolution": "Node", + "moduleResolution": "bundler", "strict": true, "sourceMap": true, "resolveJsonModule": true, diff --git a/plugins/deep-link/guest-js/index.ts b/plugins/deep-link/guest-js/index.ts index 4e076c1b..105d0b6a 100644 --- a/plugins/deep-link/guest-js/index.ts +++ b/plugins/deep-link/guest-js/index.ts @@ -5,12 +5,95 @@ import { invoke } from "@tauri-apps/api/core"; import { type UnlistenFn, listen } from "@tauri-apps/api/event"; +/** + * Get the current URLs that triggered the deep link. Use this on app load to check whether your app was started via a deep link. + * + * @example + * ```typescript + * import { getCurrent } from '@tauri-apps/plugin-deep-link'; + * const urls = await getCurrent(); + * ``` + * + * #### - **Windows / Linux**: Unsupported. + * + * @since 2.0.0 + */ export async function getCurrent(): Promise { - return await invoke("plugin:deep-link|get_current"); + return await invoke("plugin:deep-link|get_current"); +} + +/** + * Register the app as the default handler for the specified protocol. + * + * @param protocol The name of the protocol without `://`. For example, if you want your app to handle `tauri://` links, call this method with `tauri` as the protocol. + * + * @example + * ```typescript + * import { register } from '@tauri-apps/plugin-deep-link'; + * await register("my-scheme"); + * ``` + * + * #### - **macOS / Android / iOS**: Unsupported. + * + * @since 2.0.0 + */ +export async function register(protocol: string): Promise { + return await invoke("plugin:deep-link|register", { protocol }); +} + +/** + * Unregister the app as the default handler for the specified protocol. + * + * @param protocol The name of the protocol without `://`. + * + * @example + * ```typescript + * import { unregister } from '@tauri-apps/plugin-deep-link'; + * await unregister("my-scheme"); + * ``` + * + * #### - **macOS / Linux / Android / iOS**: Unsupported. + * + * @since 2.0.0 + */ +export async function unregister(protocol: string): Promise { + return await invoke("plugin:deep-link|unregister", { protocol }); +} - // return await invoke("plugin:deep-link|get_current"); +/** + * Check whether the app is the default handler for the specified protocol. + * + * @param protocol The name of the protocol without `://`. + * + * @example + * ```typescript + * import { isRegistered } from '@tauri-apps/plugin-deep-link'; + * await isRegistered("my-scheme"); + * ``` + * + * #### - **macOS / Android / iOS**: Unsupported, always returns `true`. + * + * @since 2.0.0 + */ +export async function isRegistered(protocol: string): Promise { + return await invoke("plugin:deep-link|i_registered", { protocol }); } +/** + * Helper function for the `deep-link://new-url` event to run a function each time the protocol is triggered while the app is running. Use `getCurrent` on app load to check whether your app was started via a deep link. + * + * @param protocol The name of the protocol without `://`. + * + * @example + * ```typescript + * import { onOpenUrl } from '@tauri-apps/plugin-deep-link'; + * await onOpenUrl((urls) => { console.log(urls) }); + * ``` + * + * #### - **Windows / Linux**: Unsupported, the OS will spawn a new app instance passing the URL as a CLI argument. + * + * @since 2.0.0 + */ export async function onOpenUrl( handler: (urls: string[]) => void, ): Promise { diff --git a/plugins/deep-link/permissions/autogenerated/commands/is_registered.toml b/plugins/deep-link/permissions/autogenerated/commands/is_registered.toml new file mode 100644 index 00000000..2dd73ace --- /dev/null +++ b/plugins/deep-link/permissions/autogenerated/commands/is_registered.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-is-registered" +description = "Enables the is_registered command without any pre-configured scope." +commands.allow = ["is_registered"] + +[[permission]] +identifier = "deny-is-registered" +description = "Denies the is_registered command without any pre-configured scope." +commands.deny = ["is_registered"] diff --git a/plugins/deep-link/permissions/autogenerated/commands/register.toml b/plugins/deep-link/permissions/autogenerated/commands/register.toml new file mode 100644 index 00000000..4eec17dc --- /dev/null +++ b/plugins/deep-link/permissions/autogenerated/commands/register.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-register" +description = "Enables the register command without any pre-configured scope." +commands.allow = ["register"] + +[[permission]] +identifier = "deny-register" +description = "Denies the register command without any pre-configured scope." +commands.deny = ["register"] diff --git a/plugins/deep-link/permissions/autogenerated/commands/unregister.toml b/plugins/deep-link/permissions/autogenerated/commands/unregister.toml new file mode 100644 index 00000000..5d33c97c --- /dev/null +++ b/plugins/deep-link/permissions/autogenerated/commands/unregister.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-unregister" +description = "Enables the unregister command without any pre-configured scope." +commands.allow = ["unregister"] + +[[permission]] +identifier = "deny-unregister" +description = "Denies the unregister command without any pre-configured scope." +commands.deny = ["unregister"] diff --git a/plugins/deep-link/permissions/autogenerated/reference.md b/plugins/deep-link/permissions/autogenerated/reference.md index a60764a3..583e39de 100644 --- a/plugins/deep-link/permissions/autogenerated/reference.md +++ b/plugins/deep-link/permissions/autogenerated/reference.md @@ -2,4 +2,10 @@ |------|-----| |`allow-get-current`|Enables the get_current command without any pre-configured scope.| |`deny-get-current`|Denies the get_current command without any pre-configured scope.| +|`allow-is-registered`|Enables the is_registered command without any pre-configured scope.| +|`deny-is-registered`|Denies the is_registered command without any pre-configured scope.| +|`allow-register`|Enables the register command without any pre-configured scope.| +|`deny-register`|Denies the register command without any pre-configured scope.| +|`allow-unregister`|Enables the unregister command without any pre-configured scope.| +|`deny-unregister`|Denies the unregister command without any pre-configured scope.| |`default`|Allows reading the opened deep link via the get_current command| diff --git a/plugins/deep-link/permissions/schemas/schema.json b/plugins/deep-link/permissions/schemas/schema.json index b9ca7438..3589379a 100644 --- a/plugins/deep-link/permissions/schemas/schema.json +++ b/plugins/deep-link/permissions/schemas/schema.json @@ -308,6 +308,48 @@ "deny-get-current" ] }, + { + "description": "allow-is-registered -> Enables the is_registered command without any pre-configured scope.", + "type": "string", + "enum": [ + "allow-is-registered" + ] + }, + { + "description": "deny-is-registered -> Denies the is_registered command without any pre-configured scope.", + "type": "string", + "enum": [ + "deny-is-registered" + ] + }, + { + "description": "allow-register -> Enables the register command without any pre-configured scope.", + "type": "string", + "enum": [ + "allow-register" + ] + }, + { + "description": "deny-register -> Denies the register command without any pre-configured scope.", + "type": "string", + "enum": [ + "deny-register" + ] + }, + { + "description": "allow-unregister -> Enables the unregister command without any pre-configured scope.", + "type": "string", + "enum": [ + "allow-unregister" + ] + }, + { + "description": "deny-unregister -> Denies the unregister command without any pre-configured scope.", + "type": "string", + "enum": [ + "deny-unregister" + ] + }, { "description": "default -> Allows reading the opened deep link via the get_current command", "type": "string", diff --git a/plugins/deep-link/src/commands.rs b/plugins/deep-link/src/commands.rs index 4b228e4c..078adfb1 100644 --- a/plugins/deep-link/src/commands.rs +++ b/plugins/deep-link/src/commands.rs @@ -14,3 +14,33 @@ pub(crate) async fn get_current( ) -> Result>> { deep_link.get_current() } + +#[command] +pub(crate) async fn register( + _app: AppHandle, + _window: Window, + deep_link: State<'_, DeepLink>, + protocol: String, +) -> Result<()> { + deep_link.register(protocol) +} + +#[command] +pub(crate) async fn unregister( + _app: AppHandle, + _window: Window, + deep_link: State<'_, DeepLink>, + protocol: String, +) -> Result<()> { + deep_link.unregister(protocol) +} + +#[command] +pub(crate) async fn is_registered( + _app: AppHandle, + _window: Window, + deep_link: State<'_, DeepLink>, + protocol: String, +) -> Result { + deep_link.is_registered(protocol) +} diff --git a/plugins/deep-link/src/config.rs b/plugins/deep-link/src/config.rs index 80f0a4c0..f0db1daf 100644 --- a/plugins/deep-link/src/config.rs +++ b/plugins/deep-link/src/config.rs @@ -4,9 +4,8 @@ // This module is also imported in build.rs! -#![allow(dead_code)] - use serde::{Deserialize, Deserializer}; +use tauri_utils::config::DeepLinkProtocol; #[derive(Deserialize)] pub struct AssociatedDomain { @@ -32,5 +31,16 @@ where #[derive(Deserialize)] pub struct Config { - pub domains: Vec, + /// Mobile requires `https://` urls. + pub mobile: Vec, + /// Desktop requires urls starting with `://`. + /// These urls are also active in dev mode on Android. + pub desktop: DesktopProtocol, +} + +#[derive(Deserialize)] +#[serde(untagged)] +pub enum DesktopProtocol { + One(DeepLinkProtocol), + List(Vec), } diff --git a/plugins/deep-link/src/error.rs b/plugins/deep-link/src/error.rs index 339e763b..88c71e8a 100644 --- a/plugins/deep-link/src/error.rs +++ b/plugins/deep-link/src/error.rs @@ -8,8 +8,21 @@ pub type Result = std::result::Result; #[derive(Debug, thiserror::Error)] pub enum Error { + #[error("unsupported platform")] + UnsupportedPlatform, #[error(transparent)] Io(#[from] std::io::Error), + #[error(transparent)] + Tauri(#[from] tauri::Error), + #[cfg(target_os = "windows")] + #[error(transparent)] + Windows(#[from] windows_result::Error), + #[cfg(target_os = "linux")] + #[error(transparent)] + Ini(#[from] ini::Error), + #[cfg(target_os = "linux")] + #[error(transparent)] + ParseIni(#[from] ini::ParseError), #[cfg(mobile)] #[error(transparent)] PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError), diff --git a/plugins/deep-link/src/lib.rs b/plugins/deep-link/src/lib.rs index bed7e8fb..290b8f57 100644 --- a/plugins/deep-link/src/lib.rs +++ b/plugins/deep-link/src/lib.rs @@ -82,32 +82,279 @@ mod imp { pub struct DeepLink(pub(crate) PluginHandle); impl DeepLink { - /// Get the current URLs that triggered the deep link. + /// Get the current URLs that triggered the deep link. Use this on app load to check whether your app was started via a deep link. + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux**: Unsupported, will return [`Error::UnsupportedPlatform`](`crate::Error::UnsupportedPlatform`). pub fn get_current(&self) -> crate::Result>> { self.0 .run_mobile_plugin::("getCurrent", ()) .map(|v| v.url.map(|url| vec![url])) .map_err(Into::into) } + + /// Register the app as the default handler for the specified protocol. + /// + /// - `protocol`: The name of the protocol without `://`. For example, if you want your app to handle `tauri://` links, call this method with `tauri` as the protocol. + /// + /// ## Platform-specific: + /// + /// - **macOS / Android / iOS**: Unsupported, will return [`Error::UnsupportedPlatform`](`crate::Error::UnsupportedPlatform`). + pub fn register>(&self, _protocol: S) -> crate::Result<()> { + Err(crate::Error::UnsupportedPlatform) + } + + /// Unregister the app as the default handler for the specified protocol. + /// + /// - `protocol`: The name of the protocol without `://`. + /// + /// ## Platform-specific: + /// + /// - **Linux**: Can only unregister the scheme if it was initially registered with [`register`](`Self::register`). May not work on older distros. + /// - **macOS / Android / iOS**: Unsupported, will return [`Error::UnsupportedPlatform`](`crate::Error::UnsupportedPlatform`). + pub fn unregister>(&self, _protocol: S) -> crate::Result<()> { + Err(crate::Error::UnsupportedPlatform) + } + + /// Check whether the app is the default handler for the specified protocol. + /// + /// - `protocol`: The name of the protocol without `://`. + /// + /// ## Platform-specific: + /// + /// - **macOS / Android / iOS**: Unsupported, will return [`Error::UnsupportedPlatform`](`crate::Error::UnsupportedPlatform`). + pub fn is_registered>(&self, _protocol: S) -> crate::Result { + Err(crate::Error::UnsupportedPlatform) + } } } #[cfg(not(target_os = "android"))] mod imp { use std::sync::Mutex; + #[cfg(target_os = "linux")] + use std::{ + fs::{create_dir_all, File}, + io::Write, + process::Command, + }; + #[cfg(target_os = "linux")] + use tauri::Manager; use tauri::{AppHandle, Runtime}; + #[cfg(windows)] + use windows_registry::CURRENT_USER; /// Access to the deep-link APIs. pub struct DeepLink { #[allow(dead_code)] pub(crate) app: AppHandle, + #[allow(dead_code)] pub(crate) current: Mutex>>, } impl DeepLink { - /// Get the current URLs that triggered the deep link. + /// Get the current URLs that triggered the deep link. Use this on app load to check whether your app was started via a deep link. + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux**: Unsupported, will return [`Error::UnsupportedPlatform`](`crate::Error::UnsupportedPlatform`). pub fn get_current(&self) -> crate::Result>> { - Ok(self.current.lock().unwrap().clone()) + #[cfg(not(any(windows, target_os = "linux")))] + return Ok(self.current.lock().unwrap().clone()); + #[cfg(any(windows, target_os = "linux"))] + Err(crate::Error::UnsupportedPlatform) + } + + /// Register the app as the default handler for the specified protocol. + /// + /// - `protocol`: The name of the protocol without `://`. For example, if you want your app to handle `tauri://` links, call this method with `tauri` as the protocol. + /// + /// ## Platform-specific: + /// + /// - **macOS / Android / iOS**: Unsupported, will return [`Error::UnsupportedPlatform`](`crate::Error::UnsupportedPlatform`). + pub fn register>(&self, _protocol: S) -> crate::Result<()> { + #[cfg(windows)] + { + let key_base = format!("Software\\Classes\\{}", _protocol.as_ref()); + + let exe = dunce::simplified(&tauri::utils::platform::current_exe()?) + .display() + .to_string(); + + let key_reg = CURRENT_USER.create(&key_base)?; + key_reg.set_string( + "", + &format!("URL:{} protocol", self.app.config().identifier), + )?; + key_reg.set_string("URL Protocol", "")?; + + let icon_reg = CURRENT_USER.create(format!("{key_base}\\DefaultIcon"))?; + icon_reg.set_string("", &format!("{},0", &exe))?; + + let cmd_reg = CURRENT_USER.create(format!("{key_base}\\shell\\open\\command"))?; + + cmd_reg.set_string("", &format!("{} \"%1\"", &exe))?; + + Ok(()) + } + + #[cfg(target_os = "linux")] + { + let bin = tauri::utils::platform::current_exe()?; + let file_name = format!( + "{}-handler.desktop", + bin.file_name().unwrap().to_string_lossy() + ); + let appimage = self.app.env().appimage; + let exec = appimage + .clone() + .unwrap_or_else(|| bin.into_os_string()) + .to_string_lossy() + .to_string(); + + let target = self.app.path().data_dir()?.join("applications"); + + create_dir_all(&target)?; + + let target_file = target.join(&file_name); + + let mime_type = format!("x-scheme-handler/{};", _protocol.as_ref()); + + if let Ok(mut desktop_file) = ini::Ini::load_from_file(&target_file) { + if let Some(section) = desktop_file.section_mut(Some("Desktop Entry")) { + if let Some(mimes) = section.remove("MimeType") { + section.append("MimeType", format!("{mimes};{mime_type};")) + } else { + section.append("MimeType", format!("{mime_type};")) + } + desktop_file.write_to_file(&target_file)?; + } + } else { + let mut file = File::create(target_file)?; + file.write_all( + format!( + include_str!("template.desktop"), + name = self + .app + .config() + .product_name + .clone() + .unwrap_or_else(|| file_name.clone()), + exec = exec, + mime_type = mime_type + ) + .as_bytes(), + )?; + } + + Command::new("update-desktop-database") + .arg(target) + .status()?; + + Command::new("xdg-mime") + .args(["default", &file_name, _protocol.as_ref()]) + .status()?; + + Ok(()) + } + + #[cfg(not(any(windows, target_os = "linux")))] + Err(crate::Error::UnsupportedPlatform) + } + + /// Unregister the app as the default handler for the specified protocol. + /// + /// - `protocol`: The name of the protocol without `://`. + /// + /// ## Platform-specific: + /// + /// - **Linux**: Can only unregister the scheme if it was initially registered with [`register`](`Self::register`). May not work on older distros. + /// - **macOS / Android / iOS**: Unsupported, will return [`Error::UnsupportedPlatform`](`crate::Error::UnsupportedPlatform`). + pub fn unregister>(&self, _protocol: S) -> crate::Result<()> { + #[cfg(windows)] + { + CURRENT_USER.remove_tree(format!("Software\\Classes\\{}", _protocol.as_ref()))?; + + Ok(()) + } + + #[cfg(target_os = "linux")] + { + let mimeapps_path = self.app.path().config_dir()?.join("mimeapps.list"); + let mut mimeapps = ini::Ini::load_from_file(&mimeapps_path)?; + + let file_name = format!( + "{}-handler.desktop", + tauri::utils::platform::current_exe()? + .file_name() + .unwrap() + .to_string_lossy() + ); + + if let Some(section) = mimeapps.section_mut(Some("Default Applications")) { + let scheme = format!("x-scheme-handler/{}", _protocol.as_ref()); + + if section.get(&scheme).unwrap_or_default() == file_name { + section.remove(scheme); + } + } + + mimeapps.write_to_file(mimeapps_path)?; + + Ok(()) + } + + #[cfg(not(any(windows, target_os = "linux")))] + Err(crate::Error::UnsupportedPlatform) + } + + /// Check whether the app is the default handler for the specified protocol. + /// + /// - `protocol`: The name of the protocol without `://`. + /// + /// ## Platform-specific: + /// + /// - **macOS / Android / iOS**: Unsupported, will return [`Error::UnsupportedPlatform`](`crate::Error::UnsupportedPlatform`). + pub fn is_registered>(&self, _protocol: S) -> crate::Result { + #[cfg(windows)] + { + let cmd_reg = CURRENT_USER.open(format!( + "Software\\Classes\\{}\\shell\\open\\command", + _protocol.as_ref() + ))?; + + let registered_cmd: String = cmd_reg.get_string("")?; + + let exe = dunce::simplified(&tauri::utils::platform::current_exe()?) + .display() + .to_string(); + + Ok(registered_cmd == format!("{} \"%1\"", &exe)) + } + #[cfg(target_os = "linux")] + { + let file_name = format!( + "{}-handler.desktop", + tauri::utils::platform::current_exe()? + .file_name() + .unwrap() + .to_string_lossy() + ); + + let output = Command::new("xdg-mime") + .args([ + "query", + "default", + &format!("x-scheme-handler/{}", _protocol.as_ref()), + ]) + .output()?; + + Ok(String::from_utf8_lossy(&output.stdout).contains(&file_name)) + } + + #[cfg(not(any(windows, target_os = "linux")))] + Err(crate::Error::UnsupportedPlatform) } } } @@ -128,7 +375,12 @@ impl> crate::DeepLinkExt for T { /// Initializes the plugin. pub fn init() -> TauriPlugin> { Builder::new("deep-link") - .invoke_handler(tauri::generate_handler![commands::get_current]) + .invoke_handler(tauri::generate_handler![ + commands::get_current, + commands::register, + commands::unregister, + commands::is_registered + ]) .setup(|app, api| { app.manage(init_deep_link(app, api)?); Ok(()) diff --git a/plugins/deep-link/src/template.desktop b/plugins/deep-link/src/template.desktop new file mode 100644 index 00000000..0fb89abb --- /dev/null +++ b/plugins/deep-link/src/template.desktop @@ -0,0 +1,7 @@ +[Desktop Entry] +Type=Application +Name={name} +Exec={exec} %u +Terminal=false +MimeType={mime_type} +NoDisplay=true \ No newline at end of file