Repository: dusk-network/rusk (language: rust)

Issue:
The wallet doesn't wait for the wasm to load at startup



Discussion / hints:

Fix patch:
diff --git a/web-wallet/CHANGELOG.md b/web-wallet/CHANGELOG.md
index 3f76016f68..c87f630e42 100644
--- a/web-wallet/CHANGELOG.md
+++ b/web-wallet/CHANGELOG.md
@@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ### Fixed
 
 - Fix wrong error shown in the login screen [#3226] [#3097]
+- Fix wallet not waiting for the Wasm to load at startup [#3238]
 
 ## [0.10.1] - 2024-12-18
 
@@ -470,6 +471,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 [#3203]: https://github.com/dusk-network/rusk/issues/3203
 [#3212]: https://github.com/dusk-network/rusk/issues/3212
 [#3226]: https://github.com/dusk-network/rusk/issues/3226
+[#3238]: https://github.com/dusk-network/rusk/issues/3238
 [#3245]: https://github.com/dusk-network/rusk/issues/3245
 
 <!-- VERSIONS -->
diff --git a/web-wallet/src/lib/components/Banner/Banner.svelte b/web-wallet/src/lib/components/Banner/Banner.svelte
index fe83cb8491..fbfe248268 100644
--- a/web-wallet/src/lib/components/Banner/Banner.svelte
+++ b/web-wallet/src/lib/components/Banner/Banner.svelte
@@ -1,13 +1,20 @@
+<svelte:options immutable={true} />
+
 <script>
-  import { Icon } from "$lib/dusk/components";
   import {
     mdiAlertCircleOutline,
     mdiAlertDecagramOutline,
     mdiAlertOutline,
   } from "@mdi/js";
 
+  import { makeClassName } from "$lib/dusk/string";
+  import { Icon } from "$lib/dusk/components";
+
   import "./Banner.css";
 
+  /** @type {string | undefined} */
+  export let className = undefined;
+
   /** @type {string} */
   export let title;
 
@@ -24,9 +31,11 @@
         return mdiAlertCircleOutline;
     }
   }
+
+  $: classes = makeClassName(["banner", `banner--${variant}`, className]);
 </script>
 
-<div class="banner banner--{variant}">
+<div {...$$restProps} class={classes}>
   <Icon
     path={getBannerIconPath()}
     size="large"
diff --git a/web-wallet/src/lib/stores/networkStore.js b/web-wallet/src/lib/stores/networkStore.js
index 0dea177e4e..1beb99dcfd 100644
--- a/web-wallet/src/lib/stores/networkStore.js
+++ b/web-wallet/src/lib/stores/networkStore.js
@@ -7,6 +7,7 @@ import {
   AddressSyncer,
   Network,
 } from "$lib/vendor/w3sper.js/src/mod";
+import { rejectAfter } from "$lib/dusk/promise";
 import { makeNodeUrl } from "$lib/url";
 
 function getNetworkUrl() {
@@ -50,7 +51,16 @@ const checkBlock = (height, hash) =>
     .then(getKey("checkBlock"));
 
 /** @type {NetworkStoreServices["connect"]} */
-const connect = async () => (network.connected ? network : network.connect());
+const connect = async () =>
+  network.connected
+    ? network
+    : Promise.race([
+        network.connect(),
+        rejectAfter(
+          10000,
+          new Error("Timed out while connecting to the network")
+        ),
+      ]);
 
 /** @type {NetworkStoreServices["disconnect"]} */
 const disconnect = () => network.disconnect();
diff --git a/web-wallet/src/routes/(welcome)/+layout.svelte b/web-wallet/src/routes/(welcome)/+layout.svelte
index 1793dde810..9dc3c0de11 100644
--- a/web-wallet/src/routes/(welcome)/+layout.svelte
+++ b/web-wallet/src/routes/(welcome)/+layout.svelte
@@ -1,8 +1,18 @@
 <script>
-  import { AppImage } from "$lib/components";
-  import { settingsStore } from "$lib/stores";
+  import { ErrorDetails, Throbber } from "$lib/dusk/components";
+  import { AppAnchor, AppImage, Banner } from "$lib/components";
+  import { networkStore, settingsStore } from "$lib/stores";
 
   const { darkMode } = $settingsStore;
+
+  let retryKey = 0;
+
+  /** @param {Event} event */
+  function handleRetry(event) {
+    event.preventDefault();
+
+    retryKey ^= 1;
+  }
 </script>
 
 <header>
@@ -13,5 +23,50 @@
     height="31"
   />
 </header>
+{#key retryKey}
+  {#await networkStore.connect().then(() => networkStore.init())}
+    <div class="welcome-layout__loading">
+      <Throbber />
+      <strong>CONNECTING TO THE NETWORK</strong>
+    </div>
+  {:then}
+    <slot />
+  {:catch error}
+    <Banner
+      className="welcome-layout__error"
+      title="Error while trying to connect to the network"
+      variant="error"
+    >
+      <p>
+        The Web Wallet is unable to connect to the network.<br />
+        This may be a temporary issue.<br />
+        Please try again in a few minutes by clicking <AppAnchor
+          href="/setup"
+          on:click={handleRetry}>retry</AppAnchor
+        >.
+      </p>
+      <ErrorDetails {error} summary="Error details" />
+    </Banner>
+  {/await}
+{/key}
+
+<style lang="postcss">
+  :global {
+    .welcome-layout__error,
+    .welcome-layout__loading {
+      margin-top: 10dvh;
+    }
+
+    .welcome-layout__error > .banner__content {
+      gap: var(--medium-gap);
+    }
 
-<slot />
+    .welcome-layout__loading {
+      width: 100%;
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      gap: var(--medium-gap);
+    }
+  }
+</style>
diff --git a/web-wallet/src/routes/+layout.svelte b/web-wallet/src/routes/+layout.svelte
index d6bc8d37ec..8515423898 100644
--- a/web-wallet/src/routes/+layout.svelte
+++ b/web-wallet/src/routes/+layout.svelte
@@ -1,6 +1,5 @@
 <script>
-  import { onDestroy, onMount } from "svelte";
-  import { networkStore, settingsStore } from "$lib/stores";
+  import { settingsStore } from "$lib/stores";
   import { Toast, Tooltip } from "$lib/dusk/components";
 
   import "../style/main.css";
@@ -8,15 +7,6 @@
   settingsStore.subscribe(({ darkMode }) => {
     document.documentElement.classList.toggle("dark", darkMode);
   });
-
-  onDestroy(async () => {
-    await networkStore.disconnect();
-  });
-
-  onMount(async () => {
-    await networkStore.connect();
-    await networkStore.init();
-  });
 </script>
 
 <main class="app">


Test patch:
diff --git a/web-wallet/src/lib/stores/__tests__/walletStore.spec.js b/web-wallet/src/lib/stores/__tests__/walletStore.spec.js
index 7feb161258..ca01a07ea7 100644
--- a/web-wallet/src/lib/stores/__tests__/walletStore.spec.js
+++ b/web-wallet/src/lib/stores/__tests__/walletStore.spec.js
@@ -229,9 +229,15 @@ describe("Wallet store", async () => {
       );
 
       expect(clearTimeoutSpy).toHaveBeenCalledTimes(1);
-      expect(setTimeoutSpy).toHaveBeenCalledTimes(1);
+
+      /**
+       * The first call is caused by the timeout in the network
+       * connect, so we check the last call, that is the one
+       * made in the wallet store.
+       */
+      expect(setTimeoutSpy).toHaveBeenCalledTimes(2);
       expect(clearTimeoutSpy.mock.invocationCallOrder[0]).toBeLessThan(
-        setTimeoutSpy.mock.invocationCallOrder[0]
+        setTimeoutSpy.mock.invocationCallOrder[1]
       );
 
       await vi.advanceTimersByTimeAsync(1);
diff --git a/web-wallet/src/routes/__tests__/__snapshots__/layout.spec.js.snap b/web-wallet/src/routes/__tests__/__snapshots__/layout.spec.js.snap
index a2ff8f7dc5..6819e63446 100644
--- a/web-wallet/src/routes/__tests__/__snapshots__/layout.spec.js.snap
+++ b/web-wallet/src/routes/__tests__/__snapshots__/layout.spec.js.snap
@@ -1,6 +1,6 @@
 // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
 
-exports[`Main layout > should render the main layout and disconnect from the network when dismounting 1`] = `
+exports[`Main layout > should render the main layout 1`] = `
 <div>
   <main
     class="app svelte-3y7xzm"
diff --git a/web-wallet/src/routes/__tests__/layout.spec.js b/web-wallet/src/routes/__tests__/layout.spec.js
index a8eb25f611..cd9e680c1b 100644
--- a/web-wallet/src/routes/__tests__/layout.spec.js
+++ b/web-wallet/src/routes/__tests__/layout.spec.js
@@ -1,8 +1,8 @@
-import { afterAll, afterEach, describe, expect, it, vi } from "vitest";
+import { afterEach, describe, expect, it } from "vitest";
 import { cleanup, render } from "@testing-library/svelte";
 import { get } from "svelte/store";
 
-import { networkStore, settingsStore } from "$lib/stores";
+import { settingsStore } from "$lib/stores";
 
 import MainLayout from "../+layout.svelte";
 
@@ -10,28 +10,16 @@ describe("Main layout", async () => {
   const isDarkMode = () => get(settingsStore).darkMode;
   const hasDarkClass = () =>
     document.documentElement.classList.contains("dark");
-  const networkDisconnectSpy = vi
-    .spyOn(networkStore, "disconnect")
-    .mockResolvedValue(undefined);
 
   afterEach(() => {
     cleanup();
     settingsStore.reset();
-    networkDisconnectSpy.mockClear();
   });
 
-  afterAll(() => {
-    networkDisconnectSpy.mockRestore();
-  });
-
-  it("should render the main layout and disconnect from the network when dismounting", () => {
-    const { container, unmount } = render(MainLayout);
+  it("should render the main layout", () => {
+    const { container } = render(MainLayout);
 
     expect(container).toMatchSnapshot();
-
-    unmount();
-
-    expect(networkDisconnectSpy).toHaveBeenCalledTimes(1);
   });
 
   it('should add and remove the "dark" class name to the `html` element when the `darkMode` value changes in thesettings store', () => {