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 = () => {