Merge pull request #3 from Uniswap/FoR-main
feat: FoR commits from mgtm repo
This commit is contained in:
commit
cb7132ee17
6
.env
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"
|
||||
|
21
src/assets/svg/fiat_mask.svg
Normal file
21
src/assets/svg/fiat_mask.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 3.5 MiB |
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" 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}
|
||||
|
148
src/components/FiatOnrampAnnouncement/index.tsx
Normal file
148
src/components/FiatOnrampAnnouncement/index.tsx
Normal file
@ -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>
|
||||
)
|
||||
}
|
143
src/components/FiatOnrampModal/index.tsx
Normal file
143
src/components/FiatOnrampModal/index.tsx
Normal file
@ -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',
|
||||
}
|
||||
|
5
src/featureFlags/flags/fiatOnramp.ts
Normal file
5
src/featureFlags/flags/fiatOnramp.ts
Normal file
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user