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_MOONPAY_API="https://api.moonpay.com"
|
||||
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"
|
||||
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'
|
||||
|
||||
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 {}
|
||||
|
@ -93,13 +93,13 @@ const Body = styled(ThemedText.BodySmall)`
|
||||
|
||||
export function FiatOnrampAnnouncement() {
|
||||
const { account } = useWeb3React()
|
||||
const [fiatOnrampAcknowledged, acknowledge] = useFiatOnrampAck()
|
||||
const [acks, acknowledge] = useFiatOnrampAck()
|
||||
|
||||
const handleClose = useCallback(
|
||||
(e: MouseEvent<HTMLOrSVGElement>) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
acknowledge()
|
||||
acknowledge({ user: false })
|
||||
},
|
||||
[acknowledge]
|
||||
)
|
||||
@ -107,11 +107,11 @@ export function FiatOnrampAnnouncement() {
|
||||
const toggleWalletDropdown = useToggleWalletDropdown()
|
||||
const handleClick = useCallback(() => {
|
||||
toggleWalletDropdown()
|
||||
acknowledge()
|
||||
acknowledge({ user: true })
|
||||
}, [acknowledge, toggleWalletDropdown])
|
||||
const fiatOnrampFlag = useFiatOnrampFlag()
|
||||
|
||||
if (!account || fiatOnrampAcknowledged || fiatOnrampFlag === BaseVariant.Control) {
|
||||
if (!account || acks?.user || fiatOnrampFlag === BaseVariant.Control) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
|
@ -14,8 +14,7 @@ import {
|
||||
} from 'nft/components/icons'
|
||||
import { body, bodySmall } from 'nft/css/common.css'
|
||||
import { themeVars } from 'nft/css/sprinkles.css'
|
||||
import { ReactNode, useCallback, useReducer, useRef } from 'react'
|
||||
import { CreditCard } from 'react-feather'
|
||||
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'
|
||||
@ -26,28 +25,6 @@ import * as styles from './MenuDropdown.css'
|
||||
import { NavDropdown } from './NavDropdown'
|
||||
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 = ({
|
||||
to,
|
||||
href,
|
||||
@ -74,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
|
||||
@ -139,14 +121,9 @@ 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)
|
||||
|
||||
const handleBuyCryptoClick = useCallback(() => {
|
||||
toggleOpen()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<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">
|
||||
<Column gap="16">
|
||||
<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}>
|
||||
<Icon>
|
||||
<GovernanceIcon width={24} height={24} />
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
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'
|
||||
@ -9,31 +11,58 @@ 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, CreditCard, 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, useOpenModal, 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 BuyCryptoButton = styled(ThemeButton)`
|
||||
margin-top: 12px;
|
||||
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;
|
||||
@ -81,7 +110,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;
|
||||
`
|
||||
@ -114,6 +156,11 @@ 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 BalanceWrapper = styled.div`
|
||||
padding: 16px 0;
|
||||
@ -178,7 +225,49 @@ const AuthenticatedHeader = () => {
|
||||
}, [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>
|
||||
@ -200,7 +289,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}>
|
||||
@ -224,9 +313,50 @@ const AuthenticatedHeader = () => {
|
||||
<Trans>View and sell NFTs</Trans>
|
||||
</ProfileButton>
|
||||
{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 && (
|
||||
<UNIButton onClick={openClaimModal} size={ButtonSize.medium} emphasis={ButtonEmphasis.medium}>
|
||||
|
@ -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()
|
||||
|
@ -41,11 +41,13 @@ type PopupList = Array<{ key: string; show: boolean; content: PopupContent; remo
|
||||
|
||||
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: [],
|
||||
@ -55,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
|
||||
@ -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
|
||||
|
@ -17,7 +17,7 @@ import { AppState } from '../index'
|
||||
import {
|
||||
addSerializedPair,
|
||||
addSerializedToken,
|
||||
updateFiatOnrampAcknowledged,
|
||||
updateFiatOnrampAcknowledgments,
|
||||
updateHideClosedPositions,
|
||||
updateHideNFTWelcomeModal,
|
||||
updateShowNftPromoBanner,
|
||||
@ -26,7 +26,7 @@ import {
|
||||
updateUserDeadline,
|
||||
updateUserExpertMode,
|
||||
updateUserLocale,
|
||||
updateUserSlippageTolerance
|
||||
updateUserSlippageTolerance,
|
||||
} from './reducer'
|
||||
import { SerializedPair, SerializedToken } from './types'
|
||||
|
||||
@ -106,18 +106,23 @@ export function useExpertModeManager(): [boolean, () => void] {
|
||||
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 fiatOnrampAcknowledged = useAppSelector((state) => state.user.fiatOnrampAcknowledged)
|
||||
|
||||
const toggleSetExpertMode = useCallback(
|
||||
(b = true) => {
|
||||
dispatch(updateFiatOnrampAcknowledged(b))
|
||||
const fiatOnrampAcknowledgments = useAppSelector((state) => state.user.fiatOnrampAcknowledgments)
|
||||
const setAcknowledgements = useCallback(
|
||||
(acks: Partial<FiatOnrampAcknowledgements>) => {
|
||||
dispatch(updateFiatOnrampAcknowledgments(acks))
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
return [fiatOnrampAcknowledged, toggleSetExpertMode]
|
||||
return [fiatOnrampAcknowledgments, setAcknowledgements]
|
||||
}
|
||||
export function useHideNFTWelcomeModal(): [boolean | undefined, () => void] {
|
||||
const dispatch = useAppDispatch()
|
||||
|
@ -9,7 +9,7 @@ import { SerializedPair, SerializedToken } from './types'
|
||||
const currentTimestamp = () => new Date().getTime()
|
||||
|
||||
export interface UserState {
|
||||
fiatOnrampAcknowledged: boolean
|
||||
fiatOnrampAcknowledgments: { user: boolean; system: boolean }
|
||||
|
||||
selectedWallet?: ConnectionType
|
||||
|
||||
@ -63,7 +63,7 @@ function pairKey(token0Address: string, token1Address: string) {
|
||||
}
|
||||
|
||||
export const initialState: UserState = {
|
||||
fiatOnrampAcknowledged: false,
|
||||
fiatOnrampAcknowledgments: { user: false, system: false },
|
||||
selectedWallet: undefined,
|
||||
matchesDarkMode: false,
|
||||
userDarkMode: null,
|
||||
@ -87,8 +87,8 @@ const userSlice = createSlice({
|
||||
name: 'user',
|
||||
initialState,
|
||||
reducers: {
|
||||
updateFiatOnrampAcknowledged(state, { payload }) {
|
||||
state.fiatOnrampAcknowledged = payload
|
||||
updateFiatOnrampAcknowledgments(state, { payload }: { payload: { user?: boolean; system?: boolean } }) {
|
||||
state.fiatOnrampAcknowledgments = { ...state.fiatOnrampAcknowledgments, ...payload }
|
||||
},
|
||||
updateSelectedWallet(state, { payload: { wallet } }) {
|
||||
state.selectedWallet = wallet
|
||||
@ -189,7 +189,7 @@ const userSlice = createSlice({
|
||||
export const {
|
||||
addSerializedPair,
|
||||
addSerializedToken,
|
||||
updateFiatOnrampAcknowledged,
|
||||
updateFiatOnrampAcknowledgments,
|
||||
updateSelectedWallet,
|
||||
updateHideClosedPositions,
|
||||
updateMatchesDarkMode,
|
||||
|
Loading…
Reference in New Issue
Block a user