diff --git a/cypress/e2e/landing.test.ts b/cypress/e2e/landing.test.ts index acdcd02a35..f4bbe85559 100644 --- a/cypress/e2e/landing.test.ts +++ b/cypress/e2e/landing.test.ts @@ -39,4 +39,30 @@ describe('Landing Page', () => { cy.get(getTestSelector('pool-nav-link')).last().click() cy.url().should('include', '/pools') }) + + it('does not render uk compliance banner in US', () => { + cy.visit('/swap') + cy.contains('UK disclaimer').should('not.exist') + }) + + it('renders uk compliance banner in uk', () => { + cy.intercept('https://api.uniswap.org/v1/amplitude-proxy', (req) => { + const requestBody = JSON.stringify(req.body) + const byteSize = new Blob([requestBody]).size + req.alias = 'amplitude' + req.reply( + JSON.stringify({ + code: 200, + server_upload_time: Date.now(), + payload_size_bytes: byteSize, + events_ingested: req.body.events.length, + }), + { + 'origin-country': 'GB', + } + ) + }) + cy.visit('/swap') + cy.contains('UK disclaimer') + }) }) diff --git a/src/components/NavBar/UkBanner.tsx b/src/components/NavBar/UkBanner.tsx new file mode 100644 index 0000000000..040420ea39 --- /dev/null +++ b/src/components/NavBar/UkBanner.tsx @@ -0,0 +1,82 @@ +import { t, Trans } from '@lingui/macro' +import { useOpenModal } from 'state/application/hooks' +import { ApplicationModal } from 'state/application/reducer' +import styled from 'styled-components' +import { ButtonText, ThemedText } from 'theme/components' +import { Z_INDEX } from 'theme/zIndex' + +export const UK_BANNER_HEIGHT = 64 +export const UK_BANNER_HEIGHT_MD = 112 +export const UK_BANNER_HEIGHT_SM = 136 + +const BannerWrapper = styled.div` + position: relative; + display: flex; + background-color: ${({ theme }) => theme.surface1}; + padding: 20px; + border-bottom: 1px solid ${({ theme }) => theme.surface3}; + z-index: ${Z_INDEX.fixed}; + box-sizing: border-box; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + + @media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.md}px`}) { + flex-direction: column; + } +` + +const BannerTextWrapper = styled(ThemedText.BodySecondary)` + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + @media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.md}px`}) { + @supports (-webkit-line-clamp: 2) { + white-space: initial; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + } + } + + @media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) { + @supports (-webkit-line-clamp: 3) { + white-space: initial; + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + } + } +` + +const ReadMoreWrapper = styled(ButtonText)` + flex-shrink: 0; + width: max-content; + + :focus { + text-decoration: none; + } +` + +export const bannerText = t` + This web application is provided as a tool for users to interact with the Uniswap Protocol on + their own initiative, with no endorsement or recommendation of cryptocurrency trading activities. In doing so, + Uniswap is not recommending that users or potential users engage in cryptoasset trading activity, and users or + potential users of the web application should not regard this webpage or its contents as involving any form of + recommendation, invitation or inducement to deal in cryptoassets. +` + +export function UkBanner() { + const openDisclaimer = useOpenModal(ApplicationModal.UK_DISCLAIMER) + + return ( + + {t`UK disclaimer:` + ' ' + bannerText} + + + Read more + + + + ) +} diff --git a/src/components/NavBar/UkDisclaimerModal.tsx b/src/components/NavBar/UkDisclaimerModal.tsx new file mode 100644 index 0000000000..f45c2caa00 --- /dev/null +++ b/src/components/NavBar/UkDisclaimerModal.tsx @@ -0,0 +1,62 @@ +import { Trans } from '@lingui/macro' +import { ButtonEmphasis, ButtonSize, ThemeButton } from 'components/Button' +import Column from 'components/Column' +import Modal from 'components/Modal' +import { X } from 'react-feather' +import { useCloseModal, useModalIsOpen } from 'state/application/hooks' +import { ApplicationModal } from 'state/application/reducer' +import styled from 'styled-components' +import { ButtonText, ThemedText } from 'theme/components' + +import { bannerText } from './UkBanner' + +const Wrapper = styled(Column)` + padding: 8px; +` + +const ButtonContainer = styled(Column)` + padding: 8px 12px 4px; +` + +const CloseIconWrapper = styled(ButtonText)` + display: flex; + color: ${({ theme }) => theme.neutral1}; + justify-content: flex-end; + padding: 8px 0px 4px; + + :focus { + text-decoration: none; + } +` + +const StyledThemeButton = styled(ThemeButton)` + width: 100%; +` + +export function UkDisclaimerModal() { + const isOpen = useModalIsOpen(ApplicationModal.UK_DISCLAIMER) + const closeModal = useCloseModal() + + return ( + + + closeModal()}> + + + + + Disclaimer for UK residents + + + {bannerText} + + + + closeModal()}> + Dismiss + + + + + ) +} diff --git a/src/components/TopLevelModals/index.tsx b/src/components/TopLevelModals/index.tsx index 44bddf3813..cdf4540254 100644 --- a/src/components/TopLevelModals/index.tsx +++ b/src/components/TopLevelModals/index.tsx @@ -6,6 +6,7 @@ import BaseAnnouncementBanner from 'components/Banner/BaseAnnouncementBanner' import AddressClaimModal from 'components/claim/AddressClaimModal' import ConnectedAccountBlocked from 'components/ConnectedAccountBlocked' import FiatOnrampModal from 'components/FiatOnrampModal' +import { UkDisclaimerModal } from 'components/NavBar/UkDisclaimerModal' import DevFlagsBox from 'dev/DevFlagsBox' import useAccountRiskCheck from 'hooks/useAccountRiskCheck' import Bag from 'nft/components/bag/Bag' @@ -34,6 +35,7 @@ export default function TopLevelModals() { + {shouldShowDevFlags && } ) diff --git a/src/pages/App.tsx b/src/pages/App.tsx index 9f99222a06..a43d69444f 100644 --- a/src/pages/App.tsx +++ b/src/pages/App.tsx @@ -4,6 +4,7 @@ import { getDeviceId, sendAnalyticsEvent, sendInitializationEvent, Trace, user } import ErrorBoundary from 'components/ErrorBoundary' import Loader from 'components/Icons/LoadingSpinner' import NavBar, { PageTabs } from 'components/NavBar' +import { UK_BANNER_HEIGHT, UK_BANNER_HEIGHT_MD, UK_BANNER_HEIGHT_SM, UkBanner } from 'components/NavBar/UkBanner' import { useFeatureFlagsIsLoaded } from 'featureFlags' import { useUniswapXDefaultEnabled } from 'featureFlags/flags/uniswapXDefault' import { useAtom } from 'jotai' @@ -11,6 +12,8 @@ import { useBag } from 'nft/hooks/useBag' import { lazy, Suspense, useEffect, useLayoutEffect, useMemo, useState } from 'react' import { Navigate, Route, Routes, useLocation, useSearchParams } from 'react-router-dom' import { shouldDisableNFTRoutesAtom } from 'state/application/atoms' +import { useAppSelector } from 'state/hooks' +import { AppState } from 'state/reducer' import { RouterPreference } from 'state/routing/types' import { useRouterPreference, useUserOptedOutOfUniswapX } from 'state/user/hooks' import { StatsigProvider, StatsigUser } from 'statsig-react' @@ -60,15 +63,23 @@ const MobileBottomBar = styled.div` } ` -const HeaderWrapper = styled.div<{ transparent?: boolean }>` +const HeaderWrapper = styled.div<{ transparent?: boolean; bannerIsVisible?: boolean; scrollY: number }>` ${flexRowNoWrap}; background-color: ${({ theme, transparent }) => !transparent && theme.surface1}; border-bottom: ${({ theme, transparent }) => !transparent && `1px solid ${theme.surface3}`}; width: 100%; justify-content: space-between; position: fixed; - top: 0; + top: ${({ bannerIsVisible }) => (bannerIsVisible ? Math.max(UK_BANNER_HEIGHT - scrollY, 0) : 0)}px; z-index: ${Z_INDEX.dropdown}; + + @media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.md}px`}) { + top: ${({ bannerIsVisible }) => (bannerIsVisible ? Math.max(UK_BANNER_HEIGHT_MD - scrollY, 0) : 0)}px; + } + + @media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) { + top: ${({ bannerIsVisible }) => (bannerIsVisible ? Math.max(UK_BANNER_HEIGHT_SM - scrollY, 0) : 0)}px; + } ` export default function App() { @@ -80,14 +91,18 @@ export default function App() { const currentPage = getCurrentPageFromLocation(pathname) const isDarkMode = useIsDarkMode() const [routerPreference] = useRouterPreference() - const [scrolledState, setScrolledState] = useState(false) + const [scrollY, setScrollY] = useState(0) + const scrolledState = scrollY > 0 const isUniswapXDefaultEnabled = useUniswapXDefaultEnabled() const userOptedOutOfUniswapX = useUserOptedOutOfUniswapX() const routerConfig = useRouterConfig() + const originCountry = useAppSelector((state: AppState) => state.user.originCountry) + const renderUkBannner = Boolean(originCountry) && originCountry === 'GB' + useEffect(() => { window.scrollTo(0, 0) - setScrolledState(false) + setScrollY(0) }, [pathname]) const [searchParams] = useSearchParams() @@ -148,7 +163,7 @@ export default function App() { useEffect(() => { const scrollListener = () => { - setScrolledState(window.scrollY > 0) + setScrollY(window.scrollY) } window.addEventListener('scroll', scrollListener) return () => window.removeEventListener('scroll', scrollListener) @@ -198,7 +213,8 @@ export default function App() { api: process.env.REACT_APP_STATSIG_PROXY_URL, }} > - + {renderUkBannner && } + diff --git a/src/state/application/reducer.ts b/src/state/application/reducer.ts index f8fd364858..747153d2fa 100644 --- a/src/state/application/reducer.ts +++ b/src/state/application/reducer.ts @@ -43,6 +43,7 @@ export enum ApplicationModal { TAX_SERVICE, TIME_SELECTOR, VOTE, + UK_DISCLAIMER, UNISWAP_NFT_AIRDROP_CLAIM, }