Commit abe6bf50 authored by Justin Domingue's avatar Justin Domingue Committed by GitHub

feat: extend privacy and terms (#2623)

* initial iteration

* add logging

* added hook

* polish

* remove unused import

* add hash

* addressed pr feedback

* remove autorouter icon

* use firebase store

* style

* adjust recat ga

* log remove liquidity

* update copy

* addressed pr feedback

* addressed pr feedback

* prevent privacy content from dismissing modal

* make top-level key origin

* use hostname

* restore trm
parent 36cfe627
......@@ -2,3 +2,4 @@ REACT_APP_INFURA_KEY="099fc58e0de9451d80b18d7c74caa7c1"
REACT_APP_PORTIS_ID="c0e2bf01-4b08-4fd5-ac7b-8e26b58cd236"
REACT_APP_FORTMATIC_KEY="pk_live_F937DF033A1666BF"
REACT_APP_GOOGLE_ANALYTICS_ID="UA-128182339-4"
REACT_APP_FIREBASE_KEY="AIzaSyBcZWwTcTJHj_R6ipZcrJkXdq05PuX0Rs0"
// eslint-disable-next-line no-restricted-imports
import { t } from '@lingui/macro'
import { Trans } from '@lingui/macro'
import { t, Trans } from '@lingui/macro'
import { PrivacyPolicyModal } from 'components/PrivacyPolicy'
import { CHAIN_INFO, L2_CHAIN_IDS, SupportedChainId } from 'constants/chains'
import { LOCALE_LABEL, SUPPORTED_LOCALES, SupportedLocale } from 'constants/locales'
import { useActiveLocale } from 'hooks/useActiveLocale'
import { useLocationLinkProps } from 'hooks/useLocationLinkProps'
import React, { useEffect, useRef, useState } from 'react'
import { BookOpen, Check, ChevronLeft, Code, Globe, Info, MessageCircle, Moon, PieChart, Sun } from 'react-feather'
import {
BookOpen,
Check,
ChevronLeft,
Code,
FileText,
Globe,
Info,
MessageCircle,
Moon,
PieChart,
Sun,
} from 'react-feather'
import { Link } from 'react-router-dom'
import { useDarkModeManager } from 'state/user/hooks'
import styled, { css } from 'styled-components/macro'
......@@ -202,8 +214,9 @@ export default function Menu() {
const node = useRef<HTMLDivElement>()
const open = useModalOpen(ApplicationModal.MENU)
const toggle = useToggleModal(ApplicationModal.MENU)
useOnClickOutside(node, open ? toggle : undefined)
const toggleMenu = useToggleModal(ApplicationModal.MENU)
useOnClickOutside(node, open ? toggleMenu : undefined)
const togglePrivacyPolicy = useToggleModal(ApplicationModal.PRIVACY_POLICY)
const openClaimModal = useToggleModal(ApplicationModal.ADDRESS_CLAIM)
const showUNIClaimOption = Boolean(!!account && !!chainId && !L2_CHAIN_IDS.includes(chainId))
const { infoLink } = CHAIN_INFO[chainId ? chainId : SupportedChainId.MAINNET]
......@@ -217,9 +230,10 @@ export default function Menu() {
}, [open])
return (
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/30451
<>
{/* // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/30451 */}
<StyledMenu ref={node as any}>
<StyledMenuButton onClick={toggle} aria-label={t`Menu`}>
<StyledMenuButton onClick={toggleMenu} aria-label={t`Menu`}>
<StyledMenuIcon />
</StyledMenuButton>
......@@ -272,6 +286,12 @@ export default function Menu() {
<div>{darkMode ? <Trans>Light Theme</Trans> : <Trans>Dark Theme</Trans>}</div>
{darkMode ? <Moon opacity={0.6} size={16} /> : <Sun opacity={0.6} size={16} />}
</ToggleMenuItem>
<ToggleMenuItem onClick={() => togglePrivacyPolicy()}>
<div>
<Trans>Legal & Privacy</Trans>
</div>
<FileText opacity={0.6} size={16} />
</ToggleMenuItem>
{showUNIClaimOption && (
<UNIbutton
onClick={openClaimModal}
......@@ -288,6 +308,8 @@ export default function Menu() {
}
})()}
</StyledMenu>
<PrivacyPolicyModal />
</>
)
}
......
import { Trans } from '@lingui/macro'
import Card, { DarkGreyCard } from 'components/Card'
import { AutoRow, RowBetween } from 'components/Row'
import { useEffect, useRef } from 'react'
import { ArrowDown, Info, X } from 'react-feather'
import ReactGA from 'react-ga'
import styled from 'styled-components/macro'
import { ExternalLink, TYPE } from 'theme'
import { isMobile } from 'utils/userAgent'
import { useModalOpen, useTogglePrivacyPolicy } from '../../state/application/hooks'
import { ApplicationModal } from '../../state/application/reducer'
import { AutoColumn } from '../Column'
import Modal from '../Modal'
const Wrapper = styled.div`
max-height: 70vh;
overflow: auto;
padding: 0 1rem;
`
const StyledExternalCard = styled(Card)`
background-color: ${({ theme }) => theme.primary5};
padding: 0.5rem;
width: 100%;
:hover,
:focus,
:active {
background-color: ${({ theme }) => theme.primary4};
}
`
const HoverText = styled.div`
text-decoration: none;
color: ${({ theme }) => theme.text1};
display: flex;
align-items: center;
:hover {
cursor: pointer;
}
`
const StyledLinkOut = styled(ArrowDown)`
transform: rotate(230deg);
`
const EXTERNAL_APIS = [
{
name: 'Auto Router',
description: <Trans>The app fetches the optimal trade route from a Uniswap Labs server.</Trans>,
},
{
name: 'Infura',
description: <Trans>The app fetches on-chain data and constructs contract calls with an Infura API.</Trans>,
},
{
name: 'TRM Labs',
description: (
<Trans>
The app securely collects your wallet address and shares it with TRM Labs Inc. for risk and compliance reasons.
</Trans>
),
},
{
name: 'Google Analytics',
description: <Trans>The app logs anonymized usage statistics in order to improve over time.</Trans>,
},
{
name: 'The Graph',
description: <Trans>The app fetches blockchain data from The Graph’s hosted service.</Trans>,
},
]
export function PrivacyPolicyModal() {
const node = useRef<HTMLDivElement>()
const open = useModalOpen(ApplicationModal.PRIVACY_POLICY)
const toggle = useTogglePrivacyPolicy()
useEffect(() => {
if (!open) return
ReactGA.event({
category: 'Modal',
action: 'Show Legal',
})
}, [open])
return (
<Modal isOpen={open} onDismiss={() => toggle()}>
<AutoColumn gap="12px" ref={node as any}>
<RowBetween padding="1rem 1rem 0.5rem 1rem">
<TYPE.mediumHeader>
<Trans>Legal & Privacy</Trans>
</TYPE.mediumHeader>
<HoverText onClick={() => toggle()}>
<X size={24} />
</HoverText>
</RowBetween>
<PrivacyPolicy />
</AutoColumn>
</Modal>
)
}
export function PrivacyPolicy() {
return (
<Wrapper
draggable="true"
onTouchMove={(e) => {
// prevent modal gesture handler from dismissing modal when content is scrolling
if (isMobile) {
e.stopPropagation()
}
}}
>
<AutoColumn gap="16px">
<AutoColumn gap="8px" style={{ width: '100%' }}>
<StyledExternalCard>
<ExternalLink href={'https://uniswap.org/terms-of-service'}>
<RowBetween>
<AutoRow gap="4px">
<Info size={20} />
<TYPE.main fontSize={14} color={'primaryText1'}>
<Trans>Uniswap Labs&apos; Terms of Service</Trans>
</TYPE.main>
</AutoRow>
<StyledLinkOut size={20} />
</RowBetween>
</ExternalLink>
</StyledExternalCard>
<StyledExternalCard>
<ExternalLink href={'https://uniswap.org/disclaimer/'}>
<RowBetween>
<AutoRow gap="4px">
<Info size={20} />
<TYPE.main fontSize={14} color={'primaryText1'}>
<Trans>Protocol Disclaimer</Trans>
</TYPE.main>
</AutoRow>
<StyledLinkOut size={20} />
</RowBetween>
</ExternalLink>
</StyledExternalCard>
</AutoColumn>
<TYPE.main fontSize={14}>
<Trans>This app uses the following third-party APIs:</Trans>
</TYPE.main>
<AutoColumn gap="12px">
{EXTERNAL_APIS.map(({ name, description }, i) => (
<DarkGreyCard key={i}>
<AutoColumn gap="8px">
<AutoRow gap="4px">
<Info size={18} />
<TYPE.main fontSize={14} color={'text1'}>
{name}
</TYPE.main>
</AutoRow>
<TYPE.main fontSize={14}>{description}</TYPE.main>
</AutoColumn>
</DarkGreyCard>
))}
<div />
</AutoColumn>
</AutoColumn>
</Wrapper>
)
}
......@@ -2,9 +2,12 @@ import { Trans } from '@lingui/macro'
import { AbstractConnector } from '@web3-react/abstract-connector'
import { UnsupportedChainIdError, useWeb3React } from '@web3-react/core'
import { WalletConnectConnector } from '@web3-react/walletconnect-connector'
import { AutoRow } from 'components/Row'
import { AutoColumn } from 'components/Column'
import { PrivacyPolicy } from 'components/PrivacyPolicy'
import Row, { AutoRow, RowBetween } from 'components/Row'
import { useMonitoringEventCallback } from 'hooks/useMonitoringEventCallback'
import { useEffect, useState } from 'react'
import { ArrowLeft } from 'react-feather'
import { ArrowLeft, ArrowRight, Info } from 'react-feather'
import ReactGA from 'react-ga'
import styled from 'styled-components/macro'
......@@ -19,7 +22,7 @@ import { ApplicationModal } from '../../state/application/reducer'
import { ExternalLink, TYPE } from '../../theme'
import { isMobile } from '../../utils/userAgent'
import AccountDetails from '../AccountDetails'
import { LightCard } from '../Card'
import Card, { LightCard } from '../Card'
import Modal from '../Modal'
import Option from './Option'
import PendingView from './PendingView'
......@@ -106,11 +109,22 @@ const HoverText = styled.div`
}
`
const LinkCard = styled(Card)`
background-color: ${({ theme }) => theme.primary1};
color: ${({ theme }) => theme.white};
:hover {
cursor: pointer;
filter: brightness(0.9);
}
`
const WALLET_VIEWS = {
OPTIONS: 'options',
OPTIONS_SECONDARY: 'options_secondary',
ACCOUNT: 'account',
PENDING: 'pending',
LEGAL: 'legal',
}
export default function WalletModal({
......@@ -126,6 +140,7 @@ export default function WalletModal({
const { active, account, connector, activate, error } = useWeb3React()
const [walletView, setWalletView] = useState(WALLET_VIEWS.ACCOUNT)
const previousWalletView = usePrevious(walletView)
const [pendingWallet, setPendingWallet] = useState<AbstractConnector | undefined>()
......@@ -136,6 +151,8 @@ export default function WalletModal({
const previousAccount = usePrevious(account)
const logMonitoringEvent = useMonitoringEventCallback()
// close on connection, when logged out before
useEffect(() => {
if (account && !previousAccount && walletModalOpen) {
......@@ -183,7 +200,12 @@ export default function WalletModal({
}
connector &&
activate(connector, undefined, true).catch((error) => {
activate(connector, undefined, true)
.then(async () => {
const walletAddress = await connector.getAccount()
logMonitoringEvent('wallet connected', { walletAddress })
})
.catch((error) => {
if (error instanceof UnsupportedChainIdError) {
activate(connector) // a little janky...can't use setError because the connector isn't set
} else {
......@@ -307,6 +329,27 @@ export default function WalletModal({
</UpperSection>
)
}
if (walletView === WALLET_VIEWS.LEGAL) {
return (
<UpperSection>
<HeaderRow>
<HoverText
onClick={() => {
setWalletView(previousWalletView ?? WALLET_VIEWS.ACCOUNT)
}}
>
<ArrowLeft />
</HoverText>
<Row justify="center">
<TYPE.mediumHeader>
<Trans>Legal & Privacy</Trans>
</TYPE.mediumHeader>
</Row>
</HeaderRow>
<PrivacyPolicy />
</UpperSection>
)
}
if (account && walletView === WALLET_VIEWS.ACCOUNT) {
return (
<AccountDetails
......@@ -337,24 +380,36 @@ export default function WalletModal({
) : (
<HeaderRow>
<HoverText>
<Trans>Connect to a wallet</Trans>
<Trans>Connect a wallet</Trans>
</HoverText>
</HeaderRow>
)}
<ContentWrapper>
<LightCard style={{ marginBottom: '16px' }}>
<AutoColumn gap="16px">
<LightCard>
<AutoRow style={{ flexWrap: 'nowrap' }}>
<TYPE.black fontSize={14}>
<Trans>
By connecting a wallet, you agree to Uniswap Labs’{' '}
<ExternalLink href="https://uniswap.org/terms-of-service/">Terms of Service</ExternalLink> and
acknowledge that you have read and understand the{' '}
<ExternalLink href="https://uniswap.org/disclaimer/">Uniswap protocol disclaimer</ExternalLink>.
acknowledge that you have read and understand the Uniswap{' '}
<ExternalLink href="https://uniswap.org/disclaimer/">Protocol Disclaimer</ExternalLink>.
</Trans>
</TYPE.black>
</AutoRow>
</LightCard>
<LinkCard padding=".5rem" $borderRadius=".75rem" onClick={() => setWalletView(WALLET_VIEWS.LEGAL)}>
<RowBetween>
<AutoRow gap="4px">
<Info size={20} />
<TYPE.white fontSize={14}>
<Trans>How this app uses APIs</Trans>
</TYPE.white>
</AutoRow>
<ArrowRight size={16} />
</RowBetween>
</LinkCard>
{walletView === WALLET_VIEWS.PENDING ? (
<PendingView
connector={pendingWallet}
......@@ -365,6 +420,7 @@ export default function WalletModal({
) : (
<OptionGrid>{getOptions()}</OptionGrid>
)}
</AutoColumn>
</ContentWrapper>
</UpperSection>
)
......
......@@ -212,7 +212,7 @@ function Web3StatusInner() {
return (
<Web3StatusConnect id="connect-wallet" onClick={toggleWalletModal} faded={!account}>
<Text>
<Trans>Connect to a wallet</Trans>
<Trans>Connect Wallet</Trans>
</Text>
</Web3StatusConnect>
)
......
import { initializeApp } from 'firebase/app'
import { getDatabase, push, ref } from 'firebase/database'
import { useCallback } from 'react'
import { useActiveWeb3React } from './web3'
type MonitoringEvent =
| 'wallet connected'
| 'swap'
| 'add liquidity/v3'
| 'add liquidity/v2'
| 'remove liquidity/v3'
| 'remove liquidity/v2'
const FIREBASE_API_KEY = process.env.REACT_APP_FIREBASE_KEY
const firebaseEnabled = typeof FIREBASE_API_KEY !== 'undefined'
initializeFirebase()
export function useMonitoringEventCallback() {
const { account, chainId } = useActiveWeb3React()
return useCallback(
async function log(
type: MonitoringEvent,
{ hash, walletAddress = account }: { hash?: string; walletAddress?: typeof account }
) {
if (!firebaseEnabled) return
const db = getDatabase()
if (!walletAddress) {
console.debug('Wallet address required to log monitoring events.')
return
}
try {
push(ref(db, 'trm'), {
chainId,
origin: location.origin,
signedTransactionHash: hash ?? 'n/a',
timestamp: Date.now(),
type,
walletAddress,
})
} catch (e) {
console.debug('Error adding document: ', e)
}
},
[account, chainId]
)
}
function initializeFirebase() {
if (!firebaseEnabled) return
initializeApp({
apiKey: process.env.REACT_APP_FIREBASE_KEY,
authDomain: 'interface-monitoring.firebaseapp.com',
databaseURL: 'https://interface-monitoring-default-rtdb.firebaseio.com',
projectId: 'interface-monitoring',
storageBucket: 'interface-monitoring.appspot.com',
messagingSenderId: '968187720053',
appId: '1:968187720053:web:acedf72dce629d470be33c',
})
}
......@@ -5,6 +5,7 @@ import { Currency, CurrencyAmount, Percent } from '@uniswap/sdk-core'
import { FeeAmount, NonfungiblePositionManager } from '@uniswap/v3-sdk'
import DowntimeWarning from 'components/DowntimeWarning'
import UnsupportedCurrencyFooter from 'components/swap/UnsupportedCurrencyFooter'
import { useMonitoringEventCallback } from 'hooks/useMonitoringEventCallback'
import { useCallback, useContext, useEffect, useState } from 'react'
import { AlertTriangle } from 'react-feather'
import ReactGA from 'react-ga'
......@@ -89,6 +90,8 @@ export default function AddLiquidity({
const addTransaction = useTransactionAdder()
const positionManager = useV3NFTPositionManagerContract()
const logMonitoringEvent = useMonitoringEventCallback()
// check for existing position if tokenId in url
const { position: existingPositionDetails, loading: positionLoading } = useV3PositionFromTokenId(
tokenId ? BigNumber.from(tokenId) : undefined
......@@ -345,6 +348,7 @@ export default function AddLiquidity({
action: 'Add',
label: [currencies[Field.CURRENCY_A]?.symbol, currencies[Field.CURRENCY_B]?.symbol].join('/'),
})
logMonitoringEvent('add liquidity/v3', { hash: response.hash })
})
})
.catch((error) => {
......
......@@ -4,6 +4,7 @@ import { Trans } from '@lingui/macro'
import { Currency, CurrencyAmount, Percent } from '@uniswap/sdk-core'
import UnsupportedCurrencyFooter from 'components/swap/UnsupportedCurrencyFooter'
import { SwitchLocaleLink } from 'components/SwitchLocaleLink'
import { useMonitoringEventCallback } from 'hooks/useMonitoringEventCallback'
import { useCallback, useContext, useState } from 'react'
import { Plus } from 'react-feather'
import ReactGA from 'react-ga'
......@@ -56,6 +57,8 @@ export default function AddLiquidity({
const { account, chainId, library } = useActiveWeb3React()
const theme = useContext(ThemeContext)
const logMonitoringEvent = useMonitoringEventCallback()
const currencyA = useCurrency(currencyIdA)
const currencyB = useCurrency(currencyIdB)
......@@ -203,6 +206,7 @@ export default function AddLiquidity({
action: 'Add',
label: [currencies[Field.CURRENCY_A]?.symbol, currencies[Field.CURRENCY_B]?.symbol].join('/'),
})
logMonitoringEvent('add liquidity/v2', { hash: response.hash })
})
)
.catch((error) => {
......
......@@ -19,6 +19,7 @@ import Toggle from 'components/Toggle'
import { SupportedChainId } from 'constants/chains'
import { useV3NFTPositionManagerContract } from 'hooks/useContract'
import useDebouncedChangeHandler from 'hooks/useDebouncedChangeHandler'
import { useMonitoringEventCallback } from 'hooks/useMonitoringEventCallback'
import useTheme from 'hooks/useTheme'
import useTransactionDeadline from 'hooks/useTransactionDeadline'
import { useV3PositionFromTokenId } from 'hooks/useV3Positions'
......@@ -68,6 +69,8 @@ function Remove({ tokenId }: { tokenId: BigNumber }) {
const theme = useTheme()
const { account, chainId, library } = useActiveWeb3React()
const logMonitoringEvent = useMonitoringEventCallback()
// flag for receiving WETH
const [receiveWETH, setReceiveWETH] = useState(false)
......@@ -152,6 +155,7 @@ function Remove({ tokenId }: { tokenId: BigNumber }) {
action: 'RemoveV3',
label: [liquidityValue0.currency.symbol, liquidityValue1.currency.symbol].join('/'),
})
logMonitoringEvent('remove liquidity/v3', { hash: response.hash })
setTxnHash(response.hash)
setAttemptingTxn(false)
addTransaction(response, {
......@@ -168,20 +172,21 @@ function Remove({ tokenId }: { tokenId: BigNumber }) {
console.error(error)
})
}, [
tokenId,
positionManager,
liquidityValue0,
liquidityValue1,
deadline,
allowedSlippage,
account,
addTransaction,
positionManager,
chainId,
feeValue0,
feeValue1,
library,
liquidityPercentage,
positionSDK,
liquidityPercentage,
library,
tokenId,
allowedSlippage,
logMonitoringEvent,
addTransaction,
])
const handleDismissConfirmation = useCallback(() => {
......
......@@ -3,6 +3,7 @@ import { Contract } from '@ethersproject/contracts'
import { TransactionResponse } from '@ethersproject/providers'
import { Trans } from '@lingui/macro'
import { Currency, Percent } from '@uniswap/sdk-core'
import { useMonitoringEventCallback } from 'hooks/useMonitoringEventCallback'
import { useCallback, useContext, useMemo, useState } from 'react'
import { ArrowDown, Plus } from 'react-feather'
import ReactGA from 'react-ga'
......@@ -57,6 +58,8 @@ export default function RemoveLiquidity({
const theme = useContext(ThemeContext)
const logMonitoringEvent = useMonitoringEventCallback()
// toggle wallet when disconnected
const toggleWalletModal = useWalletModalToggle()
......@@ -280,6 +283,7 @@ export default function RemoveLiquidity({
action: 'Remove',
label: [currencyA.symbol, currencyB.symbol].join('/'),
})
logMonitoringEvent('remove liquidity/v2', { hash: response.hash })
})
.catch((error: Error) => {
setAttemptingTxn(false)
......
......@@ -10,6 +10,7 @@ import SwapRoute from 'components/swap/SwapRoute'
import TradePrice from 'components/swap/TradePrice'
import UnsupportedCurrencyFooter from 'components/swap/UnsupportedCurrencyFooter'
import { MouseoverTooltip, MouseoverTooltipContent } from 'components/Tooltip'
import { useMonitoringEventCallback } from 'hooks/useMonitoringEventCallback'
import JSBI from 'jsbi'
import { useCallback, useContext, useEffect, useMemo, useState } from 'react'
import { ArrowDown, CheckCircle, HelpCircle, Info } from 'react-feather'
......@@ -80,6 +81,8 @@ export default function Swap({ history }: RouteComponentProps) {
const { account } = useActiveWeb3React()
const loadedUrlParams = useDefaultsFromURLSearch()
const logMonitoringEvent = useMonitoringEventCallback()
// token warning stuff
const [loadedInputCurrency, loadedOutputCurrency] = [
useCurrency(loadedUrlParams?.inputCurrencyId),
......@@ -285,6 +288,7 @@ export default function Swap({ history }: RouteComponentProps) {
'MH',
].join('/'),
})
logMonitoringEvent('swap', { hash })
})
.catch((error) => {
setSwapState({
......@@ -295,7 +299,17 @@ export default function Swap({ history }: RouteComponentProps) {
txHash: undefined,
})
})
}, [swapCallback, priceImpact, tradeToConfirm, showConfirm, recipient, recipientAddress, account, trade])
}, [
swapCallback,
priceImpact,
tradeToConfirm,
showConfirm,
recipient,
recipientAddress,
account,
trade,
logMonitoringEvent,
])
// errors
const [showInverted, setShowInverted] = useState<boolean>(false)
......
......@@ -51,6 +51,10 @@ export function useToggleVoteModal(): () => void {
return useToggleModal(ApplicationModal.VOTE)
}
export function useTogglePrivacyPolicy(): () => void {
return useToggleModal(ApplicationModal.PRIVACY_POLICY)
}
// returns a function that allows adding a popup
export function useAddPopup(): (content: PopupContent, key?: string, removeAfterMs?: number) => void {
const dispatch = useAppDispatch()
......
......@@ -18,6 +18,7 @@ export enum ApplicationModal {
VOTE,
POOL_OVERVIEW_OPTIONS,
NETWORK_SELECTOR,
PRIVACY_POLICY,
}
type PopupList = Array<{ key: string; show: boolean; content: PopupContent; removeAfterMs: number | null }>
......
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment