Compare commits

...

5 Commits

Author SHA1 Message Date
Connor McEwen
a9ccbdc5e2
feat: uk hotfix (#7433)
* feat: support redirects for a list of header paths (#7411)

* add country code to meta tag

* use blocked paths header

* proper types

* add test

* Update functions/components/metaTagInjector.ts

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>

* Update functions/components/metaTagInjector.ts

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>

* Update src/pages/App.tsx

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>

* pr suggestions

* skip failing e2e

* revert test change

* take file from main

---------

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>

* fix: meta tag injector uses property, not name (#7431)

* feat: uk disclaimer banner (#7428)

* feat: uk disclaimer banner

* bad merge with sitemap

* button

* cypress test

* intercept ordering

* comments

* sitemap was committed idk why

* font weights

* moving uk disclaimer

* removing trash

* fix merge issues

---------

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
Co-authored-by: Jack Short <john.short.tj@gmail.com>
2023-10-06 14:33:53 -04:00
eddie
e36b72eb82
fix: override user pref in analytics (#7421) 2023-10-05 12:10:53 -07:00
Tina
b173d00dae
feat: Move UniswapX signature expiry back to deadline (copy for staging) (#7404)
feat: Move UniswapX signature expiry back to deadline (#7402)

startTime -> deadline
2023-10-04 11:09:15 -04:00
UL Service Account
c3b02d7c8d ci: add global CODEOWNERS 2023-09-29 18:51:57 +00:00
UL Service Account
bff177d3f1 ci(t9n): download translations from crowdin 2023-09-29 18:51:57 +00:00
48 changed files with 121638 additions and 23 deletions

1
CODEOWNERS Normal file
View File

@ -0,0 +1 @@
* @uniswap/web-admins

View File

@ -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')
})
})

View File

@ -11,7 +11,7 @@ export const onRequest: PagesFunction = async ({ request, next }) => {
}
const res = next()
try {
return new HTMLRewriter().on('head', new MetaTagInjector(data)).transform(await res)
return new HTMLRewriter().on('head', new MetaTagInjector(data, request)).transform(await res)
} catch (e) {
return res
}

View File

@ -6,12 +6,15 @@ test('should append meta tag to element', () => {
} as unknown as Element
const property = 'property'
const content = 'content'
const injector = new MetaTagInjector({
title: 'test',
url: 'testUrl',
image: 'testImage',
description: 'testDescription',
})
const injector = new MetaTagInjector(
{
title: 'test',
url: 'testUrl',
image: 'testImage',
description: 'testDescription',
},
new Request('http://localhost')
)
injector.append(element, property, content)
expect(element.append).toHaveBeenCalledWith(`<meta property="${property}" content="${content}"/>`, { html: true })
@ -36,3 +39,22 @@ test('should append meta tag to element', () => {
expect(element.append).toHaveBeenCalledTimes(13)
})
test('should pass through header blocked paths', () => {
const element = {
append: jest.fn(),
} as unknown as Element
const request = new Request('http://localhost')
request.headers.set('x-blocked-paths', '/')
const injector = new MetaTagInjector(
{
title: 'test',
url: 'testUrl',
image: 'testImage',
description: 'testDescription',
},
request
)
injector.element(element)
expect(element.append).toHaveBeenCalledWith(`<meta property="x:blocked-paths" content="/"/>`, { html: true })
})

View File

@ -10,7 +10,7 @@ type MetaTagInjectorInput = {
* to inject meta tags into the <head> of an HTML document.
*/
export class MetaTagInjector implements HTMLRewriterElementContentHandlers {
constructor(private input: MetaTagInjectorInput) {}
constructor(private input: MetaTagInjectorInput, private request: Request) {}
append(element: Element, property: string, content: string) {
element.append(`<meta property="${property}" content="${content}"/>`, { html: true })
@ -38,5 +38,10 @@ export class MetaTagInjector implements HTMLRewriterElementContentHandlers {
this.append(element, 'twitter:image', this.input.image)
this.append(element, 'twitter:image:alt', this.input.title)
}
const blockedPaths = this.request.headers.get('x-blocked-paths')
if (blockedPaths) {
this.append(element, 'x:blocked-paths', blockedPaths)
}
}
}

View File

@ -8,7 +8,7 @@ export const onRequest: PagesFunction = async ({ params, request, next }) => {
const { index } = params
const collectionAddress = index[0]?.toString()
const tokenId = index[1]?.toString()
return getMetadataRequest(res, request.url, () => getAsset(collectionAddress, tokenId, request.url))
return getMetadataRequest(res, request, () => getAsset(collectionAddress, tokenId, request.url))
} catch (e) {
return res
}

View File

@ -7,7 +7,7 @@ export const onRequest: PagesFunction = async ({ params, request, next }) => {
try {
const { index } = params
const collectionAddress = index?.toString()
return getMetadataRequest(res, request.url, () => getCollection(collectionAddress, request.url))
return getMetadataRequest(res, request, () => getCollection(collectionAddress, request.url))
} catch (e) {
return res
}

View File

@ -11,7 +11,7 @@ export const onRequest: PagesFunction = async ({ params, request, next }) => {
if (!tokenAddress) {
return res
}
return getMetadataRequest(res, request.url, () => getToken(networkName, tokenAddress, request.url))
return getMetadataRequest(res, request, () => getToken(networkName, tokenAddress, request.url))
} catch (e) {
return res
}

View File

@ -4,13 +4,13 @@ import { Data } from './cache'
export async function getMetadataRequest(
res: Promise<Response>,
url: string,
request: Request,
getData: () => Promise<Data | undefined>
) {
try {
const cachedData = await getRequest(url, getData, (data): data is Data => true)
const cachedData = await getRequest(request.url, getData, (data): data is Data => true)
if (cachedData) {
return new HTMLRewriter().on('head', new MetaTagInjector(cachedData)).transform(await res)
return new HTMLRewriter().on('head', new MetaTagInjector(cachedData, request)).transform(await res)
} else {
return res
}

View File

@ -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 (
<BannerWrapper>
<BannerTextWrapper lineHeight="24px">{t`UK disclaimer:` + ' ' + bannerText}</BannerTextWrapper>
<ReadMoreWrapper>
<ThemedText.BodySecondary lineHeight="24px" color="accent1" onClick={openDisclaimer}>
<Trans>Read more</Trans>
</ThemedText.BodySecondary>
</ReadMoreWrapper>
</BannerWrapper>
)
}

View File

@ -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 (
<Modal isOpen={isOpen} onDismiss={closeModal}>
<Wrapper gap="md">
<CloseIconWrapper onClick={() => closeModal()}>
<X size={24} />
</CloseIconWrapper>
<Column gap="sm">
<ThemedText.HeadlineLarge padding="0px 8px" fontSize="24px" lineHeight="32px">
<Trans>Disclaimer for UK residents</Trans>
</ThemedText.HeadlineLarge>
<ThemedText.BodyPrimary padding="8px 8px 12px" lineHeight="24px">
{bannerText}
</ThemedText.BodyPrimary>
</Column>
<ButtonContainer gap="md">
<StyledThemeButton size={ButtonSize.large} emphasis={ButtonEmphasis.medium} onClick={() => closeModal()}>
<Trans>Dismiss</Trans>
</StyledThemeButton>
</ButtonContainer>
</Wrapper>
</Modal>
)
}

View File

@ -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 useAccountRiskCheck from 'hooks/useAccountRiskCheck'
import Bag from 'nft/components/bag/Bag'
import TransactionCompleteModal from 'nft/components/collection/TransactionCompleteModal'
@ -31,6 +32,7 @@ export default function TopLevelModals() {
<TransactionCompleteModal />
<AirdropModal />
<FiatOnrampModal />
<UkDisclaimerModal />
</>
)
}

View File

@ -90,7 +90,7 @@ export function useUniswapXSwapCallback({
const { domain, types, values } = updatedOrder.permitData()
const signature = await signTypedData(provider.getSigner(account), domain, types, values)
if (startTime < Math.floor(Date.now() / 1000)) {
if (deadline < Math.floor(Date.now() / 1000)) {
throw new SignatureExpiredError()
}
return { signature, updatedOrder }

3678
src/locales/af-ZA.po Normal file

File diff suppressed because it is too large Load Diff

3678
src/locales/ar-SA.po Normal file

File diff suppressed because it is too large Load Diff

3678
src/locales/ca-ES.po Normal file

File diff suppressed because it is too large Load Diff

3678
src/locales/cs-CZ.po Normal file

File diff suppressed because it is too large Load Diff

3678
src/locales/da-DK.po Normal file

File diff suppressed because it is too large Load Diff

3678
src/locales/de-DE.po Normal file

File diff suppressed because it is too large Load Diff

3678
src/locales/el-GR.po Normal file

File diff suppressed because it is too large Load Diff

3678
src/locales/es-ES.po Normal file

File diff suppressed because it is too large Load Diff

3678
src/locales/fi-FI.po Normal file

File diff suppressed because it is too large Load Diff

3678
src/locales/fr-FR.po Normal file

File diff suppressed because it is too large Load Diff

3678
src/locales/he-IL.po Normal file

File diff suppressed because it is too large Load Diff

3679
src/locales/hu-HU.po Normal file

File diff suppressed because it is too large Load Diff

3678
src/locales/id-ID.po Normal file

File diff suppressed because it is too large Load Diff

3678
src/locales/it-IT.po Normal file

File diff suppressed because it is too large Load Diff

3678
src/locales/ja-JP.po Normal file

File diff suppressed because it is too large Load Diff

3678
src/locales/ko-KR.po Normal file

File diff suppressed because it is too large Load Diff

3678
src/locales/nl-NL.po Normal file

File diff suppressed because it is too large Load Diff

3678
src/locales/no-NO.po Normal file

File diff suppressed because it is too large Load Diff

3678
src/locales/pl-PL.po Normal file

File diff suppressed because it is too large Load Diff

3678
src/locales/pt-BR.po Normal file

File diff suppressed because it is too large Load Diff

3678
src/locales/pt-PT.po Normal file

File diff suppressed because it is too large Load Diff

3678
src/locales/ro-RO.po Normal file

File diff suppressed because it is too large Load Diff

3678
src/locales/ru-RU.po Normal file

File diff suppressed because it is too large Load Diff

3678
src/locales/sl-SI.po Normal file

File diff suppressed because it is too large Load Diff

3678
src/locales/sr-SP.po Normal file

File diff suppressed because it is too large Load Diff

3678
src/locales/sv-SE.po Normal file

File diff suppressed because it is too large Load Diff

3678
src/locales/sw-TZ.po Normal file

File diff suppressed because it is too large Load Diff

3678
src/locales/th-TH.po Normal file

File diff suppressed because it is too large Load Diff

3678
src/locales/tr-TR.po Normal file

File diff suppressed because it is too large Load Diff

3678
src/locales/uk-UA.po Normal file

File diff suppressed because it is too large Load Diff

3678
src/locales/vi-VN.po Normal file

File diff suppressed because it is too large Load Diff

3678
src/locales/zh-CN.po Normal file

File diff suppressed because it is too large Load Diff

3678
src/locales/zh-TW.po Normal file

File diff suppressed because it is too large Load Diff

View File

@ -4,14 +4,19 @@ 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 { useInfoPoolPageEnabled } from 'featureFlags/flags/infoPoolPage'
import { useUniswapXDefaultEnabled } from 'featureFlags/flags/uniswapXDefault'
import { useAtom } from 'jotai'
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 { useRouterPreference } from 'state/user/hooks'
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'
import styled from 'styled-components'
import { SpinnerSVG } from 'theme/components'
@ -82,15 +87,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;
}
`
// this is the same svg defined in assets/images/blue-loader.svg
@ -117,12 +130,18 @@ export default function App() {
const currentPage = getCurrentPageFromLocation(pathname)
const isDarkMode = useIsDarkMode()
const [routerPreference] = useRouterPreference()
const [scrolledState, setScrolledState] = useState(false)
const infoPoolPageEnabled = useInfoPoolPageEnabled()
const [scrollY, setScrollY] = useState(0)
const scrolledState = scrollY > 0
const isUniswapXDefaultEnabled = useUniswapXDefaultEnabled()
const userOptedOutOfUniswapX = useUserOptedOutOfUniswapX()
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()
@ -165,12 +184,25 @@ export default function App() {
}, [isDarkMode])
useEffect(() => {
// If we're not in the transition period to UniswapX opt-out, set the router preference to whatever is specified.
if (!isUniswapXDefaultEnabled) {
user.set(CustomUserProperties.ROUTER_PREFERENCE, routerPreference)
return
}
// In the transition period, override the stored API preference to UniswapX if the user hasn't opted out.
if (routerPreference === RouterPreference.API && !userOptedOutOfUniswapX) {
user.set(CustomUserProperties.ROUTER_PREFERENCE, RouterPreference.X)
return
}
// Otherwise, the user has opted out or their preference is UniswapX/client, so set the preference to whatever is specified.
user.set(CustomUserProperties.ROUTER_PREFERENCE, routerPreference)
}, [routerPreference])
}, [routerPreference, isUniswapXDefaultEnabled, userOptedOutOfUniswapX])
useEffect(() => {
const scrollListener = () => {
setScrolledState(window.scrollY > 0)
setScrollY(window.scrollY)
}
window.addEventListener('scroll', scrollListener)
return () => window.removeEventListener('scroll', scrollListener)
@ -200,6 +232,12 @@ export default function App() {
return null
}
const blockedPaths = document.querySelector('meta[property="x:blocked-paths"]')?.getAttribute('content')?.split(',')
const shouldBlockPath = blockedPaths?.includes(pathname) ?? false
if (shouldBlockPath && pathname !== '/swap') {
return <Navigate to="/swap" replace />
}
return (
<ErrorBoundary>
<DarkModeQueryParamReader />
@ -214,7 +252,8 @@ export default function App() {
api: process.env.REACT_APP_STATSIG_PROXY_URL,
}}
>
<HeaderWrapper transparent={isHeaderTransparent}>
{renderUkBannner && <UkBanner />}
<HeaderWrapper transparent={isHeaderTransparent} bannerIsVisible={renderUkBannner} scrollY={scrollY}>
<NavBar blur={isHeaderTransparent} />
</HeaderWrapper>
<BodyWrapper>

View File

@ -43,6 +43,7 @@ export enum ApplicationModal {
TAX_SERVICE,
TIME_SELECTOR,
VOTE,
UK_DISCLAIMER,
UNISWAP_NFT_AIRDROP_CLAIM,
}