diff --git a/apps/desktop/src/Router.tsx b/apps/desktop/src/Router.tsx index be27e89db9..72d9d64d48 100644 --- a/apps/desktop/src/Router.tsx +++ b/apps/desktop/src/Router.tsx @@ -1,12 +1,18 @@ /* istanbul ignore file */ import { DynamicModalContext, useDynamicModal } from "@umami/components"; import { useDataPolling } from "@umami/data-polling"; -import { WalletClient, useImplicitAccounts, useResetBeaconConnections } from "@umami/state"; +import { + WalletClient, + useCurrentAccount, + useImplicitAccounts, + useResetBeaconConnections, +} from "@umami/state"; import { noop } from "lodash"; import { useEffect } from "react"; import { HashRouter, Navigate, Route, Routes } from "react-router-dom"; import { AnnouncementBanner } from "./components/AnnouncementBanner"; +import { SocialLoginWarningModal } from "./components/SocialLoginWarningModal/SocialLoginWarningModal"; import { BeaconProvider } from "./utils/beacon/BeaconProvider"; import { useDeeplinkHandler } from "./utils/useDeeplinkHandler"; import { AddressBookView } from "./views/addressBook/AddressBookView"; @@ -33,6 +39,18 @@ export const Router = () => { const LoggedInRouterWithPolling = () => { useDataPolling(); const modalDisclosure = useDynamicModal(); + const currentUser = useCurrentAccount(); + + useEffect(() => { + if (currentUser?.type === "social") { + const isInformed = localStorage.getItem("user:isSocialLoginWarningShown"); + + if (!isInformed || !JSON.parse(isInformed)) { + void modalDisclosure.openWith(, { closeOnEsc: false }); + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentUser]); return ( diff --git a/apps/desktop/src/components/Onboarding/verifySeedphrase/VerifySeedphrase.tsx b/apps/desktop/src/components/Onboarding/verifySeedphrase/VerifySeedphrase.tsx index a0f5d79828..82c7beb05e 100644 --- a/apps/desktop/src/components/Onboarding/verifySeedphrase/VerifySeedphrase.tsx +++ b/apps/desktop/src/components/Onboarding/verifySeedphrase/VerifySeedphrase.tsx @@ -62,6 +62,7 @@ export const VerifySeedphrase = ({ inputProps={{ paddingLeft: "36px", size: "md", + height: "48px", }} listProps={{ marginTop: "6px", diff --git a/apps/desktop/src/components/SocialLoginWarningModal/SocialLoginWarningModal.test.tsx b/apps/desktop/src/components/SocialLoginWarningModal/SocialLoginWarningModal.test.tsx new file mode 100644 index 0000000000..69478cde45 --- /dev/null +++ b/apps/desktop/src/components/SocialLoginWarningModal/SocialLoginWarningModal.test.tsx @@ -0,0 +1,86 @@ +import { SocialLoginWarningModal } from "./SocialLoginWarningModal"; +import { + act, + dynamicModalContextMock, + render, + screen, + userEvent, + waitFor, +} from "../../mocks/testUtils"; + +beforeEach(() => { + localStorage.clear(); +}); + +describe("", () => { + it("renders the modal with correct title and content", async () => { + render(); + + await waitFor(() => { + expect(screen.getByText("Important notice about your social account wallet")).toBeVisible(); + }); + + expect( + screen.getByText( + "Wallets created with social accounts depend on those accounts to function. Losing access to this social account will result in loosing the wallet. Enable two-factor authentication to protect your social accounts." + ) + ).toBeVisible(); + }); + + it("disables 'Continue' button when checkbox is not checked", () => { + render(); + + const button = screen.getByRole("button", { name: "Continue" }); + expect(button).toBeDisabled(); + }); + + it("enables 'Continue' button when checkbox is checked", async () => { + const user = userEvent.setup(); + render(); + + const checkbox = screen.getByRole("checkbox", { + name: "I understand and accept the risks.", + }); + await act(() => user.click(checkbox)); + + const continueButton = screen.getByRole("button", { name: "Continue" }); + expect(continueButton).toBeEnabled(); + }); + + it("sets localStorage and closes modal when 'Continue' is clicked", async () => { + const { onClose } = dynamicModalContextMock; + const user = userEvent.setup(); + render(); + + const checkbox = screen.getByRole("checkbox", { + name: "I understand and accept the risks.", + }); + await act(() => user.click(checkbox)); + + const continueButton = screen.getByRole("button", { name: "Continue" }); + await act(() => user.click(continueButton)); + + await waitFor(() => { + expect(localStorage.getItem("user:isSocialLoginWarningShown")).toBe("true"); + }); + + expect(onClose).toHaveBeenCalled(); + }); + + it("toggles checkbox state correctly", async () => { + const user = userEvent.setup(); + render(); + + const checkbox = screen.getByRole("checkbox", { + name: "I understand and accept the risks.", + }); + + expect(checkbox).not.toBeChecked(); + + await act(() => user.click(checkbox)); + expect(checkbox).toBeChecked(); + + await act(() => user.click(checkbox)); + expect(checkbox).not.toBeChecked(); + }); +}); diff --git a/apps/desktop/src/components/SocialLoginWarningModal/SocialLoginWarningModal.tsx b/apps/desktop/src/components/SocialLoginWarningModal/SocialLoginWarningModal.tsx new file mode 100644 index 0000000000..f30c4c58a1 --- /dev/null +++ b/apps/desktop/src/components/SocialLoginWarningModal/SocialLoginWarningModal.tsx @@ -0,0 +1,68 @@ +import { + Button, + Checkbox, + Flex, + Heading, + Icon, + ModalBody, + ModalContent, + ModalFooter, + ModalHeader, + Text, +} from "@chakra-ui/react"; +import { useDynamicModalContext } from "@umami/components"; +import { useState } from "react"; + +import { WarningIcon } from "../../assets/icons"; +import colors from "../../style/colors"; + +export const SocialLoginWarningModal = () => { + const { onClose } = useDynamicModalContext(); + const [isAgreed, setIsAgreed] = useState(false); + + const handleInform = () => { + localStorage.setItem("user:isSocialLoginWarningShown", "true"); + onClose(); + }; + + return ( + + + + + Important notice about your social account wallet + + + + + + Wallets created with social accounts depend on those accounts to function. Losing access + to this social account will result in loosing the wallet. Enable two-factor + authentication to protect your social accounts. + + setIsAgreed(e.target.checked)} + > + + I understand and accept the risks. + + + + + + + + + ); +}; diff --git a/apps/desktop/src/setupTests.tsx b/apps/desktop/src/setupTests.tsx index 3b80d2a8a9..8f669da0ce 100644 --- a/apps/desktop/src/setupTests.tsx +++ b/apps/desktop/src/setupTests.tsx @@ -51,10 +51,6 @@ Object.defineProperties(global, { fetch: { value: jest.fn(), writable: true }, }); -Object.defineProperty(window, "localStorage", { - value: mockLocalStorage(), -}); - beforeEach(() => { // Add missing browser APIs Object.defineProperties(global, { @@ -79,6 +75,10 @@ beforeEach(() => { // Hack for testing HashRouter: clears URL between tests. window.location.hash = ""; + Object.defineProperty(window, "localStorage", { + value: mockLocalStorage(), + }); + setupJestCanvasMock(); }); diff --git a/apps/web/src/Layout.tsx b/apps/web/src/Layout.tsx index 3768bfafef..b2f7bcb24d 100644 --- a/apps/web/src/Layout.tsx +++ b/apps/web/src/Layout.tsx @@ -18,18 +18,21 @@ export const Layout = () => { const currentUser = useCurrentAccount(); useEffect(() => { + const CLOSING_DELAY = 300; + const warnings = [ + { + key: "user:isSocialLoginWarningShown", + component: , + options: { closeOnEsc: false }, + isEnabled: () => currentUser?.type === "social", + }, { key: "user:isExtensionsWarningShown", component: , options: { closeOnEsc: false, size: "xl" }, isEnabled: () => true, }, - { - key: "user:isSocialLoginWarningShown", - component: , - isEnabled: () => currentUser?.type === "social", - }, ]; const warningsToShow = warnings.filter(warning => { @@ -38,27 +41,29 @@ export const Layout = () => { }); const showWarnings = async () => { - for (let i = 0; i < warningsToShow.length; i++) { - const warning = warningsToShow[i]; - await new Promise(resolve => { - const showModal = () => + for (const warning of warningsToShow) { + await new Promise( + resolve => void openWith(warning.component, { ...warning.options, - onClose: () => resolve(true), - }); + onClose: () => { + localStorage.setItem(warning.key, "true"); + resolve(true); + }, + }) + ); - if (i === 0) { - setTimeout(showModal, 500); - } else { - showModal(); - } - }); + // Setting a delay to ensure the modal is properly closed before the next one is opened + await new Promise(resolve => setTimeout(resolve, CLOSING_DELAY)); } }; - void showWarnings(); + if (warningsToShow.length > 0) { + // Immediate opening of the first modal causes freezes + setTimeout(() => void showWarnings(), 500); + } // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [currentUser]); return ( { localStorage.clear(); @@ -55,31 +48,11 @@ describe("", () => { await renderInModal(); const checkbox = screen.getByRole("checkbox", { - name: "I have read and understood all security guidelines", + name: "I understand and accept the risks.", }); await act(() => user.click(checkbox)); const continueButton = screen.getByRole("button", { name: "Continue" }); expect(continueButton).toBeEnabled(); }); - - it("sets localStorage and closes modal when 'Continue' is clicked", async () => { - const { onClose } = dynamicModalContextMock; - const user = userEvent.setup(); - await renderInModal(); - - const checkbox = screen.getByRole("checkbox", { - name: "I have read and understood all security guidelines", - }); - await act(() => user.click(checkbox)); - - const continueButton = screen.getByRole("button", { name: "Continue" }); - await act(() => user.click(continueButton)); - - await waitFor(() => { - expect(localStorage.getItem("user:isExtensionsWarningShown")).toBe("true"); - }); - - expect(onClose).toHaveBeenCalled(); - }); }); diff --git a/apps/web/src/components/SecurityWarningModal/SecurityWarningModal.tsx b/apps/web/src/components/SecurityWarningModal/SecurityWarningModal.tsx index 24e39bc3c9..a53785dc6a 100644 --- a/apps/web/src/components/SecurityWarningModal/SecurityWarningModal.tsx +++ b/apps/web/src/components/SecurityWarningModal/SecurityWarningModal.tsx @@ -67,7 +67,6 @@ export const SecurityWarningModal = () => { const [isAgreed, setIsAgreed] = useState(false); const handleInform = () => { - localStorage.setItem("user:isExtensionsWarningShown", "true"); onClose(); }; @@ -128,7 +127,9 @@ export const SecurityWarningModal = () => { marginX="auto" onChange={e => setIsAgreed(e.target.checked)} > - I have read and understood all security guidelines + + I understand and accept the risks. + diff --git a/apps/web/src/components/SocialLoginWarningModal/SocialLoginWarningModal.test.tsx b/apps/web/src/components/SocialLoginWarningModal/SocialLoginWarningModal.test.tsx index b4969d3fd3..c5c3ff7a11 100644 --- a/apps/web/src/components/SocialLoginWarningModal/SocialLoginWarningModal.test.tsx +++ b/apps/web/src/components/SocialLoginWarningModal/SocialLoginWarningModal.test.tsx @@ -1,12 +1,5 @@ import { SocialLoginWarningModal } from "./SocialLoginWarningModal"; -import { - act, - dynamicModalContextMock, - renderInModal, - screen, - userEvent, - waitFor, -} from "../../testUtils"; +import { act, renderInModal, screen, userEvent, waitFor } from "../../testUtils"; beforeEach(() => { localStorage.clear(); @@ -22,7 +15,7 @@ describe("", () => { expect( screen.getByText( - "Wallets created with social accounts depend on those accounts to function. Losing access means losing the wallet. Enable two-factor authentication to protect your social accounts." + "Wallets created with social accounts depend on those accounts to function. Losing access to this social account will result in loosing the wallet. Enable two-factor authentication to protect your social accounts." ) ).toBeVisible(); }); @@ -39,7 +32,7 @@ describe("", () => { await renderInModal(); const checkbox = screen.getByRole("checkbox", { - name: "I have read and understood all security guidelines", + name: "I understand and accept the risks.", }); await act(() => user.click(checkbox)); @@ -47,42 +40,19 @@ describe("", () => { expect(continueButton).toBeEnabled(); }); - it("sets localStorage and closes modal when 'Continue' is clicked", async () => { - const { onClose } = dynamicModalContextMock; - const user = userEvent.setup(); - await renderInModal(); - - const checkbox = screen.getByRole("checkbox", { - name: "I have read and understood all security guidelines", - }); - await act(() => user.click(checkbox)); - - const continueButton = screen.getByRole("button", { name: "Continue" }); - await act(() => user.click(continueButton)); - - await waitFor(() => { - expect(localStorage.getItem("user:isSocialLoginWarningShown")).toBe("true"); - }); - - expect(onClose).toHaveBeenCalled(); - }); - it("toggles checkbox state correctly", async () => { const user = userEvent.setup(); await renderInModal(); const checkbox = screen.getByRole("checkbox", { - name: "I have read and understood all security guidelines", + name: "I understand and accept the risks.", }); - // Initially unchecked expect(checkbox).not.toBeChecked(); - // Check the checkbox await act(() => user.click(checkbox)); expect(checkbox).toBeChecked(); - // Uncheck the checkbox await act(() => user.click(checkbox)); expect(checkbox).not.toBeChecked(); }); diff --git a/apps/web/src/components/SocialLoginWarningModal/SocialLoginWarningModal.tsx b/apps/web/src/components/SocialLoginWarningModal/SocialLoginWarningModal.tsx index 0f4d1cb27c..8abfa69620 100644 --- a/apps/web/src/components/SocialLoginWarningModal/SocialLoginWarningModal.tsx +++ b/apps/web/src/components/SocialLoginWarningModal/SocialLoginWarningModal.tsx @@ -21,7 +21,6 @@ export const SocialLoginWarningModal = () => { const [isAgreed, setIsAgreed] = useState(false); const handleInform = () => { - localStorage.setItem("user:isSocialLoginWarningShown", "true"); onClose(); }; @@ -37,19 +36,15 @@ export const SocialLoginWarningModal = () => { Wallets created with social accounts depend on those accounts to function. Losing access - means losing the wallet. Enable two-factor authentication to protect your social - accounts. + to this social account will result in loosing the wallet. Enable two-factor + authentication to protect your social accounts. setIsAgreed(e.target.checked)} > - - I have read and understood all security guidelines - + I understand and accept the risks. diff --git a/packages/components/src/components/DynamicDisclosure/DynamicDisclosure.tsx b/packages/components/src/components/DynamicDisclosure/DynamicDisclosure.tsx index 05a6be5bcd..f5a0a4decd 100644 --- a/packages/components/src/components/DynamicDisclosure/DynamicDisclosure.tsx +++ b/packages/components/src/components/DynamicDisclosure/DynamicDisclosure.tsx @@ -88,6 +88,7 @@ export const useDynamicDisclosure = () => { content: ReactElement, props: ThemingProps & { onClose?: () => void | Promise; + closeOnEsc?: boolean; } = {} ) => { const onClose = () => {