feat(moonpay): moonpay ip checks to determine if the user can access the fiat onramp (#10)
* feat(moonpay): useFiatOnrampAvailable * feat(moonpay): ip check with moonpay for buy crypto availability * add error state and clear up some of the sequence of logic * add button-specific spinner, put the ... menu button behind the feature flag * hide ... menu option if onramp is unavailable * add live publishable moonpay key * add initial FoR hype border flash to announcement acknowledgment * remove ... menu access to FoR feature * add tooltip and external link to info icon * nicer error display * add stale market to ack * pr feedback from zzmp * fix really weird react bug * ts fix and clear timeout * pairing staleness handler w/ zzmp
This commit is contained in:
parent
f753a5e325
commit
b427be2673
@ -5,6 +5,6 @@ REACT_APP_GOOGLE_ANALYTICS_ID="G-KDP9B6W4H8"
|
|||||||
REACT_APP_INFURA_KEY="099fc58e0de9451d80b18d7c74caa7c1"
|
REACT_APP_INFURA_KEY="099fc58e0de9451d80b18d7c74caa7c1"
|
||||||
REACT_APP_MOONPAY_API="https://api.moonpay.com"
|
REACT_APP_MOONPAY_API="https://api.moonpay.com"
|
||||||
REACT_APP_MOONPAY_LINK="https://us-central1-uniswap-mobile.cloudfunctions.net/signMoonpayLink"
|
REACT_APP_MOONPAY_LINK="https://us-central1-uniswap-mobile.cloudfunctions.net/signMoonpayLink"
|
||||||
REACT_APP_MOONPAY_PUBLISHABLE_KEY=""
|
REACT_APP_MOONPAY_PUBLISHABLE_KEY="pk_live_uQG4BJC4w3cxnqpcSqAfohdBFDTsY6E"
|
||||||
REACT_APP_FIREBASE_KEY="AIzaSyBcZWwTcTJHj_R6ipZcrJkXdq05PuX0Rs0"
|
REACT_APP_FIREBASE_KEY="AIzaSyBcZWwTcTJHj_R6ipZcrJkXdq05PuX0Rs0"
|
||||||
THE_GRAPH_SCHEMA_ENDPOINT="https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3"
|
THE_GRAPH_SCHEMA_ENDPOINT="https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3"
|
||||||
|
13
src/components/Button/LoadingButtonSpinner.tsx
Normal file
13
src/components/Button/LoadingButtonSpinner.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { SpinnerSVG } from 'theme'
|
||||||
|
|
||||||
|
const ButtonLoadingSpinner = (props: React.ComponentPropsWithoutRef<'svg'>) => (
|
||||||
|
<SpinnerSVG width="20" height="20" viewBox="0 0 20 20" fill="none" 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'
|
import { RowBetween } from '../Row'
|
||||||
|
|
||||||
|
export { default as LoadingButtonSpinner } from './LoadingButtonSpinner'
|
||||||
|
|
||||||
type ButtonProps = Omit<ButtonPropsOriginal, 'css'>
|
type ButtonProps = Omit<ButtonPropsOriginal, 'css'>
|
||||||
|
|
||||||
export const BaseButton = styled(RebassButton)<
|
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 {
|
export enum ButtonSize {
|
||||||
small,
|
small,
|
||||||
medium,
|
medium,
|
||||||
@ -466,7 +456,18 @@ function pickThemeButtonTextColor({ theme, emphasis }: { theme: DefaultTheme; em
|
|||||||
return theme.textPrimary
|
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>`
|
const BaseThemeButton = styled.button<BaseButtonProps>`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: ${pickThemeButtonBackgroundColor};
|
background-color: ${pickThemeButtonBackgroundColor};
|
||||||
@ -484,16 +485,13 @@ const BaseThemeButton = styled.button<BaseButtonProps>`
|
|||||||
padding: ${pickThemeButtonPadding};
|
padding: ${pickThemeButtonPadding};
|
||||||
position: relative;
|
position: relative;
|
||||||
transition: 150ms ease opacity;
|
transition: 150ms ease opacity;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
:active {
|
:active {
|
||||||
${ButtonOverlay} {
|
${ButtonOverlay} {
|
||||||
background-color: ${({ theme }) => theme.stateOverlayPressed};
|
background-color: ${({ theme }) => theme.stateOverlayPressed};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
:disabled {
|
|
||||||
cursor: default;
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
:focus {
|
:focus {
|
||||||
${ButtonOverlay} {
|
${ButtonOverlay} {
|
||||||
background-color: ${({ theme }) => theme.stateOverlayPressed};
|
background-color: ${({ theme }) => theme.stateOverlayPressed};
|
||||||
@ -504,6 +502,17 @@ const BaseThemeButton = styled.button<BaseButtonProps>`
|
|||||||
background-color: ${({ theme }) => theme.stateOverlayHover};
|
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 {}
|
interface ThemeButtonProps extends React.ComponentPropsWithoutRef<'button'>, BaseButtonProps {}
|
||||||
|
@ -93,13 +93,13 @@ const Body = styled(ThemedText.BodySmall)`
|
|||||||
|
|
||||||
export function FiatOnrampAnnouncement() {
|
export function FiatOnrampAnnouncement() {
|
||||||
const { account } = useWeb3React()
|
const { account } = useWeb3React()
|
||||||
const [fiatOnrampAcknowledged, acknowledge] = useFiatOnrampAck()
|
const [acks, acknowledge] = useFiatOnrampAck()
|
||||||
|
|
||||||
const handleClose = useCallback(
|
const handleClose = useCallback(
|
||||||
(e: MouseEvent<HTMLOrSVGElement>) => {
|
(e: MouseEvent<HTMLOrSVGElement>) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
acknowledge()
|
acknowledge({ user: false })
|
||||||
},
|
},
|
||||||
[acknowledge]
|
[acknowledge]
|
||||||
)
|
)
|
||||||
@ -107,11 +107,11 @@ export function FiatOnrampAnnouncement() {
|
|||||||
const toggleWalletDropdown = useToggleWalletDropdown()
|
const toggleWalletDropdown = useToggleWalletDropdown()
|
||||||
const handleClick = useCallback(() => {
|
const handleClick = useCallback(() => {
|
||||||
toggleWalletDropdown()
|
toggleWalletDropdown()
|
||||||
acknowledge()
|
acknowledge({ user: true })
|
||||||
}, [acknowledge, toggleWalletDropdown])
|
}, [acknowledge, toggleWalletDropdown])
|
||||||
const fiatOnrampFlag = useFiatOnrampFlag()
|
const fiatOnrampFlag = useFiatOnrampFlag()
|
||||||
|
|
||||||
if (!account || fiatOnrampAcknowledged || fiatOnrampFlag === BaseVariant.Control) {
|
if (!account || acks?.user || fiatOnrampFlag === BaseVariant.Control) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
@ -14,8 +14,7 @@ import {
|
|||||||
} from 'nft/components/icons'
|
} from 'nft/components/icons'
|
||||||
import { body, bodySmall } from 'nft/css/common.css'
|
import { body, bodySmall } from 'nft/css/common.css'
|
||||||
import { themeVars } from 'nft/css/sprinkles.css'
|
import { themeVars } from 'nft/css/sprinkles.css'
|
||||||
import { ReactNode, useCallback, useReducer, useRef } from 'react'
|
import { ReactNode, useReducer, useRef } from 'react'
|
||||||
import { CreditCard } from 'react-feather'
|
|
||||||
import { NavLink, NavLinkProps } from 'react-router-dom'
|
import { NavLink, NavLinkProps } from 'react-router-dom'
|
||||||
import styled from 'styled-components/macro'
|
import styled from 'styled-components/macro'
|
||||||
import { isDevelopmentEnv, isStagingEnv } from 'utils/env'
|
import { isDevelopmentEnv, isStagingEnv } from 'utils/env'
|
||||||
@ -26,28 +25,6 @@ import * as styles from './MenuDropdown.css'
|
|||||||
import { NavDropdown } from './NavDropdown'
|
import { NavDropdown } from './NavDropdown'
|
||||||
import { NavIcon } from './NavIcon'
|
import { NavIcon } from './NavIcon'
|
||||||
|
|
||||||
const BuyCryptoButton = styled.button`
|
|
||||||
background-color: transparent;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
border-radius: 12px;
|
|
||||||
color: ${({ theme }) => theme.textPrimary};
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
line-height: 24px;
|
|
||||||
outline: none;
|
|
||||||
padding: 8px;
|
|
||||||
text-decoration: none;
|
|
||||||
width: full;
|
|
||||||
white-space: nowrap;
|
|
||||||
transition-duration: ${({ theme }) => theme.transition.duration.fast};
|
|
||||||
transition-timing-function: ease-in-out;
|
|
||||||
transition-property: opacity, color, background-color;
|
|
||||||
|
|
||||||
:hover {
|
|
||||||
background: ${({ theme }) => theme.stateOverlayHover};
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const PrimaryMenuRow = ({
|
const PrimaryMenuRow = ({
|
||||||
to,
|
to,
|
||||||
href,
|
href,
|
||||||
@ -74,8 +51,13 @@ const PrimaryMenuRow = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const StyledBox = styled(Box)`
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
`
|
||||||
const PrimaryMenuRowText = ({ children }: { children: ReactNode }) => {
|
const PrimaryMenuRowText = ({ children }: { children: ReactNode }) => {
|
||||||
return <Box className={`${styles.PrimaryText} ${body}`}>{children}</Box>
|
return <StyledBox className={`${styles.PrimaryText} ${body}`}>{children}</StyledBox>
|
||||||
}
|
}
|
||||||
|
|
||||||
PrimaryMenuRow.Text = PrimaryMenuRowText
|
PrimaryMenuRow.Text = PrimaryMenuRowText
|
||||||
@ -139,14 +121,9 @@ export const MenuDropdown = () => {
|
|||||||
const [isOpen, toggleOpen] = useReducer((s) => !s, false)
|
const [isOpen, toggleOpen] = useReducer((s) => !s, false)
|
||||||
const togglePrivacyPolicy = useToggleModal(ApplicationModal.PRIVACY_POLICY)
|
const togglePrivacyPolicy = useToggleModal(ApplicationModal.PRIVACY_POLICY)
|
||||||
const openFeatureFlagsModal = useToggleModal(ApplicationModal.FEATURE_FLAGS)
|
const openFeatureFlagsModal = useToggleModal(ApplicationModal.FEATURE_FLAGS)
|
||||||
|
|
||||||
const ref = useRef<HTMLDivElement>(null)
|
const ref = useRef<HTMLDivElement>(null)
|
||||||
useOnClickOutside(ref, isOpen ? toggleOpen : undefined)
|
useOnClickOutside(ref, isOpen ? toggleOpen : undefined)
|
||||||
|
|
||||||
const handleBuyCryptoClick = useCallback(() => {
|
|
||||||
toggleOpen()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box position="relative" ref={ref}>
|
<Box position="relative" ref={ref}>
|
||||||
@ -158,14 +135,6 @@ export const MenuDropdown = () => {
|
|||||||
<NavDropdown top={{ sm: 'unset', lg: '56' }} bottom={{ sm: '56', lg: 'unset' }} right="0">
|
<NavDropdown top={{ sm: 'unset', lg: '56' }} bottom={{ sm: '56', lg: 'unset' }} right="0">
|
||||||
<Column gap="16">
|
<Column gap="16">
|
||||||
<Column paddingX="8" gap="4">
|
<Column paddingX="8" gap="4">
|
||||||
<BuyCryptoButton onClick={handleBuyCryptoClick}>
|
|
||||||
<Icon>
|
|
||||||
<CreditCard width={24} height={24} />
|
|
||||||
</Icon>
|
|
||||||
<PrimaryMenuRow.Text>
|
|
||||||
<Trans>Buy Crypto</Trans>
|
|
||||||
</PrimaryMenuRow.Text>
|
|
||||||
</BuyCryptoButton>
|
|
||||||
<PrimaryMenuRow to="/vote" close={toggleOpen}>
|
<PrimaryMenuRow to="/vote" close={toggleOpen}>
|
||||||
<Icon>
|
<Icon>
|
||||||
<GovernanceIcon width={24} height={24} />
|
<GovernanceIcon width={24} height={24} />
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { Trans } from '@lingui/macro'
|
import { Trans } from '@lingui/macro'
|
||||||
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
|
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
|
||||||
import { useWeb3React } from '@web3-react/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 { getConnection } from 'connection/utils'
|
||||||
import { getChainInfoOrDefault } from 'constants/chainInfo'
|
import { getChainInfoOrDefault } from 'constants/chainInfo'
|
||||||
import { SupportedChainId } from 'constants/chains'
|
import { SupportedChainId } from 'constants/chains'
|
||||||
@ -9,31 +11,58 @@ import { useFiatOnrampFlag } from 'featureFlags/flags/fiatOnramp'
|
|||||||
import useCopyClipboard from 'hooks/useCopyClipboard'
|
import useCopyClipboard from 'hooks/useCopyClipboard'
|
||||||
import useStablecoinPrice from 'hooks/useStablecoinPrice'
|
import useStablecoinPrice from 'hooks/useStablecoinPrice'
|
||||||
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
|
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
|
||||||
|
import ms from 'ms.macro'
|
||||||
import { useProfilePageState, useSellAsset, useWalletCollections } from 'nft/hooks'
|
import { useProfilePageState, useSellAsset, useWalletCollections } from 'nft/hooks'
|
||||||
import { useIsNftClaimAvailable } from 'nft/hooks/useIsNftClaimAvailable'
|
import { useIsNftClaimAvailable } from 'nft/hooks/useIsNftClaimAvailable'
|
||||||
import { ProfilePageStateType } from 'nft/types'
|
import { ProfilePageStateType } from 'nft/types'
|
||||||
import { useCallback, useMemo } from 'react'
|
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
import { Copy, CreditCard, ExternalLink, Power } from 'react-feather'
|
import { Copy, CreditCard, ExternalLink as ExternalLinkIcon, Info, Power } from 'react-feather'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { Text } from 'rebass'
|
import { Text } from 'rebass'
|
||||||
import { useCurrencyBalanceString } from 'state/connection/hooks'
|
import { useCurrencyBalanceString } from 'state/connection/hooks'
|
||||||
import { useAppDispatch } from 'state/hooks'
|
import { useAppDispatch } from 'state/hooks'
|
||||||
|
import { useFiatOnrampAck } from 'state/user/hooks'
|
||||||
import { updateSelectedWallet } from 'state/user/reducer'
|
import { updateSelectedWallet } from 'state/user/reducer'
|
||||||
import styled, { css } from 'styled-components/macro'
|
import styled, { css, keyframes } from 'styled-components/macro'
|
||||||
import { ThemedText } from 'theme'
|
import { ExternalLink, ThemedText } from 'theme'
|
||||||
|
|
||||||
import { shortenAddress } from '../../nft/utils/address'
|
import { shortenAddress } from '../../nft/utils/address'
|
||||||
import { useCloseModal, useOpenModal, useToggleModal } from '../../state/application/hooks'
|
import { useCloseModal, useFiatOnrampAvailability, useOpenModal, useToggleModal } from '../../state/application/hooks'
|
||||||
import { ApplicationModal } from '../../state/application/reducer'
|
import { ApplicationModal } from '../../state/application/reducer'
|
||||||
import { useUserHasAvailableClaim, useUserUnclaimedAmount } from '../../state/claim/hooks'
|
import { useUserHasAvailableClaim, useUserUnclaimedAmount } from '../../state/claim/hooks'
|
||||||
import { ButtonEmphasis, ButtonSize, ThemeButton } from '../Button'
|
|
||||||
import StatusIcon from '../Identicon/StatusIcon'
|
import StatusIcon from '../Identicon/StatusIcon'
|
||||||
import IconButton, { IconHoverText } from './IconButton'
|
import IconButton, { IconHoverText } from './IconButton'
|
||||||
|
|
||||||
const BuyCryptoButton = styled(ThemeButton)`
|
const BuyCryptoButtonBorderKeyframes = keyframes`
|
||||||
margin-top: 12px;
|
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)`
|
const WalletButton = styled(ThemeButton)`
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
@ -81,7 +110,20 @@ const USDText = styled.div`
|
|||||||
color: ${({ theme }) => theme.textSecondary};
|
color: ${({ theme }) => theme.textSecondary};
|
||||||
margin-top: 8px;
|
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`
|
const FlexContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
`
|
`
|
||||||
@ -114,6 +156,11 @@ const AccountContainer = styled(ThemedText.BodySmall)`
|
|||||||
color: ${({ theme }) => theme.textSecondary};
|
color: ${({ theme }) => theme.textSecondary};
|
||||||
margin-top: 2.5px;
|
margin-top: 2.5px;
|
||||||
`
|
`
|
||||||
|
const StyledInfoIcon = styled(Info)`
|
||||||
|
height: 12px;
|
||||||
|
width: 12px;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
`
|
||||||
|
|
||||||
const BalanceWrapper = styled.div`
|
const BalanceWrapper = styled.div`
|
||||||
padding: 16px 0;
|
padding: 16px 0;
|
||||||
@ -178,7 +225,49 @@ const AuthenticatedHeader = () => {
|
|||||||
}, [clearCollectionFilters, closeModal, navigate, resetSellAssets, setSellPageState])
|
}, [clearCollectionFilters, closeModal, navigate, resetSellAssets, setSellPageState])
|
||||||
|
|
||||||
const fiatOnrampFlag = useFiatOnrampFlag()
|
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 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 (
|
return (
|
||||||
<AuthenticatedHeaderWrapper>
|
<AuthenticatedHeaderWrapper>
|
||||||
@ -200,7 +289,7 @@ const AuthenticatedHeader = () => {
|
|||||||
<IconButton onClick={copy} Icon={Copy}>
|
<IconButton onClick={copy} Icon={Copy}>
|
||||||
{isCopied ? <Trans>Copied!</Trans> : <Trans>Copy</Trans>}
|
{isCopied ? <Trans>Copied!</Trans> : <Trans>Copy</Trans>}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton href={`${explorer}address/${account}`} target="_blank" Icon={ExternalLink}>
|
<IconButton href={`${explorer}address/${account}`} target="_blank" Icon={ExternalLinkIcon}>
|
||||||
<Trans>Explore</Trans>
|
<Trans>Explore</Trans>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton data-testid="wallet-disconnect" onClick={disconnect} Icon={Power}>
|
<IconButton data-testid="wallet-disconnect" onClick={disconnect} Icon={Power}>
|
||||||
@ -224,9 +313,50 @@ const AuthenticatedHeader = () => {
|
|||||||
<Trans>View and sell NFTs</Trans>
|
<Trans>View and sell NFTs</Trans>
|
||||||
</ProfileButton>
|
</ProfileButton>
|
||||||
{fiatOnrampFlag === BaseVariant.Enabled && (
|
{fiatOnrampFlag === BaseVariant.Enabled && (
|
||||||
<BuyCryptoButton size={ButtonSize.medium} emphasis={ButtonEmphasis.medium} onClick={openFiatOnrampModal}>
|
<>
|
||||||
<CreditCard /> <Trans>Buy crypto</Trans>
|
<BuyCryptoButton
|
||||||
</BuyCryptoButton>
|
$animateBorder={animateBuyCryptoButtonBorder}
|
||||||
|
size={ButtonSize.medium}
|
||||||
|
emphasis={ButtonEmphasis.medium}
|
||||||
|
onClick={handleBuyCryptoClick}
|
||||||
|
disabled={disableBuyCryptoButton}
|
||||||
|
>
|
||||||
|
{fiatOnrampAvailabilityLoading ? (
|
||||||
|
<>
|
||||||
|
<LoadingButtonSpinner />
|
||||||
|
<Trans>Checking availability</Trans>
|
||||||
|
</>
|
||||||
|
) : error ? (
|
||||||
|
<ThemedText.BodyPrimary>{error}</ThemedText.BodyPrimary>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<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 supported in some regions in and outside of the US. 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 && (
|
{isUnclaimed && (
|
||||||
<UNIButton onClick={openClaimModal} size={ButtonSize.medium} emphasis={ButtonEmphasis.medium}>
|
<UNIButton onClick={openClaimModal} size={ButtonSize.medium} emphasis={ButtonEmphasis.medium}>
|
||||||
|
@ -1,15 +1,85 @@
|
|||||||
import { DEFAULT_TXN_DISMISS_MS } from 'constants/misc'
|
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 { useAppDispatch, useAppSelector } from 'state/hooks'
|
||||||
|
|
||||||
import { AppState } from '../index'
|
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 {
|
export function useModalIsOpen(modal: ApplicationModal): boolean {
|
||||||
const openModal = useAppSelector((state: AppState) => state.application.openModal)
|
const openModal = useAppSelector((state: AppState) => state.application.openModal)
|
||||||
return openModal === modal
|
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 {
|
export function useToggleModal(modal: ApplicationModal): () => void {
|
||||||
const isOpen = useModalIsOpen(modal)
|
const isOpen = useModalIsOpen(modal)
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
@ -41,11 +41,13 @@ type PopupList = Array<{ key: string; show: boolean; content: PopupContent; remo
|
|||||||
|
|
||||||
export interface ApplicationState {
|
export interface ApplicationState {
|
||||||
readonly chainId: number | null
|
readonly chainId: number | null
|
||||||
|
readonly fiatOnramp: { available: boolean; availabilityChecked: boolean }
|
||||||
readonly openModal: ApplicationModal | null
|
readonly openModal: ApplicationModal | null
|
||||||
readonly popupList: PopupList
|
readonly popupList: PopupList
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: ApplicationState = {
|
const initialState: ApplicationState = {
|
||||||
|
fiatOnramp: { available: false, availabilityChecked: false },
|
||||||
chainId: null,
|
chainId: null,
|
||||||
openModal: null,
|
openModal: null,
|
||||||
popupList: [],
|
popupList: [],
|
||||||
@ -55,6 +57,9 @@ const applicationSlice = createSlice({
|
|||||||
name: 'application',
|
name: 'application',
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
|
setFiatOnrampAvailability(state, { payload: available }) {
|
||||||
|
state.fiatOnramp = { available, availabilityChecked: true }
|
||||||
|
},
|
||||||
updateChainId(state, action) {
|
updateChainId(state, action) {
|
||||||
const { chainId } = action.payload
|
const { chainId } = action.payload
|
||||||
state.chainId = chainId
|
state.chainId = chainId
|
||||||
@ -82,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
|
export default applicationSlice.reducer
|
||||||
|
@ -17,7 +17,7 @@ import { AppState } from '../index'
|
|||||||
import {
|
import {
|
||||||
addSerializedPair,
|
addSerializedPair,
|
||||||
addSerializedToken,
|
addSerializedToken,
|
||||||
updateFiatOnrampAcknowledged,
|
updateFiatOnrampAcknowledgments,
|
||||||
updateHideClosedPositions,
|
updateHideClosedPositions,
|
||||||
updateHideNFTWelcomeModal,
|
updateHideNFTWelcomeModal,
|
||||||
updateShowNftPromoBanner,
|
updateShowNftPromoBanner,
|
||||||
@ -26,7 +26,7 @@ import {
|
|||||||
updateUserDeadline,
|
updateUserDeadline,
|
||||||
updateUserExpertMode,
|
updateUserExpertMode,
|
||||||
updateUserLocale,
|
updateUserLocale,
|
||||||
updateUserSlippageTolerance
|
updateUserSlippageTolerance,
|
||||||
} from './reducer'
|
} from './reducer'
|
||||||
import { SerializedPair, SerializedToken } from './types'
|
import { SerializedPair, SerializedToken } from './types'
|
||||||
|
|
||||||
@ -106,18 +106,23 @@ export function useExpertModeManager(): [boolean, () => void] {
|
|||||||
return [expertMode, toggleSetExpertMode]
|
return [expertMode, toggleSetExpertMode]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useFiatOnrampAck(): [boolean, (b?: boolean) => void] {
|
interface FiatOnrampAcknowledgements {
|
||||||
|
user: boolean
|
||||||
|
system: boolean
|
||||||
|
}
|
||||||
|
export function useFiatOnrampAck(): [
|
||||||
|
FiatOnrampAcknowledgements,
|
||||||
|
(acknowledgements: Partial<FiatOnrampAcknowledgements>) => void
|
||||||
|
] {
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
const fiatOnrampAcknowledged = useAppSelector((state) => state.user.fiatOnrampAcknowledged)
|
const fiatOnrampAcknowledgments = useAppSelector((state) => state.user.fiatOnrampAcknowledgments)
|
||||||
|
const setAcknowledgements = useCallback(
|
||||||
const toggleSetExpertMode = useCallback(
|
(acks: Partial<FiatOnrampAcknowledgements>) => {
|
||||||
(b = true) => {
|
dispatch(updateFiatOnrampAcknowledgments(acks))
|
||||||
dispatch(updateFiatOnrampAcknowledged(b))
|
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
)
|
)
|
||||||
|
return [fiatOnrampAcknowledgments, setAcknowledgements]
|
||||||
return [fiatOnrampAcknowledged, toggleSetExpertMode]
|
|
||||||
}
|
}
|
||||||
export function useHideNFTWelcomeModal(): [boolean | undefined, () => void] {
|
export function useHideNFTWelcomeModal(): [boolean | undefined, () => void] {
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
@ -9,7 +9,7 @@ import { SerializedPair, SerializedToken } from './types'
|
|||||||
const currentTimestamp = () => new Date().getTime()
|
const currentTimestamp = () => new Date().getTime()
|
||||||
|
|
||||||
export interface UserState {
|
export interface UserState {
|
||||||
fiatOnrampAcknowledged: boolean
|
fiatOnrampAcknowledgments: { user: boolean; system: boolean }
|
||||||
|
|
||||||
selectedWallet?: ConnectionType
|
selectedWallet?: ConnectionType
|
||||||
|
|
||||||
@ -63,7 +63,7 @@ function pairKey(token0Address: string, token1Address: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const initialState: UserState = {
|
export const initialState: UserState = {
|
||||||
fiatOnrampAcknowledged: false,
|
fiatOnrampAcknowledgments: { user: false, system: false },
|
||||||
selectedWallet: undefined,
|
selectedWallet: undefined,
|
||||||
matchesDarkMode: false,
|
matchesDarkMode: false,
|
||||||
userDarkMode: null,
|
userDarkMode: null,
|
||||||
@ -87,8 +87,8 @@ const userSlice = createSlice({
|
|||||||
name: 'user',
|
name: 'user',
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
updateFiatOnrampAcknowledged(state, { payload }) {
|
updateFiatOnrampAcknowledgments(state, { payload }: { payload: { user?: boolean; system?: boolean } }) {
|
||||||
state.fiatOnrampAcknowledged = payload
|
state.fiatOnrampAcknowledgments = { ...state.fiatOnrampAcknowledgments, ...payload }
|
||||||
},
|
},
|
||||||
updateSelectedWallet(state, { payload: { wallet } }) {
|
updateSelectedWallet(state, { payload: { wallet } }) {
|
||||||
state.selectedWallet = wallet
|
state.selectedWallet = wallet
|
||||||
@ -189,7 +189,7 @@ const userSlice = createSlice({
|
|||||||
export const {
|
export const {
|
||||||
addSerializedPair,
|
addSerializedPair,
|
||||||
addSerializedToken,
|
addSerializedToken,
|
||||||
updateFiatOnrampAcknowledged,
|
updateFiatOnrampAcknowledgments,
|
||||||
updateSelectedWallet,
|
updateSelectedWallet,
|
||||||
updateHideClosedPositions,
|
updateHideClosedPositions,
|
||||||
updateMatchesDarkMode,
|
updateMatchesDarkMode,
|
||||||
|
Loading…
Reference in New Issue
Block a user