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,
}