diff --git a/web-wallet/CHANGELOG.md b/web-wallet/CHANGELOG.md index e60952784..741c600a7 100644 --- a/web-wallet/CHANGELOG.md +++ b/web-wallet/CHANGELOG.md @@ -7,8 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add autocomplete attribute on the login field [#1533] + ### Changed +- Redesign the landing page [#1534] +- Trigger the Restore flow if a user tries to access a new wallet [#1535] - Update `OperationResult` to infer error messages from arbitrary values [#1524] - Update Tabs to use native scroll behavior [#1320] @@ -183,6 +189,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#1514]: https://github.com/dusk-network/rusk/issues/1514 [#1519]: https://github.com/dusk-network/rusk/issues/1519 [#1524]: https://github.com/dusk-network/rusk/issues/1524 +[#1533]: https://github.com/dusk-network/rusk/issues/1533 +[#1534]: https://github.com/dusk-network/rusk/issues/1534 +[#1535]: https://github.com/dusk-network/rusk/issues/1535 [#1537]: https://github.com/dusk-network/rusk/issues/1537 diff --git a/web-wallet/package-lock.json b/web-wallet/package-lock.json index 8982c9cb0..648415b24 100644 --- a/web-wallet/package-lock.json +++ b/web-wallet/package-lock.json @@ -13,7 +13,6 @@ "@floating-ui/dom": "1.6.3", "@mdi/js": "7.4.47", "bip39": "3.1.0", - "css-doodle": "0.38.4", "lamb": "0.61.1", "qr-scanner": "1.4.2", "qrcode": "1.5.3", @@ -2826,15 +2825,6 @@ "node": "*" } }, - "node_modules/css-doodle": { - "version": "0.38.4", - "resolved": "https://registry.npmjs.org/css-doodle/-/css-doodle-0.38.4.tgz", - "integrity": "sha512-KM8lhXst5eOWXlF7GcFEe3ll9PpDnXHIrJ1bgAmvDoaE5B2AdVWJviOqr2sSo8qPa1zD1JDXe3rLWU6X1sz6CA==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/css-doodle" - } - }, "node_modules/css-tree": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", diff --git a/web-wallet/package.json b/web-wallet/package.json index eba4f6e06..d7475e031 100644 --- a/web-wallet/package.json +++ b/web-wallet/package.json @@ -39,7 +39,6 @@ "@floating-ui/dom": "1.6.3", "@mdi/js": "7.4.47", "bip39": "3.1.0", - "css-doodle": "0.38.4", "lamb": "0.61.1", "qr-scanner": "1.4.2", "qrcode": "1.5.3", diff --git a/web-wallet/src/lib/components/ExistingWalletNotice/ExistingWalletNotice.svelte b/web-wallet/src/lib/components/ExistingWalletNotice/ExistingWalletNotice.svelte index 21b889e76..e5a17f573 100644 --- a/web-wallet/src/lib/components/ExistingWalletNotice/ExistingWalletNotice.svelte +++ b/web-wallet/src/lib/components/ExistingWalletNotice/ExistingWalletNotice.svelte @@ -11,14 +11,15 @@

- Logging in to a new wallet will overwrite the current local wallet - cache, meaning that when you log in again with the previous - mnemonic/account you will need to wait for the wallet to sync. + Initializing a new wallet will replace your existing local wallet cache, + erasing any stored data. Ensure you have securely backed up your current + wallet's seed phrase to prevent loss. Proceeding without a backup can + lead to irreversible loss of access to your assets.

diff --git a/web-wallet/src/lib/dusk/components/FieldButtonGroup/FieldButtonGroup.svelte b/web-wallet/src/lib/dusk/components/FieldButtonGroup/FieldButtonGroup.svelte new file mode 100644 index 000000000..41f627d7a --- /dev/null +++ b/web-wallet/src/lib/dusk/components/FieldButtonGroup/FieldButtonGroup.svelte @@ -0,0 +1,46 @@ + + +
+ +
diff --git a/web-wallet/src/lib/dusk/components/Mnemonic/Mnemonic.svelte b/web-wallet/src/lib/dusk/components/Mnemonic/Mnemonic.svelte index 769b789ff..03d689296 100644 --- a/web-wallet/src/lib/dusk/components/Mnemonic/Mnemonic.svelte +++ b/web-wallet/src/lib/dusk/components/Mnemonic/Mnemonic.svelte @@ -38,6 +38,9 @@ enteredMnemonicPhrase = Array(wordLimit).fill(""); } + const isTriggeredByLogin = + enteredMnemonicPhrase.some((word) => word !== "") && currentIndex === 0; + /** * @param {string} word * @param {string} index @@ -118,63 +121,67 @@
-
- {#if type === "authenticate" && shouldShowPaste} + {#if !isTriggeredByLogin} +
+ {#if type === "authenticate" && shouldShowPaste} +
+
+ {/if} -
- {#if type === "authenticate" && enteredWordIndex.includes("")} - - handleKeyDownOnAuthenticateTextbox(e, currentIndex.toString())} - maxlength={8} - type="text" - bind:value={currentInput} - /> - {#if suggestions.length} -
- {#each suggestions as suggestion, index (`${suggestion}-${index}`)} -
- {/if} - {:else} - {#each mnemonicPhrase as word, index (`${word}-${index}`)} -
+ {/if} + {:else} + {#each mnemonicPhrase as word, index (`${word}-${index}`)} +
+ {/if}
diff --git a/web-wallet/src/lib/dusk/components/__tests__/FieldButtonGroup.spec.js b/web-wallet/src/lib/dusk/components/__tests__/FieldButtonGroup.spec.js new file mode 100644 index 000000000..dd991e11b --- /dev/null +++ b/web-wallet/src/lib/dusk/components/__tests__/FieldButtonGroup.spec.js @@ -0,0 +1,75 @@ +import { cleanup, fireEvent, render } from "@testing-library/svelte"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { FieldButtonGroup } from ".."; + +describe("FieldButtonGroup", () => { + const baseProps = { name: "test" }; + const baseOptions = { + props: baseProps, + target: document.body, + }; + + afterEach(cleanup); + + it("renders the component with default values", async () => { + const { getByRole, container } = render(FieldButtonGroup, baseOptions); + + const input = getByRole("textbox"); + const button = getByRole("button"); + + expect(input).toBeInTheDocument(); + expect(button).toBeInTheDocument(); + + expect(container.firstChild).toMatchSnapshot(); + }); + + it("updates input value on change", async () => { + const { getByRole, container } = render(FieldButtonGroup, baseOptions); + const input = getByRole("textbox"); + + await fireEvent.input(input, { target: { value: "test value" } }); + + expect(input).toHaveValue("test value"); + + expect(container.firstChild).toMatchSnapshot(); + }); + + it("triggers click event on button click", async () => { + const { getByRole, component } = render(FieldButtonGroup); + const button = getByRole("button"); + + const mockClickHandler = vi.fn(); + component.$on("click", mockClickHandler); + + await fireEvent.click(button); + + expect(mockClickHandler).toHaveBeenCalled(); + }); + + it("focuses input on focus() call", async () => { + const { getByRole, component } = render(FieldButtonGroup); + const input = getByRole("textbox"); + + component.focus(); + expect(input).toHaveFocus(); + }); + + it("should expose a method to select the element's text", () => { + const { component, getByRole } = render(FieldButtonGroup, { + ...baseProps, + value: "some input text", + }); + + const input = /** @type {HTMLInputElement} */ (getByRole("textbox")); + + component.select(); + + const selectedText = input.value.substring( + Number(input.selectionStart), + Number(input.selectionEnd) + ); + + expect(selectedText).toBe("some input text"); + }); +}); diff --git a/web-wallet/src/lib/dusk/components/__tests__/__snapshots__/FieldButtonGroup.spec.js.snap b/web-wallet/src/lib/dusk/components/__tests__/__snapshots__/FieldButtonGroup.spec.js.snap new file mode 100644 index 000000000..9258baecc --- /dev/null +++ b/web-wallet/src/lib/dusk/components/__tests__/__snapshots__/FieldButtonGroup.spec.js.snap @@ -0,0 +1,61 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`FieldButtonGroup > renders the component with default values 1`] = ` +
+ + + + + + +
+`; + +exports[`FieldButtonGroup > updates input value on change 1`] = ` +
+ + + + + + +
+`; diff --git a/web-wallet/src/lib/dusk/components/index.js b/web-wallet/src/lib/dusk/components/index.js index f32b70ee6..2fdd717a5 100644 --- a/web-wallet/src/lib/dusk/components/index.js +++ b/web-wallet/src/lib/dusk/components/index.js @@ -8,6 +8,7 @@ export { default as Checkbox } from "./Checkbox/Checkbox.svelte"; export { default as CircularIcon } from "./Icon/CircularIcon.svelte"; export { default as ErrorAlert } from "./ErrorAlert/ErrorAlert.svelte"; export { default as ErrorDetails } from "./ErrorDetails/ErrorDetails.svelte"; +export { default as FieldButtonGroup } from "./FieldButtonGroup/FieldButtonGroup.svelte"; export { default as Icon } from "./Icon/Icon.svelte"; export { default as Mnemonic } from "./Mnemonic/Mnemonic.svelte"; export { default as ProgressBar } from "./ProgressBar/ProgressBar.svelte"; diff --git a/web-wallet/src/lib/stores/index.js b/web-wallet/src/lib/stores/index.js index d2d1c8b98..56c5cf1d5 100644 --- a/web-wallet/src/lib/stores/index.js +++ b/web-wallet/src/lib/stores/index.js @@ -1,4 +1,5 @@ export { default as gasStore } from "./gasStore"; export { default as operationsStore } from "./operationsStore"; +export { default as mnemonicPhraseResetStore } from "./mnemonicPhraseResetStore"; export { default as settingsStore } from "./settingsStore"; export { default as walletStore } from "./walletStore"; diff --git a/web-wallet/src/lib/stores/mnemonicPhraseResetStore.js b/web-wallet/src/lib/stores/mnemonicPhraseResetStore.js new file mode 100644 index 000000000..5d598a968 --- /dev/null +++ b/web-wallet/src/lib/stores/mnemonicPhraseResetStore.js @@ -0,0 +1,6 @@ +import { writable } from "svelte/store"; + +/** @type {import('svelte/store').Writable} */ +const mnemonicPhraseResetStore = writable([]); + +export default mnemonicPhraseResetStore; diff --git a/web-wallet/src/routes/(app)/__tests__/layout.spec.js b/web-wallet/src/routes/(app)/__tests__/layout.spec.js index 7e40f7efb..4fc9baf88 100644 --- a/web-wallet/src/routes/(app)/__tests__/layout.spec.js +++ b/web-wallet/src/routes/(app)/__tests__/layout.spec.js @@ -33,7 +33,7 @@ describe("App layout.js", () => { redirectSpy.mockRestore(); }); - it("should check if a wallet is missing in the `walletStore` and redirect the user to the login page", async () => { + it("should check if a wallet is missing in the `walletStore` and redirect the user to the landing page", async () => { // @ts-ignore await expect(load()).rejects.toThrow(); diff --git a/web-wallet/src/routes/(welcome)/login/+page.svelte b/web-wallet/src/routes/(welcome)/login/+page.svelte index d7ad2be55..fbc5f86e4 100644 --- a/web-wallet/src/routes/(welcome)/login/+page.svelte +++ b/web-wallet/src/routes/(welcome)/login/+page.svelte @@ -1,45 +1,40 @@ -
+ + `; -exports[`Login > Password workflow > should show the password field and the link to restore the wallet if there is login info stored 1`] = ` +exports[`Login > Password workflow > should render the login page with a password field 1`] = `
+ + + `; diff --git a/web-wallet/src/routes/(welcome)/login/__tests__/page.spec.js b/web-wallet/src/routes/(welcome)/login/__tests__/page.spec.js index 5524c492e..baaa716bd 100644 --- a/web-wallet/src/routes/(welcome)/login/__tests__/page.spec.js +++ b/web-wallet/src/routes/(welcome)/login/__tests__/page.spec.js @@ -35,9 +35,6 @@ describe("Login", async () => { const walletGetPsksSpy = vi .spyOn(Wallet.prototype, "getPsks") .mockResolvedValue(addresses); - const walletResetSpy = vi - .spyOn(Wallet.prototype, "reset") - .mockResolvedValue(void 0); const mnemonic = generateMnemonic(); const pwd = "some pwd"; const loginInfo = await encryptMnemonic(mnemonic, pwd); @@ -47,34 +44,26 @@ describe("Login", async () => { const getWalletSpy = vi.spyOn(walletService, "getWallet"); const gotoSpy = vi.spyOn(navigation, "goto"); const initSpy = vi.spyOn(walletStore, "init"); - const settingsResetSpy = vi.spyOn(settingsStore, "reset"); - const confirmSpy = vi.spyOn(window, "confirm"); afterEach(async () => { cleanup(); - confirmSpy.mockClear(); getWalletSpy.mockClear(); gotoSpy.mockClear(); initSpy.mockClear(); settingsStore.reset(); - settingsResetSpy.mockClear(); walletGetPsksSpy.mockClear(); - walletResetSpy.mockClear(); walletStore.reset(); }); afterAll(() => { - confirmSpy.mockRestore(); getWalletSpy.mockRestore(); gotoSpy.mockRestore(); initSpy.mockRestore(); - settingsResetSpy.mockRestore(); walletGetPsksSpy.mockRestore(); - walletResetSpy.mockRestore(); }); describe("Mnemonic phrase workflow", () => { - it("should render the login page and show the field to enter the mnemonic phrase, if there is no login info stored", () => { + it("should render the login page with a mnemonic phrase field", () => { const { container } = render(Login, {}); expect(container.firstChild).toMatchSnapshot(); @@ -96,15 +85,13 @@ describe("Login", async () => { Number(textInput.selectionEnd) ); - expect(walletResetSpy).not.toHaveBeenCalled(); - expect(settingsResetSpy).not.toHaveBeenCalled(); expect(initSpy).not.toHaveBeenCalled(); expect(errorElement?.textContent).toMatch(/mnemonic/i); expect(textInput).toHaveFocus(); expect(selectedText).toBe(textInput.value); }); - it("should clear local data and redirect to the dashboard if the user inputs a valid mnemonic with no prior wallet created", async () => { + it("should trigger the Reset flow if the user inputs a valid mnemonic with no prior wallet created", async () => { const { container } = render(Login, {}); const form = getAsHTMLElement(container, "form"); const textInput = getTextInput(container); @@ -113,77 +100,15 @@ describe("Login", async () => { await fireEvent.submit(form, { currentTarget: form }); await vi.waitUntil(() => gotoSpy.mock.calls.length > 0); - expect(confirmSpy).not.toHaveBeenCalled(); expect(getWalletSpy).toHaveBeenCalledTimes(1); expect(getWalletSpy).toHaveBeenCalledWith(seed); - expect(walletResetSpy).toHaveBeenCalledTimes(1); - expect(settingsResetSpy).toHaveBeenCalledTimes(1); - expect(get(settingsStore).userId).toBe(userId); - expect(initSpy).toHaveBeenCalledTimes(1); - expect(initSpy).toHaveBeenCalledWith(expect.any(Wallet)); - expect(gotoSpy).toHaveBeenCalledTimes(1); - expect(gotoSpy).toHaveBeenCalledWith("/dashboard"); - }); - - it("should trigger a notice if the user inputs a valid mnemonic different from the last one used, then clear local data and redirect to dashboard if the notice is accepted", async () => { - settingsStore.update(setKey("userId", "some-user-id")); - confirmSpy.mockReturnValue(true); - - const { container } = render(Login, {}); - const form = getAsHTMLElement(container, "form"); - const textInput = getTextInput(container); - - await fireEvent.input(textInput, { target: { value: mnemonic } }); - await fireEvent.submit(form, { currentTarget: form }); - await vi.waitUntil(() => confirmSpy.mock.calls.length > 0); - - expect(confirmSpy).toHaveBeenCalledOnce(); - expect(getWalletSpy).toHaveBeenCalledTimes(1); - expect(getWalletSpy).toHaveBeenCalledWith(seed); - expect(walletResetSpy).toHaveBeenCalledTimes(1); - expect(settingsResetSpy).toHaveBeenCalledTimes(1); - expect(get(settingsStore).userId).toBe(userId); - expect(initSpy).toHaveBeenCalledTimes(1); - expect(initSpy).toHaveBeenCalledWith(expect.any(Wallet)); - expect(gotoSpy).toHaveBeenCalledTimes(1); - expect(gotoSpy).toHaveBeenCalledWith("/dashboard"); - }); - - it("should trigger a notice if the user inputs a valid mnemonic different from the last one used and show an error if the notice is declined", async () => { - settingsStore.update(setKey("userId", "some-user-id")); - confirmSpy.mockReturnValue(false); - - const { container } = render(Login, {}); - const form = getAsHTMLElement(container, "form"); - const textInput = getTextInput(container); - - expect(getErrorElement()).toBeNull(); - - await fireEvent.input(textInput, { target: { value: mnemonic } }); - await fireEvent.submit(form, { currentTarget: form }); - await vi.waitUntil(() => confirmSpy.mock.calls.length > 0); - - expect(confirmSpy).toHaveBeenCalledOnce(); - - const errorElement = await vi.waitUntil(getErrorElement); - const selectedText = textInput.value.substring( - Number(textInput.selectionStart), - Number(textInput.selectionEnd) - ); - - expect(getWalletSpy).toHaveBeenCalledTimes(1); - expect(getWalletSpy).toHaveBeenCalledWith(seed); - expect(walletResetSpy).not.toHaveBeenCalled(); - expect(settingsResetSpy).not.toHaveBeenCalled(); - expect(get(settingsStore).userId).not.toBe(userId); + expect(get(settingsStore).userId).toBe(""); expect(initSpy).not.toHaveBeenCalled(); - expect(gotoSpy).not.toHaveBeenCalled(); - expect(errorElement?.textContent).toBe("Existing wallet detected"); - expect(textInput).toHaveFocus(); - expect(selectedText).toBe(textInput.value); + expect(gotoSpy).toHaveBeenCalledTimes(1); + expect(gotoSpy).toHaveBeenCalledWith("/setup/restore"); }); - it("should not show a notice and should not clear local data if the entered mnemonic is the last one used", async () => { + it("should redirect to the dashboard if the entered mnemonic is the last one used", async () => { settingsStore.update(setKey("userId", userId)); const { container } = render(Login, {}); @@ -194,11 +119,8 @@ describe("Login", async () => { await fireEvent.submit(form, { currentTarget: form }); await vi.waitUntil(() => gotoSpy.mock.calls.length > 0); - expect(confirmSpy).not.toHaveBeenCalled(); expect(getWalletSpy).toHaveBeenCalledTimes(1); expect(getWalletSpy).toHaveBeenCalledWith(seed); - expect(walletResetSpy).not.toHaveBeenCalled(); - expect(settingsResetSpy).not.toHaveBeenCalled(); expect(get(settingsStore).userId).toBe(userId); expect(initSpy).toHaveBeenCalledTimes(1); expect(initSpy).toHaveBeenCalledWith(expect.any(Wallet)); @@ -206,41 +128,6 @@ describe("Login", async () => { expect(gotoSpy).toHaveBeenCalledWith("/dashboard"); }); - it("should show an error if the clearing of local data fails", async () => { - settingsStore.update(setKey("userId", "")); - - const errorMessage = "Failed to delete data"; - - walletResetSpy.mockRejectedValueOnce(new Error(errorMessage)); - - const { container } = render(Login, {}); - const form = getAsHTMLElement(container, "form"); - const textInput = getTextInput(container); - - expect(getErrorElement()).toBeNull(); - - await fireEvent.input(textInput, { target: { value: mnemonic } }); - await fireEvent.submit(form, { currentTarget: form }); - - const errorElement = await vi.waitUntil(getErrorElement); - const selectedText = textInput.value.substring( - Number(textInput.selectionStart), - Number(textInput.selectionEnd) - ); - - expect(confirmSpy).not.toHaveBeenCalled(); - expect(getWalletSpy).toHaveBeenCalledTimes(1); - expect(getWalletSpy).toHaveBeenCalledWith(seed); - expect(walletResetSpy).toHaveBeenCalledTimes(1); - expect(settingsResetSpy).not.toHaveBeenCalled(); - expect(get(settingsStore).userId).not.toBe(userId); - expect(initSpy).not.toHaveBeenCalled(); - expect(gotoSpy).not.toHaveBeenCalled(); - expect(errorElement?.textContent).toBe(errorMessage); - expect(textInput).toHaveFocus(); - expect(selectedText).toBe(textInput.value); - }); - it("should trim and lower case the entered mnemonic before validating it", async () => { settingsStore.update(setKey("userId", userId)); @@ -254,11 +141,8 @@ describe("Login", async () => { await fireEvent.submit(form, { currentTarget: form }); await vi.waitUntil(() => gotoSpy.mock.calls.length > 0); - expect(confirmSpy).not.toHaveBeenCalled(); expect(getWalletSpy).toHaveBeenCalledTimes(1); expect(getWalletSpy).toHaveBeenCalledWith(seed); - expect(walletResetSpy).not.toHaveBeenCalled(); - expect(settingsResetSpy).not.toHaveBeenCalled(); expect(get(settingsStore).userId).toBe(userId); expect(initSpy).toHaveBeenCalledTimes(1); expect(initSpy).toHaveBeenCalledWith(expect.any(Wallet)); @@ -274,7 +158,7 @@ describe("Login", async () => { return () => loginInfoStorage.remove(); }); - it("should show the password field and the link to restore the wallet if there is login info stored", () => { + it("should render the login page with a password field", () => { const { container } = render(Login, {}); expect(container.firstChild).toMatchSnapshot(); @@ -296,9 +180,6 @@ describe("Login", async () => { Number(textInput.selectionEnd) ); - expect(confirmSpy).not.toHaveBeenCalled(); - expect(walletResetSpy).not.toHaveBeenCalled(); - expect(settingsResetSpy).not.toHaveBeenCalled(); expect(initSpy).not.toHaveBeenCalled(); expect(errorElement?.textContent).toMatch(/password/i); expect(textInput).toHaveFocus(); @@ -309,7 +190,7 @@ describe("Login", async () => { * This is not a possible situation, in theory, but * the workflow is able to deal with it. */ - it("should clear local data and redirect to the dashboard if the user inputs the correct password with no prior wallet created", async () => { + it("should trigger the Reset flow if the user inputs the correct password with no prior wallet created", async () => { settingsStore.update(setKey("userId", "")); const { container } = render(Login, {}); @@ -324,25 +205,19 @@ describe("Login", async () => { await vi.waitUntil(() => gotoSpy.mock.calls.length > 0); expect(getErrorElement()).toBeNull(); - expect(confirmSpy).not.toHaveBeenCalled(); expect(getWalletSpy).toHaveBeenCalledTimes(1); expect(getWalletSpy).toHaveBeenCalledWith(seed); - expect(walletResetSpy).toHaveBeenCalledTimes(1); - expect(settingsResetSpy).toHaveBeenCalledTimes(1); - expect(get(settingsStore).userId).toBe(userId); - expect(initSpy).toHaveBeenCalledTimes(1); - expect(initSpy).toHaveBeenCalledWith(expect.any(Wallet)); + expect(initSpy).not.toHaveBeenCalled(); expect(gotoSpy).toHaveBeenCalledTimes(1); - expect(gotoSpy).toHaveBeenCalledWith("/dashboard"); + expect(gotoSpy).toHaveBeenCalledWith("/setup/restore"); }); /** * This is not a possible situation, in theory, but * the workflow is able to deal with it. */ - it("should ask to clear local data and redirect to the dashboard if the user inputs the correct password for a mnemonic different from the last one used", async () => { + it("should trigger the Reset flow if the user inputs the correct password for a mnemonic different from the last one used", async () => { settingsStore.update(setKey("userId", "some-fake-id")); - confirmSpy.mockReturnValue(true); const { container } = render(Login, {}); const form = getAsHTMLElement(container, "form"); @@ -355,57 +230,14 @@ describe("Login", async () => { await vi.waitUntil(() => gotoSpy.mock.calls.length > 0); - expect(confirmSpy).toHaveBeenCalledOnce(); - expect(getWalletSpy).toHaveBeenCalledTimes(1); - expect(getWalletSpy).toHaveBeenCalledWith(seed); - expect(walletResetSpy).toHaveBeenCalledTimes(1); - expect(settingsResetSpy).toHaveBeenCalledTimes(1); - expect(get(settingsStore).userId).toBe(userId); - expect(initSpy).toHaveBeenCalledTimes(1); - expect(initSpy).toHaveBeenCalledWith(expect.any(Wallet)); - expect(gotoSpy).toHaveBeenCalledTimes(1); - expect(gotoSpy).toHaveBeenCalledWith("/dashboard"); - }); - - /** - * This is not a possible situation, in theory, but - * the workflow is able to deal with it. - */ - it("should ask to clear local data and show an error if the user inputs the correct password for a mnemonic different from the last one used and decline the notice", async () => { - settingsStore.update(setKey("userId", "some-fake-id")); - confirmSpy.mockReturnValue(false); - - const { container } = render(Login, {}); - const form = getAsHTMLElement(container, "form"); - const textInput = getTextInput(container); - - expect(getErrorElement()).toBeNull(); - - await fireEvent.input(textInput, { target: { value: pwd } }); - await fireEvent.submit(form, { currentTarget: form }); - await vi.waitUntil(() => confirmSpy.mock.calls.length > 0); - - expect(confirmSpy).toHaveBeenCalledOnce(); - - const errorElement = await vi.waitUntil(getErrorElement); - const selectedText = textInput.value.substring( - Number(textInput.selectionStart), - Number(textInput.selectionEnd) - ); - expect(getWalletSpy).toHaveBeenCalledTimes(1); expect(getWalletSpy).toHaveBeenCalledWith(seed); - expect(walletResetSpy).not.toHaveBeenCalled(); - expect(settingsResetSpy).not.toHaveBeenCalled(); - expect(get(settingsStore).userId).not.toBe(userId); expect(initSpy).not.toHaveBeenCalled(); - expect(gotoSpy).not.toHaveBeenCalled(); - expect(errorElement?.textContent).toBe("Existing wallet detected"); - expect(textInput).toHaveFocus(); - expect(selectedText).toBe(textInput.value); + expect(gotoSpy).toHaveBeenCalledTimes(1); + expect(gotoSpy).toHaveBeenCalledWith("/setup/restore"); }); - it("should not show a notice and clear local data if the entered password is for the last used mnemonic", async () => { + it("should redirect to the Dashboard if the entered password is for the last used mnemonic", async () => { settingsStore.update(setKey("userId", userId)); const { container } = render(Login, {}); @@ -416,11 +248,8 @@ describe("Login", async () => { await fireEvent.submit(form, { currentTarget: form }); await vi.waitUntil(() => gotoSpy.mock.calls.length > 0); - expect(confirmSpy).not.toHaveBeenCalled(); expect(getWalletSpy).toHaveBeenCalledTimes(1); expect(getWalletSpy).toHaveBeenCalledWith(seed); - expect(walletResetSpy).not.toHaveBeenCalled(); - expect(settingsResetSpy).not.toHaveBeenCalled(); expect(get(settingsStore).userId).toBe(userId); expect(initSpy).toHaveBeenCalledTimes(1); expect(initSpy).toHaveBeenCalledWith(expect.any(Wallet)); @@ -428,42 +257,6 @@ describe("Login", async () => { expect(gotoSpy).toHaveBeenCalledWith("/dashboard"); }); - it("should show an error if the clearing of local data fails", async () => { - settingsStore.update(setKey("userId", "some-fake-id")); - confirmSpy.mockReturnValue(true); - - const errorMessage = "Failed to delete data"; - - walletResetSpy.mockRejectedValueOnce(new Error(errorMessage)); - - const { container } = render(Login, {}); - const form = getAsHTMLElement(container, "form"); - const textInput = getTextInput(container); - - expect(getErrorElement()).toBeNull(); - - await fireEvent.input(textInput, { target: { value: pwd } }); - await fireEvent.submit(form, { currentTarget: form }); - await vi.waitUntil(() => confirmSpy.mock.calls.length > 0); - - const errorElement = await vi.waitUntil(getErrorElement); - const selectedText = textInput.value.substring( - Number(textInput.selectionStart), - Number(textInput.selectionEnd) - ); - - expect(getWalletSpy).toHaveBeenCalledTimes(1); - expect(getWalletSpy).toHaveBeenCalledWith(seed); - expect(walletResetSpy).toHaveBeenCalledTimes(1); - expect(settingsResetSpy).not.toHaveBeenCalled(); - expect(get(settingsStore).userId).not.toBe(userId); - expect(initSpy).not.toHaveBeenCalled(); - expect(gotoSpy).not.toHaveBeenCalled(); - expect(errorElement?.textContent).toBe(errorMessage); - expect(textInput).toHaveFocus(); - expect(selectedText).toBe(textInput.value); - }); - it("should trim the entered password before validating it", async () => { settingsStore.update(setKey("userId", userId)); @@ -477,11 +270,8 @@ describe("Login", async () => { await fireEvent.submit(form, { currentTarget: form }); await vi.waitUntil(() => gotoSpy.mock.calls.length > 0); - expect(confirmSpy).not.toHaveBeenCalled(); expect(getWalletSpy).toHaveBeenCalledTimes(1); expect(getWalletSpy).toHaveBeenCalledWith(seed); - expect(walletResetSpy).not.toHaveBeenCalled(); - expect(settingsResetSpy).not.toHaveBeenCalled(); expect(get(settingsStore).userId).toBe(userId); expect(initSpy).toHaveBeenCalledTimes(1); expect(initSpy).toHaveBeenCalledWith(expect.any(Wallet)); diff --git a/web-wallet/src/routes/(welcome)/setup/+page.js b/web-wallet/src/routes/(welcome)/setup/+page.js new file mode 100644 index 000000000..a85a2bf2a --- /dev/null +++ b/web-wallet/src/routes/(welcome)/setup/+page.js @@ -0,0 +1,6 @@ +import { redirect } from "$lib/navigation"; + +/** @type {import('./$types').PageLoad} */ +export function load() { + redirect(301, "/"); +} diff --git a/web-wallet/src/routes/(welcome)/setup/+page.svelte b/web-wallet/src/routes/(welcome)/setup/+page.svelte deleted file mode 100644 index 480b57ac8..000000000 --- a/web-wallet/src/routes/(welcome)/setup/+page.svelte +++ /dev/null @@ -1,82 +0,0 @@ - - - - -
-
-

- Unlocking the Future:
- Your DUSK Native Wallet -

- -
-
- - -
- - -
-
-
- -
- Web Wallet v{import.meta.env.APP_VERSION} ({import.meta.env - .APP_BUILD_INFO}) -
- - diff --git a/web-wallet/src/routes/(welcome)/setup/Animation.svelte b/web-wallet/src/routes/(welcome)/setup/Animation.svelte deleted file mode 100644 index 733982f9a..000000000 --- a/web-wallet/src/routes/(welcome)/setup/Animation.svelte +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - diff --git a/web-wallet/src/routes/(welcome)/setup/TermsOfService.svelte b/web-wallet/src/routes/(welcome)/setup/TermsOfService.svelte index 491114e80..6e5a70c15 100644 --- a/web-wallet/src/routes/(welcome)/setup/TermsOfService.svelte +++ b/web-wallet/src/routes/(welcome)/setup/TermsOfService.svelte @@ -20,7 +20,7 @@
diff --git a/web-wallet/src/routes/(welcome)/setup/__tests__/__snapshots__/page.spec.js.snap b/web-wallet/src/routes/(welcome)/setup/__tests__/__snapshots__/page.spec.js.snap deleted file mode 100644 index 3ccb512c1..000000000 --- a/web-wallet/src/routes/(welcome)/setup/__tests__/__snapshots__/page.spec.js.snap +++ /dev/null @@ -1,181 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`Setup > should render the Setup page 1`] = ` -
-
-
-

- Unlocking the Future: -
- - Your - - DUSK - - Native Wallet -

- -
- - - - - - - - - - - - - - - - Access wallet - - - - - - - -
-
-
- -
- - Web Wallet v - 0.1.5 - ( - hash1234 2024-01-12 - ) - -
- -
-`; diff --git a/web-wallet/src/routes/(welcome)/setup/__tests__/page.spec.js b/web-wallet/src/routes/(welcome)/setup/__tests__/page.spec.js deleted file mode 100644 index 1b2b2bb21..000000000 --- a/web-wallet/src/routes/(welcome)/setup/__tests__/page.spec.js +++ /dev/null @@ -1,13 +0,0 @@ -import { afterEach, describe, expect, it } from "vitest"; -import { cleanup, render } from "@testing-library/svelte"; -import Setup from "../+page.svelte"; - -describe("Setup", () => { - afterEach(cleanup); - - it("should render the Setup page", () => { - const { container } = render(Setup, {}); - - expect(container.firstChild).toMatchSnapshot(); - }); -}); diff --git a/web-wallet/src/routes/(welcome)/setup/create/+page.svelte b/web-wallet/src/routes/(welcome)/setup/create/+page.svelte index 2efb07623..73b60cd8a 100644 --- a/web-wallet/src/routes/(welcome)/setup/create/+page.svelte +++ b/web-wallet/src/routes/(welcome)/setup/create/+page.svelte @@ -69,7 +69,7 @@ showStepper={true} backButton={{ disabled: false, - href: "/setup", + href: "/", isAnchor: true, }} nextButton={{ disabled: !agreementAccepted }} diff --git a/web-wallet/src/routes/(welcome)/setup/create/__tests__/__snapshots__/page.spec.js.snap b/web-wallet/src/routes/(welcome)/setup/create/__tests__/__snapshots__/page.spec.js.snap index 55aa6a8cc..b525956ec 100644 --- a/web-wallet/src/routes/(welcome)/setup/create/__tests__/__snapshots__/page.spec.js.snap +++ b/web-wallet/src/routes/(welcome)/setup/create/__tests__/__snapshots__/page.spec.js.snap @@ -3034,7 +3034,7 @@ exports[`Create > should render the \`Securely store your seed phrase!\` agreeme should render the Existing Wallet notice step of the Create fl class="flex flex-col gap-1" >

- Logging in to a new wallet will overwrite the current local wallet - cache, meaning that when you log in again with the previous - mnemonic/account you will need to wait for the wallet to sync. + Initializing a new wallet will replace your existing local wallet cache, + erasing any stored data. Ensure you have securely backed up your current + wallet's seed phrase to prevent loss. Proceeding without a backup can + lead to irreversible loss of access to your assets.

should render the Existing Wallet notice step of the Create fl should render the Terms of Service step of the Create flow if should render the Existing Wallet notice step of the Restore class="flex flex-col gap-1" >

- Logging in to a new wallet will overwrite the current local wallet - cache, meaning that when you log in again with the previous - mnemonic/account you will need to wait for the wallet to sync. + Initializing a new wallet will replace your existing local wallet cache, + erasing any stored data. Ensure you have securely backed up your current + wallet's seed phrase to prevent loss. Proceeding without a backup can + lead to irreversible loss of access to your assets.

should render the Existing Wallet notice step of the Restore should render the Mnemonic Authenticate step after accepting should render the Terms of Service step of the Restore flow i { redirectSpy.mockRestore(); }); - it("should redirect the user to the setup page", async () => { + it("should redirect the user to the landing page", async () => { // @ts-ignore expect(async () => await load()).rejects.toThrow(); expect(redirectSpy).toHaveBeenCalledTimes(1); - expect(redirectSpy).toHaveBeenCalledWith(301, "/setup"); + expect(redirectSpy).toHaveBeenCalledWith(301, "/login"); }); }); diff --git a/web-wallet/src/style/dusk-components/field-button-group.css b/web-wallet/src/style/dusk-components/field-button-group.css new file mode 100644 index 000000000..46fba0cca --- /dev/null +++ b/web-wallet/src/style/dusk-components/field-button-group.css @@ -0,0 +1,12 @@ +.dusk-field-button-group { + width: 100%; + background-color: white; + padding: 6px; + display: grid; + grid-template-columns: 1fr auto; + justify-content: space-between; + gap: var(--small-gap); + background-color: var(--control-bg-color); + /* calculated using the formula outerRadius = innerRadius+padding */ + border-radius: 1.875em; +} diff --git a/web-wallet/src/style/dusk-components/textbox.css b/web-wallet/src/style/dusk-components/textbox.css index bdd5ba5a7..0fc27398d 100644 --- a/web-wallet/src/style/dusk-components/textbox.css +++ b/web-wallet/src/style/dusk-components/textbox.css @@ -6,6 +6,7 @@ border-radius: var(--control-border-radius-size); border-style: solid; border-width: var(--control-border-size); + overflow: hidden; } .dusk-textbox:disabled, diff --git a/web-wallet/src/style/main.css b/web-wallet/src/style/main.css index 2b89a089f..b1acdaf36 100644 --- a/web-wallet/src/style/main.css +++ b/web-wallet/src/style/main.css @@ -10,8 +10,9 @@ @import url("./dusk-components/checkbox.css"); @import url("./dusk-components/error-alert.css"); @import url("./dusk-components/error-details.css"); -@import url("./dusk-components/mnemonic.css"); +@import url("./dusk-components/field-button-group.css"); @import url("./dusk-components/icon.css"); +@import url("./dusk-components/mnemonic.css"); @import url("./dusk-components/progress-bar.css"); @import url("./dusk-components/select.css"); @import url("./dusk-components/stepper.css"); @@ -25,9 +26,9 @@ @import url("./dusk-components/wizard.css"); @import url("./dusk-components/words.css"); @import url("./app-components/headings.css"); +@import url("./app-components/horizontal-rules.css"); @import url("./app-components/marks.css"); @import url("./app-components/notice.css"); -@import url("./app-components/horizontal-rules.css"); @font-face { font-family: "Soehne";