tauri/
plugin.rs

1// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-License-Identifier: MIT
4
5//! The Tauri plugin extension to expand Tauri functionality.
6
7use crate::{
8  app::UriSchemeResponder,
9  ipc::{Invoke, InvokeHandler, ScopeObject, ScopeValue},
10  manager::webview::UriSchemeProtocol,
11  utils::config::PluginConfig,
12  webview::PageLoadPayload,
13  AppHandle, Error, RunEvent, Runtime, UriSchemeContext, Webview, Window,
14};
15use serde::{
16  de::{Deserialize, DeserializeOwned, Deserializer, Error as DeError},
17  Serialize, Serializer,
18};
19use serde_json::Value as JsonValue;
20use tauri_macros::default_runtime;
21use thiserror::Error;
22use url::Url;
23
24use std::{
25  borrow::Cow,
26  collections::HashMap,
27  fmt::{self, Debug},
28  sync::Arc,
29};
30
31/// Mobile APIs.
32#[cfg(mobile)]
33pub mod mobile;
34
35/// The plugin interface.
36pub trait Plugin<R: Runtime>: Send {
37  /// The plugin name. Used as key on the plugin config object.
38  fn name(&self) -> &'static str;
39
40  /// Initializes the plugin.
41  #[allow(unused_variables)]
42  fn initialize(
43    &mut self,
44    app: &AppHandle<R>,
45    config: JsonValue,
46  ) -> Result<(), Box<dyn std::error::Error>> {
47    Ok(())
48  }
49
50  /// Add the provided JavaScript to a list of scripts that should be run after the global object has been created,
51  /// but before the HTML document has been parsed and before any other script included by the HTML document is run.
52  ///
53  /// Since it runs on all top-level document and child frame page navigations,
54  /// it's recommended to check the `window.location` to guard your script from running on unexpected origins.
55  ///
56  /// The script is wrapped into its own context with `(function () { /* your script here */ })();`,
57  /// so global variables must be assigned to `window` instead of implicitly declared.
58  fn initialization_script(&self) -> Option<String> {
59    None
60  }
61
62  /// Callback invoked when the window is created.
63  #[allow(unused_variables)]
64  fn window_created(&mut self, window: Window<R>) {}
65
66  /// Callback invoked when the webview is created.
67  #[allow(unused_variables)]
68  fn webview_created(&mut self, webview: Webview<R>) {}
69
70  /// Callback invoked when webview tries to navigate to the given Url. Returning falses cancels navigation.
71  #[allow(unused_variables)]
72  fn on_navigation(&mut self, webview: &Webview<R>, url: &Url) -> bool {
73    true
74  }
75
76  /// Callback invoked when the webview performs a navigation to a page.
77  #[allow(unused_variables)]
78  fn on_page_load(&mut self, webview: &Webview<R>, payload: &PageLoadPayload<'_>) {}
79
80  /// Callback invoked when the event loop receives a new event.
81  #[allow(unused_variables)]
82  fn on_event(&mut self, app: &AppHandle<R>, event: &RunEvent) {}
83
84  /// Extend commands to [`crate::Builder::invoke_handler`].
85  #[allow(unused_variables)]
86  fn extend_api(&mut self, invoke: Invoke<R>) -> bool {
87    false
88  }
89}
90
91type SetupHook<R, C> =
92  dyn FnOnce(&AppHandle<R>, PluginApi<R, C>) -> Result<(), Box<dyn std::error::Error>> + Send;
93type OnWindowReady<R> = dyn FnMut(Window<R>) + Send;
94type OnWebviewReady<R> = dyn FnMut(Webview<R>) + Send;
95type OnEvent<R> = dyn FnMut(&AppHandle<R>, &RunEvent) + Send;
96type OnNavigation<R> = dyn Fn(&Webview<R>, &Url) -> bool + Send;
97type OnPageLoad<R> = dyn FnMut(&Webview<R>, &PageLoadPayload<'_>) + Send;
98type OnDrop<R> = dyn FnOnce(AppHandle<R>) + Send;
99
100/// A handle to a plugin.
101#[derive(Debug)]
102#[allow(dead_code)]
103pub struct PluginHandle<R: Runtime> {
104  name: &'static str,
105  handle: AppHandle<R>,
106}
107
108impl<R: Runtime> Clone for PluginHandle<R> {
109  fn clone(&self) -> Self {
110    Self {
111      name: self.name,
112      handle: self.handle.clone(),
113    }
114  }
115}
116
117impl<R: Runtime> PluginHandle<R> {
118  /// Returns the application handle.
119  pub fn app(&self) -> &AppHandle<R> {
120    &self.handle
121  }
122}
123
124/// Api exposed to the plugin setup hook.
125#[derive(Clone)]
126#[allow(dead_code)]
127pub struct PluginApi<R: Runtime, C: DeserializeOwned> {
128  handle: AppHandle<R>,
129  name: &'static str,
130  raw_config: Arc<JsonValue>,
131  config: C,
132}
133
134impl<R: Runtime, C: DeserializeOwned> PluginApi<R, C> {
135  /// Returns the plugin configuration.
136  pub fn config(&self) -> &C {
137    &self.config
138  }
139
140  /// Returns the application handle.
141  pub fn app(&self) -> &AppHandle<R> {
142    &self.handle
143  }
144
145  /// Gets the global scope defined on the permissions that are part of the app ACL.
146  pub fn scope<T: ScopeObject>(&self) -> crate::Result<ScopeValue<T>> {
147    self
148      .handle
149      .manager
150      .runtime_authority
151      .lock()
152      .unwrap()
153      .scope_manager
154      .get_global_scope_typed(&self.handle, self.name)
155  }
156}
157
158/// Errors that can happen during [`Builder`].
159#[derive(Debug, Clone, Hash, PartialEq, Error)]
160#[non_exhaustive]
161pub enum BuilderError {
162  /// Plugin attempted to use a reserved name.
163  #[error("plugin uses reserved name: {0}")]
164  ReservedName(String),
165}
166
167const RESERVED_PLUGIN_NAMES: &[&str] = &["core", "tauri"];
168
169/// Builds a [`TauriPlugin`].
170///
171/// This Builder offers a more concise way to construct Tauri plugins than implementing the Plugin trait directly.
172///
173/// # Conventions
174///
175/// When using the Builder Pattern it is encouraged to export a function called `init` that constructs and returns the plugin.
176/// While plugin authors can provide every possible way to construct a plugin,
177/// sticking to the `init` function convention helps users to quickly identify the correct function to call.
178///
179/// ```rust
180/// use tauri::{plugin::{Builder, TauriPlugin}, Runtime};
181///
182/// pub fn init<R: Runtime>() -> TauriPlugin<R> {
183///   Builder::new("example")
184///     .build()
185/// }
186/// ```
187///
188/// When plugins expose more complex configuration options, it can be helpful to provide a Builder instead:
189///
190/// ```rust
191/// use tauri::{plugin::{Builder as PluginBuilder, TauriPlugin}, Runtime};
192///
193/// pub struct Builder {
194///   option_a: String,
195///   option_b: String,
196///   option_c: bool
197/// }
198///
199/// impl Default for Builder {
200///   fn default() -> Self {
201///     Self {
202///       option_a: "foo".to_string(),
203///       option_b: "bar".to_string(),
204///       option_c: false
205///     }
206///   }
207/// }
208///
209/// impl Builder {
210///   pub fn new() -> Self {
211///     Default::default()
212///   }
213///
214///   pub fn option_a(mut self, option_a: String) -> Self {
215///     self.option_a = option_a;
216///     self
217///   }
218///
219///   pub fn option_b(mut self, option_b: String) -> Self {
220///     self.option_b = option_b;
221///     self
222///   }
223///
224///   pub fn option_c(mut self, option_c: bool) -> Self {
225///     self.option_c = option_c;
226///     self
227///   }
228///
229///   pub fn build<R: Runtime>(self) -> TauriPlugin<R> {
230///     PluginBuilder::new("example")
231///       .setup(move |app_handle, api| {
232///         // use the options here to do stuff
233///         println!("a: {}, b: {}, c: {}", self.option_a, self.option_b, self.option_c);
234///
235///         Ok(())
236///       })
237///       .build()
238///   }
239/// }
240/// ```
241pub struct Builder<R: Runtime, C: DeserializeOwned = ()> {
242  name: &'static str,
243  invoke_handler: Box<InvokeHandler<R>>,
244  setup: Option<Box<SetupHook<R, C>>>,
245  js_init_script: Option<String>,
246  on_navigation: Box<OnNavigation<R>>,
247  on_page_load: Box<OnPageLoad<R>>,
248  on_window_ready: Box<OnWindowReady<R>>,
249  on_webview_ready: Box<OnWebviewReady<R>>,
250  on_event: Box<OnEvent<R>>,
251  on_drop: Option<Box<OnDrop<R>>>,
252  uri_scheme_protocols: HashMap<String, Arc<UriSchemeProtocol<R>>>,
253}
254
255impl<R: Runtime, C: DeserializeOwned> Builder<R, C> {
256  /// Creates a new Plugin builder.
257  pub fn new(name: &'static str) -> Self {
258    Self {
259      name,
260      setup: None,
261      js_init_script: None,
262      invoke_handler: Box::new(|_| false),
263      on_navigation: Box::new(|_, _| true),
264      on_page_load: Box::new(|_, _| ()),
265      on_window_ready: Box::new(|_| ()),
266      on_webview_ready: Box::new(|_| ()),
267      on_event: Box::new(|_, _| ()),
268      on_drop: None,
269      uri_scheme_protocols: Default::default(),
270    }
271  }
272
273  /// Defines the JS message handler callback.
274  /// It is recommended you use the [tauri::generate_handler] to generate the input to this method, as the input type is not considered stable yet.
275  ///
276  /// # Examples
277  ///
278  /// ```rust
279  /// use tauri::{plugin::{Builder, TauriPlugin}, Runtime};
280  ///
281  /// #[tauri::command]
282  /// async fn foobar<R: Runtime>(app: tauri::AppHandle<R>, window: tauri::Window<R>) -> Result<(), String> {
283  ///   println!("foobar");
284  ///
285  ///   Ok(())
286  /// }
287  ///
288  /// fn init<R: Runtime>() -> TauriPlugin<R> {
289  ///   Builder::new("example")
290  ///     .invoke_handler(tauri::generate_handler![foobar])
291  ///     .build()
292  /// }
293  ///
294  /// ```
295  /// [tauri::generate_handler]: ../macro.generate_handler.html
296  #[must_use]
297  pub fn invoke_handler<F>(mut self, invoke_handler: F) -> Self
298  where
299    F: Fn(Invoke<R>) -> bool + Send + Sync + 'static,
300  {
301    self.invoke_handler = Box::new(invoke_handler);
302    self
303  }
304
305  /// Sets the provided JavaScript to be run after the global object has been created,
306  /// but before the HTML document has been parsed and before any other script included by the HTML document is run.
307  ///
308  /// Since it runs on all top-level document and child frame page navigations,
309  /// it's recommended to check the `window.location` to guard your script from running on unexpected origins.
310  ///
311  /// The script is wrapped into its own context with `(function () { /* your script here */ })();`,
312  /// so global variables must be assigned to `window` instead of implicitly declared.
313  ///
314  /// Note that calling this function multiple times overrides previous values.
315  ///
316  /// # Examples
317  ///
318  /// ```rust
319  /// use tauri::{plugin::{Builder, TauriPlugin}, Runtime};
320  ///
321  /// const INIT_SCRIPT: &str = r#"
322  ///   if (window.location.origin === 'https://tauri.app') {
323  ///     console.log("hello world from js init script");
324  ///
325  ///     window.__MY_CUSTOM_PROPERTY__ = { foo: 'bar' };
326  ///   }
327  /// "#;
328  ///
329  /// fn init<R: Runtime>() -> TauriPlugin<R> {
330  ///   Builder::new("example")
331  ///     .js_init_script(INIT_SCRIPT.to_string())
332  ///     .build()
333  /// }
334  /// ```
335  #[must_use]
336  pub fn js_init_script(mut self, js_init_script: String) -> Self {
337    self.js_init_script = Some(js_init_script);
338    self
339  }
340
341  /// Define a closure that runs when the plugin is registered.
342  ///
343  /// # Examples
344  ///
345  /// ```rust
346  /// use tauri::{plugin::{Builder, TauriPlugin}, Runtime, Manager};
347  /// use std::path::PathBuf;
348  ///
349  /// #[derive(Debug, Default)]
350  /// struct PluginState {
351  ///    dir: Option<PathBuf>
352  /// }
353  ///
354  /// fn init<R: Runtime>() -> TauriPlugin<R> {
355  /// Builder::new("example")
356  ///   .setup(|app, api| {
357  ///     app.manage(PluginState::default());
358  ///
359  ///     Ok(())
360  ///   })
361  ///   .build()
362  /// }
363  /// ```
364  #[must_use]
365  pub fn setup<F>(mut self, setup: F) -> Self
366  where
367    F: FnOnce(&AppHandle<R>, PluginApi<R, C>) -> Result<(), Box<dyn std::error::Error>>
368      + Send
369      + 'static,
370  {
371    self.setup.replace(Box::new(setup));
372    self
373  }
374
375  /// Callback invoked when the webview tries to navigate to a URL. Returning false cancels the navigation.
376  ///
377  /// #Example
378  ///
379  /// ```
380  /// use tauri::{plugin::{Builder, TauriPlugin}, Runtime};
381  ///
382  /// fn init<R: Runtime>() -> TauriPlugin<R> {
383  ///   Builder::new("example")
384  ///     .on_navigation(|webview, url| {
385  ///       // allow the production URL or localhost on dev
386  ///       url.scheme() == "tauri" || (cfg!(dev) && url.host_str() == Some("localhost"))
387  ///     })
388  ///     .build()
389  /// }
390  /// ```
391  #[must_use]
392  pub fn on_navigation<F>(mut self, on_navigation: F) -> Self
393  where
394    F: Fn(&Webview<R>, &Url) -> bool + Send + 'static,
395  {
396    self.on_navigation = Box::new(on_navigation);
397    self
398  }
399
400  /// Callback invoked when the webview performs a navigation to a page.
401  ///
402  /// # Examples
403  ///
404  /// ```rust
405  /// use tauri::{plugin::{Builder, TauriPlugin}, Runtime};
406  ///
407  /// fn init<R: Runtime>() -> TauriPlugin<R> {
408  ///   Builder::new("example")
409  ///     .on_page_load(|webview, payload| {
410  ///       println!("{:?} URL {} in webview {}", payload.event(), payload.url(), webview.label());
411  ///     })
412  ///     .build()
413  /// }
414  /// ```
415  #[must_use]
416  pub fn on_page_load<F>(mut self, on_page_load: F) -> Self
417  where
418    F: FnMut(&Webview<R>, &PageLoadPayload<'_>) + Send + 'static,
419  {
420    self.on_page_load = Box::new(on_page_load);
421    self
422  }
423
424  /// Callback invoked when the window is created.
425  ///
426  /// # Examples
427  ///
428  /// ```rust
429  /// use tauri::{plugin::{Builder, TauriPlugin}, Runtime};
430  ///
431  /// fn init<R: Runtime>() -> TauriPlugin<R> {
432  ///   Builder::new("example")
433  ///     .on_window_ready(|window| {
434  ///       println!("created window {}", window.label());
435  ///     })
436  ///     .build()
437  /// }
438  /// ```
439  #[must_use]
440  pub fn on_window_ready<F>(mut self, on_window_ready: F) -> Self
441  where
442    F: FnMut(Window<R>) + Send + 'static,
443  {
444    self.on_window_ready = Box::new(on_window_ready);
445    self
446  }
447
448  /// Callback invoked when the webview is created.
449  ///
450  /// # Examples
451  ///
452  /// ```rust
453  /// use tauri::{plugin::{Builder, TauriPlugin}, Runtime};
454  ///
455  /// fn init<R: Runtime>() -> TauriPlugin<R> {
456  ///   Builder::new("example")
457  ///     .on_webview_ready(|webview| {
458  ///       println!("created webview {}", webview.label());
459  ///     })
460  ///     .build()
461  /// }
462  /// ```
463  #[must_use]
464  pub fn on_webview_ready<F>(mut self, on_webview_ready: F) -> Self
465  where
466    F: FnMut(Webview<R>) + Send + 'static,
467  {
468    self.on_webview_ready = Box::new(on_webview_ready);
469    self
470  }
471
472  /// Callback invoked when the event loop receives a new event.
473  ///
474  /// # Examples
475  ///
476  /// ```rust
477  /// use tauri::{plugin::{Builder, TauriPlugin}, RunEvent, Runtime};
478  ///
479  /// fn init<R: Runtime>() -> TauriPlugin<R> {
480  ///   Builder::new("example")
481  ///     .on_event(|app_handle, event| {
482  ///       match event {
483  ///         RunEvent::ExitRequested { api, .. } => {
484  ///           // Prevents the app from exiting.
485  ///           // This will cause the core thread to continue running in the background even without any open windows.
486  ///           api.prevent_exit();
487  ///         }
488  ///         // Ignore all other cases.
489  ///         _ => {}
490  ///       }
491  ///     })
492  ///     .build()
493  /// }
494  /// ```
495  #[must_use]
496  pub fn on_event<F>(mut self, on_event: F) -> Self
497  where
498    F: FnMut(&AppHandle<R>, &RunEvent) + Send + 'static,
499  {
500    self.on_event = Box::new(on_event);
501    self
502  }
503
504  /// Callback invoked when the plugin is dropped.
505  ///
506  /// # Examples
507  ///
508  /// ```rust
509  /// use tauri::{plugin::{Builder, TauriPlugin}, Runtime};
510  ///
511  /// fn init<R: Runtime>() -> TauriPlugin<R> {
512  ///   Builder::new("example")
513  ///     .on_drop(|app| {
514  ///       println!("plugin has been dropped and is no longer running");
515  ///       // you can run cleanup logic here
516  ///     })
517  ///     .build()
518  /// }
519  /// ```
520  #[must_use]
521  pub fn on_drop<F>(mut self, on_drop: F) -> Self
522  where
523    F: FnOnce(AppHandle<R>) + Send + 'static,
524  {
525    self.on_drop.replace(Box::new(on_drop));
526    self
527  }
528
529  /// Registers a URI scheme protocol available to all webviews.
530  ///
531  /// Leverages [setURLSchemeHandler](https://developer.apple.com/documentation/webkit/wkwebviewconfiguration/2875766-seturlschemehandler) on macOS,
532  /// [AddWebResourceRequestedFilter](https://docs.microsoft.com/en-us/dotnet/api/microsoft.web.webview2.core.corewebview2.addwebresourcerequestedfilter?view=webview2-dotnet-1.0.774.44) on Windows
533  /// and [webkit-web-context-register-uri-scheme](https://webkitgtk.org/reference/webkit2gtk/stable/WebKitWebContext.html#webkit-web-context-register-uri-scheme) on Linux.
534  ///
535  /// # Known limitations
536  ///
537  /// URI scheme protocols are registered when the webview is created. Due to this limitation, if the plugin is registered after a webview has been created, this protocol won't be available.
538  ///
539  /// # Arguments
540  ///
541  /// * `uri_scheme` The URI scheme to register, such as `example`.
542  /// * `protocol` the protocol associated with the given URI scheme. It's a function that takes an URL such as `example://localhost/asset.css`.
543  ///
544  /// # Examples
545  ///
546  /// ```rust
547  /// use tauri::{plugin::{Builder, TauriPlugin}, Runtime};
548  ///
549  /// fn init<R: Runtime>() -> TauriPlugin<R> {
550  ///   Builder::new("myplugin")
551  ///     .register_uri_scheme_protocol("myscheme", |_ctx, req| {
552  ///       http::Response::builder().body(Vec::new()).unwrap()
553  ///     })
554  ///     .build()
555  /// }
556  /// ```
557  ///
558  /// # Warning
559  ///
560  /// Pages loaded from a custom protocol will have a different Origin on different platforms.
561  /// Servers which enforce CORS will need to add the exact same Origin header (or `*`) in `Access-Control-Allow-Origin`
562  /// if you wish to send requests with native `fetch` and `XmlHttpRequest` APIs. Here are the
563  /// different Origin headers across platforms:
564  ///
565  /// - macOS, iOS and Linux: `<scheme_name>://localhost/<path>` (so it will be `my-scheme://localhost/path/to/page).
566  /// - Windows and Android: `http://<scheme_name>.localhost/<path>` by default (so it will be `http://my-scheme.localhost/path/to/page`).
567  ///   To use `https` instead of `http`, use [`super::webview::WebviewBuilder::use_https_scheme`].
568  #[must_use]
569  pub fn register_uri_scheme_protocol<
570    N: Into<String>,
571    T: Into<Cow<'static, [u8]>>,
572    H: Fn(UriSchemeContext<'_, R>, http::Request<Vec<u8>>) -> http::Response<T>
573      + Send
574      + Sync
575      + 'static,
576  >(
577    mut self,
578    uri_scheme: N,
579    protocol: H,
580  ) -> Self {
581    self.uri_scheme_protocols.insert(
582      uri_scheme.into(),
583      Arc::new(UriSchemeProtocol {
584        protocol: Box::new(move |ctx, request, responder| {
585          responder.respond(protocol(ctx, request))
586        }),
587      }),
588    );
589    self
590  }
591
592  /// Similar to [`Self::register_uri_scheme_protocol`] but with an asynchronous responder that allows you
593  /// to process the request in a separate thread and respond asynchronously.
594  ///
595  /// # Arguments
596  ///
597  /// * `uri_scheme` The URI scheme to register, such as `example`.
598  /// * `protocol` the protocol associated with the given URI scheme. It's a function that takes an URL such as `example://localhost/asset.css`.
599  ///
600  /// # Examples
601  ///
602  /// ```rust
603  /// use tauri::{plugin::{Builder, TauriPlugin}, Runtime};
604  ///
605  /// fn init<R: Runtime>() -> TauriPlugin<R> {
606  ///   Builder::new("myplugin")
607  ///     .register_asynchronous_uri_scheme_protocol("app-files", |_ctx, request, responder| {
608  ///       // skip leading `/`
609  ///       let path = request.uri().path()[1..].to_string();
610  ///       std::thread::spawn(move || {
611  ///         if let Ok(data) = std::fs::read(path) {
612  ///           responder.respond(
613  ///             http::Response::builder()
614  ///               .body(data)
615  ///               .unwrap()
616  ///           );
617  ///         } else {
618  ///           responder.respond(
619  ///             http::Response::builder()
620  ///               .status(http::StatusCode::BAD_REQUEST)
621  ///               .header(http::header::CONTENT_TYPE, mime::TEXT_PLAIN.essence_str())
622  ///               .body("failed to read file".as_bytes().to_vec())
623  ///               .unwrap()
624  ///           );
625  ///         }
626  ///       });
627  ///     })
628  ///     .build()
629  /// }
630  /// ```
631  ///
632  /// # Warning
633  ///
634  /// Pages loaded from a custom protocol will have a different Origin on different platforms.
635  /// Servers which enforce CORS will need to add the exact same Origin header (or `*`) in `Access-Control-Allow-Origin`
636  /// if you wish to send requests with native `fetch` and `XmlHttpRequest` APIs. Here are the
637  /// different Origin headers across platforms:
638  ///
639  /// - macOS, iOS and Linux: `<scheme_name>://localhost/<path>` (so it will be `my-scheme://localhost/path/to/page).
640  /// - Windows and Android: `http://<scheme_name>.localhost/<path>` by default (so it will be `http://my-scheme.localhost/path/to/page`).
641  ///   To use `https` instead of `http`, use [`super::webview::WebviewBuilder::use_https_scheme`].
642  #[must_use]
643  pub fn register_asynchronous_uri_scheme_protocol<
644    N: Into<String>,
645    H: Fn(UriSchemeContext<'_, R>, http::Request<Vec<u8>>, UriSchemeResponder) + Send + Sync + 'static,
646  >(
647    mut self,
648    uri_scheme: N,
649    protocol: H,
650  ) -> Self {
651    self.uri_scheme_protocols.insert(
652      uri_scheme.into(),
653      Arc::new(UriSchemeProtocol {
654        protocol: Box::new(protocol),
655      }),
656    );
657    self
658  }
659
660  /// Builds the [`TauriPlugin`].
661  pub fn try_build(self) -> Result<TauriPlugin<R, C>, BuilderError> {
662    if let Some(&reserved) = RESERVED_PLUGIN_NAMES.iter().find(|&r| r == &self.name) {
663      return Err(BuilderError::ReservedName(reserved.into()));
664    }
665
666    Ok(TauriPlugin {
667      name: self.name,
668      app: None,
669      invoke_handler: self.invoke_handler,
670      setup: self.setup,
671      js_init_script: self.js_init_script,
672      on_navigation: self.on_navigation,
673      on_page_load: self.on_page_load,
674      on_window_ready: self.on_window_ready,
675      on_webview_ready: self.on_webview_ready,
676      on_event: self.on_event,
677      on_drop: self.on_drop,
678      uri_scheme_protocols: self.uri_scheme_protocols,
679    })
680  }
681
682  /// Builds the [`TauriPlugin`].
683  ///
684  /// # Panics
685  ///
686  /// If the builder returns an error during [`Self::try_build`], then this method will panic.
687  pub fn build(self) -> TauriPlugin<R, C> {
688    self.try_build().expect("valid plugin")
689  }
690}
691
692/// Plugin struct that is returned by the [`Builder`]. Should only be constructed through the builder.
693pub struct TauriPlugin<R: Runtime, C: DeserializeOwned = ()> {
694  name: &'static str,
695  app: Option<AppHandle<R>>,
696  invoke_handler: Box<InvokeHandler<R>>,
697  setup: Option<Box<SetupHook<R, C>>>,
698  js_init_script: Option<String>,
699  on_navigation: Box<OnNavigation<R>>,
700  on_page_load: Box<OnPageLoad<R>>,
701  on_window_ready: Box<OnWindowReady<R>>,
702  on_webview_ready: Box<OnWebviewReady<R>>,
703  on_event: Box<OnEvent<R>>,
704  on_drop: Option<Box<OnDrop<R>>>,
705  uri_scheme_protocols: HashMap<String, Arc<UriSchemeProtocol<R>>>,
706}
707
708impl<R: Runtime, C: DeserializeOwned> Drop for TauriPlugin<R, C> {
709  fn drop(&mut self) {
710    if let (Some(on_drop), Some(app)) = (self.on_drop.take(), self.app.take()) {
711      on_drop(app);
712    }
713  }
714}
715
716impl<R: Runtime, C: DeserializeOwned> Plugin<R> for TauriPlugin<R, C> {
717  fn name(&self) -> &'static str {
718    self.name
719  }
720
721  fn initialize(
722    &mut self,
723    app: &AppHandle<R>,
724    config: JsonValue,
725  ) -> Result<(), Box<dyn std::error::Error>> {
726    self.app.replace(app.clone());
727    if let Some(s) = self.setup.take() {
728      (s)(
729        app,
730        PluginApi {
731          name: self.name,
732          handle: app.clone(),
733          raw_config: Arc::new(config.clone()),
734          config: serde_json::from_value(config).map_err(|err| {
735            format!(
736              "Error deserializing 'plugins.{}' within your Tauri configuration: {err}",
737              self.name
738            )
739          })?,
740        },
741      )?;
742    }
743
744    for (uri_scheme, protocol) in &self.uri_scheme_protocols {
745      app
746        .manager
747        .webview
748        .register_uri_scheme_protocol(uri_scheme, protocol.clone())
749    }
750    Ok(())
751  }
752
753  fn initialization_script(&self) -> Option<String> {
754    self.js_init_script.clone()
755  }
756
757  fn window_created(&mut self, window: Window<R>) {
758    (self.on_window_ready)(window)
759  }
760
761  fn webview_created(&mut self, webview: Webview<R>) {
762    (self.on_webview_ready)(webview)
763  }
764
765  fn on_navigation(&mut self, webview: &Webview<R>, url: &Url) -> bool {
766    (self.on_navigation)(webview, url)
767  }
768
769  fn on_page_load(&mut self, webview: &Webview<R>, payload: &PageLoadPayload<'_>) {
770    (self.on_page_load)(webview, payload)
771  }
772
773  fn on_event(&mut self, app: &AppHandle<R>, event: &RunEvent) {
774    (self.on_event)(app, event)
775  }
776
777  fn extend_api(&mut self, invoke: Invoke<R>) -> bool {
778    (self.invoke_handler)(invoke)
779  }
780}
781
782/// Plugin collection type.
783#[default_runtime(crate::Wry, wry)]
784pub(crate) struct PluginStore<R: Runtime> {
785  store: Vec<Box<dyn Plugin<R>>>,
786}
787
788impl<R: Runtime> fmt::Debug for PluginStore<R> {
789  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
790    let plugins: Vec<&str> = self.store.iter().map(|plugins| plugins.name()).collect();
791    f.debug_struct("PluginStore")
792      .field("plugins", &plugins)
793      .finish()
794  }
795}
796
797impl<R: Runtime> Default for PluginStore<R> {
798  fn default() -> Self {
799    Self { store: Vec::new() }
800  }
801}
802
803impl<R: Runtime> PluginStore<R> {
804  /// Adds a plugin to the store.
805  ///
806  /// Returns `true` if a plugin with the same name is already in the store.
807  pub fn register(&mut self, plugin: Box<dyn Plugin<R>>) -> bool {
808    let len = self.store.len();
809    self.store.retain(|p| p.name() != plugin.name());
810    let result = len != self.store.len();
811    self.store.push(plugin);
812    result
813  }
814
815  /// Removes the plugin with the given name from the store.
816  pub fn unregister(&mut self, plugin: &'static str) -> bool {
817    let len = self.store.len();
818    self.store.retain(|p| p.name() != plugin);
819    len != self.store.len()
820  }
821
822  /// Initializes the given plugin.
823  pub(crate) fn initialize(
824    &self,
825    plugin: &mut Box<dyn Plugin<R>>,
826    app: &AppHandle<R>,
827    config: &PluginConfig,
828  ) -> crate::Result<()> {
829    initialize(plugin, app, config)
830  }
831
832  /// Initializes all plugins in the store.
833  pub(crate) fn initialize_all(
834    &mut self,
835    app: &AppHandle<R>,
836    config: &PluginConfig,
837  ) -> crate::Result<()> {
838    self
839      .store
840      .iter_mut()
841      .try_for_each(|plugin| initialize(plugin, app, config))
842  }
843
844  /// Generates an initialization script from all plugins in the store.
845  pub(crate) fn initialization_script(&self) -> Vec<String> {
846    self
847      .store
848      .iter()
849      .filter_map(|p| p.initialization_script())
850      .map(|script| format!("(function () {{ {script} }})();"))
851      .collect()
852  }
853
854  /// Runs the created hook for all plugins in the store.
855  pub(crate) fn window_created(&mut self, window: Window<R>) {
856    self.store.iter_mut().for_each(|plugin| {
857      #[cfg(feature = "tracing")]
858      let _span = tracing::trace_span!("plugin::hooks::created", name = plugin.name()).entered();
859      plugin.window_created(window.clone())
860    })
861  }
862
863  /// Runs the webview created hook for all plugins in the store.
864  pub(crate) fn webview_created(&mut self, webview: Webview<R>) {
865    self
866      .store
867      .iter_mut()
868      .for_each(|plugin| plugin.webview_created(webview.clone()))
869  }
870
871  pub(crate) fn on_navigation(&mut self, webview: &Webview<R>, url: &Url) -> bool {
872    for plugin in self.store.iter_mut() {
873      #[cfg(feature = "tracing")]
874      let _span =
875        tracing::trace_span!("plugin::hooks::on_navigation", name = plugin.name()).entered();
876      if !plugin.on_navigation(webview, url) {
877        return false;
878      }
879    }
880    true
881  }
882
883  /// Runs the on_page_load hook for all plugins in the store.
884  pub(crate) fn on_page_load(&mut self, webview: &Webview<R>, payload: &PageLoadPayload<'_>) {
885    self.store.iter_mut().for_each(|plugin| {
886      #[cfg(feature = "tracing")]
887      let _span =
888        tracing::trace_span!("plugin::hooks::on_page_load", name = plugin.name()).entered();
889      plugin.on_page_load(webview, payload)
890    })
891  }
892
893  /// Runs the on_event hook for all plugins in the store.
894  pub(crate) fn on_event(&mut self, app: &AppHandle<R>, event: &RunEvent) {
895    self
896      .store
897      .iter_mut()
898      .for_each(|plugin| plugin.on_event(app, event))
899  }
900
901  /// Runs the plugin `extend_api` hook if it exists. Returns whether the invoke message was handled or not.
902  ///
903  /// The message is not handled when the plugin exists **and** the command does not.
904  pub(crate) fn extend_api(&mut self, plugin: &str, invoke: Invoke<R>) -> bool {
905    for p in self.store.iter_mut() {
906      if p.name() == plugin {
907        #[cfg(feature = "tracing")]
908        let _span = tracing::trace_span!("plugin::hooks::ipc", name = plugin).entered();
909        return p.extend_api(invoke);
910      }
911    }
912    invoke.resolver.reject(format!("plugin {plugin} not found"));
913    true
914  }
915}
916
917#[cfg_attr(feature = "tracing", tracing::instrument(name = "plugin::hooks::initialize", skip(plugin, app), fields(name = plugin.name())))]
918fn initialize<R: Runtime>(
919  plugin: &mut Box<dyn Plugin<R>>,
920  app: &AppHandle<R>,
921  config: &PluginConfig,
922) -> crate::Result<()> {
923  plugin
924    .initialize(
925      app,
926      config.0.get(plugin.name()).cloned().unwrap_or_default(),
927    )
928    .map_err(|e| Error::PluginInitialization(plugin.name().to_string(), e.to_string()))
929}
930
931/// Permission state.
932#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
933#[cfg_attr(feature = "specta", derive(specta::Type))]
934pub enum PermissionState {
935  /// Permission access has been granted.
936  Granted,
937  /// Permission access has been denied.
938  Denied,
939  /// Permission must be requested.
940  #[default]
941  Prompt,
942  /// Permission must be requested, but you must explain to the user why your app needs that permission. **Android only**.
943  PromptWithRationale,
944}
945
946impl std::fmt::Display for PermissionState {
947  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
948    match self {
949      Self::Granted => write!(f, "granted"),
950      Self::Denied => write!(f, "denied"),
951      Self::Prompt => write!(f, "prompt"),
952      Self::PromptWithRationale => write!(f, "prompt-with-rationale"),
953    }
954  }
955}
956
957impl Serialize for PermissionState {
958  fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
959  where
960    S: Serializer,
961  {
962    serializer.serialize_str(self.to_string().as_ref())
963  }
964}
965
966impl<'de> Deserialize<'de> for PermissionState {
967  fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
968  where
969    D: Deserializer<'de>,
970  {
971    let s = <String as Deserialize>::deserialize(deserializer)?;
972    match s.to_lowercase().as_str() {
973      "granted" => Ok(Self::Granted),
974      "denied" => Ok(Self::Denied),
975      "prompt" => Ok(Self::Prompt),
976      "prompt-with-rationale" => Ok(Self::PromptWithRationale),
977      _ => Err(DeError::custom(format!("unknown permission state '{s}'"))),
978    }
979  }
980}