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

Merge pull request #5740 from Uniswap/FoR-publish

feat: FoR publish
parents e584a5fa 97236033
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 source diff could not be displayed because it is too large. You can view the blob instead.
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,12 +5,14 @@ import styled, { DefaultTheme, useTheme } from 'styled-components/macro' ...@@ -5,12 +5,14 @@ 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'>
const ButtonOverlay = styled.div` const ButtonOverlay = styled.div`
background-color: transparent; background-color: transparent;
bottom: 0; bottom: 0;
border-radius: 16px; border-radius: inherit;
height: 100%; height: 100%;
left: 0; left: 0;
position: absolute; position: absolute;
...@@ -19,6 +21,7 @@ const ButtonOverlay = styled.div` ...@@ -19,6 +21,7 @@ const ButtonOverlay = styled.div`
transition: 150ms ease background-color; transition: 150ms ease background-color;
width: 100%; width: 100%;
` `
type BaseButtonProps = { type BaseButtonProps = {
padding?: string padding?: string
width?: string width?: string
...@@ -504,16 +507,13 @@ const BaseThemeButton = styled.button<BaseThemeButtonProps>` ...@@ -504,16 +507,13 @@ const BaseThemeButton = styled.button<BaseThemeButtonProps>`
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};
...@@ -524,6 +524,17 @@ const BaseThemeButton = styled.button<BaseThemeButtonProps>` ...@@ -524,6 +524,17 @@ const BaseThemeButton = styled.button<BaseThemeButtonProps>`
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'>, BaseThemeButtonProps {} interface ThemeButtonProps extends React.ComponentPropsWithoutRef<'button'>, BaseThemeButtonProps {}
......
import { BaseVariant, FeatureFlag, featureFlagSettings, useUpdateFlag } from 'featureFlags' import { BaseVariant, FeatureFlag, featureFlagSettings, useUpdateFlag } from 'featureFlags'
import { useFiatOnrampFlag } from 'featureFlags/flags/fiatOnramp'
import { LandingRedirectVariant, useLandingRedirectFlag } from 'featureFlags/flags/landingRedirect' import { LandingRedirectVariant, useLandingRedirectFlag } from 'featureFlags/flags/landingRedirect'
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'
...@@ -215,6 +216,12 @@ export default function FeatureFlagModal() { ...@@ -215,6 +216,12 @@ export default function FeatureFlagModal() {
featureFlag={FeatureFlag.landingRedirect} featureFlag={FeatureFlag.landingRedirect}
label="Landing Page Redirect" label="Landing Page Redirect"
/> />
<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])
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)
......
...@@ -10,7 +10,6 @@ const baseNavDropdown = style([ ...@@ -10,7 +10,6 @@ const baseNavDropdown = style([
borderWidth: '1px', borderWidth: '1px',
paddingBottom: '8', paddingBottom: '8',
paddingTop: '8', paddingTop: '8',
zIndex: '2',
}), }),
{ {
boxShadow: '0px 4px 12px 0px #00000026', boxShadow: '0px 4px 12px 0px #00000026',
......
import { Box, BoxProps } from 'nft/components/Box' import { Box, BoxProps } from 'nft/components/Box'
import { useIsMobile } from 'nft/hooks' import { useIsMobile } from 'nft/hooks'
import { ForwardedRef, forwardRef } from 'react' import { ForwardedRef, forwardRef } from 'react'
import { Z_INDEX } from 'theme/zIndex'
import * as styles from './NavDropdown.css' import * as styles from './NavDropdown.css'
export const NavDropdown = forwardRef((props: BoxProps, ref: ForwardedRef<HTMLElement>) => { export const NavDropdown = forwardRef((props: BoxProps, ref: ForwardedRef<HTMLElement>) => {
const isMobile = useIsMobile() const isMobile = useIsMobile()
return <Box ref={ref} className={isMobile ? styles.mobileNavDropdown : styles.NavDropdown} {...props} /> return (
<Box
ref={ref}
style={{ zIndex: Z_INDEX.modal }}
className={isMobile ? styles.mobileNavDropdown : styles.NavDropdown}
{...props}
/>
)
}) })
NavDropdown.displayName = 'NavDropdown' NavDropdown.displayName = 'NavDropdown'
...@@ -4,4 +4,4 @@ export const LARGE_MEDIA_BREAKPOINT = '840px' ...@@ -4,4 +4,4 @@ export const LARGE_MEDIA_BREAKPOINT = '840px'
export const MEDIUM_MEDIA_BREAKPOINT = '720px' export const MEDIUM_MEDIA_BREAKPOINT = '720px'
export const SMALL_MEDIA_BREAKPOINT = '540px' export const SMALL_MEDIA_BREAKPOINT = '540px'
export const MOBILE_MEDIA_BREAKPOINT = '420px' export const MOBILE_MEDIA_BREAKPOINT = '420px'
export const SMALL_MOBILE_MEDIA_BREAKPOINT = '390px' // export const SMALL_MOBILE_MEDIA_BREAKPOINT = '390px'
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 { lazy } from 'react' import { lazy } from 'react'
import { useLocation } from 'react-router-dom'
import { useModalIsOpen, useToggleModal } from 'state/application/hooks' import { useModalIsOpen, useToggleModal } from 'state/application/hooks'
import { ApplicationModal } from 'state/application/reducer' import { ApplicationModal } from 'state/application/reducer'
...@@ -17,21 +18,18 @@ export default function TopLevelModals() { ...@@ -17,21 +18,18 @@ export default function TopLevelModals() {
const addressClaimToggle = useToggleModal(ApplicationModal.ADDRESS_CLAIM) const addressClaimToggle = useToggleModal(ApplicationModal.ADDRESS_CLAIM)
const blockedAccountModalOpen = useModalIsOpen(ApplicationModal.BLOCKED_ACCOUNT) const blockedAccountModalOpen = useModalIsOpen(ApplicationModal.BLOCKED_ACCOUNT)
const { account } = useWeb3React() const { account } = useWeb3React()
const location = useLocation()
const pageShowsNftPromoBanner =
location.pathname.startsWith('/swap') ||
location.pathname.startsWith('/tokens') ||
location.pathname.startsWith('/pool')
useAccountRiskCheck(account) useAccountRiskCheck(account)
const open = Boolean(blockedAccountModalOpen && account) const accountBlocked = Boolean(blockedAccountModalOpen && account)
const fiatOnrampFlagEnabled = useFiatOnrampFlag() === BaseVariant.Enabled
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 />} {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 ? (
......
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import { formatUSDPrice } from '@uniswap/conedison/format'
import { CurrencyAmount, Token } from '@uniswap/sdk-core' import { CurrencyAmount, Token } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core' import { useWeb3React } from '@web3-react/core'
import { ButtonEmphasis, ButtonSize, LoadingButtonSpinner, ThemeButton } from 'components/Button'
import Tooltip from 'components/Tooltip'
import { getConnection } from 'connection/utils' import { getConnection } from 'connection/utils'
import { getChainInfoOrDefault } from 'constants/chainInfo' import { getChainInfoOrDefault } from 'constants/chainInfo'
import { SupportedChainId } from 'constants/chains' import { SupportedChainId } from 'constants/chains'
import { BaseVariant } from 'featureFlags'
import { useFiatOnrampFlag } from 'featureFlags/flags/fiatOnramp'
import useCopyClipboard from 'hooks/useCopyClipboard' import useCopyClipboard from 'hooks/useCopyClipboard'
import useStablecoinPrice from 'hooks/useStablecoinPrice' import useStablecoinPrice from 'hooks/useStablecoinPrice'
import useNativeCurrency from 'lib/hooks/useNativeCurrency' import useNativeCurrency from 'lib/hooks/useNativeCurrency'
import ms from 'ms.macro'
import { useProfilePageState, useSellAsset, useWalletCollections } from 'nft/hooks' import { useProfilePageState, useSellAsset, useWalletCollections } from 'nft/hooks'
import { useIsNftClaimAvailable } from 'nft/hooks/useIsNftClaimAvailable' import { useIsNftClaimAvailable } from 'nft/hooks/useIsNftClaimAvailable'
import { ProfilePageStateType } from 'nft/types' import { ProfilePageStateType } from 'nft/types'
import { useCallback, useMemo } from 'react' import { useCallback, useEffect, useMemo, useState } from 'react'
import { Copy, ExternalLink, Power } from 'react-feather' import { Copy, CreditCard, ExternalLink as ExternalLinkIcon, Info, Power } from 'react-feather'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { Text } from 'rebass' import { Text } from 'rebass'
import { useCurrencyBalanceString } from 'state/connection/hooks' import { useCurrencyBalanceString } from 'state/connection/hooks'
import { useAppDispatch } from 'state/hooks' import { useAppDispatch } from 'state/hooks'
import { useFiatOnrampAck } from 'state/user/hooks'
import { updateSelectedWallet } from 'state/user/reducer' import { updateSelectedWallet } from 'state/user/reducer'
import styled, { css } from 'styled-components/macro' import styled, { css, keyframes } from 'styled-components/macro'
import { ThemedText } from 'theme' import { ExternalLink, ThemedText } from 'theme'
import { shortenAddress } from '../../nft/utils/address' import { shortenAddress } from '../../nft/utils/address'
import { useCloseModal, useToggleModal } from '../../state/application/hooks' import { useCloseModal, useFiatOnrampAvailability, useOpenModal, useToggleModal } from '../../state/application/hooks'
import { ApplicationModal } from '../../state/application/reducer' import { ApplicationModal } from '../../state/application/reducer'
import { useUserHasAvailableClaim, useUserUnclaimedAmount } from '../../state/claim/hooks' import { useUserHasAvailableClaim, useUserUnclaimedAmount } from '../../state/claim/hooks'
import { ButtonEmphasis, ButtonSize, ThemeButton } from '../Button'
import StatusIcon from '../Identicon/StatusIcon' import StatusIcon from '../Identicon/StatusIcon'
import IconButton, { IconHoverText } from './IconButton' import IconButton, { IconHoverText } from './IconButton'
const 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)` const WalletButton = styled(ThemeButton)`
border-radius: 12px; border-radius: 12px;
padding-top: 10px; padding-top: 10px;
...@@ -75,7 +111,20 @@ const USDText = styled.div` ...@@ -75,7 +111,20 @@ const USDText = styled.div`
color: ${({ theme }) => theme.textSecondary}; color: ${({ theme }) => theme.textSecondary};
margin-top: 8px; margin-top: 8px;
` `
const FiatOnrampNotAvailableText = styled(ThemedText.Caption)`
align-items: center;
color: ${({ theme }) => theme.textSecondary};
display: flex;
justify-content: center;
`
const FiatOnrampAvailabilityExternalLink = styled(ExternalLink)`
align-items: center;
display: flex;
height: 14px;
justify-content: center;
margin-left: 6px;
width: 14px;
`
const FlexContainer = styled.div` const FlexContainer = styled.div`
display: flex; display: flex;
` `
...@@ -108,7 +157,14 @@ const AccountContainer = styled(ThemedText.BodySmall)` ...@@ -108,7 +157,14 @@ const AccountContainer = styled(ThemedText.BodySmall)`
color: ${({ theme }) => theme.textSecondary}; color: ${({ theme }) => theme.textSecondary};
margin-top: 2.5px; margin-top: 2.5px;
` `
const StyledInfoIcon = styled(Info)`
height: 12px;
width: 12px;
flex: 1 1 auto;
`
const StyledLoadingButtonSpinner = styled(LoadingButtonSpinner)`
fill: ${({ theme }) => theme.accentAction};
`
const BalanceWrapper = styled.div` const BalanceWrapper = styled.div`
padding: 16px 0; padding: 16px 0;
` `
...@@ -137,7 +193,6 @@ const AuthenticatedHeader = () => { ...@@ -137,7 +193,6 @@ const AuthenticatedHeader = () => {
} = getChainInfoOrDefault(chainId ? chainId : SupportedChainId.MAINNET) } = getChainInfoOrDefault(chainId ? chainId : SupportedChainId.MAINNET)
const navigate = useNavigate() const navigate = useNavigate()
const closeModal = useCloseModal(ApplicationModal.WALLET_DROPDOWN) const closeModal = useCloseModal(ApplicationModal.WALLET_DROPDOWN)
const setSellPageState = useProfilePageState((state) => state.setProfilePageState) const setSellPageState = useProfilePageState((state) => state.setProfilePageState)
const resetSellAssets = useSellAsset((state) => state.reset) const resetSellAssets = useSellAsset((state) => state.reset)
const clearCollectionFilters = useWalletCollections((state) => state.clearCollectionFilters) const clearCollectionFilters = useWalletCollections((state) => state.clearCollectionFilters)
...@@ -147,7 +202,7 @@ const AuthenticatedHeader = () => { ...@@ -147,7 +202,7 @@ const AuthenticatedHeader = () => {
const isUnclaimed = useUserHasAvailableClaim(account) const isUnclaimed = useUserHasAvailableClaim(account)
const connectionType = getConnection(connector).type const connectionType = getConnection(connector).type
const nativeCurrency = useNativeCurrency() const nativeCurrency = useNativeCurrency()
const nativeCurrencyPrice = useStablecoinPrice(nativeCurrency ?? undefined) || 0 const nativeCurrencyPrice = useStablecoinPrice(nativeCurrency ?? undefined)
const openClaimModal = useToggleModal(ApplicationModal.ADDRESS_CLAIM) const openClaimModal = useToggleModal(ApplicationModal.ADDRESS_CLAIM)
const openNftModal = useToggleModal(ApplicationModal.UNISWAP_NFT_AIRDROP_CLAIM) const openNftModal = useToggleModal(ApplicationModal.UNISWAP_NFT_AIRDROP_CLAIM)
const disconnect = useCallback(() => { const disconnect = useCallback(() => {
...@@ -159,18 +214,63 @@ const AuthenticatedHeader = () => { ...@@ -159,18 +214,63 @@ const AuthenticatedHeader = () => {
}, [connector, dispatch]) }, [connector, dispatch])
const amountUSD = useMemo(() => { const amountUSD = useMemo(() => {
if (!nativeCurrencyPrice || !balanceString) return undefined
const price = parseFloat(nativeCurrencyPrice.toFixed(5)) const price = parseFloat(nativeCurrencyPrice.toFixed(5))
const balance = parseFloat(balanceString || '0') const balance = parseFloat(balanceString)
return price * balance return price * balance
}, [balanceString, nativeCurrencyPrice]) }, [balanceString, nativeCurrencyPrice])
const navigateToProfile = () => { const navigateToProfile = useCallback(() => {
resetSellAssets() resetSellAssets()
setSellPageState(ProfilePageStateType.VIEWING) setSellPageState(ProfilePageStateType.VIEWING)
clearCollectionFilters() clearCollectionFilters()
navigate('/nfts/profile') navigate('/nfts/profile')
closeModal() 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 ( return (
<AuthenticatedHeaderWrapper> <AuthenticatedHeaderWrapper>
...@@ -192,7 +292,7 @@ const AuthenticatedHeader = () => { ...@@ -192,7 +292,7 @@ const AuthenticatedHeader = () => {
<IconButton onClick={copy} Icon={Copy}> <IconButton onClick={copy} Icon={Copy}>
{isCopied ? <Trans>Copied!</Trans> : <Trans>Copy</Trans>} {isCopied ? <Trans>Copied!</Trans> : <Trans>Copy</Trans>}
</IconButton> </IconButton>
<IconButton href={`${explorer}address/${account}`} target="_blank" Icon={ExternalLink}> <IconButton href={`${explorer}address/${account}`} target="_blank" Icon={ExternalLinkIcon}>
<Trans>Explore</Trans> <Trans>Explore</Trans>
</IconButton> </IconButton>
<IconButton data-testid="wallet-disconnect" onClick={disconnect} Icon={Power}> <IconButton data-testid="wallet-disconnect" onClick={disconnect} Icon={Power}>
...@@ -205,7 +305,7 @@ const AuthenticatedHeader = () => { ...@@ -205,7 +305,7 @@ const AuthenticatedHeader = () => {
<Text fontSize={36} fontWeight={400}> <Text fontSize={36} fontWeight={400}>
{balanceString} {nativeCurrencySymbol} {balanceString} {nativeCurrencySymbol}
</Text> </Text>
<USDText>${amountUSD.toFixed(2)} USD</USDText> {amountUSD !== undefined && <USDText>{formatUSDPrice(amountUSD)} USD</USDText>}
</BalanceWrapper> </BalanceWrapper>
<ProfileButton <ProfileButton
data-testid="nft-view-self-nfts" data-testid="nft-view-self-nfts"
...@@ -215,6 +315,44 @@ const AuthenticatedHeader = () => { ...@@ -215,6 +315,44 @@ const AuthenticatedHeader = () => {
> >
<Trans>View and sell NFTs</Trans> <Trans>View and sell NFTs</Trans>
</ProfileButton> </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 && ( {isUnclaimed && (
<UNIButton onClick={openClaimModal} size={ButtonSize.medium} emphasis={ButtonEmphasis.medium}> <UNIButton onClick={openClaimModal} size={ButtonSize.medium} emphasis={ButtonEmphasis.medium}>
<Trans>Claim</Trans> {unclaimedAmount?.toFixed(0, { groupSeparator: ',' } ?? '-')} <Trans>reward</Trans> <Trans>Claim</Trans> {unclaimedAmount?.toFixed(0, { groupSeparator: ',' } ?? '-')} <Trans>reward</Trans>
......
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import { TraceEvent } from '@uniswap/analytics' import { sendAnalyticsEvent, 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'
...@@ -9,7 +10,7 @@ import { Portal } from 'nft/components/common/Portal' ...@@ -9,7 +10,7 @@ import { Portal } from 'nft/components/common/Portal'
import { useIsNftClaimAvailable } from 'nft/hooks/useIsNftClaimAvailable' import { useIsNftClaimAvailable } from 'nft/hooks/useIsNftClaimAvailable'
import { getIsValidSwapQuote } from 'pages/Swap' import { getIsValidSwapQuote } from 'pages/Swap'
import { darken } from 'polished' import { darken } from 'polished'
import { useMemo, useRef } from 'react' import { useCallback, useMemo, useRef } from 'react'
import { AlertTriangle, ChevronDown, ChevronUp } from 'react-feather' import { AlertTriangle, ChevronDown, ChevronUp } from 'react-feather'
import { useAppSelector } from 'state/hooks' import { useAppSelector } from 'state/hooks'
import { useDerivedSwapInfo } from 'state/swap/hooks' import { useDerivedSwapInfo } from 'state/swap/hooks'
...@@ -205,6 +206,10 @@ function Web3StatusInner() { ...@@ -205,6 +206,10 @@ function Web3StatusInner() {
const validSwapQuote = getIsValidSwapQuote(trade, tradeState, swapInputError) const validSwapQuote = getIsValidSwapQuote(trade, tradeState, swapInputError)
const theme = useTheme() const theme = useTheme()
const toggleWalletDropdown = useToggleWalletDropdown() const toggleWalletDropdown = useToggleWalletDropdown()
const handleWalletDropdownClick = useCallback(() => {
sendAnalyticsEvent('FOR Account Dropdown Button Clicks')
toggleWalletDropdown()
}, [toggleWalletDropdown])
const toggleWalletModal = useToggleWalletModal() const toggleWalletModal = useToggleWalletModal()
const walletIsOpen = useModalIsOpen(ApplicationModal.WALLET_DROPDOWN) const walletIsOpen = useModalIsOpen(ApplicationModal.WALLET_DROPDOWN)
const isClaimAvailable = useIsNftClaimAvailable((state) => state.isClaimAvailable) const isClaimAvailable = useIsNftClaimAvailable((state) => state.isClaimAvailable)
...@@ -221,13 +226,12 @@ function Web3StatusInner() { ...@@ -221,13 +226,12 @@ function Web3StatusInner() {
const pending = sortedRecentTransactions.filter((tx) => !tx.receipt).map((tx) => tx.hash) const pending = sortedRecentTransactions.filter((tx) => !tx.receipt).map((tx) => tx.hash)
const hasPendingTransactions = !!pending.length const hasPendingTransactions = !!pending.length
const toggleWallet = toggleWalletDropdown
if (!chainId) { if (!chainId) {
return null return null
} else if (error) { } else if (error) {
return ( return (
<Web3StatusError onClick={toggleWallet}> <Web3StatusError onClick={handleWalletDropdownClick}>
<NetworkIcon /> <NetworkIcon />
<Text> <Text>
<Trans>Error</Trans> <Trans>Error</Trans>
...@@ -243,7 +247,7 @@ function Web3StatusInner() { ...@@ -243,7 +247,7 @@ function Web3StatusInner() {
return ( return (
<Web3StatusConnected <Web3StatusConnected
data-testid="web3-status-connected" data-testid="web3-status-connected"
onClick={toggleWallet} onClick={handleWalletDropdownClick}
pending={hasPendingTransactions} pending={hasPendingTransactions}
isClaimAvailable={isClaimAvailable} isClaimAvailable={isClaimAvailable}
> >
...@@ -281,7 +285,7 @@ function Web3StatusInner() { ...@@ -281,7 +285,7 @@ function Web3StatusInner() {
<Trans>Connect</Trans> <Trans>Connect</Trans>
</StyledConnectButton> </StyledConnectButton>
<VerticalDivider /> <VerticalDivider />
<ChevronWrapper onClick={toggleWalletDropdown} data-testid="navbar-toggle-dropdown"> <ChevronWrapper onClick={handleWalletDropdownClick} data-testid="navbar-toggle-dropdown">
{walletIsOpen ? <ChevronUp {...chevronProps} /> : <ChevronDown {...chevronProps} />} {walletIsOpen ? <ChevronUp {...chevronProps} /> : <ChevronDown {...chevronProps} />}
</ChevronWrapper> </ChevronWrapper>
</Web3StatusConnectWrapper> </Web3StatusConnectWrapper>
...@@ -312,6 +316,7 @@ export default function Web3Status() { ...@@ -312,6 +316,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',
landingRedirect = 'landingRedirect', landingRedirect = 'landingRedirect',
......
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
export function useFiatOnrampFlag(): BaseVariant {
return useBaseFlag(FeatureFlag.fiatOnramp)
}
/* eslint-disable @typescript-eslint/no-var-requires */
import Modal from 'components/Modal'
import { useState } from 'react'
import { X } from 'react-feather'
import styled, { useTheme } from 'styled-components/macro'
import { ExternalLink } from 'theme/components'
import { ThemedText } from 'theme/components/text'
const Container = styled.div`
position: relative;
display: flex;
padding: 30% 24px 24px;
overflow: hidden;
height: fit-content;
user-select: none;
`
const CloseButton = styled(X)`
position: absolute;
top: 20px;
right: 24px;
cursor: pointer;
`
const Background = styled.img`
position: absolute;
top: 0;
left: 0;
width: 100%;
object-fit: contain;
`
const Content = styled.div`
display: flex;
flex-direction: column;
z-index: 1;
gap: 16px;
`
const Link = styled(ExternalLink)`
color: ${({ theme }) => theme.accentActive};
stroke: ${({ theme }) => theme.accentActive};
`
const Title = styled(ThemedText.LargeHeader)`
@media (max-width: ${({ theme }) => theme.breakpoint.xl}px) {
font-size: 20px !important;
}
`
const Paragraph = styled(ThemedText.BodySecondary)`
line-height: 24px;
@media (max-width: ${({ theme }) => theme.breakpoint.xl}px) {
font-size: 14px !important;
line-height: 20px;
}
`
const BACKGROUND_IMAGE = {
dark: {
src: require('../../../assets/images/welcomeModal-dark.jpg').default,
srcSet: `
${require('../../../assets/images/welcomeModal-dark@2x.jpg').default} 2x,
${require('../../../assets/images/welcomeModal-dark@3x.jpg').default} 3x,
`,
},
light: {
src: require('../../../assets/images/welcomeModal-light.jpg').default,
srcSet: `
${require('../../../assets/images/welcomeModal-light@2x.jpg').default} 2x,
${require('../../../assets/images/welcomeModal-light@3x.jpg').default} 3x,
`,
},
}
export function WelcomeModal({ onDismissed }: { onDismissed: () => void }) {
const [isOpen, setIsOpen] = useState(true)
const dismiss = () => {
setIsOpen(false)
setTimeout(() => onDismissed())
}
const theme = useTheme()
return (
<Modal isOpen={isOpen} onSwipe={dismiss} maxWidth={720} isBottomSheet={false}>
<Container data-testid="nft-welcome-modal">
<Background
{...(theme.darkMode ? BACKGROUND_IMAGE.dark : BACKGROUND_IMAGE.light)}
alt="Welcome modal background"
draggable={false}
/>
<Content>
<Title>Introducing NFTs on Uniswap</Title>
<Paragraph>
You can now buy and sell NFTs on Uniswap across marketplaces. Trade here to find more listings and better
prices. <br />
<br />
NFTs on Uniswap replaces Genie, which was{' '}
<Link href="https://uniswap.org/blog/genie" title="Uniswap Labs has acquired Genie">
acquired{' '}
</Link>{' '}
by Uniswap Labs earlier this year. If you have used Genie in the past, you may be eligible for a USDC
airdrop.{' '}
<Link
href="https://uniswap.org/blog/uniswap-nft-aggregator-announcement"
title="Uniswap NFT aggregator announcement"
>
Learn more.
</Link>
</Paragraph>
<CloseButton data-testid="nft-intro-modal" size={24} onClick={dismiss} />
</Content>
</Container>
</Modal>
)
}
import { Trans } from '@lingui/macro'
import { LARGE_MEDIA_BREAKPOINT, SMALL_MOBILE_MEDIA_BREAKPOINT } from 'components/Tokens/constants'
import { Box } from 'nft/components/Box'
import { bodySmall, subhead } from 'nft/css/common.css'
import { X } from 'react-feather'
import { useNavigate } from 'react-router-dom'
import { Link } from 'react-router-dom'
import { useHideNftPromoBanner } from 'state/user/hooks'
import styled, { css } from 'styled-components/macro'
import { ClickableStyle } from 'theme'
import { Z_INDEX } from 'theme/zIndex'
import nftPromoImage1 from '../nftExploreBanner/nftArt1.png'
import nftPromoImage2 from '../nftExploreBanner/nftArt2.png'
import nftPromoImage3 from '../nftExploreBanner/nftArt3.png'
function getRandom(list: any[]) {
return list[Math.floor(Math.random() * list.length)]
}
const randomizedNftImage = getRandom([nftPromoImage1, nftPromoImage2, nftPromoImage3])
const PopupContainer = styled.div<{ show: boolean }>`
background-color: ${({ theme }) => theme.backgroundSurface};
box-shadow: ${({ theme }) => theme.deepShadow};
border: 1px solid ${({ theme }) => theme.backgroundOutline};
border-radius: 12px;
cursor: pointer;
color: ${({ theme }) => theme.textPrimary};
display: ${({ show }) => (show ? 'flex' : 'none')};
flex-direction: column;
position: fixed;
right: clamp(0px, 1vw, 16px);
z-index: ${Z_INDEX.sticky};
transition: ${({
theme: {
transition: { duration, timing },
},
}) => `${duration.slow} opacity ${timing.in}`};
width: 98vw;
bottom: 55px;
@media screen and (min-width: ${LARGE_MEDIA_BREAKPOINT}) {
bottom: 48px;
}
@media screen and (min-width: ${SMALL_MOBILE_MEDIA_BREAKPOINT}) {
width: 391px;
}
:hover {
border: double 1px transparent;
border-radius: 12px;
background-image: ${({ theme }) =>
`linear-gradient(${theme.backgroundSurface}, ${theme.backgroundSurface}),
radial-gradient(circle at top left, hsla(299, 100%, 87%, 1), hsla(299, 100%, 61%, 1))`};
background-origin: border-box;
background-clip: padding-box, border-box;
}
`
const InnerContainer = styled.div`
overflow: hidden;
display: flex;
position: relative;
gap: 8px;
padding: 12px;
`
const TextContainer = styled.div`
display: flex;
flex-direction: column;
flex: 1;
justify-content: flex-start;
`
const StyledXButton = styled(X)`
color: ${({ theme }) => theme.textSecondary};
&:hover {
opacity: ${({ theme }) => theme.opacity.hover};
}
&:active {
opacity: ${({ theme }) => theme.opacity.click};
}
`
const StyledImageContainer = styled(Box)`
width: 20%;
cursor: pointer;
aspectratio: 1;
transition: transform 0.25s ease 0s;
object-fit: contain;
`
const LinkStyle = css`
color: ${({ theme }) => theme.accentActive};
stroke: ${({ theme }) => theme.accentActive};
`
const StyledLink = styled(Link)`
${ClickableStyle}
${LinkStyle}
`
export default function NftExploreBanner() {
const [hideNftPromoBanner, toggleHideNftPromoBanner] = useHideNftPromoBanner()
const navigate = useNavigate()
const navigateToNfts = () => {
navigate('/nfts')
toggleHideNftPromoBanner()
}
return (
<PopupContainer show={!hideNftPromoBanner} onClick={navigateToNfts}>
<InnerContainer>
<StyledImageContainer as="img" src={randomizedNftImage} draggable={false} />
<TextContainer>
{/* <HeaderText> */}
<div className={subhead}>
<Trans>Introducing NFTs on Uniswap</Trans>
</div>
{/* </HeaderText> */}
{/* <Description> */}
<div className={bodySmall}>
<Trans>Buy and sell NFTs across more listings at better prices.</Trans>{' '}
<StyledLink to="/nfts">
<Trans>Explore NFTs</Trans>
</StyledLink>{' '}
</div>
</TextContainer>
{/* </Description> */}
<StyledXButton
size={20}
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
toggleHideNftPromoBanner()
}}
/>
</InnerContainer>
</PopupContainer>
)
}
...@@ -2,10 +2,8 @@ import { Trace } from '@uniswap/analytics' ...@@ -2,10 +2,8 @@ import { Trace } from '@uniswap/analytics'
import { PageName } from '@uniswap/analytics-events' import { PageName } from '@uniswap/analytics-events'
import Banner from 'nft/components/explore/Banner' import Banner from 'nft/components/explore/Banner'
import TrendingCollections from 'nft/components/explore/TrendingCollections' import TrendingCollections from 'nft/components/explore/TrendingCollections'
import { WelcomeModal } from 'nft/components/explore/WelcomeModal'
import { useBag } from 'nft/hooks' import { useBag } from 'nft/hooks'
import { useEffect } from 'react' import { useEffect } from 'react'
import { useHideNFTWelcomeModal } from 'state/user/hooks'
import styled from 'styled-components/macro' import styled from 'styled-components/macro'
const ExploreContainer = styled.div` const ExploreContainer = styled.div`
...@@ -25,7 +23,6 @@ const ExploreContainer = styled.div` ...@@ -25,7 +23,6 @@ const ExploreContainer = styled.div`
const NftExplore = () => { const NftExplore = () => {
const setBagExpanded = useBag((state) => state.setBagExpanded) const setBagExpanded = useBag((state) => state.setBagExpanded)
const [isModalHidden, hideModal] = useHideNFTWelcomeModal()
useEffect(() => { useEffect(() => {
setBagExpanded({ bagExpanded: false, manualClose: false }) setBagExpanded({ bagExpanded: false, manualClose: false })
...@@ -38,7 +35,6 @@ const NftExplore = () => { ...@@ -38,7 +35,6 @@ const NftExplore = () => {
<Banner /> <Banner />
<TrendingCollections /> <TrendingCollections />
</ExploreContainer> </ExploreContainer>
{!isModalHidden && <WelcomeModal onDismissed={hideModal} />}
</Trace> </Trace>
</> </>
) )
......
...@@ -100,7 +100,7 @@ const HeaderWrapper = styled.div<{ transparent?: boolean }>` ...@@ -100,7 +100,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};
` `
function getCurrentPageFromLocation(locationPathname: string): PageName | undefined { function getCurrentPageFromLocation(locationPathname: string): PageName | undefined {
......
...@@ -35,7 +35,7 @@ const Gradient = styled.div<{ isDarkMode: boolean }>` ...@@ -35,7 +35,7 @@ const Gradient = styled.div<{ isDarkMode: boolean }>`
isDarkMode isDarkMode
? 'linear-gradient(rgba(8, 10, 24, 0) 0%, rgb(8 10 24 / 100%) 45%)' ? 'linear-gradient(rgba(8, 10, 24, 0) 0%, rgb(8 10 24 / 100%) 45%)'
: 'linear-gradient(rgba(255, 255, 255, 0) 0%, rgb(255 255 255 /100%) 45%)'}; : 'linear-gradient(rgba(255, 255, 255, 0) 0%, rgb(255 255 255 /100%) 45%)'};
z-index: ${Z_INDEX.dropdown}; z-index: ${Z_INDEX.under_dropdown};
pointer-events: none; pointer-events: none;
` `
...@@ -58,7 +58,7 @@ const ContentContainer = styled.div<{ isDarkMode: boolean }>` ...@@ -58,7 +58,7 @@ const ContentContainer = styled.div<{ isDarkMode: boolean }>`
max-width: min(720px, 90%); max-width: min(720px, 90%);
position: sticky; position: sticky;
bottom: 0; bottom: 0;
z-index: ${Z_INDEX.dropdown}; z-index: ${Z_INDEX.under_dropdown};
padding: 32px 0 80px; padding: 32px 0 80px;
transition: ${({ theme }) => `${theme.transition.duration.medium} ${theme.transition.timing.ease} opacity`}; transition: ${({ theme }) => `${theme.transition.duration.medium} ${theme.transition.timing.ease} opacity`};
......
import { sendAnalyticsEvent } from '@uniswap/analytics'
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()
sendAnalyticsEvent('MoonPay Geochecker', { success: result })
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 +93,11 @@ export function useCloseModal(_modal: ApplicationModal): () => void { ...@@ -21,6 +93,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)
......
...@@ -17,9 +17,8 @@ import { AppState } from '../index' ...@@ -17,9 +17,8 @@ import { AppState } from '../index'
import { import {
addSerializedPair, addSerializedPair,
addSerializedToken, addSerializedToken,
updateFiatOnrampAcknowledgments,
updateHideClosedPositions, updateHideClosedPositions,
updateHideNFTWelcomeModal,
updateShowNftPromoBanner,
updateUserClientSideRouter, updateUserClientSideRouter,
updateUserDarkMode, updateUserDarkMode,
updateUserDeadline, updateUserDeadline,
...@@ -105,13 +104,24 @@ export function useExpertModeManager(): [boolean, () => void] { ...@@ -105,13 +104,24 @@ export function useExpertModeManager(): [boolean, () => void] {
return [expertMode, toggleSetExpertMode] return [expertMode, toggleSetExpertMode]
} }
export function useHideNFTWelcomeModal(): [boolean | undefined, () => void] { interface FiatOnrampAcknowledgements {
renderCount: number
system: boolean
user: boolean
}
export function useFiatOnrampAck(): [
FiatOnrampAcknowledgements,
(acknowledgements: Partial<FiatOnrampAcknowledgements>) => void
] {
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const hideNFTWelcomeModal = useAppSelector((state) => state.user.hideNFTWelcomeModal) const fiatOnrampAcknowledgments = useAppSelector((state) => state.user.fiatOnrampAcknowledgments)
const hideModal = useCallback(() => { const setAcknowledgements = useCallback(
dispatch(updateHideNFTWelcomeModal({ hideNFTWelcomeModal: true })) (acks: Partial<FiatOnrampAcknowledgements>) => {
}, [dispatch]) dispatch(updateFiatOnrampAcknowledgments(acks))
return [hideNFTWelcomeModal, hideModal] },
[dispatch]
)
return [fiatOnrampAcknowledgments, setAcknowledgements]
} }
export function useClientSideRouter(): [boolean, (userClientSideRouter: boolean) => void] { export function useClientSideRouter(): [boolean, (userClientSideRouter: boolean) => void] {
...@@ -258,17 +268,6 @@ export function useURLWarningVisible(): boolean { ...@@ -258,17 +268,6 @@ export function useURLWarningVisible(): boolean {
return useAppSelector((state: AppState) => state.user.URLWarningVisible) return useAppSelector((state: AppState) => state.user.URLWarningVisible)
} }
export function useHideNftPromoBanner(): [boolean, () => void] {
const dispatch = useAppDispatch()
const hideNftPromoBanner = useAppSelector((state) => state.user.hideNFTPromoBanner)
const toggleHideNftPromoBanner = useCallback(() => {
dispatch(updateShowNftPromoBanner({ hideNFTPromoBanner: true }))
}, [dispatch])
return [hideNftPromoBanner, toggleHideNftPromoBanner]
}
/** /**
* Given two tokens return the liquidity token that represents its liquidity shares * Given two tokens return the liquidity token that represents its liquidity shares
* @param tokenA one of the two tokens * @param tokenA one of the two tokens
......
...@@ -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
}, },
...@@ -117,12 +126,6 @@ const userSlice = createSlice({ ...@@ -117,12 +126,6 @@ const userSlice = createSlice({
updateHideClosedPositions(state, action) { updateHideClosedPositions(state, action) {
state.userHideClosedPositions = action.payload.userHideClosedPositions state.userHideClosedPositions = action.payload.userHideClosedPositions
}, },
updateHideNFTWelcomeModal(state, action) {
state.hideNFTWelcomeModal = action.payload.hideNFTWelcomeModal
},
updateShowNftPromoBanner(state, action) {
state.hideNFTPromoBanner = action.payload.hideNFTPromoBanner
},
addSerializedToken(state, { payload: { serializedToken } }) { addSerializedToken(state, { payload: { serializedToken } }) {
if (!state.tokens) { if (!state.tokens) {
state.tokens = {} state.tokens = {}
...@@ -181,18 +184,17 @@ const userSlice = createSlice({ ...@@ -181,18 +184,17 @@ const userSlice = createSlice({
}) })
export const { export const {
updateSelectedWallet,
addSerializedPair, addSerializedPair,
addSerializedToken, addSerializedToken,
updateFiatOnrampAcknowledgments,
updateSelectedWallet,
updateHideClosedPositions, updateHideClosedPositions,
updateMatchesDarkMode, updateMatchesDarkMode,
updateUserClientSideRouter, updateUserClientSideRouter,
updateHideNFTWelcomeModal,
updateUserDarkMode, updateUserDarkMode,
updateUserDeadline, updateUserDeadline,
updateUserExpertMode, updateUserExpertMode,
updateUserLocale, updateUserLocale,
updateUserSlippageTolerance, updateUserSlippageTolerance,
updateShowNftPromoBanner,
} = userSlice.actions } = userSlice.actions
export default userSlice.reducer export default userSlice.reducer
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
export enum Z_INDEX { export enum Z_INDEX {
deprecated_zero = 0, deprecated_zero = 0,
deprecated_content = 1, deprecated_content = 1,
under_dropdown = 990,
dropdown = 1000, dropdown = 1000,
sticky = 1020, sticky = 1020,
fixed = 1030, fixed = 1030,
......
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