Commit cb7132ee authored by Jordan Frankfurt's avatar Jordan Frankfurt Committed by GitHub

Merge pull request #3 from Uniswap/FoR-main

feat: FoR commits from mgtm repo
parents ad2472ea 0fa4859a
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_AMPLITUDE_PROXY_URL="https://api.uniswap.org/v1/amplitude-proxy"
REACT_APP_AWS_API_REGION="us-east-2" REACT_APP_AWS_API_REGION="us-east-2"
REACT_APP_AWS_API_ENDPOINT="https://beta.api.uniswap.org/v1/graphql" 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_TEMP_API_URL="https://temp.api.uniswap.org/v1"
REACT_APP_SENTRY_DSN="https://a3c62e400b8748b5a8d007150e2f38b7@o1037921.ingest.sentry.io/4504255148851200" REACT_APP_SENTRY_DSN="https://a3c62e400b8748b5a8d007150e2f38b7@o1037921.ingest.sentry.io/4504255148851200"
ESLINT_NO_DEV_ERRORS=true 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"
REACT_APP_AMPLITUDE_PROXY_URL="https://api.uniswap.org/v1/amplitude-proxy" 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_FORTMATIC_KEY="pk_live_F937DF033A1666BF"
REACT_APP_GOOGLE_ANALYTICS_ID="G-KDP9B6W4H8" 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_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" THE_GRAPH_SCHEMA_ENDPOINT="https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3"
This diff is collapsed.
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' ...@@ -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 ...@@ -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 ...@@ -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>` ...@@ -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>` ...@@ -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 {}
......
import { BaseVariant, FeatureFlag, featureFlagSettings, useUpdateFlag } from 'featureFlags' import { BaseVariant, FeatureFlag, featureFlagSettings, useUpdateFlag } from 'featureFlags'
import { useFiatOnrampFlag } from 'featureFlags/flags/fiatOnramp'
import { Permit2Variant, usePermit2Flag } from 'featureFlags/flags/permit2' import { Permit2Variant, usePermit2Flag } from 'featureFlags/flags/permit2'
import { TraceJsonRpcVariant, useTraceJsonRpcFlag } from 'featureFlags/flags/traceJsonRpc' import { TraceJsonRpcVariant, useTraceJsonRpcFlag } from 'featureFlags/flags/traceJsonRpc'
import { useAtomValue, useUpdateAtom } from 'jotai/utils' import { useAtomValue, useUpdateAtom } from 'jotai/utils'
...@@ -208,6 +209,12 @@ export default function FeatureFlagModal() { ...@@ -208,6 +209,12 @@ export default function FeatureFlagModal() {
featureFlag={FeatureFlag.permit2} featureFlag={FeatureFlag.permit2}
label="Permit 2 / Universal Router" label="Permit 2 / Universal Router"
/> />
<FeatureFlagOption
variant={BaseVariant}
value={useFiatOnrampFlag()}
featureFlag={FeatureFlag.fiatOnramp}
label="Fiat on-ramp"
/>
<FeatureFlagGroup name="Debug"> <FeatureFlagGroup name="Debug">
<FeatureFlagOption <FeatureFlagOption
variant={TraceJsonRpcVariant} variant={TraceJsonRpcVariant}
......
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>
)
}
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' ...@@ -9,7 +9,7 @@ import { isMobile } from '../../utils/userAgent'
const AnimatedDialogOverlay = animated(DialogOverlay) const AnimatedDialogOverlay = animated(DialogOverlay)
const StyledDialogOverlay = styled(AnimatedDialogOverlay)<{ scrollOverlay?: boolean }>` const StyledDialogOverlay = styled(AnimatedDialogOverlay)<{ $scrollOverlay?: boolean }>`
&[data-reach-dialog-overlay] { &[data-reach-dialog-overlay] {
z-index: ${Z_INDEX.modalBackdrop}; z-index: ${Z_INDEX.modalBackdrop};
background-color: transparent; background-color: transparent;
...@@ -17,7 +17,7 @@ const StyledDialogOverlay = styled(AnimatedDialogOverlay)<{ scrollOverlay?: bool ...@@ -17,7 +17,7 @@ const StyledDialogOverlay = styled(AnimatedDialogOverlay)<{ scrollOverlay?: bool
display: flex; display: flex;
align-items: center; align-items: center;
overflow-y: ${({ scrollOverlay }) => scrollOverlay && 'scroll'}; overflow-y: ${({ $scrollOverlay }) => $scrollOverlay && 'scroll'};
justify-content: center; justify-content: center;
background-color: ${({ theme }) => theme.backgroundScrim}; background-color: ${({ theme }) => theme.backgroundScrim};
...@@ -89,7 +89,7 @@ interface ModalProps { ...@@ -89,7 +89,7 @@ interface ModalProps {
maxWidth?: number maxWidth?: number
initialFocusRef?: React.RefObject<any> initialFocusRef?: React.RefObject<any>
children?: React.ReactNode children?: React.ReactNode
scrollOverlay?: boolean $scrollOverlay?: boolean
hideBorder?: boolean hideBorder?: boolean
isBottomSheet?: boolean isBottomSheet?: boolean
} }
...@@ -103,7 +103,7 @@ export default function Modal({ ...@@ -103,7 +103,7 @@ export default function Modal({
initialFocusRef, initialFocusRef,
children, children,
onSwipe = onDismiss, onSwipe = onDismiss,
scrollOverlay, $scrollOverlay,
isBottomSheet = isMobile, isBottomSheet = isMobile,
hideBorder = false, hideBorder = false,
}: ModalProps) { }: ModalProps) {
...@@ -136,7 +136,7 @@ export default function Modal({ ...@@ -136,7 +136,7 @@ export default function Modal({
onDismiss={onDismiss} onDismiss={onDismiss}
initialFocusRef={initialFocusRef} initialFocusRef={initialFocusRef}
unstable_lockFocusAcrossFrames={false} unstable_lockFocusAcrossFrames={false}
scrollOverlay={scrollOverlay} $scrollOverlay={$scrollOverlay}
> >
<StyledDialogContent <StyledDialogContent
{...(isMobile {...(isMobile
...@@ -149,7 +149,7 @@ export default function Modal({ ...@@ -149,7 +149,7 @@ export default function Modal({
$minHeight={minHeight} $minHeight={minHeight}
$maxHeight={maxHeight} $maxHeight={maxHeight}
$isBottomSheet={isBottomSheet} $isBottomSheet={isBottomSheet}
$scrollOverlay={scrollOverlay} $scrollOverlay={$scrollOverlay}
$hideBorder={hideBorder} $hideBorder={hideBorder}
$maxWidth={maxWidth} $maxWidth={maxWidth}
> >
......
...@@ -16,6 +16,7 @@ import { body, bodySmall } from 'nft/css/common.css' ...@@ -16,6 +16,7 @@ import { body, bodySmall } from 'nft/css/common.css'
import { themeVars } from 'nft/css/sprinkles.css' import { themeVars } from 'nft/css/sprinkles.css'
import { ReactNode, useReducer, useRef } from 'react' import { ReactNode, useReducer, useRef } from 'react'
import { NavLink, NavLinkProps } from 'react-router-dom' import { NavLink, NavLinkProps } from 'react-router-dom'
import styled from 'styled-components/macro'
import { isDevelopmentEnv, isStagingEnv } from 'utils/env' import { isDevelopmentEnv, isStagingEnv } from 'utils/env'
import { useToggleModal } from '../../state/application/hooks' import { useToggleModal } from '../../state/application/hooks'
...@@ -50,8 +51,13 @@ const PrimaryMenuRow = ({ ...@@ -50,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
...@@ -115,7 +121,6 @@ export const MenuDropdown = () => { ...@@ -115,7 +121,6 @@ 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)
......
import { useWeb3React } from '@web3-react/core' import { useWeb3React } from '@web3-react/core'
import AddressClaimModal from 'components/claim/AddressClaimModal' import AddressClaimModal from 'components/claim/AddressClaimModal'
import ConnectedAccountBlocked from 'components/ConnectedAccountBlocked' 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 useAccountRiskCheck from 'hooks/useAccountRiskCheck'
import NftExploreBanner from 'nft/components/nftExploreBanner/NftExploreBanner' import NftExploreBanner from 'nft/components/nftExploreBanner/NftExploreBanner'
import { lazy } from 'react' import { lazy } from 'react'
...@@ -18,20 +21,23 @@ export default function TopLevelModals() { ...@@ -18,20 +21,23 @@ export default function TopLevelModals() {
const blockedAccountModalOpen = useModalIsOpen(ApplicationModal.BLOCKED_ACCOUNT) const blockedAccountModalOpen = useModalIsOpen(ApplicationModal.BLOCKED_ACCOUNT)
const { account } = useWeb3React() const { account } = useWeb3React()
const location = useLocation() const location = useLocation()
const fiatOnrampFlagEnabled = useFiatOnrampFlag() === BaseVariant.Enabled
const pageShowsNftPromoBanner = const pageShowsNftPromoBanner =
location.pathname.startsWith('/swap') || !fiatOnrampFlagEnabled &&
location.pathname.startsWith('/tokens') || (location.pathname.startsWith('/swap') ||
location.pathname.startsWith('/pool') location.pathname.startsWith('/tokens') ||
location.pathname.startsWith('/pool'))
useAccountRiskCheck(account) useAccountRiskCheck(account)
const open = Boolean(blockedAccountModalOpen && account) const accountBlocked = Boolean(blockedAccountModalOpen && account)
return ( return (
<> <>
<AddressClaimModal isOpen={addressClaimOpen} onDismiss={addressClaimToggle} /> <AddressClaimModal isOpen={addressClaimOpen} onDismiss={addressClaimToggle} />
<ConnectedAccountBlocked account={account} isOpen={open} /> <ConnectedAccountBlocked account={account} isOpen={accountBlocked} />
<Bag /> <Bag />
<TransactionCompleteModal /> <TransactionCompleteModal />
<AirdropModal /> <AirdropModal />
{pageShowsNftPromoBanner && <NftExploreBanner />} {pageShowsNftPromoBanner && <NftExploreBanner />}
{fiatOnrampFlagEnabled && <FiatOnrampModal />}
</> </>
) )
} }
...@@ -348,7 +348,7 @@ export default function TransactionConfirmationModal({ ...@@ -348,7 +348,7 @@ export default function TransactionConfirmationModal({
// confirmation screen // confirmation screen
return ( return (
<Modal isOpen={isOpen} scrollOverlay={true} onDismiss={onDismiss} maxHeight={90}> <Modal isOpen={isOpen} $scrollOverlay={true} onDismiss={onDismiss} maxHeight={90}>
{isL2ChainId(chainId) && (hash || attemptingTxn) ? ( {isL2ChainId(chainId) && (hash || attemptingTxn) ? (
<L2Content chainId={chainId} hash={hash} onDismiss={onDismiss} pendingText={pendingText} /> <L2Content chainId={chainId} hash={hash} onDismiss={onDismiss} pendingText={pendingText} />
) : attemptingTxn ? ( ) : attemptingTxn ? (
......
...@@ -2,6 +2,7 @@ import { Trans } from '@lingui/macro' ...@@ -2,6 +2,7 @@ import { Trans } from '@lingui/macro'
import { TraceEvent } from '@uniswap/analytics' import { TraceEvent } from '@uniswap/analytics'
import { BrowserEvent, ElementName, EventName } from '@uniswap/analytics-events' import { BrowserEvent, ElementName, EventName } from '@uniswap/analytics-events'
import { useWeb3React } from '@web3-react/core' import { useWeb3React } from '@web3-react/core'
import { FiatOnrampAnnouncement } from 'components/FiatOnrampAnnouncement'
import { IconWrapper } from 'components/Identicon/StatusIcon' import { IconWrapper } from 'components/Identicon/StatusIcon'
import WalletDropdown from 'components/WalletDropdown' import WalletDropdown from 'components/WalletDropdown'
import { getConnection } from 'connection/utils' import { getConnection } from 'connection/utils'
...@@ -312,6 +313,7 @@ export default function Web3Status() { ...@@ -312,6 +313,7 @@ export default function Web3Status() {
return ( return (
<span ref={ref}> <span ref={ref}>
<Web3StatusInner /> <Web3StatusInner />
<FiatOnrampAnnouncement />
<WalletModal ENSName={ENSName ?? undefined} pendingTransactions={pending} confirmedTransactions={confirmed} /> <WalletModal ENSName={ENSName ?? undefined} pendingTransactions={pending} confirmedTransactions={confirmed} />
<Portal> <Portal>
<span ref={walletRef}> <span ref={walletRef}>
......
export enum FeatureFlag { export enum FeatureFlag {
fiatOnramp = 'fiatOnramp',
traceJsonRpc = 'traceJsonRpc', traceJsonRpc = 'traceJsonRpc',
permit2 = 'permit2', permit2 = 'permit2',
} }
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
export function useFiatOnrampFlag(): BaseVariant {
return useBaseFlag(FeatureFlag.fiatOnramp)
}
...@@ -111,7 +111,7 @@ const HeaderWrapper = styled.div<{ transparent?: boolean }>` ...@@ -111,7 +111,7 @@ const HeaderWrapper = styled.div<{ transparent?: boolean }>`
justify-content: space-between; justify-content: space-between;
position: fixed; position: fixed;
top: 0; top: 0;
z-index: ${Z_INDEX.sticky}; z-index: ${Z_INDEX.dropdown};
` `
const Marginer = styled.div` const Marginer = styled.div`
......
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()
...@@ -21,6 +91,11 @@ export function useCloseModal(_modal: ApplicationModal): () => void { ...@@ -21,6 +91,11 @@ export function useCloseModal(_modal: ApplicationModal): () => void {
return useCallback(() => dispatch(setOpenModal(null)), [dispatch]) 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 { export function useToggleWalletModal(): () => void {
return useToggleModal(ApplicationModal.WALLET) return useToggleModal(ApplicationModal.WALLET)
} }
......
...@@ -15,36 +15,39 @@ export type PopupContent = ...@@ -15,36 +15,39 @@ export type PopupContent =
export enum ApplicationModal { export enum ApplicationModal {
ADDRESS_CLAIM, ADDRESS_CLAIM,
UNISWAP_NFT_AIRDROP_CLAIM,
BLOCKED_ACCOUNT, BLOCKED_ACCOUNT,
DELEGATE,
CLAIM_POPUP, CLAIM_POPUP,
DELEGATE,
EXECUTE,
FEATURE_FLAGS,
FIAT_ONRAMP,
MENU, MENU,
NETWORK_FILTER,
NETWORK_SELECTOR, NETWORK_SELECTOR,
POOL_OVERVIEW_OPTIONS, POOL_OVERVIEW_OPTIONS,
PRIVACY_POLICY, PRIVACY_POLICY,
QUEUE,
SELF_CLAIM, SELF_CLAIM,
SETTINGS, SETTINGS,
SHARE,
TIME_SELECTOR,
VOTE, VOTE,
WALLET, WALLET,
WALLET_DROPDOWN, WALLET_DROPDOWN,
QUEUE, UNISWAP_NFT_AIRDROP_CLAIM,
EXECUTE,
TIME_SELECTOR,
SHARE,
NETWORK_FILTER,
FEATURE_FLAGS,
} }
type PopupList = Array<{ key: string; show: boolean; content: PopupContent; removeAfterMs: number | null }> type PopupList = Array<{ key: string; show: boolean; content: PopupContent; removeAfterMs: number | null }>
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: [],
...@@ -54,6 +57,9 @@ const applicationSlice = createSlice({ ...@@ -54,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
...@@ -81,5 +87,6 @@ const applicationSlice = createSlice({ ...@@ -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 export default applicationSlice.reducer
import { useWeb3React } from '@web3-react/core' import { useWeb3React } from '@web3-react/core'
import useDebounce from 'hooks/useDebounce' import useDebounce from 'hooks/useDebounce'
import useIsWindowVisible from 'hooks/useIsWindowVisible' import useIsWindowVisible from 'hooks/useIsWindowVisible'
import { useEffect, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { useAppDispatch } from 'state/hooks' import { useAppDispatch } from 'state/hooks'
import { supportedChainId } from 'utils/supportedChainId' import { supportedChainId } from 'utils/supportedChainId'
import { updateChainId } from './reducer' import { useCloseModal } from './hooks'
import { ApplicationModal, updateChainId } from './reducer'
export default function Updater(): null { export default function Updater(): null {
const { chainId, provider } = useWeb3React() const { account, chainId, provider } = useWeb3React()
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const windowVisible = useIsWindowVisible() const windowVisible = useIsWindowVisible()
const [activeChainId, setActiveChainId] = useState(chainId) 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(() => { useEffect(() => {
if (provider && chainId && windowVisible) { if (provider && chainId && windowVisible) {
setActiveChainId(chainId) setActiveChainId(chainId)
......
...@@ -4,6 +4,8 @@ import { useWeb3React } from '@web3-react/core' ...@@ -4,6 +4,8 @@ import { useWeb3React } from '@web3-react/core'
import { L2_CHAIN_IDS } from 'constants/chains' import { L2_CHAIN_IDS } from 'constants/chains'
import { SupportedLocale } from 'constants/locales' import { SupportedLocale } from 'constants/locales'
import { L2_DEADLINE_FROM_NOW } from 'constants/misc' import { L2_DEADLINE_FROM_NOW } from 'constants/misc'
import { BaseVariant } from 'featureFlags'
import { useFiatOnrampFlag } from 'featureFlags/flags/fiatOnramp'
import JSBI from 'jsbi' import JSBI from 'jsbi'
import { useCallback, useMemo } from 'react' import { useCallback, useMemo } from 'react'
import { shallowEqual } from 'react-redux' import { shallowEqual } from 'react-redux'
...@@ -17,6 +19,7 @@ import { AppState } from '../index' ...@@ -17,6 +19,7 @@ import { AppState } from '../index'
import { import {
addSerializedPair, addSerializedPair,
addSerializedToken, addSerializedToken,
updateFiatOnrampAcknowledgments,
updateHideClosedPositions, updateHideClosedPositions,
updateHideNFTWelcomeModal, updateHideNFTWelcomeModal,
updateShowNftPromoBanner, updateShowNftPromoBanner,
...@@ -105,9 +108,29 @@ export function useExpertModeManager(): [boolean, () => void] { ...@@ -105,9 +108,29 @@ export function useExpertModeManager(): [boolean, () => void] {
return [expertMode, toggleSetExpertMode] 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] { export function useHideNFTWelcomeModal(): [boolean | undefined, () => void] {
const dispatch = useAppDispatch() 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(() => { const hideModal = useCallback(() => {
dispatch(updateHideNFTWelcomeModal({ hideNFTWelcomeModal: true })) dispatch(updateHideNFTWelcomeModal({ hideNFTWelcomeModal: true }))
}, [dispatch]) }, [dispatch])
......
...@@ -9,6 +9,8 @@ import { SerializedPair, SerializedToken } from './types' ...@@ -9,6 +9,8 @@ import { SerializedPair, SerializedToken } from './types'
const currentTimestamp = () => new Date().getTime() const currentTimestamp = () => new Date().getTime()
export interface UserState { export interface UserState {
fiatOnrampAcknowledgments: { renderCount: number; system: boolean; user: boolean }
selectedWallet?: ConnectionType selectedWallet?: ConnectionType
// the timestamp of the last updateVersion action // the timestamp of the last updateVersion action
...@@ -61,6 +63,7 @@ function pairKey(token0Address: string, token1Address: string) { ...@@ -61,6 +63,7 @@ function pairKey(token0Address: string, token1Address: string) {
} }
export const initialState: UserState = { export const initialState: UserState = {
fiatOnrampAcknowledgments: { renderCount: 0, system: false, user: false },
selectedWallet: undefined, selectedWallet: undefined,
matchesDarkMode: false, matchesDarkMode: false,
userDarkMode: null, userDarkMode: null,
...@@ -84,6 +87,12 @@ const userSlice = createSlice({ ...@@ -84,6 +87,12 @@ const userSlice = createSlice({
name: 'user', name: 'user',
initialState, initialState,
reducers: { reducers: {
updateFiatOnrampAcknowledgments(
state,
{ payload }: { payload: Partial<{ renderCount: number; user: boolean; system: boolean }> }
) {
state.fiatOnrampAcknowledgments = { ...state.fiatOnrampAcknowledgments, ...payload }
},
updateSelectedWallet(state, { payload: { wallet } }) { updateSelectedWallet(state, { payload: { wallet } }) {
state.selectedWallet = wallet state.selectedWallet = wallet
}, },
...@@ -181,9 +190,10 @@ const userSlice = createSlice({ ...@@ -181,9 +190,10 @@ const userSlice = createSlice({
}) })
export const { export const {
updateSelectedWallet,
addSerializedPair, addSerializedPair,
addSerializedToken, addSerializedToken,
updateFiatOnrampAcknowledgments,
updateSelectedWallet,
updateHideClosedPositions, updateHideClosedPositions,
updateMatchesDarkMode, updateMatchesDarkMode,
updateUserClientSideRouter, updateUserClientSideRouter,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment