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;
};