diff --git a/apps/web/src/Layout.tsx b/apps/web/src/Layout.tsx index 15621e8953..cd62496606 100644 --- a/apps/web/src/Layout.tsx +++ b/apps/web/src/Layout.tsx @@ -1,14 +1,27 @@ import { Grid, GridItem } from "@chakra-ui/react"; +import { useDynamicModalContext } from "@umami/components"; import { useDataPolling } from "@umami/data-polling"; +import { useEffect } from "react"; import { Footer } from "./components/Footer"; import { Header } from "./components/Header"; import { Main } from "./components/Main"; import { Navbar } from "./components/Navbar"; +import { SecurityWarningModal } from "./components/SecurityWarningModal"; import { Sidebar } from "./components/Sidebar"; export const Layout = () => { useDataPolling(); + const { openWith } = useDynamicModalContext(); + + useEffect(() => { + const isInformed = localStorage.getItem("user:isInformed"); + + if (!isInformed || !JSON.parse(isInformed)) { + void openWith(, { closeOnEsc: false, size: "xl" }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); return ( + + + + diff --git a/apps/web/src/assets/icons/thumbs-up.svg b/apps/web/src/assets/icons/thumbs-up.svg new file mode 100644 index 0000000000..82fe10b555 --- /dev/null +++ b/apps/web/src/assets/icons/thumbs-up.svg @@ -0,0 +1,5 @@ + + + diff --git a/apps/web/src/components/SecurityWarningModal/SecurityWarningModal.test.tsx b/apps/web/src/components/SecurityWarningModal/SecurityWarningModal.test.tsx new file mode 100644 index 0000000000..5480f3c524 --- /dev/null +++ b/apps/web/src/components/SecurityWarningModal/SecurityWarningModal.test.tsx @@ -0,0 +1,85 @@ +import { SecurityWarningModal } from "./SecurityWarningModal"; +import { + act, + dynamicModalContextMock, + 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("Browser Extension Security Tips")).toBeVisible(); + }); + + expect( + screen.getByText( + "Please carefully review these guidelines to protect your wallet from potential security risks" + ) + ).toBeVisible(); + }); + + it("renders all accordion items", async () => { + 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 => { + expect(screen.getByText(title)).toBeVisible(); + }); + }); + + it("disables 'Got it' button when not all items are opened", async () => { + await renderInModal(); + + const button = screen.getByRole("button", { name: "Got it" }); + expect(button).toBeDisabled(); + }); + + it("enables 'Got it' button when all items are opened", async () => { + const user = userEvent.setup(); + await renderInModal(); + + const accordionButtons = screen.getAllByTestId("accordion-button"); + for (const button of accordionButtons) { + await act(() => user.click(button)); + } + + const gotItButton = screen.getByRole("button", { name: "Got it" }); + expect(gotItButton).toBeEnabled(); + }); + + it("sets localStorage and closes modal when 'Got it' is clicked", async () => { + const { onClose } = dynamicModalContextMock; + const user = userEvent.setup(); + await renderInModal(); + + const accordionButtons = screen.getAllByTestId("accordion-button"); + for (const button of accordionButtons) { + await act(() => user.click(button)); + } + + const gotItButton = screen.getByRole("button", { name: "Got it" }); + await act(() => user.click(gotItButton)); + + await waitFor(() => { + expect(localStorage.getItem("user:isInformed")).toBe("true"); + }); + + expect(onClose).toHaveBeenCalled(); + }); +}); diff --git a/apps/web/src/components/SecurityWarningModal/SecurityWarningModal.tsx b/apps/web/src/components/SecurityWarningModal/SecurityWarningModal.tsx new file mode 100644 index 0000000000..1fd2b441fd --- /dev/null +++ b/apps/web/src/components/SecurityWarningModal/SecurityWarningModal.tsx @@ -0,0 +1,150 @@ +import { + Accordion, + AccordionButton, + AccordionIcon, + AccordionItem, + AccordionPanel, + Button, + Flex, + Heading, + Icon, + ModalBody, + ModalContent, + ModalFooter, + ModalHeader, + Text, +} from "@chakra-ui/react"; +import { useDynamicModalContext } from "@umami/components"; +import { useState } from "react"; + +import { + AlertCircleIcon, + AlertIcon, + CheckmarkIcon, + LockIcon, + RefreshIcon, + ThumbsUpIcon, +} from "../../assets/icons"; +import { useColor } from "../../styles/useColor"; + +const accordionItems = [ + { + icon: CheckmarkIcon, + title: "Install Extensions Only from Trusted Sources", + content: + "Use only official platforms like the Chrome Web Store or Firefox Add-ons, as these include security reviews. Avoid third-party websites and direct download links.", + }, + { + icon: ThumbsUpIcon, + title: "Review Permissions and Ratings", + content: + "Before installing, check extension reviews, download counts, and requested permissions. Extensions that ask for access to sensitive data (like local storage or clipboard) should be trusted and necessary.", + }, + { + icon: LockIcon, + title: "Maintain a Separate Browser for Financial Activities", + content: + "Use a dedicated browser with no extensions installed. This minimizes risk by isolating financial activities from other browsing.", + }, + { + icon: RefreshIcon, + title: "Keep Your Browser Updated", + content: + "Regularly update your browser and extensions to ensure you have the latest security features and bug fixes.", + }, + { + icon: AlertCircleIcon, + title: "Stay Alert to Social Engineering Risks", + content: + "Avoid installing extensions prompted by emails, ads, or pop-ups, as these may use deceptive methods to gain access. Reliable services generally don’t push extensions, so question any unexpected installation requests.", + }, +]; + +export const SecurityWarningModal = () => { + const { onClose } = useDynamicModalContext(); + const color = useColor(); + const [openedAccordionItems, setOpenedAccordionItems] = useState>(new Set()); + + const handleInform = () => { + localStorage.setItem("user:isInformed", "true"); + onClose(); + }; + + return ( + + + + + Browser Extension Security Tips + + Please carefully review these guidelines to protect your wallet from potential security + risks + + + + + + setOpenedAccordionItems( + prev => new Set([...prev, e as number].filter(item => item > -1)) + ) + } + > + {accordionItems.map(({ title, content, icon }, index) => ( + + + + + {title} + + + + + {content} + + + ))} + + + + + + + ); +}; diff --git a/apps/web/src/components/SecurityWarningModal/index.ts b/apps/web/src/components/SecurityWarningModal/index.ts new file mode 100644 index 0000000000..2a13574550 --- /dev/null +++ b/apps/web/src/components/SecurityWarningModal/index.ts @@ -0,0 +1 @@ +export * from "./SecurityWarningModal"; diff --git a/apps/web/src/styles/theme.ts b/apps/web/src/styles/theme.ts index 5d87d33633..bbd7bbe37f 100644 --- a/apps/web/src/styles/theme.ts +++ b/apps/web/src/styles/theme.ts @@ -568,6 +568,8 @@ const theme = extendTheme({ marginTop: "15px", }, dialog: { + transition: + "width 0.2s ease-in-out, min-width 0.2s ease-in-out, max-width 0.2s ease-in-out", boxShadow: "2px 4px 12px 0px rgba(45, 55, 72, 0.05)", borderTopRightRadius: "30px", borderTopLeftRadius: "30px", diff --git a/packages/components/src/components/DynamicDisclosure/DynamicDisclosure.tsx b/packages/components/src/components/DynamicDisclosure/DynamicDisclosure.tsx index b527ea9a02..05a6be5bcd 100644 --- a/packages/components/src/components/DynamicDisclosure/DynamicDisclosure.tsx +++ b/packages/components/src/components/DynamicDisclosure/DynamicDisclosure.tsx @@ -23,6 +23,7 @@ interface DynamicDisclosureContextType { content: ReactElement, props?: ThemingProps & { onClose?: () => void | Promise; + closeOnEsc?: boolean; } ) => Promise; onClose: () => void; @@ -60,7 +61,10 @@ export const useDynamicDrawerContext = () => useContext(DynamicDrawerContext); type DisclosureStackItem = { content: ReactElement; - props: ThemingProps & { onClose: () => void | Promise }; + props: ThemingProps & { + onClose: () => void | Promise; + closeOnEsc?: boolean; + }; formValues: Record; };