Merge pull request #3 from Uniswap/FoR-main

feat: FoR commits from mgtm repo
This commit is contained in:
Jordan Frankfurt 2022-12-16 11:10:30 -06:00 committed by GitHub
commit cb7132ee17
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 697 additions and 66 deletions

6
.env

@ -1,7 +1,11 @@
REACT_APP_INFURA_KEY="4bf032f2d38a4ed6bb975b80d6340847"
# These API keys are intentionally public. Please do not report them - thank you for your concern.
REACT_APP_AMPLITUDE_PROXY_URL="https://api.uniswap.org/v1/amplitude-proxy"
REACT_APP_AWS_API_REGION="us-east-2"
REACT_APP_AWS_API_ENDPOINT="https://beta.api.uniswap.org/v1/graphql"
REACT_APP_TEMP_API_URL="https://temp.api.uniswap.org/v1"
REACT_APP_SENTRY_DSN="https://a3c62e400b8748b5a8d007150e2f38b7@o1037921.ingest.sentry.io/4504255148851200"
ESLINT_NO_DEV_ERRORS=true
REACT_APP_INFURA_KEY="4bf032f2d38a4ed6bb975b80d6340847"
REACT_APP_MOONPAY_API="https://api.moonpay.com"
REACT_APP_MOONPAY_LINK="https://us-central1-uniswap-mobile.cloudfunctions.net/signMoonpayLinkStaging?platform=web"
REACT_APP_MOONPAY_PUBLISHABLE_KEY="pk_test_DycfESRid31UaSxhI5yWKe1r5E5kKSz"

@ -1,7 +1,10 @@
REACT_APP_AMPLITUDE_PROXY_URL="https://api.uniswap.org/v1/amplitude-proxy"
REACT_APP_INFURA_KEY="099fc58e0de9451d80b18d7c74caa7c1"
REACT_APP_AWS_API_ENDPOINT="https://api.uniswap.org/v1/graphql"
REACT_APP_FORTMATIC_KEY="pk_live_F937DF033A1666BF"
REACT_APP_GOOGLE_ANALYTICS_ID="G-KDP9B6W4H8"
REACT_APP_INFURA_KEY="099fc58e0de9451d80b18d7c74caa7c1"
REACT_APP_MOONPAY_API="https://api.moonpay.com"
REACT_APP_MOONPAY_LINK="https://us-central1-uniswap-mobile.cloudfunctions.net/signMoonpayLink?platform=web"
REACT_APP_MOONPAY_PUBLISHABLE_KEY="pk_live_uQG4BJC4w3cxnqpcSqAfohdBFDTsY6E"
REACT_APP_FIREBASE_KEY="AIzaSyBcZWwTcTJHj_R6ipZcrJkXdq05PuX0Rs0"
REACT_APP_AWS_API_ENDPOINT="https://api.uniswap.org/v1/graphql"
THE_GRAPH_SCHEMA_ENDPOINT="https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3"

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 3.5 MiB

@ -0,0 +1,13 @@
import { SpinnerSVG } from 'theme'
const ButtonLoadingSpinner = (props: React.ComponentPropsWithoutRef<'svg'>) => (
<SpinnerSVG width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
opacity="0.1"
d="M18.8334 10.0003C18.8334 14.6027 15.1025 18.3337 10.5001 18.3337C5.89771 18.3337 2.16675 14.6027 2.16675 10.0003C2.16675 5.39795 5.89771 1.66699 10.5001 1.66699C15.1025 1.66699 18.8334 5.39795 18.8334 10.0003ZM4.66675 10.0003C4.66675 13.222 7.27842 15.8337 10.5001 15.8337C13.7217 15.8337 16.3334 13.222 16.3334 10.0003C16.3334 6.77867 13.7217 4.16699 10.5001 4.16699C7.27842 4.16699 4.66675 6.77867 4.66675 10.0003Z"
/>
<path d="M17.5834 10.0003C18.2738 10.0003 18.843 9.4376 18.7398 8.755C18.6392 8.0891 18.458 7.43633 18.1991 6.8113C17.7803 5.80025 17.1665 4.88159 16.3926 4.10777C15.6188 3.33395 14.7002 2.72012 13.6891 2.30133C13.0641 2.04243 12.4113 1.86121 11.7454 1.76057C11.0628 1.6574 10.5001 2.22664 10.5001 2.91699C10.5001 3.60735 11.066 4.15361 11.7405 4.30041C12.0789 4.37406 12.4109 4.47786 12.7324 4.61103C13.4401 4.90418 14.0832 5.33386 14.6249 5.87554C15.1665 6.41721 15.5962 7.06027 15.8894 7.76801C16.0225 8.08949 16.1264 8.42147 16.2 8.75986C16.3468 9.43443 16.8931 10.0003 17.5834 10.0003Z" />
</SpinnerSVG>
)
export default ButtonLoadingSpinner

@ -5,6 +5,8 @@ import styled, { DefaultTheme, useTheme } from 'styled-components/macro'
import { RowBetween } from '../Row'
export { default as LoadingButtonSpinner } from './LoadingButtonSpinner'
type ButtonProps = Omit<ButtonPropsOriginal, 'css'>
export const BaseButton = styled(RebassButton)<
@ -362,18 +364,6 @@ export function ButtonRadioChecked({ active = false, children, ...rest }: { acti
}
}
const ButtonOverlay = styled.div`
background-color: transparent;
bottom: 0;
border-radius: 16px;
height: 100%;
left: 0;
position: absolute;
right: 0;
top: 0;
transition: 150ms ease background-color;
width: 100%;
`
export enum ButtonSize {
small,
medium,
@ -466,7 +456,18 @@ function pickThemeButtonTextColor({ theme, emphasis }: { theme: DefaultTheme; em
return theme.textPrimary
}
}
const ButtonOverlay = styled.div`
background-color: transparent;
bottom: 0;
border-radius: inherit;
height: 100%;
left: 0;
position: absolute;
right: 0;
top: 0;
transition: 150ms ease background-color;
width: 100%;
`
const BaseThemeButton = styled.button<BaseButtonProps>`
align-items: center;
background-color: ${pickThemeButtonBackgroundColor};
@ -484,16 +485,13 @@ const BaseThemeButton = styled.button<BaseButtonProps>`
padding: ${pickThemeButtonPadding};
position: relative;
transition: 150ms ease opacity;
user-select: none;
:active {
${ButtonOverlay} {
background-color: ${({ theme }) => theme.stateOverlayPressed};
}
}
:disabled {
cursor: default;
opacity: 0.6;
}
:focus {
${ButtonOverlay} {
background-color: ${({ theme }) => theme.stateOverlayPressed};
@ -504,6 +502,17 @@ const BaseThemeButton = styled.button<BaseButtonProps>`
background-color: ${({ theme }) => theme.stateOverlayHover};
}
}
:disabled {
cursor: default;
opacity: 0.6;
}
:disabled:active,
:disabled:focus,
:disabled:hover {
${ButtonOverlay} {
background-color: transparent;
}
}
`
interface ThemeButtonProps extends React.ComponentPropsWithoutRef<'button'>, BaseButtonProps {}

@ -1,4 +1,5 @@
import { BaseVariant, FeatureFlag, featureFlagSettings, useUpdateFlag } from 'featureFlags'
import { useFiatOnrampFlag } from 'featureFlags/flags/fiatOnramp'
import { Permit2Variant, usePermit2Flag } from 'featureFlags/flags/permit2'
import { TraceJsonRpcVariant, useTraceJsonRpcFlag } from 'featureFlags/flags/traceJsonRpc'
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
@ -208,6 +209,12 @@ export default function FeatureFlagModal() {
featureFlag={FeatureFlag.permit2}
label="Permit 2 / Universal Router"
/>
<FeatureFlagOption
variant={BaseVariant}
value={useFiatOnrampFlag()}
featureFlag={FeatureFlag.fiatOnramp}
label="Fiat on-ramp"
/>
<FeatureFlagGroup name="Debug">
<FeatureFlagOption
variant={TraceJsonRpcVariant}

@ -0,0 +1,148 @@
import { Trans } from '@lingui/macro'
import { useWeb3React } from '@web3-react/core'
import fiatMaskUrl from 'assets/svg/fiat_mask.svg'
import { BaseVariant } from 'featureFlags'
import { useFiatOnrampFlag } from 'featureFlags/flags/fiatOnramp'
import { useCallback, useEffect, useState } from 'react'
import { X } from 'react-feather'
import { useToggleWalletDropdown } from 'state/application/hooks'
import { useAppSelector } from 'state/hooks'
import { useFiatOnrampAck } from 'state/user/hooks'
import styled from 'styled-components/macro'
import { ThemedText } from 'theme'
import { isMobile } from 'utils/userAgent'
const Arrow = styled.div`
top: -4px;
height: 16px;
position: absolute;
right: 16px;
width: 16px;
::before {
background: hsl(315.75, 93%, 83%);
border-top: none;
border-left: none;
box-sizing: border-box;
content: '';
height: 16px;
position: absolute;
transform: rotate(45deg);
width: 16px;
}
`
const ArrowWrapper = styled.div`
position: absolute;
right: 16px;
top: 90%;
width: 100%;
max-width: 320px;
min-height: 92px;
@media screen and (min-width: ${({ theme }) => theme.breakpoint.lg}px) {
right: 36px;
}
`
const CloseIcon = styled(X)`
color: white;
cursor: pointer;
position: absolute;
top: 8px;
right: 8px;
z-index: 1;
`
const Wrapper = styled.button`
background: radial-gradient(105% 250% at 100% 5%, hsla(318, 95%, 85%) 1%, hsla(331, 80%, 75%, 0.1) 84%),
linear-gradient(180deg, hsla(296, 92%, 67%, 0.5) 0%, hsla(313, 96%, 60%, 0.5) 130%);
background-color: hsla(297, 93%, 68%, 1);
border-radius: 12px;
border: none;
cursor: pointer;
outline: none;
overflow: hidden;
position: relative;
text-align: start;
max-width: 320px;
min-height: 92px;
width: 100%;
:before {
background-image: url(${fiatMaskUrl});
background-repeat: no-repeat;
content: '';
height: 100%;
position: absolute;
right: -154px; // roughly width of fiat mask image
top: 0;
width: 100%;
}
`
const Header = styled(ThemedText.SubHeader)`
color: white;
margin: 0;
padding: 12px 12px 4px;
position: relative;
`
const Body = styled(ThemedText.BodySmall)`
color: white;
margin: 0 12px 12px 12px !important;
position: relative;
`
const ANNOUNCEMENT_RENDERED = 'FiatOnrampAnnouncement-rendered'
const ANNOUNCEMENT_DISMISSED = 'FiatOnrampAnnouncement-dismissed'
const MAX_RENDER_COUNT = 3
export function FiatOnrampAnnouncement() {
const { account } = useWeb3React()
const [acks, acknowledge] = useFiatOnrampAck()
const [locallyDismissed, setLocallyDismissed] = useState(false)
useEffect(() => {
if (!sessionStorage.getItem(ANNOUNCEMENT_RENDERED)) {
acknowledge({ renderCount: acks.renderCount + 1 })
sessionStorage.setItem(ANNOUNCEMENT_RENDERED, 'true')
}
}, [acknowledge, acks.renderCount])
const handleClose = useCallback(() => {
setLocallyDismissed(true)
sessionStorage.setItem(ANNOUNCEMENT_DISMISSED, 'true')
}, [])
const toggleWalletDropdown = useToggleWalletDropdown()
const handleClick = useCallback(() => {
toggleWalletDropdown()
acknowledge({ user: true })
}, [acknowledge, toggleWalletDropdown])
const fiatOnrampFlag = useFiatOnrampFlag()
const openModal = useAppSelector((state) => state.application.openModal)
if (
!account ||
acks?.user ||
fiatOnrampFlag === BaseVariant.Control ||
locallyDismissed ||
sessionStorage.getItem(ANNOUNCEMENT_DISMISSED) ||
acks.renderCount >= MAX_RENDER_COUNT ||
isMobile ||
openModal !== null
) {
return null
}
return (
<ArrowWrapper>
<Arrow />
<CloseIcon onClick={handleClose} />
<Wrapper onClick={handleClick}>
<Header>
<Trans>Buy crypto</Trans>
</Header>
<Body>
<Trans>Get tokens at the best prices in web3 on Uniswap, powered by Moonpay.</Trans>
</Body>
</Wrapper>
</ArrowWrapper>
)
}

@ -0,0 +1,143 @@
import { Trans } from '@lingui/macro'
import { useWeb3React } from '@web3-react/core'
import { useCallback, useEffect, useState } from 'react'
import { useCloseModal, useModalIsOpen } from 'state/application/hooks'
import { ApplicationModal } from 'state/application/reducer'
import styled, { useTheme } from 'styled-components/macro'
import { CustomLightSpinner, ThemedText } from 'theme'
import Circle from '../../assets/images/blue-loader.svg'
import Modal from '../Modal'
const Wrapper = styled.div`
background-color: ${({ theme }) => theme.backgroundSurface};
border-radius: 20px;
box-shadow: ${({ theme }) => theme.deepShadow};
display: flex;
flex-flow: column nowrap;
margin: 0;
min-height: 720px;
min-width: 375px;
position: relative;
width: 100%;
`
const ErrorText = styled(ThemedText.BodyPrimary)`
color: ${({ theme }) => theme.accentFailure};
margin: auto !important;
text-align: center;
width: 90%;
`
const StyledIframe = styled.iframe`
background-color: ${({ theme }) => theme.white};
border-radius: 12px;
bottom: 0;
left: 0;
height: calc(100% - 16px);
margin: 8px;
padding: 0;
position: absolute;
right: 0;
top: 0;
width: calc(100% - 16px);
`
const StyledSpinner = styled(CustomLightSpinner)`
bottom: 0;
left: 0;
margin: auto;
position: absolute;
right: 0;
top: 0;
`
const MOONPAY_SUPPORTED_CURRENCY_CODES = [
'eth',
'eth_arbitrum',
'eth_optimism',
'eth_polygon',
'weth',
'wbtc',
'matic_polygon',
'polygon',
'usdc_arbitrum',
'usdc_optimism',
'usdc_polygon',
]
export default function FiatOnrampModal() {
const { account } = useWeb3React()
const theme = useTheme()
const closeModal = useCloseModal(ApplicationModal.FIAT_ONRAMP)
const fiatOnrampModalOpen = useModalIsOpen(ApplicationModal.FIAT_ONRAMP)
const [signedIframeUrl, setSignedIframeUrl] = useState<string | null>(null)
const [error, setError] = useState<string | null>(null)
const [loading, setLoading] = useState(false)
const fetchSignedIframeUrl = useCallback(async () => {
if (!account) {
setError('Please connect an account before making a purchase.')
return
}
setLoading(true)
setError(null)
try {
const signedIframeUrlFetchEndpoint = process.env.REACT_APP_MOONPAY_LINK as string
const res = await fetch(signedIframeUrlFetchEndpoint, {
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
method: 'POST',
body: JSON.stringify({
colorCode: theme.accentAction,
defaultCurrencyCode: 'eth',
redirectUrl: 'https://app.uniswap.org/#/swap',
walletAddresses: JSON.stringify(
MOONPAY_SUPPORTED_CURRENCY_CODES.reduce(
(acc, currencyCode) => ({
...acc,
[currencyCode]: account,
}),
{}
)
),
}),
})
const { url } = await res.json()
setSignedIframeUrl(url)
} catch (e) {
console.log('there was an error fetching the link', e)
setError(e.toString())
} finally {
setLoading(false)
}
}, [account, theme.accentAction])
useEffect(() => {
fetchSignedIframeUrl()
}, [fetchSignedIframeUrl])
return (
<Modal isOpen={fiatOnrampModalOpen} onDismiss={closeModal} maxHeight={720}>
<Wrapper data-testid="fiat-onramp-modal">
{error ? (
<>
<ThemedText.MediumHeader>
<Trans>Moonpay Fiat On-ramp iframe</Trans>
</ThemedText.MediumHeader>
<ErrorText>
<Trans>something went wrong!</Trans>
<br />
{error}
</ErrorText>
</>
) : loading ? (
<StyledSpinner src={Circle} alt="loading spinner" size="90px" />
) : (
<StyledIframe src={signedIframeUrl ?? ''} frameBorder="0" title="fiat-onramp-iframe" />
)}
</Wrapper>
</Modal>
)
}

@ -9,7 +9,7 @@ import { isMobile } from '../../utils/userAgent'
const AnimatedDialogOverlay = animated(DialogOverlay)
const StyledDialogOverlay = styled(AnimatedDialogOverlay)<{ scrollOverlay?: boolean }>`
const StyledDialogOverlay = styled(AnimatedDialogOverlay)<{ $scrollOverlay?: boolean }>`
&[data-reach-dialog-overlay] {
z-index: ${Z_INDEX.modalBackdrop};
background-color: transparent;
@ -17,7 +17,7 @@ const StyledDialogOverlay = styled(AnimatedDialogOverlay)<{ scrollOverlay?: bool
display: flex;
align-items: center;
overflow-y: ${({ scrollOverlay }) => scrollOverlay && 'scroll'};
overflow-y: ${({ $scrollOverlay }) => $scrollOverlay && 'scroll'};
justify-content: center;
background-color: ${({ theme }) => theme.backgroundScrim};
@ -89,7 +89,7 @@ interface ModalProps {
maxWidth?: number
initialFocusRef?: React.RefObject<any>
children?: React.ReactNode
scrollOverlay?: boolean
$scrollOverlay?: boolean
hideBorder?: boolean
isBottomSheet?: boolean
}
@ -103,7 +103,7 @@ export default function Modal({
initialFocusRef,
children,
onSwipe = onDismiss,
scrollOverlay,
$scrollOverlay,
isBottomSheet = isMobile,
hideBorder = false,
}: ModalProps) {
@ -136,7 +136,7 @@ export default function Modal({
onDismiss={onDismiss}
initialFocusRef={initialFocusRef}
unstable_lockFocusAcrossFrames={false}
scrollOverlay={scrollOverlay}
$scrollOverlay={$scrollOverlay}
>
<StyledDialogContent
{...(isMobile
@ -149,7 +149,7 @@ export default function Modal({
$minHeight={minHeight}
$maxHeight={maxHeight}
$isBottomSheet={isBottomSheet}
$scrollOverlay={scrollOverlay}
$scrollOverlay={$scrollOverlay}
$hideBorder={hideBorder}
$maxWidth={maxWidth}
>

@ -16,6 +16,7 @@ import { body, bodySmall } from 'nft/css/common.css'
import { themeVars } from 'nft/css/sprinkles.css'
import { ReactNode, useReducer, useRef } from 'react'
import { NavLink, NavLinkProps } from 'react-router-dom'
import styled from 'styled-components/macro'
import { isDevelopmentEnv, isStagingEnv } from 'utils/env'
import { useToggleModal } from '../../state/application/hooks'
@ -50,8 +51,13 @@ const PrimaryMenuRow = ({
)
}
const StyledBox = styled(Box)`
align-items: center;
display: flex;
justify-content: center;
`
const PrimaryMenuRowText = ({ children }: { children: ReactNode }) => {
return <Box className={`${styles.PrimaryText} ${body}`}>{children}</Box>
return <StyledBox className={`${styles.PrimaryText} ${body}`}>{children}</StyledBox>
}
PrimaryMenuRow.Text = PrimaryMenuRowText
@ -115,7 +121,6 @@ export const MenuDropdown = () => {
const [isOpen, toggleOpen] = useReducer((s) => !s, false)
const togglePrivacyPolicy = useToggleModal(ApplicationModal.PRIVACY_POLICY)
const openFeatureFlagsModal = useToggleModal(ApplicationModal.FEATURE_FLAGS)
const ref = useRef<HTMLDivElement>(null)
useOnClickOutside(ref, isOpen ? toggleOpen : undefined)

@ -1,6 +1,9 @@
import { useWeb3React } from '@web3-react/core'
import AddressClaimModal from 'components/claim/AddressClaimModal'
import ConnectedAccountBlocked from 'components/ConnectedAccountBlocked'
import FiatOnrampModal from 'components/FiatOnrampModal'
import { BaseVariant } from 'featureFlags'
import { useFiatOnrampFlag } from 'featureFlags/flags/fiatOnramp'
import useAccountRiskCheck from 'hooks/useAccountRiskCheck'
import NftExploreBanner from 'nft/components/nftExploreBanner/NftExploreBanner'
import { lazy } from 'react'
@ -18,20 +21,23 @@ export default function TopLevelModals() {
const blockedAccountModalOpen = useModalIsOpen(ApplicationModal.BLOCKED_ACCOUNT)
const { account } = useWeb3React()
const location = useLocation()
const fiatOnrampFlagEnabled = useFiatOnrampFlag() === BaseVariant.Enabled
const pageShowsNftPromoBanner =
location.pathname.startsWith('/swap') ||
!fiatOnrampFlagEnabled &&
(location.pathname.startsWith('/swap') ||
location.pathname.startsWith('/tokens') ||
location.pathname.startsWith('/pool')
location.pathname.startsWith('/pool'))
useAccountRiskCheck(account)
const open = Boolean(blockedAccountModalOpen && account)
const accountBlocked = Boolean(blockedAccountModalOpen && account)
return (
<>
<AddressClaimModal isOpen={addressClaimOpen} onDismiss={addressClaimToggle} />
<ConnectedAccountBlocked account={account} isOpen={open} />
<ConnectedAccountBlocked account={account} isOpen={accountBlocked} />
<Bag />
<TransactionCompleteModal />
<AirdropModal />
{pageShowsNftPromoBanner && <NftExploreBanner />}
{fiatOnrampFlagEnabled && <FiatOnrampModal />}
</>
)
}

@ -348,7 +348,7 @@ export default function TransactionConfirmationModal({
// confirmation screen
return (
<Modal isOpen={isOpen} scrollOverlay={true} onDismiss={onDismiss} maxHeight={90}>
<Modal isOpen={isOpen} $scrollOverlay={true} onDismiss={onDismiss} maxHeight={90}>
{isL2ChainId(chainId) && (hash || attemptingTxn) ? (
<L2Content chainId={chainId} hash={hash} onDismiss={onDismiss} pendingText={pendingText} />
) : attemptingTxn ? (

@ -1,33 +1,69 @@
import { Trans } from '@lingui/macro'
import { formatUSDPrice } from '@uniswap/conedison/format'
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import { ButtonEmphasis, ButtonSize, LoadingButtonSpinner, ThemeButton } from 'components/Button'
import Tooltip from 'components/Tooltip'
import { getConnection } from 'connection/utils'
import { getChainInfoOrDefault } from 'constants/chainInfo'
import { SupportedChainId } from 'constants/chains'
import { BaseVariant } from 'featureFlags'
import { useFiatOnrampFlag } from 'featureFlags/flags/fiatOnramp'
import useCopyClipboard from 'hooks/useCopyClipboard'
import useStablecoinPrice from 'hooks/useStablecoinPrice'
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
import ms from 'ms.macro'
import { useProfilePageState, useSellAsset, useWalletCollections } from 'nft/hooks'
import { useIsNftClaimAvailable } from 'nft/hooks/useIsNftClaimAvailable'
import { ProfilePageStateType } from 'nft/types'
import { useCallback, useMemo } from 'react'
import { Copy, ExternalLink, Power } from 'react-feather'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { Copy, CreditCard, ExternalLink as ExternalLinkIcon, Info, Power } from 'react-feather'
import { useNavigate } from 'react-router-dom'
import { Text } from 'rebass'
import { useCurrencyBalanceString } from 'state/connection/hooks'
import { useAppDispatch } from 'state/hooks'
import { useFiatOnrampAck } from 'state/user/hooks'
import { updateSelectedWallet } from 'state/user/reducer'
import styled, { css } from 'styled-components/macro'
import { ThemedText } from 'theme'
import styled, { css, keyframes } from 'styled-components/macro'
import { ExternalLink, ThemedText } from 'theme'
import { shortenAddress } from '../../nft/utils/address'
import { useCloseModal, useToggleModal } from '../../state/application/hooks'
import { useCloseModal, useFiatOnrampAvailability, useOpenModal, useToggleModal } from '../../state/application/hooks'
import { ApplicationModal } from '../../state/application/reducer'
import { useUserHasAvailableClaim, useUserUnclaimedAmount } from '../../state/claim/hooks'
import { ButtonEmphasis, ButtonSize, ThemeButton } from '../Button'
import StatusIcon from '../Identicon/StatusIcon'
import IconButton, { IconHoverText } from './IconButton'
const BuyCryptoButtonBorderKeyframes = keyframes`
0% {
border-color: transparent;
}
33% {
border-color: hsla(225, 95%, 63%, 1);
}
66% {
border-color: hsla(267, 95%, 63%, 1);
}
100% {
border-color: transparent;
}
`
const BuyCryptoButton = styled(ThemeButton)<{ $animateBorder: boolean }>`
border-color: transparent;
border-radius: 12px;
border-style: solid;
border-width: 1px;
height: 40px;
margin-top: 12px;
animation-direction: alternate;
animation-duration: ${({ theme }) => theme.transition.duration.slow};
animation-fill-mode: none;
animation-iteration-count: 2;
animation-name: ${BuyCryptoButtonBorderKeyframes};
animation-play-state: ${({ $animateBorder }) => ($animateBorder ? 'running' : 'paused')};
animation-timing-function: ${({ theme }) => theme.transition.timing.inOut};
`
const WalletButton = styled(ThemeButton)`
border-radius: 12px;
padding-top: 10px;
@ -75,7 +111,20 @@ const USDText = styled.div`
color: ${({ theme }) => theme.textSecondary};
margin-top: 8px;
`
const FiatOnrampNotAvailableText = styled(ThemedText.Caption)`
align-items: center;
color: ${({ theme }) => theme.textSecondary};
display: flex;
justify-content: center;
`
const FiatOnrampAvailabilityExternalLink = styled(ExternalLink)`
align-items: center;
display: flex;
height: 14px;
justify-content: center;
margin-left: 6px;
width: 14px;
`
const FlexContainer = styled.div`
display: flex;
`
@ -108,7 +157,14 @@ const AccountContainer = styled(ThemedText.BodySmall)`
color: ${({ theme }) => theme.textSecondary};
margin-top: 2.5px;
`
const StyledInfoIcon = styled(Info)`
height: 12px;
width: 12px;
flex: 1 1 auto;
`
const StyledLoadingButtonSpinner = styled(LoadingButtonSpinner)`
fill: ${({ theme }) => theme.accentAction};
`
const BalanceWrapper = styled.div`
padding: 16px 0;
`
@ -137,7 +193,6 @@ const AuthenticatedHeader = () => {
} = getChainInfoOrDefault(chainId ? chainId : SupportedChainId.MAINNET)
const navigate = useNavigate()
const closeModal = useCloseModal(ApplicationModal.WALLET_DROPDOWN)
const setSellPageState = useProfilePageState((state) => state.setProfilePageState)
const resetSellAssets = useSellAsset((state) => state.reset)
const clearCollectionFilters = useWalletCollections((state) => state.clearCollectionFilters)
@ -147,7 +202,7 @@ const AuthenticatedHeader = () => {
const isUnclaimed = useUserHasAvailableClaim(account)
const connectionType = getConnection(connector).type
const nativeCurrency = useNativeCurrency()
const nativeCurrencyPrice = useStablecoinPrice(nativeCurrency ?? undefined) || 0
const nativeCurrencyPrice = useStablecoinPrice(nativeCurrency ?? undefined)
const openClaimModal = useToggleModal(ApplicationModal.ADDRESS_CLAIM)
const openNftModal = useToggleModal(ApplicationModal.UNISWAP_NFT_AIRDROP_CLAIM)
const disconnect = useCallback(() => {
@ -159,18 +214,64 @@ const AuthenticatedHeader = () => {
}, [connector, dispatch])
const amountUSD = useMemo(() => {
if (!nativeCurrencyPrice || !balanceString) return undefined
const price = parseFloat(nativeCurrencyPrice.toFixed(5))
const balance = parseFloat(balanceString || '0')
const balance = parseFloat(balanceString)
return price * balance
}, [balanceString, nativeCurrencyPrice])
const navigateToProfile = () => {
const navigateToProfile = useCallback(() => {
resetSellAssets()
setSellPageState(ProfilePageStateType.VIEWING)
clearCollectionFilters()
navigate('/nfts/profile')
closeModal()
}, [clearCollectionFilters, closeModal, navigate, resetSellAssets, setSellPageState])
const fiatOnrampFlag = useFiatOnrampFlag()
// animate the border of the buy crypto button when a user navigates here from the feature announcement
// can be removed when components/FiatOnrampAnnouncment.tsx is no longer used
const [acknowledgements, acknowledge] = useFiatOnrampAck()
const animateBuyCryptoButtonBorder = acknowledgements?.user && !acknowledgements.system
useEffect(() => {
let stale = false
let timeoutId = 0
if (animateBuyCryptoButtonBorder) {
timeoutId = setTimeout(() => {
if (stale) return
acknowledge({ system: true })
}, ms`2 seconds`) as unknown as number
// as unknown as number is necessary so it's not incorrectly typed as a NodeJS.Timeout
}
return () => {
stale = true
clearTimeout(timeoutId)
}
}, [acknowledge, animateBuyCryptoButtonBorder])
const openFiatOnrampModal = useOpenModal(ApplicationModal.FIAT_ONRAMP)
const [shouldCheck, setShouldCheck] = useState(false)
const {
available: fiatOnrampAvailable,
availabilityChecked: fiatOnrampAvailabilityChecked,
error,
loading: fiatOnrampAvailabilityLoading,
} = useFiatOnrampAvailability(shouldCheck, openFiatOnrampModal)
const handleBuyCryptoClick = useCallback(() => {
if (!fiatOnrampAvailabilityChecked) {
setShouldCheck(true)
} else if (fiatOnrampAvailable) {
openFiatOnrampModal()
}
}, [fiatOnrampAvailabilityChecked, fiatOnrampAvailable, openFiatOnrampModal])
const disableBuyCryptoButton = Boolean(
error || (!fiatOnrampAvailable && fiatOnrampAvailabilityChecked) || fiatOnrampAvailabilityLoading
)
const [showFiatOnrampUnavailableTooltip, setShow] = useState<boolean>(false)
const openFiatOnrampUnavailableTooltip = useCallback(() => setShow(true), [setShow])
const closeFiatOnrampUnavailableTooltip = useCallback(() => setShow(false), [setShow])
return (
<AuthenticatedHeaderWrapper>
@ -192,7 +293,7 @@ const AuthenticatedHeader = () => {
<IconButton onClick={copy} Icon={Copy}>
{isCopied ? <Trans>Copied!</Trans> : <Trans>Copy</Trans>}
</IconButton>
<IconButton href={`${explorer}address/${account}`} target="_blank" Icon={ExternalLink}>
<IconButton href={`${explorer}address/${account}`} target="_blank" Icon={ExternalLinkIcon}>
<Trans>Explore</Trans>
</IconButton>
<IconButton data-testid="wallet-disconnect" onClick={disconnect} Icon={Power}>
@ -205,7 +306,7 @@ const AuthenticatedHeader = () => {
<Text fontSize={36} fontWeight={400}>
{balanceString} {nativeCurrencySymbol}
</Text>
<USDText>${amountUSD.toFixed(2)} USD</USDText>
{amountUSD !== undefined && <USDText>{formatUSDPrice(amountUSD)} USD</USDText>}
</BalanceWrapper>
<ProfileButton
data-testid="nft-view-self-nfts"
@ -215,6 +316,44 @@ const AuthenticatedHeader = () => {
>
<Trans>View and sell NFTs</Trans>
</ProfileButton>
{fiatOnrampFlag === BaseVariant.Enabled && (
<>
<BuyCryptoButton
$animateBorder={animateBuyCryptoButtonBorder}
size={ButtonSize.medium}
emphasis={ButtonEmphasis.medium}
onClick={handleBuyCryptoClick}
disabled={disableBuyCryptoButton}
>
{error ? (
<ThemedText.BodyPrimary>{error}</ThemedText.BodyPrimary>
) : (
<>
{fiatOnrampAvailabilityLoading ? <StyledLoadingButtonSpinner /> : <CreditCard />}{' '}
<Trans>Buy crypto</Trans>
</>
)}
</BuyCryptoButton>
{Boolean(!fiatOnrampAvailable && fiatOnrampAvailabilityChecked) && (
<FiatOnrampNotAvailableText marginTop="8px">
<Trans>Not available in your region</Trans>
<Tooltip
show={showFiatOnrampUnavailableTooltip}
text={<Trans>Moonpay is not available in some regions. Click to learn more.</Trans>}
>
<FiatOnrampAvailabilityExternalLink
onMouseEnter={openFiatOnrampUnavailableTooltip}
onMouseLeave={closeFiatOnrampUnavailableTooltip}
style={{ color: 'inherit' }}
href="https://support.uniswap.org/hc/en-us/articles/10966551707533-Why-is-MoonPay-not-supported-in-my-region-"
>
<StyledInfoIcon />
</FiatOnrampAvailabilityExternalLink>
</Tooltip>
</FiatOnrampNotAvailableText>
)}
</>
)}
{isUnclaimed && (
<UNIButton onClick={openClaimModal} size={ButtonSize.medium} emphasis={ButtonEmphasis.medium}>
<Trans>Claim</Trans> {unclaimedAmount?.toFixed(0, { groupSeparator: ',' } ?? '-')} <Trans>reward</Trans>

@ -2,6 +2,7 @@ import { Trans } from '@lingui/macro'
import { TraceEvent } from '@uniswap/analytics'
import { BrowserEvent, ElementName, EventName } from '@uniswap/analytics-events'
import { useWeb3React } from '@web3-react/core'
import { FiatOnrampAnnouncement } from 'components/FiatOnrampAnnouncement'
import { IconWrapper } from 'components/Identicon/StatusIcon'
import WalletDropdown from 'components/WalletDropdown'
import { getConnection } from 'connection/utils'
@ -312,6 +313,7 @@ export default function Web3Status() {
return (
<span ref={ref}>
<Web3StatusInner />
<FiatOnrampAnnouncement />
<WalletModal ENSName={ENSName ?? undefined} pendingTransactions={pending} confirmedTransactions={confirmed} />
<Portal>
<span ref={walletRef}>

@ -1,4 +1,5 @@
export enum FeatureFlag {
fiatOnramp = 'fiatOnramp',
traceJsonRpc = 'traceJsonRpc',
permit2 = 'permit2',
}

@ -0,0 +1,5 @@
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
export function useFiatOnrampFlag(): BaseVariant {
return useBaseFlag(FeatureFlag.fiatOnramp)
}

@ -111,7 +111,7 @@ const HeaderWrapper = styled.div<{ transparent?: boolean }>`
justify-content: space-between;
position: fixed;
top: 0;
z-index: ${Z_INDEX.sticky};
z-index: ${Z_INDEX.dropdown};
`
const Marginer = styled.div`

@ -1,15 +1,85 @@
import { DEFAULT_TXN_DISMISS_MS } from 'constants/misc'
import { useCallback, useMemo } from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useAppDispatch, useAppSelector } from 'state/hooks'
import { AppState } from '../index'
import { addPopup, ApplicationModal, PopupContent, removePopup, setOpenModal } from './reducer'
import {
addPopup,
ApplicationModal,
PopupContent,
removePopup,
setFiatOnrampAvailability,
setOpenModal,
} from './reducer'
export function useModalIsOpen(modal: ApplicationModal): boolean {
const openModal = useAppSelector((state: AppState) => state.application.openModal)
return openModal === modal
}
/** @ref https://dashboard.moonpay.com/api_reference/client_side_api#ip_addresses */
interface MoonpayIPAddressesResponse {
alpha3?: string
isAllowed?: boolean
isBuyAllowed?: boolean
isSellAllowed?: boolean
}
async function getMoonpayAvailability(): Promise<boolean> {
const moonpayPublishableKey = process.env.REACT_APP_MOONPAY_PUBLISHABLE_KEY
if (!moonpayPublishableKey) {
throw new Error('Must provide a publishable key for moonpay.')
}
const moonpayApiURI = process.env.REACT_APP_MOONPAY_API
if (!moonpayApiURI) {
throw new Error('Must provide an api endpoint for moonpay.')
}
const res = await fetch(`${moonpayApiURI}/v4/ip_address?apiKey=${moonpayPublishableKey}`)
const data = await (res.json() as Promise<MoonpayIPAddressesResponse>)
return data.isBuyAllowed ?? false
}
export function useFiatOnrampAvailability(shouldCheck: boolean, callback?: () => void) {
const dispatch = useAppDispatch()
const { available, availabilityChecked } = useAppSelector((state: AppState) => state.application.fiatOnramp)
const [error, setError] = useState<string | null>(null)
const [loading, setLoading] = useState(false)
useEffect(() => {
async function checkAvailability() {
setError(null)
setLoading(true)
try {
const result = await getMoonpayAvailability()
if (stale) return
dispatch(setFiatOnrampAvailability(result))
if (result && callback) {
callback()
}
} catch (e) {
console.error('Error checking onramp availability', e.toString())
if (stale) return
setError('Error, try again later.')
dispatch(setFiatOnrampAvailability(false))
} finally {
if (stale) return
setLoading(false)
}
}
if (!availabilityChecked && shouldCheck) {
checkAvailability()
}
let stale = false
return () => {
stale = true
}
}, [availabilityChecked, callback, dispatch, shouldCheck])
return { available, availabilityChecked, loading, error }
}
export function useToggleModal(modal: ApplicationModal): () => void {
const isOpen = useModalIsOpen(modal)
const dispatch = useAppDispatch()
@ -21,6 +91,11 @@ export function useCloseModal(_modal: ApplicationModal): () => void {
return useCallback(() => dispatch(setOpenModal(null)), [dispatch])
}
export function useOpenModal(modal: ApplicationModal): () => void {
const dispatch = useAppDispatch()
return useCallback(() => dispatch(setOpenModal(modal)), [dispatch, modal])
}
export function useToggleWalletModal(): () => void {
return useToggleModal(ApplicationModal.WALLET)
}

@ -15,36 +15,39 @@ export type PopupContent =
export enum ApplicationModal {
ADDRESS_CLAIM,
UNISWAP_NFT_AIRDROP_CLAIM,
BLOCKED_ACCOUNT,
DELEGATE,
CLAIM_POPUP,
DELEGATE,
EXECUTE,
FEATURE_FLAGS,
FIAT_ONRAMP,
MENU,
NETWORK_FILTER,
NETWORK_SELECTOR,
POOL_OVERVIEW_OPTIONS,
PRIVACY_POLICY,
QUEUE,
SELF_CLAIM,
SETTINGS,
SHARE,
TIME_SELECTOR,
VOTE,
WALLET,
WALLET_DROPDOWN,
QUEUE,
EXECUTE,
TIME_SELECTOR,
SHARE,
NETWORK_FILTER,
FEATURE_FLAGS,
UNISWAP_NFT_AIRDROP_CLAIM,
}
type PopupList = Array<{ key: string; show: boolean; content: PopupContent; removeAfterMs: number | null }>
export interface ApplicationState {
readonly chainId: number | null
readonly fiatOnramp: { available: boolean; availabilityChecked: boolean }
readonly openModal: ApplicationModal | null
readonly popupList: PopupList
}
const initialState: ApplicationState = {
fiatOnramp: { available: false, availabilityChecked: false },
chainId: null,
openModal: null,
popupList: [],
@ -54,6 +57,9 @@ const applicationSlice = createSlice({
name: 'application',
initialState,
reducers: {
setFiatOnrampAvailability(state, { payload: available }) {
state.fiatOnramp = { available, availabilityChecked: true }
},
updateChainId(state, action) {
const { chainId } = action.payload
state.chainId = chainId
@ -81,5 +87,6 @@ const applicationSlice = createSlice({
},
})
export const { updateChainId, setOpenModal, addPopup, removePopup } = applicationSlice.actions
export const { updateChainId, setFiatOnrampAvailability, setOpenModal, addPopup, removePopup } =
applicationSlice.actions
export default applicationSlice.reducer

@ -1,19 +1,29 @@
import { useWeb3React } from '@web3-react/core'
import useDebounce from 'hooks/useDebounce'
import useIsWindowVisible from 'hooks/useIsWindowVisible'
import { useEffect, useState } from 'react'
import { useEffect, useRef, useState } from 'react'
import { useAppDispatch } from 'state/hooks'
import { supportedChainId } from 'utils/supportedChainId'
import { updateChainId } from './reducer'
import { useCloseModal } from './hooks'
import { ApplicationModal, updateChainId } from './reducer'
export default function Updater(): null {
const { chainId, provider } = useWeb3React()
const { account, chainId, provider } = useWeb3React()
const dispatch = useAppDispatch()
const windowVisible = useIsWindowVisible()
const [activeChainId, setActiveChainId] = useState(chainId)
const closeModal = useCloseModal(ApplicationModal.WALLET_DROPDOWN)
const previousAccountValue = useRef(account)
useEffect(() => {
if (account && account !== previousAccountValue.current) {
previousAccountValue.current = account
closeModal()
}
}, [account, closeModal])
useEffect(() => {
if (provider && chainId && windowVisible) {
setActiveChainId(chainId)

@ -4,6 +4,8 @@ import { useWeb3React } from '@web3-react/core'
import { L2_CHAIN_IDS } from 'constants/chains'
import { SupportedLocale } from 'constants/locales'
import { L2_DEADLINE_FROM_NOW } from 'constants/misc'
import { BaseVariant } from 'featureFlags'
import { useFiatOnrampFlag } from 'featureFlags/flags/fiatOnramp'
import JSBI from 'jsbi'
import { useCallback, useMemo } from 'react'
import { shallowEqual } from 'react-redux'
@ -17,6 +19,7 @@ import { AppState } from '../index'
import {
addSerializedPair,
addSerializedToken,
updateFiatOnrampAcknowledgments,
updateHideClosedPositions,
updateHideNFTWelcomeModal,
updateShowNftPromoBanner,
@ -105,9 +108,29 @@ export function useExpertModeManager(): [boolean, () => void] {
return [expertMode, toggleSetExpertMode]
}
interface FiatOnrampAcknowledgements {
renderCount: number
system: boolean
user: boolean
}
export function useFiatOnrampAck(): [
FiatOnrampAcknowledgements,
(acknowledgements: Partial<FiatOnrampAcknowledgements>) => void
] {
const dispatch = useAppDispatch()
const fiatOnrampAcknowledgments = useAppSelector((state) => state.user.fiatOnrampAcknowledgments)
const setAcknowledgements = useCallback(
(acks: Partial<FiatOnrampAcknowledgements>) => {
dispatch(updateFiatOnrampAcknowledgments(acks))
},
[dispatch]
)
return [fiatOnrampAcknowledgments, setAcknowledgements]
}
export function useHideNFTWelcomeModal(): [boolean | undefined, () => void] {
const dispatch = useAppDispatch()
const hideNFTWelcomeModal = useAppSelector((state) => state.user.hideNFTWelcomeModal)
const fiatOnrampFlagEnabled = useFiatOnrampFlag() === BaseVariant.Enabled
const hideNFTWelcomeModal = useAppSelector((state) => state.user.hideNFTWelcomeModal) || fiatOnrampFlagEnabled
const hideModal = useCallback(() => {
dispatch(updateHideNFTWelcomeModal({ hideNFTWelcomeModal: true }))
}, [dispatch])

@ -9,6 +9,8 @@ import { SerializedPair, SerializedToken } from './types'
const currentTimestamp = () => new Date().getTime()
export interface UserState {
fiatOnrampAcknowledgments: { renderCount: number; system: boolean; user: boolean }
selectedWallet?: ConnectionType
// the timestamp of the last updateVersion action
@ -61,6 +63,7 @@ function pairKey(token0Address: string, token1Address: string) {
}
export const initialState: UserState = {
fiatOnrampAcknowledgments: { renderCount: 0, system: false, user: false },
selectedWallet: undefined,
matchesDarkMode: false,
userDarkMode: null,
@ -84,6 +87,12 @@ const userSlice = createSlice({
name: 'user',
initialState,
reducers: {
updateFiatOnrampAcknowledgments(
state,
{ payload }: { payload: Partial<{ renderCount: number; user: boolean; system: boolean }> }
) {
state.fiatOnrampAcknowledgments = { ...state.fiatOnrampAcknowledgments, ...payload }
},
updateSelectedWallet(state, { payload: { wallet } }) {
state.selectedWallet = wallet
},
@ -181,9 +190,10 @@ const userSlice = createSlice({
})
export const {
updateSelectedWallet,
addSerializedPair,
addSerializedToken,
updateFiatOnrampAcknowledgments,
updateSelectedWallet,
updateHideClosedPositions,
updateMatchesDarkMode,
updateUserClientSideRouter,