From ef93ae58063e27c9a8daeca08a94414139d53d21 Mon Sep 17 00:00:00 2001 From: James Mealy Date: Mon, 29 Apr 2024 16:20:15 +0200 Subject: [PATCH] feat: display legal disclaimer when swapping for the first time (#3623) * Display legal disclaimer to first time swappers * check for consent within swap component * fix: remove test address from blocked addresses * fix: remove test code --- .../common/BlockedAddress/index.stories.tsx | 25 --------- .../common/BlockedAddress/index.tsx | 53 ++++++------------- .../common/Disclaimer/index.stories.tsx | 39 ++++++++++++++ src/components/common/Disclaimer/index.tsx | 50 +++++++++++++++++ .../common/Disclaimer/styles.module.css | 14 +++++ .../LegalDisclaimerContent/index.tsx} | 19 ++++--- .../LegalDisclaimerContent/styles.module.css | 8 +++ .../safe-apps/SafeAppsInfoModal/index.tsx | 4 +- .../SafeAppsInfoModal/styles.module.css | 9 ---- src/features/swap/index.tsx | 15 ++++++ src/features/swap/useSwapConsent.ts | 22 ++++++++ 11 files changed, 174 insertions(+), 84 deletions(-) delete mode 100644 src/components/common/BlockedAddress/index.stories.tsx create mode 100644 src/components/common/Disclaimer/index.stories.tsx create mode 100644 src/components/common/Disclaimer/index.tsx create mode 100644 src/components/common/Disclaimer/styles.module.css rename src/components/{safe-apps/SafeAppsInfoModal/LegalDisclaimer.tsx => common/LegalDisclaimerContent/index.tsx} (64%) create mode 100644 src/components/common/LegalDisclaimerContent/styles.module.css create mode 100644 src/features/swap/useSwapConsent.ts diff --git a/src/components/common/BlockedAddress/index.stories.tsx b/src/components/common/BlockedAddress/index.stories.tsx deleted file mode 100644 index f2d837c70c..0000000000 --- a/src/components/common/BlockedAddress/index.stories.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react' -import BlockedAddress from './index' - -const meta = { - component: BlockedAddress, - parameters: { - componentSubtitle: 'Renders an information block intended for users whose address is blocked by OFAC', - }, - tags: ['autodocs'], -} satisfies Meta - -export default meta -type Story = StoryObj - -export const Default: Story = { - args: { - address: '0xD3a484faEa53313eF85b5916C9302a3E304ae622', - }, - parameters: { - design: { - type: 'figma', - url: 'https://www.figma.com/file/VyA38zUPbJ2zflzCIYR6Nu/Swap?node-id=6167%3A14371&mode=dev', - }, - }, -} diff --git a/src/components/common/BlockedAddress/index.tsx b/src/components/common/BlockedAddress/index.tsx index 7b9db8073c..e4817ea1f3 100644 --- a/src/components/common/BlockedAddress/index.tsx +++ b/src/components/common/BlockedAddress/index.tsx @@ -1,51 +1,28 @@ import type { ReactElement } from 'react' -import { Box, Button, Divider, Paper, Stack, SvgIcon, Typography, useMediaQuery, useTheme } from '@mui/material' -import Link from 'next/link' -import InfoIcon from '@/public/images/notifications/info.svg' -import css from './styles.module.css' +import { useMediaQuery, useTheme } from '@mui/material' import { shortenAddress } from '@/utils/formatters' import { useRouter } from 'next/router' +import Disclaimer from '@/components/common/Disclaimer' import { AppRoutes } from '@/config/routes' -export const BlockedAddress = ({ address }: { address: string }): ReactElement => { +export const BlockedAddress = ({ address }: { address?: string }): ReactElement => { const theme = useTheme() const isMobile = useMediaQuery(theme.breakpoints.down('sm')) - const displayAddress = isMobile ? shortenAddress(address) : address + const displayAddress = address && isMobile ? shortenAddress(address) : address const router = useRouter() - return ( -
- - ({ borderBottom: `1px solid ${palette.border.light}` })} - > - {displayAddress} + const handleAccept = () => { + router.push({ pathname: AppRoutes.home, query: router.query }) + } - - - - - Blocked Address - - - This signer address is blocked by the Safe interface, due to being associated with the blocked activities by - the U.S. Department of Treasury in the Specially Designated Nationals (SDN) list.{' '} - - - - - - - - - -
+ return ( + ) } diff --git a/src/components/common/Disclaimer/index.stories.tsx b/src/components/common/Disclaimer/index.stories.tsx new file mode 100644 index 0000000000..a8f522deed --- /dev/null +++ b/src/components/common/Disclaimer/index.stories.tsx @@ -0,0 +1,39 @@ +import type { Meta, StoryObj } from '@storybook/react' +import Disclaimer from './index' +import LegalDisclaimerContent from '@/components/common/LegalDisclaimerContent' + +const meta = { + component: Disclaimer, + parameters: { + componentSubtitle: 'Renders a Block for displaying information to the user, with a button to accept.', + }, + tags: ['autodocs'], +} satisfies Meta + +export default meta +type Story = StoryObj + +export const BlockedAddress: Story = { + args: { + subtitle: '0xD3a484faEa53313eF85b5916C9302a3E304ae622', + title: 'Blocked Address', + content: + 'This signer address is blocked by the Safe interface, due to being associated with the blocked activities by the U.S. Department of Treasury in the Specially Designated Nationals (SDN) list.', + onAccept: () => {}, + }, + parameters: { + design: { + type: 'figma', + url: 'https://www.figma.com/file/VyA38zUPbJ2zflzCIYR6Nu/Swap?node-id=6167%3A14371&mode=dev', + }, + }, +} + +export const LegalDisclaimer: Story = { + args: { + title: 'Legal Disclaimer', + content: , + buttonText: 'Continue', + onAccept: () => {}, + }, +} diff --git a/src/components/common/Disclaimer/index.tsx b/src/components/common/Disclaimer/index.tsx new file mode 100644 index 0000000000..de5ff7b106 --- /dev/null +++ b/src/components/common/Disclaimer/index.tsx @@ -0,0 +1,50 @@ +import type { ReactElement, ReactNode } from 'react' +import { Box, Button, Divider, Paper, Stack, SvgIcon, Typography } from '@mui/material' +import InfoIcon from '@/public/images/notifications/info.svg' +import css from './styles.module.css' + +export const Disclaimer = ({ + title, + subtitle, + buttonText, + content, + onAccept, +}: { + title: string + subtitle?: string + buttonText?: string + content: ReactNode + onAccept: () => void +}): ReactElement => { + return ( +
+ + ({ borderBottom: `1px solid ${palette.border.light}` })} + > + {subtitle && {subtitle}} + + + + + + {title} + + {content} + + + + + + +
+ ) +} + +export default Disclaimer diff --git a/src/components/common/Disclaimer/styles.module.css b/src/components/common/Disclaimer/styles.module.css new file mode 100644 index 0000000000..f69a019100 --- /dev/null +++ b/src/components/common/Disclaimer/styles.module.css @@ -0,0 +1,14 @@ +.container { + height: 100%; + display: flex; + justify-content: center; + align-items: center; +} + +.iconCircle { + color: var(--color-info-main); + border-radius: 50%; + display: flex; + padding: var(--space-1); + background: #d7f6ff; +} diff --git a/src/components/safe-apps/SafeAppsInfoModal/LegalDisclaimer.tsx b/src/components/common/LegalDisclaimerContent/index.tsx similarity index 64% rename from src/components/safe-apps/SafeAppsInfoModal/LegalDisclaimer.tsx rename to src/components/common/LegalDisclaimerContent/index.tsx index a3fe4a7bba..8bc6270c89 100644 --- a/src/components/safe-apps/SafeAppsInfoModal/LegalDisclaimer.tsx +++ b/src/components/common/LegalDisclaimerContent/index.tsx @@ -4,18 +4,17 @@ import { Typography } from '@mui/material' import css from './styles.module.css' -const LegalDisclaimer = (): JSX.Element => ( +const LegalDisclaimerContent = ({ withTitle = true }: { withTitle?: boolean }): JSX.Element => (
- - Before starting to use Safe dApps... - - - Disclaimer - + {withTitle && ( + + Disclaimer + + )}
- You are now accessing third-party apps, which we do not own, control, maintain or audit. We are not liable for - any loss you may suffer in connection with interacting with the apps, which is at your own risk. + You are now accessing a third-party app, which we do not own, control, maintain or audit. We are not liable for + any loss you may suffer in connection with interacting with the app, which is at your own risk. @@ -33,4 +32,4 @@ const LegalDisclaimer = (): JSX.Element => (
) -export default LegalDisclaimer +export default LegalDisclaimerContent diff --git a/src/components/common/LegalDisclaimerContent/styles.module.css b/src/components/common/LegalDisclaimerContent/styles.module.css new file mode 100644 index 0000000000..926ac0a491 --- /dev/null +++ b/src/components/common/LegalDisclaimerContent/styles.module.css @@ -0,0 +1,8 @@ +.disclaimerContainer p, +.disclaimerContainer h3 { + line-height: 24px; +} + +.disclaimerInner p { + text-align: justify; +} diff --git a/src/components/safe-apps/SafeAppsInfoModal/index.tsx b/src/components/safe-apps/SafeAppsInfoModal/index.tsx index 924c2597fc..33b448aedd 100644 --- a/src/components/safe-apps/SafeAppsInfoModal/index.tsx +++ b/src/components/safe-apps/SafeAppsInfoModal/index.tsx @@ -4,12 +4,12 @@ import { Grid, LinearProgress } from '@mui/material' import type { BrowserPermission } from '@/hooks/safe-apps/permissions' import Slider from './Slider' -import LegalDisclaimer from './LegalDisclaimer' import AllowedFeaturesList from './AllowedFeaturesList' import type { AllowedFeatures, AllowedFeatureSelection } from '../types' import { PermissionStatus } from '../types' import UnknownAppWarning from './UnknownAppWarning' import { getOrigin } from '../utils' +import LegalDisclaimerContent from '@/components/common/LegalDisclaimerContent' type SafeAppsInfoModalProps = { onCancel: () => void @@ -135,7 +135,7 @@ const SafeAppsInfoModal = ({ /> - {!isConsentAccepted && } + {!isConsentAccepted && } {!isPermissionsReviewCompleted && ( { const { safeAddress } = useSafeInfo() const wallet = useWallet() + const { isConsentAccepted, onAccept } = useSwapConsent() const [toasts, setToasts] = useState([]) @@ -208,6 +212,17 @@ const SwapWidget = ({ sell }: Params) => { return } + if (!isConsentAccepted) { + return ( + } + onAccept={onAccept} + buttonText="Continue" + /> + ) + } + if (!isSupportedChainForSwap(Number(chainId))) { return ( diff --git a/src/features/swap/useSwapConsent.ts b/src/features/swap/useSwapConsent.ts new file mode 100644 index 0000000000..38e85d0fe2 --- /dev/null +++ b/src/features/swap/useSwapConsent.ts @@ -0,0 +1,22 @@ +import { useCallback } from 'react' +import useLocalStorage from '@/services/local-storage/useLocalStorage' + +const SWAPS_CONSENT_STORAGE_KEY = 'swapDisclaimerAccepted' + +const useSwapConsent = (): { + isConsentAccepted: boolean + onAccept: () => void +} => { + const [isConsentAccepted = false, setIsConsentAccepted] = useLocalStorage(SWAPS_CONSENT_STORAGE_KEY) + + const onAccept = useCallback(() => { + setIsConsentAccepted(true) + }, [setIsConsentAccepted]) + + return { + isConsentAccepted, + onAccept, + } +} + +export default useSwapConsent