feat(deep-link): implement getCurrent on Windows/Linux (#1759)

* feat(deep-link): implement getCurrent on Windows/Linux

checks std::env::args() on initialization, also includes integration with the single-instance plugin

* fmt

* update docs, fix event

* add register_all function

* expose api

* be nicer on docs, clippy
This commit is contained in:
Lucas Fernandes Nogueira
2024-09-10 16:00:42 -03:00
committed by GitHub
parent 77680f6ed8
commit 64a6240f79
13 changed files with 171 additions and 24 deletions
+26 -3
View File
@@ -7,7 +7,7 @@
use serde::{Deserialize, Deserializer};
use tauri_utils::config::DeepLinkProtocol;
#[derive(Deserialize)]
#[derive(Deserialize, Clone)]
pub struct AssociatedDomain {
#[serde(deserialize_with = "deserialize_associated_host")]
pub host: String,
@@ -29,7 +29,7 @@ where
}
}
#[derive(Deserialize)]
#[derive(Deserialize, Clone)]
pub struct Config {
/// Mobile requires `https://<host>` urls.
#[serde(default)]
@@ -41,7 +41,7 @@ pub struct Config {
pub desktop: DesktopProtocol,
}
#[derive(Deserialize)]
#[derive(Deserialize, Clone)]
#[serde(untagged)]
#[allow(unused)] // Used in tauri-bundler
pub enum DesktopProtocol {
@@ -54,3 +54,26 @@ impl Default for DesktopProtocol {
Self::List(Vec::new())
}
}
impl DesktopProtocol {
#[allow(dead_code)]
pub fn contains_scheme(&self, scheme: &String) -> bool {
match self {
Self::One(protocol) => protocol.schemes.contains(scheme),
Self::List(protocols) => protocols
.iter()
.any(|protocol| protocol.schemes.contains(scheme)),
}
}
#[allow(dead_code)]
pub fn schemes(&self) -> Vec<String> {
match self {
Self::One(protocol) => protocol.schemes.clone(),
Self::List(protocols) => protocols
.iter()
.flat_map(|protocol| protocol.schemes.clone())
.collect(),
}
}
}
+94 -16
View File
@@ -2,7 +2,6 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use serde::de::DeserializeOwned;
use tauri::{
plugin::{Builder, PluginApi, TauriPlugin},
AppHandle, Manager, Runtime,
@@ -17,12 +16,14 @@ pub use error::{Error, Result};
#[cfg(target_os = "android")]
const PLUGIN_IDENTIFIER: &str = "app.tauri.deep_link";
fn init_deep_link<R: Runtime, C: DeserializeOwned>(
fn init_deep_link<R: Runtime>(
app: &AppHandle<R>,
_api: PluginApi<R, C>,
api: PluginApi<R, Option<config::Config>>,
) -> crate::Result<DeepLink<R>> {
#[cfg(target_os = "android")]
{
let _api = api;
use tauri::{
ipc::{Channel, InvokeResponseBody},
Emitter,
@@ -59,11 +60,28 @@ fn init_deep_link<R: Runtime, C: DeserializeOwned>(
return Ok(DeepLink(handle));
}
#[cfg(not(target_os = "android"))]
Ok(DeepLink {
#[cfg(target_os = "ios")]
return Ok(DeepLink {
app: app.clone(),
current: Default::default(),
})
config: api.config().clone(),
});
#[cfg(desktop)]
{
let args = std::env::args();
let current = if let Some(config) = api.config() {
imp::deep_link_from_args(config, args)
} else {
None
};
Ok(DeepLink {
app: app.clone(),
current: std::sync::Mutex::new(current.map(|url| vec![url])),
config: api.config().clone(),
})
}
}
#[cfg(target_os = "android")]
@@ -90,10 +108,6 @@ mod imp {
impl<R: Runtime> DeepLink<R> {
/// 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<Option<Vec<url::Url>>> {
self.0
.run_mobile_plugin::<GetCurrentResponse>("getCurrent", ())
@@ -154,23 +168,87 @@ mod imp {
/// Access to the deep-link APIs.
pub struct DeepLink<R: Runtime> {
#[allow(dead_code)]
pub(crate) app: AppHandle<R>,
#[allow(dead_code)]
pub(crate) current: Mutex<Option<Vec<url::Url>>>,
pub(crate) config: Option<crate::config::Config>,
}
pub(crate) fn deep_link_from_args<S: AsRef<str>, I: Iterator<Item = S>>(
config: &crate::config::Config,
mut args: I,
) -> Option<url::Url> {
if cfg!(windows) || cfg!(target_os = "linux") {
args.next(); // bin name
let arg = args.next();
let maybe_deep_link = args.next().is_none(); // single argument
if !maybe_deep_link {
return None;
}
if let Some(url) = arg.and_then(|arg| arg.as_ref().parse::<url::Url>().ok()) {
if config.desktop.contains_scheme(&url.scheme().to_string()) {
return Some(url);
} else if cfg!(debug_assertions) {
log::warn!("argument {url} does not match any configured deep link scheme; skipping it");
}
}
}
None
}
impl<R: Runtime> DeepLink<R> {
/// Checks if the provided list of arguments (which should match [`std::env::args`])
/// contains a deep link argument (for Linux and Windows).
///
/// On Linux and Windows the deep links trigger a new app instance with the deep link URL as its only argument.
///
/// This function does what it can to verify if the argument is actually a deep link, though it could also be a regular CLI argument.
/// To enhance its checks, we only match deep links against the schemes defined in the Tauri configuration
/// i.e. dynamic schemes WON'T be processed.
///
/// This function updates the [`Self::get_current`] value and emits a `deep-link://new-url` event.
#[cfg(desktop)]
pub fn handle_cli_arguments<S: AsRef<str>, I: Iterator<Item = S>>(&self, args: I) {
use tauri::Emitter;
let Some(config) = &self.config else {
return;
};
if let Some(url) = deep_link_from_args(config, args) {
let mut current = self.current.lock().unwrap();
current.replace(vec![url.clone()]);
let _ = self.app.emit("deep-link://new-url", vec![url]);
}
}
/// 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`).
/// - **Windows / Linux**: This function reads the command line arguments and checks if there's only one value, which must be an URL with scheme matching one of the configured values.
/// Note that you must manually check the arguments when registering deep link schemes dynamically with [`Self::register`].
/// Additionally, the deep link might have been provided as a CLI argument so you should check if its format matches what you expect.
pub fn get_current(&self) -> crate::Result<Option<Vec<url::Url>>> {
#[cfg(not(any(windows, target_os = "linux")))]
return Ok(self.current.lock().unwrap().clone());
#[cfg(any(windows, target_os = "linux"))]
Err(crate::Error::UnsupportedPlatform)
}
/// Registers all schemes defined in the configuration file.
///
/// This is useful to ensure the schemes are registered even if the user did not install the app properly
/// (e.g. an AppImage that was not properly registered with an AppImage launcher).
pub fn register_all(&self) -> crate::Result<()> {
let Some(config) = &self.config else {
return Ok(());
};
for scheme in config.desktop.schemes() {
self.register(scheme)?;
}
Ok(())
}
/// Register the app as the default handler for the specified protocol.