Skip to content

Commit

Permalink
Add social login 2fa warning (#2234)
Browse files Browse the repository at this point in the history
  • Loading branch information
OKendigelyan authored Dec 10, 2024
1 parent ce58089 commit 4263bbc
Show file tree
Hide file tree
Showing 11 changed files with 356 additions and 54 deletions.
20 changes: 19 additions & 1 deletion apps/desktop/src/Router.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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(<SocialLoginWarningModal />, { closeOnEsc: false });
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentUser]);

return (
<HashRouter>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export const VerifySeedphrase = ({
inputProps={{
paddingLeft: "36px",
size: "md",
height: "48px",
}}
listProps={{
marginTop: "6px",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { SocialLoginWarningModal } from "./SocialLoginWarningModal";
import {
act,
dynamicModalContextMock,
render,
screen,
userEvent,
waitFor,
} from "../../mocks/testUtils";

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

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

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

const button = screen.getByRole("button", { name: "Continue" });
expect(button).toBeDisabled();
});

it("enables 'Continue' button when checkbox is checked", async () => {
const user = userEvent.setup();
render(<SocialLoginWarningModal />);

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

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

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();
});
});
Original file line number Diff line number Diff line change
@@ -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 (
<ModalContent>
<ModalHeader>
<Flex
alignItems="center"
justifyContent="center"
flexDirection="column"
gap="16px"
textAlign="center"
>
<Icon as={WarningIcon} width="22px" stroke={colors.gray[450]} />
<Heading size="xl">Important notice about your social account wallet</Heading>
</Flex>
</ModalHeader>
<ModalBody marginTop="24px">
<Flex alignItems="center" flexDirection="column" gap="16px" textAlign="center">
<Text color={colors.gray[400]} fontSize="md">
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.
</Text>
<Checkbox
marginTop="16px"
color={colors.gray[400]}
isChecked={isAgreed}
onChange={e => setIsAgreed(e.target.checked)}
>
<Text fontSize="sm" fontWeight="bold">
I understand and accept the risks.
</Text>
</Checkbox>
</Flex>
</ModalBody>
<ModalFooter>
<Button width="full" isDisabled={!isAgreed} onClick={handleInform} variant="primary">
Continue
</Button>
</ModalFooter>
</ModalContent>
);
};
8 changes: 4 additions & 4 deletions apps/desktop/src/setupTests.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand All @@ -79,6 +75,10 @@ beforeEach(() => {
// Hack for testing HashRouter: clears URL between tests.
window.location.hash = "";

Object.defineProperty(window, "localStorage", {
value: mockLocalStorage(),
});

setupJestCanvasMock();
});

Expand Down
53 changes: 46 additions & 7 deletions apps/web/src/Layout.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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(<SecurityWarningModal />, { closeOnEsc: false, size: "xl" });
}, 500);
const warnings = [
{
key: "user:isSocialLoginWarningShown",
component: <SocialLoginWarningModal />,
options: { closeOnEsc: false },
isEnabled: () => currentUser?.type === "social",
},
{
key: "user:isExtensionsWarningShown",
component: <SecurityWarningModal />,
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 (
<Grid
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,5 @@
import { SecurityWarningModal } from "./SecurityWarningModal";
import {
act,
dynamicModalContextMock,
renderInModal,
screen,
userEvent,
waitFor,
} from "../../testUtils";
import { act, renderInModal, screen, userEvent, waitFor } from "../../testUtils";

beforeEach(() => {
localStorage.clear();
Expand All @@ -27,18 +20,16 @@ describe("<SecurityWarningModal />", () => {
).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(<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 => {
await waitFor(() => {
expect(screen.getByText(title)).toBeVisible();
});
});
Expand All @@ -55,31 +46,11 @@ describe("<SecurityWarningModal />", () => {
await renderInModal(<SecurityWarningModal />);

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

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();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ export const SecurityWarningModal = () => {
const [isAgreed, setIsAgreed] = useState(false);

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

Expand Down Expand Up @@ -128,10 +127,12 @@ export const SecurityWarningModal = () => {
marginX="auto"
onChange={e => setIsAgreed(e.target.checked)}
>
<Text whiteSpace="break-spaces">I have read and understood all security guidelines</Text>
<Text fontWeight="bold" whiteSpace="break-spaces">
I understand and accept the risks.
</Text>
</Checkbox>
</ModalBody>
<ModalFooter flexDirection="column" gap="16px">
<ModalFooter>
<Button width="full" isDisabled={!isAgreed} onClick={handleInform} variant="primary">
Continue
</Button>
Expand Down
Loading

3 comments on commit 4263bbc

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Title Lines Statements Branches Functions
apps/desktop Coverage: 83%
83.81% (1786/2131) 79.58% (850/1068) 78.27% (454/580)
apps/web Coverage: 83%
83.81% (1786/2131) 79.58% (850/1068) 78.27% (454/580)
packages/components Coverage: 97%
97.51% (196/201) 95.91% (94/98) 88.13% (52/59)
packages/core Coverage: 81%
82.47% (207/251) 72.72% (88/121) 81.35% (48/59)
packages/crypto Coverage: 100%
100% (43/43) 90.9% (10/11) 100% (7/7)
packages/data-polling Coverage: 97%
95.27% (141/148) 87.5% (21/24) 92.85% (39/42)
packages/multisig Coverage: 98%
98.47% (129/131) 85.71% (18/21) 100% (36/36)
packages/social-auth Coverage: 100%
100% (21/21) 100% (11/11) 100% (3/3)
packages/state Coverage: 85%
84.76% (818/965) 80.86% (186/230) 78.59% (301/383)
packages/tezos Coverage: 89%
88.72% (118/133) 94.59% (35/37) 86.84% (33/38)
packages/tzkt Coverage: 89%
87.32% (62/71) 87.5% (14/16) 80.48% (33/41)

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Title Lines Statements Branches Functions
apps/desktop Coverage: 83%
83.81% (1786/2131) 79.58% (850/1068) 78.27% (454/580)
apps/web Coverage: 83%
83.81% (1786/2131) 79.58% (850/1068) 78.27% (454/580)
packages/components Coverage: 97%
97.51% (196/201) 95.91% (94/98) 88.13% (52/59)
packages/core Coverage: 81%
82.47% (207/251) 72.72% (88/121) 81.35% (48/59)
packages/crypto Coverage: 100%
100% (43/43) 90.9% (10/11) 100% (7/7)
packages/data-polling Coverage: 97%
95.27% (141/148) 87.5% (21/24) 92.85% (39/42)
packages/multisig Coverage: 98%
98.47% (129/131) 85.71% (18/21) 100% (36/36)
packages/social-auth Coverage: 100%
100% (21/21) 100% (11/11) 100% (3/3)
packages/state Coverage: 85%
84.76% (818/965) 80.86% (186/230) 78.59% (301/383)
packages/tezos Coverage: 89%
88.72% (118/133) 94.59% (35/37) 86.84% (33/38)
packages/tzkt Coverage: 89%
87.32% (62/71) 87.5% (14/16) 80.48% (33/41)

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Title Lines Statements Branches Functions
apps/desktop Coverage: 83%
83.81% (1786/2131) 79.58% (850/1068) 78.27% (454/580)
apps/web Coverage: 83%
83.81% (1786/2131) 79.58% (850/1068) 78.27% (454/580)
packages/components Coverage: 97%
97.51% (196/201) 95.91% (94/98) 88.13% (52/59)
packages/core Coverage: 81%
82.47% (207/251) 72.72% (88/121) 81.35% (48/59)
packages/crypto Coverage: 100%
100% (43/43) 90.9% (10/11) 100% (7/7)
packages/data-polling Coverage: 97%
95.27% (141/148) 87.5% (21/24) 92.85% (39/42)
packages/multisig Coverage: 98%
98.47% (129/131) 85.71% (18/21) 100% (36/36)
packages/social-auth Coverage: 100%
100% (21/21) 100% (11/11) 100% (3/3)
packages/state Coverage: 85%
84.76% (818/965) 80.86% (186/230) 78.59% (301/383)
packages/tezos Coverage: 89%
88.72% (118/133) 94.59% (35/37) 86.84% (33/38)
packages/tzkt Coverage: 89%
87.32% (62/71) 87.5% (14/16) 80.48% (33/41)

Please sign in to comment.