Skip to content

Commit

Permalink
Add SecurityWarningModal to the layout page
Browse files Browse the repository at this point in the history
  • Loading branch information
OKendigelyan committed Nov 8, 2024
1 parent 9968222 commit 3a04a4c
Show file tree
Hide file tree
Showing 9 changed files with 274 additions and 1 deletion.
13 changes: 13 additions & 0 deletions apps/web/src/Layout.tsx
Original file line number Diff line number Diff line change
@@ -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(<SecurityWarningModal />, { closeOnEsc: false, size: "xl" });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

return (
<Grid
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/assets/icons/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export { default as PencilIcon } from "./pencil.svg";
export { default as PlusIcon } from "./plus.svg";
export { default as PlusCircleIcon } from "./plus-circle.svg";
export { default as PyramidIcon } from "./pyramid.svg";
export { default as RefreshIcon } from "./refresh.svg";
export { default as RadioIcon } from "./radio.svg";
export { default as RedditIcon } from "./reddit.svg";
export { default as ScanIcon } from "./scan.svg";
Expand All @@ -60,6 +61,7 @@ export { default as TagIcon } from "./tag.svg";
export { default as TezosLogoIcon } from "./tezos-logo.svg";
export { default as TezIcon } from "./tez.svg";
export { default as ThreeDotsIcon } from "./three-dots.svg";
export { default as ThumbsUpIcon } from "./thumbs-up.svg";
export { default as TrashIcon } from "./trash.svg";
export { default as TwitterIcon } from "./twitter.svg";
export { default as UnknownIcon } from "./unknown-contact.svg";
Expand Down
11 changes: 11 additions & 0 deletions apps/web/src/assets/icons/refresh.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions apps/web/src/assets/icons/thumbs-up.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { SecurityWarningModal } from "./SecurityWarningModal";
import {
act,
dynamicModalContextMock,
renderInModal,
screen,
userEvent,
waitFor,
} from "../../testUtils";

beforeEach(() => {
localStorage.clear();
});

describe("<SecurityWarningModal />", () => {
it("renders the modal with correct title and content", async () => {
await renderInModal(<SecurityWarningModal />);

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(<SecurityWarningModal />);

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(<SecurityWarningModal />);

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(<SecurityWarningModal />);

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(<SecurityWarningModal />);

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();
});
});
150 changes: 150 additions & 0 deletions apps/web/src/components/SecurityWarningModal/SecurityWarningModal.tsx
Original file line number Diff line number Diff line change
@@ -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<Set<number>>(new Set());

const handleInform = () => {
localStorage.setItem("user:isInformed", "true");
onClose();
};

return (
<ModalContent>
<ModalHeader>
<Flex alignItems="center" justifyContent="center" flexDirection="column" gap="16px">
<AlertIcon width="22px" color={color("400")} />
<Heading size="xl">Browser Extension Security Tips</Heading>
<Text
width="full"
marginTop="-4px"
color={color("700")}
fontWeight="400"
textAlign="center"
size="md"
>
Please carefully review these guidelines to protect your wallet from potential security
risks
</Text>
</Flex>
</ModalHeader>
<ModalBody>
<Accordion
as={Flex}
flexDirection="column"
gap="12px"
allowToggle
onChange={e =>
setOpenedAccordionItems(
prev => new Set([...prev, e as number].filter(item => item > -1))
)
}
>
{accordionItems.map(({ title, content, icon }, index) => (
<AccordionItem key={title} border="none">
<AccordionButton
gap="16px"
padding="12px 24px"
textAlign="left"
background={color("100")}
borderRadius="6px"
_hover={{
background: color("300"),
}}
_expanded={{
borderBottomRadius: "0",
}}
cursor="pointer"
data-testid="accordion-button"
>
<Icon
as={icon}
boxSize="24px"
color={openedAccordionItems.has(index) ? color("greenDark") : color("400")}
/>
<Heading width="100%" color={color("900")} size="md">
{title}
</Heading>
<AccordionIcon />
</AccordionButton>
<AccordionPanel padding="8px 24px" background={color("100")} borderBottomRadius="6px">
<Text>{content}</Text>
</AccordionPanel>
</AccordionItem>
))}
</Accordion>
</ModalBody>
<ModalFooter>
<Button
width="full"
isDisabled={openedAccordionItems.size !== accordionItems.length}
onClick={handleInform}
variant="primary"
>
Got it
</Button>
</ModalFooter>
</ModalContent>
);
};
1 change: 1 addition & 0 deletions apps/web/src/components/SecurityWarningModal/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./SecurityWarningModal";
2 changes: 2 additions & 0 deletions apps/web/src/styles/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ interface DynamicDisclosureContextType {
content: ReactElement,
props?: ThemingProps & {
onClose?: () => void | Promise<void>;
closeOnEsc?: boolean;
}
) => Promise<void>;
onClose: () => void;
Expand Down Expand Up @@ -60,7 +61,10 @@ export const useDynamicDrawerContext = () => useContext(DynamicDrawerContext);

type DisclosureStackItem = {
content: ReactElement;
props: ThemingProps & { onClose: () => void | Promise<void> };
props: ThemingProps & {
onClose: () => void | Promise<void>;
closeOnEsc?: boolean;
};
formValues: Record<string, any>;
};

Expand Down

0 comments on commit 3a04a4c

Please sign in to comment.