Commit dfd9196a authored by aballerr's avatar aballerr Committed by GitHub

feat: Wallet p0 (#4368)

* P0 Wallet
Co-authored-by: default avatarCharles Bachmeier <charlie@genie.xyz>
Co-authored-by: default avatarAlex Ball <alexball@UNISWAP-MAC-038.fios-router.home>
parent c4362297
......@@ -4,6 +4,7 @@ import { Phase1Variant, usePhase1Flag } from 'featureFlags/flags/phase1'
import { RedesignVariant, useRedesignFlag } from 'featureFlags/flags/redesign'
import { TokensVariant, useTokensFlag } from 'featureFlags/flags/tokens'
import { TokenSafetyVariant, useTokenSafetyFlag } from 'featureFlags/flags/tokenSafety'
import { useWalletFlag, WalletVariant } from 'featureFlags/flags/wallet'
import { useAtomValue } from 'jotai/utils'
import { ReactNode, useState } from 'react'
import { X } from 'react-feather'
......@@ -195,6 +196,12 @@ export default function FeatureFlagModal() {
featureFlag={FeatureFlag.tokenSafety}
label="Token Safety"
/>
<FeatureFlagOption
variants={Object.values(WalletVariant)}
value={useWalletFlag()}
featureFlag={FeatureFlag.wallet}
label="Wallet Flag"
/>
<SaveButton onClick={() => window.location.reload()}>Save Settings</SaveButton>
</Modal>
)
......
import { Trans } from '@lingui/macro'
import useScrollPosition from '@react-hook/window-scroll'
import { useWeb3React } from '@web3-react/core'
import WalletDropdown from 'components/WalletDropdown'
import { getChainInfoOrDefault } from 'constants/chainInfo'
import { SupportedChainId } from 'constants/chains'
import { TokensVariant, useTokensFlag } from 'featureFlags/flags/tokens'
import { useWalletFlag, WalletVariant } from 'featureFlags/flags/wallet'
import { darken } from 'polished'
import { NavLink, useLocation } from 'react-router-dom'
import { Text } from 'rebass'
......@@ -217,6 +219,12 @@ const StyledNavLink = styled(NavLink)`
}
`
const WalletDropdownWrapper = styled.div`
position: absolute;
top: 75px;
right: 20px;
`
const StyledExternalLink = styled(ExternalLink)`
${({ theme }) => theme.flexRowNoWrap}
align-items: left;
......@@ -244,6 +252,7 @@ const StyledExternalLink = styled(ExternalLink)`
`
export default function Header() {
const walletFlag = useWalletFlag()
const tokensFlag = useTokensFlag()
const { account, chainId } = useWeb3React()
......@@ -346,6 +355,11 @@ export default function Header() {
) : null}
<Web3Status />
</AccountElement>
{walletFlag === WalletVariant.Enabled && (
<WalletDropdownWrapper>
<WalletDropdown />
</WalletDropdownWrapper>
)}
</HeaderElement>
<HeaderElement>
<Menu />
......
......@@ -21,7 +21,7 @@ const IconWrapper = styled.div<{ size?: number }>`
`};
`
export default function StatusIcon({ connectionType }: { connectionType: ConnectionType }) {
export default function StatusIcon({ connectionType, size }: { connectionType: ConnectionType; size?: number }) {
let image
switch (connectionType) {
case ConnectionType.INJECTED:
......@@ -38,5 +38,5 @@ export default function StatusIcon({ connectionType }: { connectionType: Connect
break
}
return <IconWrapper size={16}>{image}</IconWrapper>
return <IconWrapper size={size ?? 16}>{image}</IconWrapper>
}
import { Trans } from '@lingui/macro'
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import { getConnection } from 'connection/utils'
import { getChainInfoOrDefault } from 'constants/chainInfo'
import { SupportedChainId } from 'constants/chains'
import useCopyClipboard from 'hooks/useCopyClipboard'
import useStablecoinPrice from 'hooks/useStablecoinPrice'
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
import { useCallback, useMemo } from 'react'
import { Copy, ExternalLink, Power } from 'react-feather'
import { Text } from 'rebass'
import { useCurrencyBalanceString } from 'state/connection/hooks'
import { useAppDispatch } from 'state/hooks'
import { updateSelectedWallet } from 'state/user/reducer'
import styled from 'styled-components/macro'
import { shortenAddress } from '../../nft/utils/address'
import { useToggleModal } from '../../state/application/hooks'
import { ApplicationModal } from '../../state/application/reducer'
import { useUserHasAvailableClaim, useUserUnclaimedAmount } from '../../state/claim/hooks'
import { ButtonPrimary } from '../Button'
import StatusIcon from '../Identicon/StatusIcon'
import IconButton, { IconHoverText } from './IconButton'
const UNIbutton = styled(ButtonPrimary)`
background: linear-gradient(to right, #9139b0 0%, #4261d6 100%);
border-radius: 12px;
padding-top: 10px;
padding-bottom: 10px;
margin-top: 12px;
color: white;
border: none;
`
const Column = styled.div`
display: flex;
flex-direction: column;
text-align: center;
`
const IconContainer = styled.div`
display: flex;
align-items: center;
& > a,
& > button {
margin-right: 8px;
}
& > button:last-child {
margin-right: 0px;
${IconHoverText}:last-child {
left: 0px;
}
}
`
const USDText = styled.div`
font-size: 16px;
font-weight: 500;
color: ${({ theme }) => theme.textSecondary};
margin-top: 8px;
`
const FlexContainer = styled.div`
display: flex;
`
const StatusWrapper = styled.div`
display: inline-block;
margin-top: 4px;
`
const BalanceWrapper = styled.div`
padding: 16px 0;
`
const HeaderWrapper = styled.div`
margin-bottom: 12px;
display: flex;
justify-content: space-between;
`
const AuthenticatedHeader = () => {
const { account, chainId, connector } = useWeb3React()
const [isCopied, setCopied] = useCopyClipboard()
const copy = useCallback(() => {
setCopied(account || '')
}, [account, setCopied])
const dispatch = useAppDispatch()
const balanceString = useCurrencyBalanceString(account ?? '')
const {
nativeCurrency: { symbol: nativeCurrencySymbol },
explorer,
} = getChainInfoOrDefault(chainId ? chainId : SupportedChainId.MAINNET)
const unclaimedAmount: CurrencyAmount<Token> | undefined = useUserUnclaimedAmount(account)
const isUnclaimed = useUserHasAvailableClaim(account)
const connectionType = getConnection(connector).type
const nativeCurrency = useNativeCurrency()
const nativeCurrencyPrice = useStablecoinPrice(nativeCurrency ?? undefined) || 0
const openClaimModal = useToggleModal(ApplicationModal.ADDRESS_CLAIM)
const disconnect = useCallback(() => {
if (connector && connector.deactivate) {
connector.deactivate()
}
connector.resetState()
dispatch(updateSelectedWallet({ wallet: undefined }))
}, [connector, dispatch])
const amountUSD = useMemo(() => {
const price = parseFloat(nativeCurrencyPrice.toFixed(5))
const balance = parseFloat(balanceString || '0')
return price * balance
}, [balanceString, nativeCurrencyPrice])
return (
<>
<HeaderWrapper>
<StatusWrapper>
<FlexContainer>
<StatusIcon connectionType={connectionType} size={24} />
<Text fontSize={16} fontWeight={600} marginTop="2.5px">
{account && shortenAddress(account, 2, 4)}
</Text>
</FlexContainer>
</StatusWrapper>
<IconContainer>
<IconButton onClick={copy} Icon={Copy} text={isCopied ? <Trans>Copied!</Trans> : <Trans>Copy</Trans>} />
<IconButton href={`${explorer}address/${account}`} Icon={ExternalLink} text={<Trans>Explore</Trans>} />
<IconButton onClick={disconnect} Icon={Power} text={<Trans>Disconnect</Trans>} />
</IconContainer>
</HeaderWrapper>
<Column>
<BalanceWrapper>
<Text fontSize={36} fontWeight={400}>
{balanceString} {nativeCurrencySymbol}
</Text>
<USDText>${amountUSD.toFixed(2)} USD</USDText>
</BalanceWrapper>
{isUnclaimed && (
<UNIbutton onClick={openClaimModal}>
<Trans>Claim</Trans> {unclaimedAmount?.toFixed(0, { groupSeparator: ',' } ?? '-')} <Trans>reward</Trans>
</UNIbutton>
)}
</Column>
</>
)
}
export default AuthenticatedHeader
import { Trans } from '@lingui/macro'
import { useWeb3React } from '@web3-react/core'
import { useActiveLocale } from 'hooks/useActiveLocale'
import { useMemo } from 'react'
import { ChevronRight, Moon, Sun } from 'react-feather'
import { useToggleWalletModal } from 'state/application/hooks'
import { useDarkModeManager } from 'state/user/hooks'
import styled from 'styled-components/macro'
import { useAllTransactions } from '../../state/transactions/hooks'
import AuthenticatedHeader from './AuthenticatedHeader'
import { MenuState } from './index'
const ConnectButton = styled.button`
border: none;
outline: none;
border-radius: 12px;
height: 44px;
width: 288px;
background-color: ${({ theme }) => theme.accentAction};
color: white;
font-weight: 600;
font-size: 16px;
cursor: pointer;
`
const Divider = styled.div`
border: 1px solid ${({ theme }) => theme.backgroundOutline};
margin-top: 16px;
margin-bottom: 16px;
`
const ToggleMenuItem = styled.button`
background-color: transparent;
margin: 0;
border: none;
cursor: pointer;
display: flex;
flex: 1;
flex-direction: row;
align-items: center;
padding: 8px 0px;
justify-content: space-between;
font-size: 14px;
font-weight: 400;
width: 100%;
margin-bottom: 8px;
color: ${({ theme }) => theme.textSecondary};
:hover {
text-decoration: none;
}
`
const FlexContainer = styled.div`
display: flex;
`
const PendingBadge = styled.span`
background-color: ${({ theme }) => theme.accentActionSoft};
color: ${({ theme }) => theme.deprecated_primary3};
font-weight: 600;
padding: 4px 8px;
border-radius: 4px;
`
const IconWrap = styled.span`
display: inline-block;
margin-top: auto;
margin-bottom: auto;
margin-left: 4px;
height: 16px;
`
const DefaultMenuWrap = styled.div`
padding: 0 16px;
width: 100%;
height: 100%;
`
const DefaultText = styled.span`
font-size: 14px;
font-weight: 400;
`
const CenterVertically = styled.div`
margin-top: auto;
margin-bottom: auto;
`
const WalletDropdown = ({ setMenu }: { setMenu: (state: MenuState) => void }) => {
const { account } = useWeb3React()
const isAuthenticated = !!account
const [darkMode, toggleDarkMode] = useDarkModeManager()
const activeLocale = useActiveLocale()
const ISO = activeLocale.split('-')[0].toUpperCase()
const allTransactions = useAllTransactions()
const toggleWalletModal = useToggleWalletModal()
const pendingTransactions = useMemo(
() => Object.values(allTransactions).filter((tx) => !tx.receipt),
[allTransactions]
)
return (
<DefaultMenuWrap>
{isAuthenticated ? (
<AuthenticatedHeader />
) : (
<ConnectButton onClick={toggleWalletModal}>Connect wallet</ConnectButton>
)}
<Divider />
{isAuthenticated && (
<ToggleMenuItem onClick={() => setMenu(MenuState.TRANSACTIONS)}>
<DefaultText>
<Trans>Transactions</Trans>{' '}
{pendingTransactions.length > 0 && (
<PendingBadge>
{pendingTransactions.length} <Trans>Pending</Trans>
</PendingBadge>
)}
</DefaultText>
<IconWrap>
<ChevronRight size={16} strokeWidth={3} />
</IconWrap>
</ToggleMenuItem>
)}
<ToggleMenuItem onClick={() => setMenu(MenuState.LANGUAGE)}>
<DefaultText>
<Trans>Language</Trans>
</DefaultText>
<FlexContainer>
<CenterVertically>
<DefaultText>{ISO}</DefaultText>
</CenterVertically>
<IconWrap>
<ChevronRight size={16} strokeWidth={3} />
</IconWrap>
</FlexContainer>
</ToggleMenuItem>
<ToggleMenuItem onClick={toggleDarkMode}>
<DefaultText>{darkMode ? <Trans> Light theme</Trans> : <Trans>Dark theme</Trans>}</DefaultText>
<IconWrap>{darkMode ? <Sun size={16} /> : <Moon size={16} />}</IconWrap>
</ToggleMenuItem>
</DefaultMenuWrap>
)
}
export default WalletDropdown
import { Icon } from 'react-feather'
import styled, { css } from 'styled-components/macro'
export const IconHoverText = styled.span`
color: ${({ theme }) => theme.textPrimary};
position: absolute;
top: 28px;
border-radius: 8px;
transform: translateX(-50%);
opacity: 0;
font-size: 12px;
padding: 5px;
left: 10px;
`
const IconStyles = css`
background-color: ${({ theme }) => theme.backgroundInteractive};
border-radius: 12px;
display: inline-block;
cursor: pointer;
position: relative;
height: 32px;
width: 32px;
color: ${({ theme }) => theme.textPrimary};
:hover {
background-color: ${({ theme }) => theme.hoverState};
transition: background-color 200ms linear;
${IconHoverText} {
opacity: 1;
}
}
:active {
background-color: ${({ theme }) => theme.backgroundSurface};
transition: background-color 50ms linear;
}
`
const IconBlockLink = styled.a`
${IconStyles};
`
const IconBlockButton = styled.button`
${IconStyles};
border: none;
outline: none;
`
const IconWrapper = styled.span`
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 16px;
height: 16px;
`
interface IconButtonProps {
text: React.ReactNode
Icon: Icon
onClick?: () => void
href?: string
}
const IconButton = ({ Icon, onClick, text, href }: IconButtonProps) => {
return href ? (
<IconBlockLink href={href} target="_blank">
<IconWrapper>
<Icon strokeWidth={1.5} size={16} />
<IconHoverText>{text}</IconHoverText>
</IconWrapper>
</IconBlockLink>
) : (
<IconBlockButton onClick={onClick}>
<IconWrapper>
<Icon strokeWidth={1.5} size={16} />
<IconHoverText>{text}</IconHoverText>
</IconWrapper>
</IconBlockButton>
)
}
export default IconButton
import { Trans } from '@lingui/macro'
import { LOCALE_LABEL, SUPPORTED_LOCALES, SupportedLocale } from 'constants/locales'
import { useActiveLocale } from 'hooks/useActiveLocale'
import { useLocationLinkProps } from 'hooks/useLocationLinkProps'
import { Check } from 'react-feather'
import { Link } from 'react-router-dom'
import { Text } from 'rebass'
import styled, { useTheme } from 'styled-components/macro'
import { SlideOutMenu } from './SlideOutMenu'
const InternalMenuItem = styled(Link)`
flex: 1;
padding: 0.5rem 0.5rem;
color: ${({ theme }) => theme.textTertiary};
:hover {
cursor: pointer;
}
`
const InternalLinkMenuItem = styled(InternalMenuItem)<{ isActive: boolean }>`
display: flex;
flex-direction: row;
align-items: center;
padding: 12px 16px;
justify-content: space-between;
text-decoration: none;
background-color: ${({ isActive, theme }) => isActive && theme.accentActionSoft};
color: ${({ theme }) => theme.textPrimary};
:hover {
cursor: pointer;
}
`
const LanguageWrap = styled.div`
margin-top: 16px;
`
function LanguageMenuItem({ locale, isActive }: { locale: SupportedLocale; isActive: boolean }) {
const { to, onClick } = useLocationLinkProps(locale)
const theme = useTheme()
if (!to) return null
return (
<InternalLinkMenuItem isActive={isActive} onClick={onClick} to={to}>
<Text fontSize={16} fontWeight={400} lineHeight="24px">
{LOCALE_LABEL[locale]}
</Text>
{isActive && <Check color={theme.accentAction} opacity={1} size={20} />}
</InternalLinkMenuItem>
)
}
const LanguageMenu = ({ onClose }: { onClose: () => void }) => {
const activeLocale = useActiveLocale()
return (
<SlideOutMenu title={<Trans>Language</Trans>} onClose={onClose}>
<LanguageWrap>
{SUPPORTED_LOCALES.map((locale) => (
<LanguageMenuItem locale={locale} isActive={activeLocale === locale} key={locale} />
))}
</LanguageWrap>
</SlideOutMenu>
)
}
export default LanguageMenu
import { ChevronLeft } from 'react-feather'
import styled from 'styled-components/macro'
const BackSection = styled.div`
position: relative;
display: flex;
padding: 0 16px;
color: ${({ theme }) => theme.textSecondary};
cursor: default;
:hover {
text-decoration: none;
}
`
const Menu = styled.div`
width: 100%;
height: 100%;
font-size: 16px;
overflow-y: scroll;
`
const Header = styled.span`
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
height: 200px;
`
const ClearAll = styled.div`
display: inline-block;
cursor: pointer;
margin-left: auto;
color: ${({ theme }) => theme.accentAction};
font-weight: 600;
font-size: 14px;
margin-top: auto;
margin-bottom: auto;
`
export const SlideOutMenu = ({
children,
onClose,
title,
onClear,
}: {
onClose: () => void
title: React.ReactNode
children: React.ReactNode
onClear?: () => void
}) => (
<Menu>
<BackSection>
<ChevronLeft cursor="pointer" onClick={onClose} size={24} />
<Header>{title}</Header>
{onClear && <ClearAll onClick={onClear}>Clear All</ClearAll>}
</BackSection>
{children}
</Menu>
)
import { Trans } from '@lingui/macro'
import { SlideOutMenu } from './SlideOutMenu'
export const TransactionHistoryMenu = ({ onClose }: { onClose: () => void }) => (
<SlideOutMenu onClose={onClose} onClear={undefined} title={<Trans>Transactions</Trans>}>
<div />
</SlideOutMenu>
)
import { useState } from 'react'
import styled from 'styled-components/macro'
import DefaultMenu from './DefaultMenu'
import LanguageMenu from './LanguageMenu'
import { TransactionHistoryMenu } from './TransactionMenu'
const WalletWrapper = styled.div`
border-radius: 12px;
width: 320px;
max-height: 376px;
display: flex;
flex-direction: column;
font-size: 16px;
top: 60px;
right: 70px;
background-color: ${({ theme }) => theme.backgroundSurface};
padding: 16px 0;
`
export enum MenuState {
DEFAULT = 'DEFAULT',
LANGUAGE = 'LANGUAGE',
TRANSACTIONS = 'TRANSACTIONS',
}
const WalletDropdown = () => {
const [menu, setMenu] = useState<MenuState>(MenuState.DEFAULT)
return (
<WalletWrapper>
{menu === MenuState.TRANSACTIONS && <TransactionHistoryMenu onClose={() => setMenu(MenuState.DEFAULT)} />}
{menu === MenuState.LANGUAGE && <LanguageMenu onClose={() => setMenu(MenuState.DEFAULT)} />}
{menu === MenuState.DEFAULT && <DefaultMenu setMenu={setMenu} />}
</WalletWrapper>
)
}
export default WalletDropdown
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
export function useWalletFlag(): BaseVariant {
return useBaseFlag(FeatureFlag.wallet)
}
export { BaseVariant as WalletVariant }
......@@ -55,6 +55,7 @@ export enum BaseVariant {
export enum FeatureFlag {
navBar = 'navBar',
wallet = 'wallet',
phase1 = 'phase1',
redesign = 'redesign',
tokens = 'tokens',
......
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