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

Issue:
web-wallet: Misleading Error Message When Unlocking Wallet Without Internet Connection
### Description
Currently, when a user attempts to unlock their Wallet without an internet connection, they are presented with an **"invalid mnemonic" error**. This is misleading and may cause users to believe there is an issue with the mnemonic they entered, when the actual problem is the lack of internet connectivity.

As the Web Wallet uses a service worker, it is possible for the application to remain loaded in the browser even when there is no active internet connection. While this behavior is handled correctly in some areas (e.g., sync failures are displayed with appropriate status messages on the dashboard), it is not yet addressed in the pre-unlocked views.

**Affected Areas**
- Unlock screen
- Create wallet flow
- Restore wallet flow

### Expected Behavior
The application should properly handle scenarios where the user's internet connection is unavailable:
- Detect lack of internet connectivity.
- Display a clear and specific error message to inform the user about the connectivity issue.
- Prevent misleading error messages like "invalid mnemonic."

### Steps to Reproduce
- Load the Web Wallet while online.
- Disconnect from the internet.
- Attempt to unlock the wallet with a valid mnemonic.

### Actual Behavior
The user sees an **"invalid mnemonic" error**, despite entering a correct mnemonic.

### Proposed Solution
- Implement connectivity checks for the pre-unlocked views.
- Update error messaging to accurately reflect the connectivity status when applicable.

### Additional Context
This issue is particularly relevant as users may frequently encounter this scenario when working with service worker-based applications.


Discussion / hints:

Fix patch:
diff --git a/web-wallet/CHANGELOG.md b/web-wallet/CHANGELOG.md
index 3118dcbfd4..3b99844675 100644
--- a/web-wallet/CHANGELOG.md
+++ b/web-wallet/CHANGELOG.md
@@ -17,6 +17,8 @@ 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]
+
 ## [0.10.1] - 2024-12-18
 
 ### Fixed
@@ -452,6 +454,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 [#3073]: https://github.com/dusk-network/rusk/issues/3073
 [#3076]: https://github.com/dusk-network/rusk/issues/3076
 [#3081]: https://github.com/dusk-network/rusk/issues/3081
+[#3097]: https://github.com/dusk-network/rusk/issues/3097
 [#3098]: https://github.com/dusk-network/rusk/issues/3098
 [#3099]: https://github.com/dusk-network/rusk/issues/3099
 [#3113]: https://github.com/dusk-network/rusk/issues/3113
@@ -462,6 +465,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 [#3179]: https://github.com/dusk-network/rusk/issues/3179
 [#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
 
 <!-- VERSIONS -->
 
diff --git a/web-wallet/src/lib/errors/index.js b/web-wallet/src/lib/errors/index.js
new file mode 100644
index 0000000000..3659355455
--- /dev/null
+++ b/web-wallet/src/lib/errors/index.js
@@ -0,0 +1,3 @@
+export { default as InvalidMnemonicError } from "./invalid-mnemonic-error";
+export { default as InvalidPasswordError } from "./invalid-password-error";
+export { default as MismatchedWalletError } from "./mismatched-wallet-error";
diff --git a/web-wallet/src/lib/errors/invalid-mnemonic-error.js b/web-wallet/src/lib/errors/invalid-mnemonic-error.js
new file mode 100644
index 0000000000..e9c3fef503
--- /dev/null
+++ b/web-wallet/src/lib/errors/invalid-mnemonic-error.js
@@ -0,0 +1,8 @@
+class InvalidMnemonicError extends Error {
+  constructor() {
+    super("Invalid mnemonic");
+    this.name = "InvalidMnemonicError";
+  }
+}
+
+export default InvalidMnemonicError;
diff --git a/web-wallet/src/lib/errors/invalid-password-error.js b/web-wallet/src/lib/errors/invalid-password-error.js
new file mode 100644
index 0000000000..f6683b066d
--- /dev/null
+++ b/web-wallet/src/lib/errors/invalid-password-error.js
@@ -0,0 +1,8 @@
+class InvalidPasswordError extends Error {
+  constructor() {
+    super("Wrong password");
+    this.name = "InvalidPasswordError";
+  }
+}
+
+export default InvalidPasswordError;
diff --git a/web-wallet/src/lib/errors/mismatched-wallet-error.js b/web-wallet/src/lib/errors/mismatched-wallet-error.js
new file mode 100644
index 0000000000..23d43510b1
--- /dev/null
+++ b/web-wallet/src/lib/errors/mismatched-wallet-error.js
@@ -0,0 +1,8 @@
+class MismatchedWalletError extends Error {
+  constructor() {
+    super("Mismatched wallet address or no existing wallet");
+    this.name = "MismatchedWalletError";
+  }
+}
+
+export default MismatchedWalletError;
diff --git a/web-wallet/src/routes/(welcome)/login/+page.svelte b/web-wallet/src/routes/(welcome)/login/+page.svelte
index 23085dd43f..c592b0fdfb 100644
--- a/web-wallet/src/routes/(welcome)/login/+page.svelte
+++ b/web-wallet/src/routes/(welcome)/login/+page.svelte
@@ -4,7 +4,14 @@
   import { mdiArrowLeft, mdiKeyOutline } from "@mdi/js";
   import { validateMnemonic } from "bip39";
 
+  import { getErrorFrom } from "$lib/dusk/error";
   import { Button, Textbox } from "$lib/dusk/components";
+  import {
+    InvalidMnemonicError,
+    InvalidPasswordError,
+    MismatchedWalletError,
+  } from "$lib/errors";
+
   import { AppAnchor, AppAnchorButton, Banner } from "$lib/components";
   import { IconHeadingCard } from "$lib/containers/Cards";
   import { goto } from "$lib/navigation";
@@ -20,9 +27,6 @@
   } from "$lib/wallet";
   import loginInfoStorage from "$lib/services/loginInfoStorage";
 
-  const localDataCheckErrorMsg =
-    "Mismatched wallet address or no existing wallet";
-
   /** @type {(seed: Uint8Array) => Promise<import("$lib/vendor/w3sper.js/src/mod").ProfileGenerator>} */
   async function checkLocalData(seed) {
     const profileGenerator = profileGeneratorFrom(seed);
@@ -30,7 +34,7 @@
     const currentAddress = $settingsStore.userId;
 
     if (!currentAddress || currentAddress !== defaultAddress) {
-      throw new Error(localDataCheckErrorMsg);
+      throw new MismatchedWalletError();
     }
 
     return profileGenerator;
@@ -40,12 +44,12 @@
   const getSeedFromMnemonicAsync = async (mnemonic) =>
     validateMnemonic(mnemonic)
       ? getSeedFromMnemonic(mnemonic)
-      : Promise.reject(new Error("Invalid mnemonic"));
+      : Promise.reject(new InvalidMnemonicError());
 
   /** @type {(loginInfo: MnemonicEncryptInfo) => (pwd: string) => Promise<Uint8Array>} */
   const getSeedFromInfo = (loginInfo) => (pwd) =>
     decryptMnemonic(loginInfo, pwd).then(getSeedFromMnemonic, () =>
-      Promise.reject(new Error("Wrong password"))
+      Promise.reject(new InvalidPasswordError())
     );
 
   const loginInfo = loginInfoStorage.get();
@@ -57,8 +61,8 @@
   /** @type {string} */
   let secretText = "";
 
-  /** @type {null|"invalid-password"|"invalid-mnemonic"} */
-  let error = null;
+  /** @type {Error} */
+  let error;
 
   /** @type {import("svelte/elements").FormEventHandler<HTMLFormElement>} */
   function handleUnlockWalletSubmit() {
@@ -72,16 +76,16 @@
       .then((profileGenerator) => walletStore.init(profileGenerator))
       .then(() => goto("/dashboard"))
       .catch((err) => {
-        if (err.message === localDataCheckErrorMsg) {
+        if (err instanceof MismatchedWalletError) {
           const enteredMnemonicPhrase = secretText.split(" ");
           mnemonicPhraseResetStore.set(enteredMnemonicPhrase);
           goto("/setup/restore");
+
           return;
+        } else {
+          error = err instanceof Error ? err : getErrorFrom(err);
         }
-        error =
-          err.message === "Wrong password"
-            ? "invalid-password"
-            : "invalid-mnemonic";
+
         fldSecret.focus();
         fldSecret.select();
       });
@@ -113,15 +117,14 @@
           type="password"
           autocomplete="current-password"
         />
-        {#if error === "invalid-mnemonic"}
+        {#if error instanceof InvalidMnemonicError}
           <Banner title="Invalid mnemonic phrase" variant="error">
             <p>
               Please ensure you have entered your 12-word mnemonic phrase, with
               a space separating each word.
             </p>
           </Banner>
-        {/if}
-        {#if error === "invalid-password"}
+        {:else if error instanceof InvalidPasswordError}
           <Banner title="Invalid password" variant="error">
             <p>
               Please ensure the password entered matches the one you have set up
@@ -129,6 +132,13 @@
               you can <AppAnchor href="/setup/restore">restore</AppAnchor> your wallet.
             </p>
           </Banner>
+        {:else if error}
+          <Banner
+            title={error.name.replace(/(\w)([A-Z])/g, "$1 $2")}
+            variant="error"
+          >
+            <p>{error.message}</p>
+          </Banner>
         {/if}
         <Button text="Unlock Wallet" type="submit" />
         {#if modeLabel === "Password"}


Test patch:
diff --git a/web-wallet/src/routes/(welcome)/login/__tests__/__snapshots__/page.spec.js.snap b/web-wallet/src/routes/(welcome)/login/__tests__/__snapshots__/page.spec.js.snap
index 836f724ab0..f06439819a 100644
--- a/web-wallet/src/routes/(welcome)/login/__tests__/__snapshots__/page.spec.js.snap
+++ b/web-wallet/src/routes/(welcome)/login/__tests__/__snapshots__/page.spec.js.snap
@@ -77,7 +77,6 @@ exports[`Login > Mnemonic phrase workflow > should render the login page and sho
           
            
            
-           
           <button
             class="dusk-button dusk-button--type--submit dusk-button--variant--primary dusk-button--size--default"
             type="submit"
@@ -204,7 +203,6 @@ exports[`Login > Password workflow > should show the password field and the link
           
            
            
-           
           <button
             class="dusk-button dusk-button--type--submit dusk-button--variant--primary dusk-button--size--default"
             type="submit"