From 54a222086d38c0e2067f1f3df213ecaece3ee050 Mon Sep 17 00:00:00 2001
From: Lucas Nogueira <lucas@tauri.app>
Date: Thu, 26 Oct 2023 11:35:01 -0300
Subject: [PATCH 1/3] refactor(core): implement on_page_load event using wry
 hook

---
 .changes/on-page-load-hook.md                 |   6 +
 .changes/refactor-on-page-load.md             |   6 +
 core/tauri-runtime-wry/src/lib.rs             |  15 ++
 core/tauri-runtime/src/window.rs              |  23 +-
 core/tauri/scripts/init.js                    |  15 +-
 core/tauri/src/app.rs                         |  25 +-
 core/tauri/src/manager.rs                     |  49 ++--
 core/tauri/src/plugin.rs                      |  17 +-
 core/tauri/src/window/mod.rs                  | 234 +++++++++++-------
 examples/api/src-tauri/src/lib.rs             |  32 ++-
 examples/multiwindow/index.html               |  12 +-
 examples/multiwindow/main.rs                  |  14 +-
 examples/parent-window/main.rs                |  18 +-
 .../jest/fixtures/app/src-tauri/src/main.rs   |  18 +-
 14 files changed, 292 insertions(+), 192 deletions(-)
 create mode 100644 .changes/on-page-load-hook.md
 create mode 100644 .changes/refactor-on-page-load.md

diff --git a/.changes/on-page-load-hook.md b/.changes/on-page-load-hook.md
new file mode 100644
index 000000000000..340a39bd553d
--- /dev/null
+++ b/.changes/on-page-load-hook.md
@@ -0,0 +1,6 @@
+---
+"tauri-runtime": patch:feat
+"tauri-runtime-wry": patch:feat
+---
+
+Added `on_page_load` hook for `PendingWindow`.
diff --git a/.changes/refactor-on-page-load.md b/.changes/refactor-on-page-load.md
new file mode 100644
index 000000000000..d241ac25bdd5
--- /dev/null
+++ b/.changes/refactor-on-page-load.md
@@ -0,0 +1,6 @@
+---
+"tauri": patch:breaking
+---
+
+Added `WindowBuilder::on_page_load` and refactored the `Builder::on_page_load` handler to take references.
+The page load hook is now triggered for load started and finished events, to determine what triggered it see `PageLoadPayload::event`.
diff --git a/core/tauri-runtime-wry/src/lib.rs b/core/tauri-runtime-wry/src/lib.rs
index c9eef74d8f08..38b7abdb2752 100644
--- a/core/tauri-runtime-wry/src/lib.rs
+++ b/core/tauri-runtime-wry/src/lib.rs
@@ -2725,6 +2725,21 @@ fn create_webview<T: UserEvent, F: Fn(RawWindow) + Send + 'static>(
         .unwrap_or(true)
     });
   }
+
+  if let Some(page_load_handler) = pending.on_page_load_handler {
+    webview_builder = webview_builder.with_on_page_load_handler(move |event, url| {
+      let _ = Url::parse(&url).map(|url| {
+        page_load_handler(
+          url,
+          match event {
+            wry::webview::PageLoadEvent::Started => tauri_runtime::window::PageLoadEvent::Started,
+            wry::webview::PageLoadEvent::Finished => tauri_runtime::window::PageLoadEvent::Finished,
+          },
+        )
+      });
+    });
+  }
+
   if let Some(user_agent) = webview_attributes.user_agent {
     webview_builder = webview_builder.with_user_agent(&user_agent);
   }
diff --git a/core/tauri-runtime/src/window.rs b/core/tauri-runtime/src/window.rs
index 2e15a2c22981..af07005c38c4 100644
--- a/core/tauri-runtime/src/window.rs
+++ b/core/tauri-runtime/src/window.rs
@@ -34,6 +34,17 @@ type WebResourceRequestHandler =
 
 type NavigationHandler = dyn Fn(&Url) -> bool + Send;
 
+type OnPageLoadHandler = dyn Fn(Url, PageLoadEvent) + Send;
+
+/// Kind of event for the page load handler.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum PageLoadEvent {
+  /// Page started to load.
+  Started,
+  /// Page finished loading.
+  Finished,
+}
+
 /// UI scaling utilities.
 pub mod dpi;
 
@@ -238,6 +249,8 @@ pub struct PendingWindow<T: UserEvent, R: Runtime<T>> {
     Option<Box<dyn Fn(CreationContext<'_, '_>) -> Result<(), jni::errors::Error> + Send>>,
 
   pub web_resource_request_handler: Option<Box<WebResourceRequestHandler>>,
+
+  pub on_page_load_handler: Option<Box<OnPageLoadHandler>>,
 }
 
 pub fn is_label_valid(label: &str) -> bool {
@@ -270,11 +283,12 @@ impl<T: UserEvent, R: Runtime<T>> PendingWindow<T, R> {
         uri_scheme_protocols: Default::default(),
         label,
         ipc_handler: None,
-        navigation_handler: Default::default(),
+        navigation_handler: None,
         url: "tauri://localhost".to_string(),
         #[cfg(target_os = "android")]
         on_webview_created: None,
-        web_resource_request_handler: Default::default(),
+        web_resource_request_handler: None,
+        on_page_load_handler: None,
       })
     }
   }
@@ -298,11 +312,12 @@ impl<T: UserEvent, R: Runtime<T>> PendingWindow<T, R> {
         uri_scheme_protocols: Default::default(),
         label,
         ipc_handler: None,
-        navigation_handler: Default::default(),
+        navigation_handler: None,
         url: "tauri://localhost".to_string(),
         #[cfg(target_os = "android")]
         on_webview_created: None,
-        web_resource_request_handler: Default::default(),
+        web_resource_request_handler: None,
+        on_page_load_handler: None,
       })
     }
   }
diff --git a/core/tauri/scripts/init.js b/core/tauri/scripts/init.js
index f9e0e66716e2..2636e666bc0e 100644
--- a/core/tauri/scripts/init.js
+++ b/core/tauri/scripts/init.js
@@ -12,21 +12,8 @@
   __RAW_core_script__
 
   __RAW_event_initialization_script__
-  ;(function () {
-    __RAW_bundle_script__
-  })()
 
-  if (window.ipc) {
-    window.__TAURI_INTERNALS__.invoke('__initialized', {
-      url: window.location.href
-    })
-  } else {
-    window.addEventListener('DOMContentLoaded', function () {
-      window.__TAURI_INTERNALS__.invoke('__initialized', {
-        url: window.location.href
-      })
-    })
-  }
+  __RAW_bundle_script__
 
   __RAW_plugin_initialization_script__
 })()
diff --git a/core/tauri/src/app.rs b/core/tauri/src/app.rs
index fa993606971b..211e3eb9d5f7 100644
--- a/core/tauri/src/app.rs
+++ b/core/tauri/src/app.rs
@@ -19,6 +19,7 @@ use crate::{
   sealed::{ManagerBase, RuntimeOrDispatch},
   utils::config::Config,
   utils::{assets::Assets, Env},
+  window::PageLoadPayload,
   Context, DeviceEventFilter, EventLoopMessage, Icon, Manager, Monitor, Runtime, Scopes,
   StateManager, Theme, Window,
 };
@@ -30,7 +31,6 @@ use crate::tray::{TrayIcon, TrayIconBuilder, TrayIconEvent, TrayIconId};
 #[cfg(desktop)]
 use crate::window::WindowMenu;
 use raw_window_handle::HasRawDisplayHandle;
-use serde::Deserialize;
 use serialize_to_javascript::{default_template, DefaultTemplate, Template};
 use tauri_macros::default_runtime;
 #[cfg(desktop)]
@@ -68,20 +68,7 @@ pub(crate) type GlobalWindowEventListener<R> = Box<dyn Fn(GlobalWindowEvent<R>)
 pub type SetupHook<R> =
   Box<dyn FnOnce(&mut App<R>) -> Result<(), Box<dyn std::error::Error>> + Send>;
 /// A closure that is run once every time a window is created and loaded.
-pub type OnPageLoad<R> = dyn Fn(Window<R>, PageLoadPayload) + Send + Sync + 'static;
-
-/// The payload for the [`OnPageLoad`] hook.
-#[derive(Debug, Clone, Deserialize)]
-pub struct PageLoadPayload {
-  url: String,
-}
-
-impl PageLoadPayload {
-  /// The page URL.
-  pub fn url(&self) -> &str {
-    &self.url
-  }
-}
+pub type OnPageLoad<R> = dyn Fn(&Window<R>, &PageLoadPayload<'_>) + Send + Sync + 'static;
 
 /// Api exposed on the `ExitRequested` event.
 #[derive(Debug)]
@@ -982,7 +969,7 @@ pub struct Builder<R: Runtime> {
   setup: SetupHook<R>,
 
   /// Page load hook.
-  on_page_load: Box<OnPageLoad<R>>,
+  on_page_load: Option<Arc<OnPageLoad<R>>>,
 
   /// windows to create when starting up.
   pending_windows: Vec<PendingWindow<EventLoopMessage, R>>,
@@ -1040,7 +1027,7 @@ impl<R: Runtime> Builder<R> {
       .render_default(&Default::default())
       .unwrap()
       .into_string(),
-      on_page_load: Box::new(|_, _| ()),
+      on_page_load: None,
       pending_windows: Default::default(),
       plugins: PluginStore::default(),
       uri_scheme_protocols: Default::default(),
@@ -1130,9 +1117,9 @@ impl<R: Runtime> Builder<R> {
   #[must_use]
   pub fn on_page_load<F>(mut self, on_page_load: F) -> Self
   where
-    F: Fn(Window<R>, PageLoadPayload) + Send + Sync + 'static,
+    F: Fn(&Window<R>, &PageLoadPayload<'_>) + Send + Sync + 'static,
   {
-    self.on_page_load = Box::new(on_page_load);
+    self.on_page_load.replace(Arc::new(on_page_load));
     self
   }
 
diff --git a/core/tauri/src/manager.rs b/core/tauri/src/manager.rs
index 2154b0979e81..e53a5b877f14 100644
--- a/core/tauri/src/manager.rs
+++ b/core/tauri/src/manager.rs
@@ -27,12 +27,8 @@ use tauri_utils::{
   html::{SCRIPT_NONCE_TOKEN, STYLE_NONCE_TOKEN},
 };
 
-use crate::event::EmitArgs;
 use crate::{
-  app::{
-    AppHandle, GlobalWindowEvent, GlobalWindowEventListener, OnPageLoad, PageLoadPayload,
-    UriSchemeResponder,
-  },
+  app::{AppHandle, GlobalWindowEvent, GlobalWindowEventListener, OnPageLoad, UriSchemeResponder},
   event::{assert_event_name_is_valid, Event, EventId, Listeners},
   ipc::{Invoke, InvokeHandler, InvokeResponder},
   pattern::PatternJavascript,
@@ -52,6 +48,7 @@ use crate::{
   Context, EventLoopMessage, Icon, Manager, Pattern, Runtime, Scopes, StateManager, Window,
   WindowEvent,
 };
+use crate::{event::EmitArgs, window::PageLoadPayload};
 
 #[cfg(desktop)]
 use crate::app::GlobalMenuEventListener;
@@ -232,7 +229,7 @@ pub struct InnerWindowManager<R: Runtime> {
   invoke_handler: Box<InvokeHandler<R>>,
 
   /// The page load hook, invoked when the webview performs a navigation.
-  on_page_load: Box<OnPageLoad<R>>,
+  on_page_load: Option<Arc<OnPageLoad<R>>>,
 
   config: Arc<Config>,
   assets: Arc<dyn Assets>,
@@ -339,7 +336,7 @@ impl<R: Runtime> WindowManager<R> {
     #[allow(unused_mut)] mut context: Context<impl Assets>,
     plugins: PluginStore<R>,
     invoke_handler: Box<InvokeHandler<R>>,
-    on_page_load: Box<OnPageLoad<R>>,
+    on_page_load: Option<Arc<OnPageLoad<R>>>,
     uri_scheme_protocols: HashMap<String, Arc<UriSchemeProtocol<R>>>,
     state: StateManager,
     window_event_listeners: Vec<GlobalWindowEventListener<R>>,
@@ -685,6 +682,32 @@ impl<R: Runtime> WindowManager<R> {
       registered_scheme_protocols.push("ipc".into());
     }
 
+    let label = pending.label.clone();
+    let manager = self.clone();
+    let on_page_load_handler = pending.on_page_load_handler.take();
+    pending
+      .on_page_load_handler
+      .replace(Box::new(move |url, event| {
+        let payload = PageLoadPayload { url: &url, event };
+
+        if let Some(w) = manager.get_window(&label) {
+          if let Some(on_page_load) = &manager.inner.on_page_load {
+            on_page_load(&w, &payload);
+          }
+
+          manager
+            .inner
+            .plugins
+            .lock()
+            .unwrap()
+            .on_page_load(&w, &payload);
+        }
+
+        if let Some(handler) = &on_page_load_handler {
+          handler(url, event);
+        }
+      }));
+
     #[cfg(feature = "protocol-asset")]
     if !registered_scheme_protocols.contains(&"asset".into()) {
       let asset_scope = self.state().get::<crate::Scopes>().asset_protocol.clone();
@@ -869,16 +892,6 @@ impl<R: Runtime> WindowManager<R> {
     (self.inner.invoke_handler)(invoke)
   }
 
-  pub fn run_on_page_load(&self, window: Window<R>, payload: PageLoadPayload) {
-    (self.inner.on_page_load)(window.clone(), payload.clone());
-    self
-      .inner
-      .plugins
-      .lock()
-      .expect("poisoned plugin store")
-      .on_page_load(window, payload);
-  }
-
   pub fn extend_api(&self, plugin: &str, invoke: Invoke<R>) -> bool {
     self
       .inner
@@ -1376,7 +1389,7 @@ mod test {
       context,
       PluginStore::default(),
       Box::new(|_| false),
-      Box::new(|_, _| ()),
+      None,
       Default::default(),
       StateManager::new(),
       Default::default(),
diff --git a/core/tauri/src/plugin.rs b/core/tauri/src/plugin.rs
index 9f6a14260fd1..87bb6e9aa48e 100644
--- a/core/tauri/src/plugin.rs
+++ b/core/tauri/src/plugin.rs
@@ -5,11 +5,12 @@
 //! The Tauri plugin extension to expand Tauri functionality.
 
 use crate::{
-  app::{PageLoadPayload, UriSchemeResponder},
+  app::UriSchemeResponder,
   error::Error,
   ipc::{Invoke, InvokeHandler},
   manager::UriSchemeProtocol,
   utils::config::PluginConfig,
+  window::PageLoadPayload,
   AppHandle, RunEvent, Runtime, Window,
 };
 use serde::de::DeserializeOwned;
@@ -62,7 +63,7 @@ pub trait Plugin<R: Runtime>: Send {
 
   /// Callback invoked when the webview performs a navigation to a page.
   #[allow(unused_variables)]
-  fn on_page_load(&mut self, window: Window<R>, payload: PageLoadPayload) {}
+  fn on_page_load(&mut self, window: &Window<R>, payload: &PageLoadPayload<'_>) {}
 
   /// Callback invoked when the event loop receives a new event.
   #[allow(unused_variables)]
@@ -80,7 +81,7 @@ type SetupHook<R, C> =
 type OnWebviewReady<R> = dyn FnMut(Window<R>) + Send;
 type OnEvent<R> = dyn FnMut(&AppHandle<R>, &RunEvent) + Send;
 type OnNavigation<R> = dyn Fn(&Window<R>, &Url) -> bool + Send;
-type OnPageLoad<R> = dyn FnMut(Window<R>, PageLoadPayload) + Send;
+type OnPageLoad<R> = dyn FnMut(&Window<R>, &PageLoadPayload<'_>) + Send;
 type OnDrop<R> = dyn FnOnce(AppHandle<R>) + Send;
 
 /// A handle to a plugin.
@@ -368,7 +369,7 @@ impl<R: Runtime, C: DeserializeOwned> Builder<R, C> {
   /// fn init<R: Runtime>() -> TauriPlugin<R> {
   ///   Builder::new("example")
   ///     .on_page_load(|window, payload| {
-  ///       println!("Loaded URL {} in window {}", payload.url(), window.label());
+  ///       println!("{} URL {} in window {}", payload.event(), payload.url(), window.label());
   ///     })
   ///     .build()
   /// }
@@ -376,7 +377,7 @@ impl<R: Runtime, C: DeserializeOwned> Builder<R, C> {
   #[must_use]
   pub fn on_page_load<F>(mut self, on_page_load: F) -> Self
   where
-    F: FnMut(Window<R>, PageLoadPayload) + Send + 'static,
+    F: FnMut(&Window<R>, &PageLoadPayload<'_>) + Send + 'static,
   {
     self.on_page_load = Box::new(on_page_load);
     self
@@ -652,7 +653,7 @@ impl<R: Runtime, C: DeserializeOwned> Plugin<R> for TauriPlugin<R, C> {
     (self.on_navigation)(window, url)
   }
 
-  fn on_page_load(&mut self, window: Window<R>, payload: PageLoadPayload) {
+  fn on_page_load(&mut self, window: &Window<R>, payload: &PageLoadPayload<'_>) {
     (self.on_page_load)(window, payload)
   }
 
@@ -750,11 +751,11 @@ impl<R: Runtime> PluginStore<R> {
   }
 
   /// Runs the on_page_load hook for all plugins in the store.
-  pub(crate) fn on_page_load(&mut self, window: Window<R>, payload: PageLoadPayload) {
+  pub(crate) fn on_page_load(&mut self, window: &Window<R>, payload: &PageLoadPayload<'_>) {
     self
       .store
       .iter_mut()
-      .for_each(|plugin| plugin.on_page_load(window.clone(), payload.clone()))
+      .for_each(|plugin| plugin.on_page_load(window, payload))
   }
 
   /// Runs the on_event hook for all plugins in the store.
diff --git a/core/tauri/src/window/mod.rs b/core/tauri/src/window/mod.rs
index aa3122338265..6dd3fad58623 100644
--- a/core/tauri/src/window/mod.rs
+++ b/core/tauri/src/window/mod.rs
@@ -7,6 +7,7 @@
 pub(crate) mod plugin;
 
 use http::HeaderMap;
+pub use tauri_runtime::window::PageLoadEvent;
 pub use tauri_utils::{config::Color, WindowEffect as Effect, WindowEffectState as EffectState};
 use url::Url;
 
@@ -65,12 +66,32 @@ pub(crate) type WebResourceRequestHandler =
 pub(crate) type NavigationHandler = dyn Fn(&Url) -> bool + Send;
 pub(crate) type UriSchemeProtocolHandler =
   Box<dyn Fn(http::Request<Vec<u8>>, UriSchemeResponder) + Send + Sync>;
+pub(crate) type OnPageLoad<R> = dyn Fn(Window<R>, PageLoadPayload<'_>) + Send + Sync + 'static;
 
 #[derive(Clone, Serialize)]
 struct WindowCreatedEvent {
   label: String,
 }
 
+/// The payload for the [`OnPageLoad`] hook.
+#[derive(Debug, Clone)]
+pub struct PageLoadPayload<'a> {
+  pub(crate) url: &'a Url,
+  pub(crate) event: PageLoadEvent,
+}
+
+impl<'a> PageLoadPayload<'a> {
+  /// The page URL.
+  pub fn url(&self) -> &'a Url {
+    self.url
+  }
+
+  /// The page load event.
+  pub fn event(&self) -> PageLoadEvent {
+    self.event
+  }
+}
+
 /// Monitor descriptor.
 #[derive(Debug, Clone, Serialize)]
 #[serde(rename_all = "camelCase")]
@@ -128,6 +149,7 @@ pub struct WindowBuilder<'a, R: Runtime> {
   pub(crate) webview_attributes: WebviewAttributes,
   web_resource_request_handler: Option<Box<WebResourceRequestHandler>>,
   navigation_handler: Option<Box<NavigationHandler>>,
+  on_page_load_handler: Option<Box<OnPageLoad<R>>>,
   #[cfg(desktop)]
   on_menu_event: Option<crate::app::GlobalMenuEventListener<Window<R>>>,
 }
@@ -206,6 +228,7 @@ impl<'a, R: Runtime> WindowBuilder<'a, R> {
       webview_attributes: WebviewAttributes::new(url),
       web_resource_request_handler: None,
       navigation_handler: None,
+      on_page_load_handler: None,
       #[cfg(desktop)]
       on_menu_event: None,
     }
@@ -250,6 +273,7 @@ impl<'a, R: Runtime> WindowBuilder<'a, R> {
       navigation_handler: None,
       #[cfg(desktop)]
       on_menu_event: None,
+      on_page_load_handler: None,
     };
 
     builder
@@ -329,6 +353,35 @@ impl<'a, R: Runtime> WindowBuilder<'a, R> {
     self
   }
 
+  /// Defines a closure to be executed when the webview navigates to a URL. Returning `false` cancels the navigation.
+  ///
+  /// # Examples
+  ///
+  /// ```rust,no_run
+  /// use tauri::{
+  ///   utils::config::{Csp, CspDirectiveSources, WindowUrl},
+  ///   window::WindowBuilder,
+  /// };
+  /// use http::header::HeaderValue;
+  /// use std::collections::HashMap;
+  /// tauri::Builder::default()
+  ///   .setup(|app| {
+  ///     WindowBuilder::new(app, "core", WindowUrl::App("index.html".into()))
+  ///       .on_page_load(|window, payload| {
+  ///         println!("{:?} {}", payload.event(), payload.url());
+  ///       })
+  ///       .build()?;
+  ///     Ok(())
+  ///   });
+  /// ```
+  pub fn on_page_load<F: Fn(Window<R>, PageLoadPayload<'_>) + Send + Sync + 'static>(
+    mut self,
+    f: F,
+  ) -> Self {
+    self.on_page_load_handler.replace(Box::new(f));
+    self
+  }
+
   /// Registers a global menu event listener.
   ///
   /// Note that this handler is called for any menu event,
@@ -381,6 +434,18 @@ impl<'a, R: Runtime> WindowBuilder<'a, R> {
     pending.navigation_handler = self.navigation_handler.take();
     pending.web_resource_request_handler = self.web_resource_request_handler.take();
 
+    if let Some(on_page_load_handler) = self.on_page_load_handler.take() {
+      let label = pending.label.clone();
+      let manager = self.app_handle.manager.clone();
+      pending
+        .on_page_load_handler
+        .replace(Box::new(move |url, event| {
+          if let Some(w) = manager.get_window(&label) {
+            on_page_load_handler(w, PageLoadPayload { url: &url, event });
+          }
+        }));
+    }
+
     let labels = self.manager.labels().into_iter().collect::<Vec<_>>();
     let pending = self
       .manager
@@ -2155,110 +2220,99 @@ impl<R: Runtime> Window<R> {
       request.error,
     );
 
-    match request.cmd.as_str() {
-      "__initialized" => match request.body.deserialize() {
-        Ok(payload) => {
-          manager.run_on_page_load(self, payload);
-          resolver.resolve(());
-        }
-        Err(e) => resolver.reject(e.to_string()),
-      },
-      _ => {
-        #[cfg(mobile)]
-        let app_handle = self.app_handle.clone();
-
-        let message = InvokeMessage::new(
-          self,
-          manager.state(),
-          request.cmd.to_string(),
-          request.body,
-          request.headers,
-        );
-
-        let mut invoke = Invoke {
-          message,
-          resolver: resolver.clone(),
-        };
-
-        if !is_local && scope.is_none() {
-          invoke.resolver.reject(scope_not_found_error_message);
-        } else if request.cmd.starts_with("plugin:") {
-          let command = invoke.message.command.replace("plugin:", "");
-          let mut tokens = command.split('|');
-          // safe to unwrap: split always has a least one item
-          let plugin = tokens.next().unwrap();
-          invoke.message.command = tokens
-            .next()
-            .map(|c| c.to_string())
-            .unwrap_or_else(String::new);
-
-          if !(is_local
-            || plugin == crate::ipc::channel::CHANNEL_PLUGIN_NAME
-            || scope
-              .map(|s| s.plugins().contains(&plugin.into()))
-              .unwrap_or(true))
-          {
-            invoke.resolver.reject(IPC_SCOPE_DOES_NOT_ALLOW);
-            return;
-          }
+    #[cfg(mobile)]
+    let app_handle = self.app_handle.clone();
+
+    let message = InvokeMessage::new(
+      self,
+      manager.state(),
+      request.cmd.to_string(),
+      request.body,
+      request.headers,
+    );
 
-          let command = invoke.message.command.clone();
+    let mut invoke = Invoke {
+      message,
+      resolver: resolver.clone(),
+    };
+
+    if !is_local && scope.is_none() {
+      invoke.resolver.reject(scope_not_found_error_message);
+    } else if request.cmd.starts_with("plugin:") {
+      let command = invoke.message.command.replace("plugin:", "");
+      let mut tokens = command.split('|');
+      // safe to unwrap: split always has a least one item
+      let plugin = tokens.next().unwrap();
+      invoke.message.command = tokens
+        .next()
+        .map(|c| c.to_string())
+        .unwrap_or_else(String::new);
+
+      if !(is_local
+        || plugin == crate::ipc::channel::CHANNEL_PLUGIN_NAME
+        || scope
+          .map(|s| s.plugins().contains(&plugin.into()))
+          .unwrap_or(true))
+      {
+        invoke.resolver.reject(IPC_SCOPE_DOES_NOT_ALLOW);
+        return;
+      }
+
+      let command = invoke.message.command.clone();
 
-          #[cfg(mobile)]
-          let message = invoke.message.clone();
+      #[cfg(mobile)]
+      let message = invoke.message.clone();
 
-          #[allow(unused_mut)]
-          let mut handled = manager.extend_api(plugin, invoke);
+      #[allow(unused_mut)]
+      let mut handled = manager.extend_api(plugin, invoke);
 
-          #[cfg(mobile)]
-          {
-            if !handled {
-              handled = true;
+      #[cfg(mobile)]
+      {
+        if !handled {
+          handled = true;
 
-              fn load_channels<R: Runtime>(payload: &serde_json::Value, window: &Window<R>) {
-                if let serde_json::Value::Object(map) = payload {
-                  for v in map.values() {
-                    if let serde_json::Value::String(s) = v {
-                      if s.starts_with(crate::ipc::channel::IPC_PAYLOAD_PREFIX) {
-                        crate::ipc::Channel::load_from_ipc(window.clone(), s);
-                      }
-                    }
+          fn load_channels<R: Runtime>(payload: &serde_json::Value, window: &Window<R>) {
+            if let serde_json::Value::Object(map) = payload {
+              for v in map.values() {
+                if let serde_json::Value::String(s) = v {
+                  if s.starts_with(crate::ipc::channel::IPC_PAYLOAD_PREFIX) {
+                    crate::ipc::Channel::load_from_ipc(window.clone(), s);
                   }
                 }
               }
-
-              let payload = message.payload.into_json();
-              // initialize channels
-              load_channels(&payload, &message.window);
-
-              let resolver_ = resolver.clone();
-              if let Err(e) = crate::plugin::mobile::run_command(
-                plugin,
-                &app_handle,
-                message.command,
-                payload,
-                move |response| match response {
-                  Ok(r) => resolver_.resolve(r),
-                  Err(e) => resolver_.reject(e),
-                },
-              ) {
-                resolver.reject(e.to_string());
-                return;
-              }
             }
           }
 
-          if !handled {
-            resolver.reject(format!("Command {command} not found"));
-          }
-        } else {
-          let command = invoke.message.command.clone();
-          let handled = manager.run_invoke_handler(invoke);
-          if !handled {
-            resolver.reject(format!("Command {command} not found"));
+          let payload = message.payload.into_json();
+          // initialize channels
+          load_channels(&payload, &message.window);
+
+          let resolver_ = resolver.clone();
+          if let Err(e) = crate::plugin::mobile::run_command(
+            plugin,
+            &app_handle,
+            message.command,
+            payload,
+            move |response| match response {
+              Ok(r) => resolver_.resolve(r),
+              Err(e) => resolver_.reject(e),
+            },
+          ) {
+            resolver.reject(e.to_string());
+            return;
           }
         }
       }
+
+      if !handled {
+        resolver.reject(format!("Command {command} not found"));
+      }
+    } else {
+      let command = invoke.message.command.clone();
+      let handled = manager.run_invoke_handler(invoke);
+      if !handled {
+        resolver.reject(format!("Command {command} not found"));
+      }
     }
   }
 
diff --git a/examples/api/src-tauri/src/lib.rs b/examples/api/src-tauri/src/lib.rs
index 9c08b55ec587..5370b589dd01 100644
--- a/examples/api/src-tauri/src/lib.rs
+++ b/examples/api/src-tauri/src/lib.rs
@@ -12,7 +12,11 @@ mod cmd;
 mod tray;
 
 use serde::Serialize;
-use tauri::{ipc::Channel, window::WindowBuilder, App, AppHandle, RunEvent, Runtime, WindowUrl};
+use tauri::{
+  ipc::Channel,
+  window::{PageLoadEvent, WindowBuilder},
+  App, AppHandle, RunEvent, Runtime, WindowUrl,
+};
 use tauri_plugin_sample::{PingRequest, SampleExt};
 
 #[cfg(desktop)]
@@ -122,18 +126,20 @@ pub fn run_app<R: Runtime, F: FnOnce(&App<R>) + Send + 'static>(
 
       Ok(())
     })
-    .on_page_load(|window, _| {
-      let window_ = window.clone();
-      window.listen("js-event", move |event| {
-        println!("got js-event with message '{:?}'", event.payload());
-        let reply = Reply {
-          data: "something else".to_string(),
-        };
-
-        window_
-          .emit("rust-event", Some(reply))
-          .expect("failed to emit");
-      });
+    .on_page_load(|window, payload| {
+      if payload.event() == PageLoadEvent::Finished {
+        let window_ = window.clone();
+        window.listen("js-event", move |event| {
+          println!("got js-event with message '{:?}'", event.payload());
+          let reply = Reply {
+            data: "something else".to_string(),
+          };
+
+          window_
+            .emit("rust-event", Some(reply))
+            .expect("failed to emit");
+        });
+      }
     });
 
   #[allow(unused_mut)]
diff --git a/examples/multiwindow/index.html b/examples/multiwindow/index.html
index d415b1ae3322..70a5cb43e450 100644
--- a/examples/multiwindow/index.html
+++ b/examples/multiwindow/index.html
@@ -14,8 +14,8 @@
     <div id="response"></div>
 
     <script>
-      const WebviewWindow = window.__TAURI__.window.WebviewWindow
-      const appWindow = window.__TAURI__.window.appWindow
+      const TauriWindow = window.__TAURI__.window.Window
+      const appWindow = window.__TAURI__.window.getCurrent()
       const windowLabel = appWindow.label
       const windowLabelContainer = document.getElementById('window-label')
       windowLabelContainer.innerText = 'This is the ' + windowLabel + ' window.'
@@ -23,7 +23,7 @@
       const container = document.getElementById('container')
 
       function createWindowMessageBtn(label) {
-        const tauriWindow = WebviewWindow.getByLabel(label)
+        const tauriWindow = TauriWindow.getByLabel(label)
         const button = document.createElement('button')
         button.innerText = 'Send message to ' + label
         button.addEventListener('click', function () {
@@ -51,16 +51,16 @@
       const createWindowButton = document.createElement('button')
       createWindowButton.innerHTML = 'Create window'
       createWindowButton.addEventListener('click', function () {
-        const webviewWindow = new WebviewWindow(
+        const tauriWindow = new TauriWindow(
           Math.random().toString().replace('.', ''),
           {
             tabbingIdentifier: windowLabel
           }
         )
-        webviewWindow.once('tauri://created', function () {
+        tauriWindow.once('tauri://created', function () {
           responseContainer.innerHTML += 'Created new webview'
         })
-        webviewWindow.once('tauri://error', function (e) {
+        tauriWindow.once('tauri://error', function (e) {
           responseContainer.innerHTML += 'Error creating new webview'
         })
       })
diff --git a/examples/multiwindow/main.rs b/examples/multiwindow/main.rs
index 6559d51e9760..dfc7fd81a6c6 100644
--- a/examples/multiwindow/main.rs
+++ b/examples/multiwindow/main.rs
@@ -4,15 +4,17 @@
 
 #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
 
-use tauri::WindowBuilder;
+use tauri::{window::PageLoadEvent, WindowBuilder};
 
 fn main() {
   tauri::Builder::default()
-    .on_page_load(|window, _payload| {
-      let label = window.label().to_string();
-      window.listen("clicked".to_string(), move |_payload| {
-        println!("got 'clicked' event on window '{label}'");
-      });
+    .on_page_load(|window, payload| {
+      if payload.event() == PageLoadEvent::Finished {
+        let label = window.label().to_string();
+        window.listen("clicked".to_string(), move |_payload| {
+          println!("got 'clicked' event on window '{label}'");
+        });
+      }
     })
     .setup(|app| {
       #[allow(unused_mut)]
diff --git a/examples/parent-window/main.rs b/examples/parent-window/main.rs
index a0bf7e00beb2..ed301e276626 100644
--- a/examples/parent-window/main.rs
+++ b/examples/parent-window/main.rs
@@ -4,7 +4,11 @@
 
 #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
 
-use tauri::{command, window::WindowBuilder, Window, WindowUrl};
+use tauri::{
+  command,
+  window::{PageLoadEvent, WindowBuilder},
+  Window, WindowUrl,
+};
 
 #[command]
 async fn create_child_window(id: String, window: Window) {
@@ -22,11 +26,13 @@ async fn create_child_window(id: String, window: Window) {
 
 fn main() {
   tauri::Builder::default()
-    .on_page_load(|window, _payload| {
-      let label = window.label().to_string();
-      window.listen("clicked".to_string(), move |_payload| {
-        println!("got 'clicked' event on window '{label}'");
-      });
+    .on_page_load(|window, payload| {
+      if payload.event() == PageLoadEvent::Finished {
+        let label = window.label().to_string();
+        window.listen("clicked".to_string(), move |_payload| {
+          println!("got 'clicked' event on window '{label}'");
+        });
+      }
     })
     .invoke_handler(tauri::generate_handler![create_child_window])
     .setup(|app| {
diff --git a/tooling/cli/node/test/jest/fixtures/app/src-tauri/src/main.rs b/tooling/cli/node/test/jest/fixtures/app/src-tauri/src/main.rs
index 482229eb1fa1..dc7b1060ae2d 100644
--- a/tooling/cli/node/test/jest/fixtures/app/src-tauri/src/main.rs
+++ b/tooling/cli/node/test/jest/fixtures/app/src-tauri/src/main.rs
@@ -9,14 +9,16 @@ fn exit(window: tauri::Window) {
 
 fn main() {
   tauri::Builder::default()
-    .on_page_load(|window, _| {
-      let window_ = window.clone();
-      window.listen("hello".into(), move |_| {
-        window_
-          .emit(&"reply".to_string(), Some("{ msg: 'TEST' }".to_string()))
-          .unwrap();
-      });
-      window.eval("window.onTauriInit()").unwrap();
+    .on_page_load(|window, payload| {
+      if payload.event() == tauri::window::PageLoadEvent::Finished {
+        let window_ = window.clone();
+        window.listen("hello".into(), move |_| {
+          window_
+            .emit(&"reply".to_string(), Some("{ msg: 'TEST' }".to_string()))
+            .unwrap();
+        });
+        window.eval("window.onTauriInit()").unwrap();
+      }
     })
     .invoke_handler(tauri::generate_handler![exit])
     .run(tauri::generate_context!())

From 18a9414070af3b7ee314d11cde550300dc93db36 Mon Sep 17 00:00:00 2001
From: Lucas Nogueira <lucas@tauri.app>
Date: Thu, 26 Oct 2023 12:58:39 -0300
Subject: [PATCH 2/3] fix doctest

---
 core/tauri/src/plugin.rs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/core/tauri/src/plugin.rs b/core/tauri/src/plugin.rs
index 87bb6e9aa48e..9a601f24887c 100644
--- a/core/tauri/src/plugin.rs
+++ b/core/tauri/src/plugin.rs
@@ -369,7 +369,7 @@ impl<R: Runtime, C: DeserializeOwned> Builder<R, C> {
   /// fn init<R: Runtime>() -> TauriPlugin<R> {
   ///   Builder::new("example")
   ///     .on_page_load(|window, payload| {
-  ///       println!("{} URL {} in window {}", payload.event(), payload.url(), window.label());
+  ///       println!("{:?} URL {} in window {}", payload.event(), payload.url(), window.label());
   ///     })
   ///     .build()
   /// }

From 78e2b53ea546f17de701f031b4a298e9a9fe1af9 Mon Sep 17 00:00:00 2001
From: Lucas Nogueira <lucas@tauri.app>
Date: Fri, 27 Oct 2023 08:33:54 -0300
Subject: [PATCH 3/3] update docs

---
 core/tauri-config-schema/schema.json |  2 +-
 core/tauri-runtime/src/window.rs     |  2 +-
 core/tauri-utils/src/config.rs       |  2 +-
 core/tauri/src/lib.rs                |  2 +-
 core/tauri/src/menu/mod.rs           |  2 +-
 core/tauri/src/window/mod.rs         | 17 +++++++++++++----
 tooling/cli/schema.json              |  2 +-
 7 files changed, 19 insertions(+), 10 deletions(-)

diff --git a/core/tauri-config-schema/schema.json b/core/tauri-config-schema/schema.json
index a91fdf16eed6..b4a96b2847de 100644
--- a/core/tauri-config-schema/schema.json
+++ b/core/tauri-config-schema/schema.json
@@ -1198,7 +1198,7 @@
           }
         },
         "name": {
-          "description": "The name. Maps to `CFBundleTypeName` on macOS. Default to ext[0]",
+          "description": "The name. Maps to `CFBundleTypeName` on macOS. Default to `ext[0]`",
           "type": [
             "string",
             "null"
diff --git a/core/tauri-runtime/src/window.rs b/core/tauri-runtime/src/window.rs
index af07005c38c4..6a436f56eab1 100644
--- a/core/tauri-runtime/src/window.rs
+++ b/core/tauri-runtime/src/window.rs
@@ -357,7 +357,7 @@ pub struct DetachedWindow<T: UserEvent, R: Runtime<T>> {
   /// Name of the window
   pub label: String,
 
-  /// The [`Dispatch`](crate::Dispatch) associated with the window.
+  /// The [`Dispatch`] associated with the window.
   pub dispatcher: R::Dispatcher,
 }
 
diff --git a/core/tauri-utils/src/config.rs b/core/tauri-utils/src/config.rs
index 88d18529ffe5..5d054c9a2f86 100644
--- a/core/tauri-utils/src/config.rs
+++ b/core/tauri-utils/src/config.rs
@@ -703,7 +703,7 @@ impl<'d> serde::Deserialize<'d> for AssociationExt {
 pub struct FileAssociation {
   /// File extensions to associate with this app. e.g. 'png'
   pub ext: Vec<AssociationExt>,
-  /// The name. Maps to `CFBundleTypeName` on macOS. Default to ext[0]
+  /// The name. Maps to `CFBundleTypeName` on macOS. Default to `ext[0]`
   pub name: Option<String>,
   /// The association description. Windows-only. It is displayed on the `Type` column on Windows Explorer.
   pub description: Option<String>,
diff --git a/core/tauri/src/lib.rs b/core/tauri/src/lib.rs
index a3838444a18d..b1297ac975ae 100644
--- a/core/tauri/src/lib.rs
+++ b/core/tauri/src/lib.rs
@@ -696,7 +696,7 @@ pub trait Manager<R: Runtime>: sealed::ManagerBase<R> {
   /// If the state for the `T` type has previously been set, the state is unchanged and false is returned. Otherwise true is returned.
   ///
   /// Managed state can be retrieved by any command handler via the
-  /// [`State`](crate::State) guard. In particular, if a value of type `T`
+  /// [`State`] guard. In particular, if a value of type `T`
   /// is managed by Tauri, adding `State<T>` to the list of arguments in a
   /// command handler instructs Tauri to retrieve the managed value.
   /// Additionally, [`state`](Self#method.state) can be used to retrieve the value manually.
diff --git a/core/tauri/src/menu/mod.rs b/core/tauri/src/menu/mod.rs
index bdfef026836a..eaba4b52e730 100644
--- a/core/tauri/src/menu/mod.rs
+++ b/core/tauri/src/menu/mod.rs
@@ -47,7 +47,7 @@ impl From<muda::MenuEvent> for MenuEvent {
   }
 }
 
-/// Application metadata for the [`PredefinedMenuItem::about`](crate::PredefinedMenuItem::about).
+/// Application metadata for the [`PredefinedMenuItem::about`].
 #[derive(Debug, Clone, Default)]
 pub struct AboutMetadata {
   /// Sets the application name.
diff --git a/core/tauri/src/window/mod.rs b/core/tauri/src/window/mod.rs
index 6dd3fad58623..38349526efca 100644
--- a/core/tauri/src/window/mod.rs
+++ b/core/tauri/src/window/mod.rs
@@ -73,7 +73,7 @@ struct WindowCreatedEvent {
   label: String,
 }
 
-/// The payload for the [`OnPageLoad`] hook.
+/// The payload for the [`WindowBuilder::on_page_load`] hook.
 #[derive(Debug, Clone)]
 pub struct PageLoadPayload<'a> {
   pub(crate) url: &'a Url,
@@ -353,14 +353,16 @@ impl<'a, R: Runtime> WindowBuilder<'a, R> {
     self
   }
 
-  /// Defines a closure to be executed when the webview navigates to a URL. Returning `false` cancels the navigation.
+  /// Defines a closure to be executed when a page load event is triggered.
+  /// The event can be either [`PageLoadEvent::Started`] if the page has started loading
+  /// or [`PageLoadEvent::Finished`] when the page finishes loading.
   ///
   /// # Examples
   ///
   /// ```rust,no_run
   /// use tauri::{
   ///   utils::config::{Csp, CspDirectiveSources, WindowUrl},
-  ///   window::WindowBuilder,
+  ///   window::{PageLoadEvent, WindowBuilder},
   /// };
   /// use http::header::HeaderValue;
   /// use std::collections::HashMap;
@@ -368,7 +370,14 @@ impl<'a, R: Runtime> WindowBuilder<'a, R> {
   ///   .setup(|app| {
   ///     WindowBuilder::new(app, "core", WindowUrl::App("index.html".into()))
   ///       .on_page_load(|window, payload| {
-  ///         println!("{:?} {}", payload.event(), payload.url());
+  ///         match payload.event() {
+  ///           PageLoadEvent::Started => {
+  ///             println!("{} finished loading", payload.url());
+  ///           }
+  ///           PageLoadEvent::Finished => {
+  ///             println!("{} finished loading", payload.url());
+  ///           }
+  ///         }
   ///       })
   ///       .build()?;
   ///     Ok(())
diff --git a/tooling/cli/schema.json b/tooling/cli/schema.json
index a91fdf16eed6..b4a96b2847de 100644
--- a/tooling/cli/schema.json
+++ b/tooling/cli/schema.json
@@ -1198,7 +1198,7 @@
           }
         },
         "name": {
-          "description": "The name. Maps to `CFBundleTypeName` on macOS. Default to ext[0]",
+          "description": "The name. Maps to `CFBundleTypeName` on macOS. Default to `ext[0]`",
           "type": [
             "string",
             "null"