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}