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 101d9724dd..b2f7bcb24d 100644 --- a/apps/web/src/Layout.tsx +++ b/apps/web/src/Layout.tsx @@ -1,6 +1,7 @@ import { Grid, GridItem } from "@chakra-ui/react"; import { useDynamicModalContext } from "@umami/components"; import { useDataPolling } from "@umami/data-polling"; +import { useCurrentAccount } from "@umami/state"; import { useEffect } from "react"; import { Footer } from "./components/Footer"; @@ -9,22 +10,60 @@ import { Main } from "./components/Main"; import { Navbar } from "./components/Navbar"; import { SecurityWarningModal } from "./components/SecurityWarningModal"; import { Sidebar } from "./components/Sidebar"; +import { SocialLoginWarningModal } from "./components/SocialLoginWarningModal/SocialLoginWarningModal"; export const Layout = () => { useDataPolling(); const { openWith } = useDynamicModalContext(); + const currentUser = useCurrentAccount(); useEffect(() => { - const isInformed = localStorage.getItem("user:isExtensionsWarningShown"); + const CLOSING_DELAY = 300; - if (!isInformed || !JSON.parse(isInformed)) { - // Trick to ensure the modal is rendered after the initial render - setTimeout(() => { - void openWith(, { closeOnEsc: false, size: "xl" }); - }, 500); + const warnings = [ + { + key: "user:isSocialLoginWarningShown", + component: , + options: { closeOnEsc: false }, + isEnabled: () => currentUser?.type === "social", + }, + { + key: "user:isExtensionsWarningShown", + component: , + options: { closeOnEsc: false, size: "xl" }, + isEnabled: () => true, + }, + ]; + + const warningsToShow = warnings.filter(warning => { + const isInformed = localStorage.getItem(warning.key); + return (!isInformed || !JSON.parse(isInformed)) && warning.isEnabled(); + }); + + const showWarnings = async () => { + for (const warning of warningsToShow) { + await new Promise( + resolve => + void openWith(warning.component, { + ...warning.options, + onClose: () => { + localStorage.setItem(warning.key, "true"); + resolve(true); + }, + }) + ); + + // Setting a delay to ensure the modal is properly closed before the next one is opened + await new Promise(resolve => setTimeout(resolve, CLOSING_DELAY)); + } + }; + + 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(); @@ -27,18 +20,16 @@ describe("", () => { ).toBeVisible(); }); - it("renders all accordion items", async () => { + it.each([ + "Install extensions only from trusted sources", + "Review permissions and ratings", + "Maintain a separate browser for financial activities", + "Keep your browser updated", + "Stay alert to social engineering risks", + ])("renders %s accordion item", async title => { await renderInModal(); - const expectedTitles = [ - "Install extensions only from trusted sources", - "Review permissions and ratings", - "Maintain a separate browser for financial activities", - "Keep your browser updated", - "Stay alert to social engineering risks", - ]; - - expectedTitles.forEach(title => { + await waitFor(() => { expect(screen.getByText(title)).toBeVisible(); }); }); @@ -55,31 +46,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 6c345ae045..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,10 +127,12 @@ 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 new file mode 100644 index 0000000000..c5c3ff7a11 --- /dev/null +++ b/apps/web/src/components/SocialLoginWarningModal/SocialLoginWarningModal.test.tsx @@ -0,0 +1,59 @@ +import { SocialLoginWarningModal } from "./SocialLoginWarningModal"; +import { act, renderInModal, screen, userEvent, waitFor } from "../../testUtils"; + +beforeEach(() => { + localStorage.clear(); +}); + +describe("", () => { + it("renders the modal with correct title and content", async () => { + await renderInModal(); + + 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", async () => { + await renderInModal(); + + const button = screen.getByRole("button", { name: "Continue" }); + expect(button).toBeDisabled(); + }); + + it("enables 'Continue' button when checkbox is checked", async () => { + const user = userEvent.setup(); + await renderInModal(); + + 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("toggles checkbox state correctly", async () => { + const user = userEvent.setup(); + await renderInModal(); + + 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/web/src/components/SocialLoginWarningModal/SocialLoginWarningModal.tsx b/apps/web/src/components/SocialLoginWarningModal/SocialLoginWarningModal.tsx new file mode 100644 index 0000000000..8abfa69620 --- /dev/null +++ b/apps/web/src/components/SocialLoginWarningModal/SocialLoginWarningModal.tsx @@ -0,0 +1,58 @@ +import { + Button, + Checkbox, + Flex, + Heading, + ModalBody, + ModalContent, + ModalFooter, + ModalHeader, + Text, +} from "@chakra-ui/react"; +import { useDynamicModalContext } from "@umami/components"; +import { useState } from "react"; + +import { AlertIcon } from "../../assets/icons"; +import { useColor } from "../../styles/useColor"; + +export const SocialLoginWarningModal = () => { + const { onClose } = useDynamicModalContext(); + const color = useColor(); + const [isAgreed, setIsAgreed] = useState(false); + + const handleInform = () => { + 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/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 = () => {