Commit a965c379 authored by cartcrom's avatar cartcrom Committed by GitHub

feat: Unicons (#6061)

* feat: unicons

* fix: linted

* fix: deduplicate
parent e8c689e1
import { getWalletMeta } from '@uniswap/conedison/provider/meta'
import { useWeb3React } from '@web3-react/core'
import { MouseoverTooltip } from 'components/Tooltip'
import { Unicon } from 'components/Unicon'
import { ConnectionType } from 'connection'
import useENSAvatar from 'hooks/useENSAvatar'
import ms from 'ms.macro'
import { PropsWithChildren } from 'react'
import styled from 'styled-components/macro'
import { ThemedText } from 'theme'
import { flexColumnNoWrap } from 'theme/styles'
import CoinbaseWalletIcon from '../../assets/images/coinbaseWalletIcon.svg'
......@@ -50,14 +56,53 @@ const Socks = () => {
)
}
const useIcon = (connectionType: ConnectionType) => {
const { account } = useWeb3React()
const Divider = styled.div`
border-bottom: 1px solid ${({ theme }) => theme.backgroundOutline};
margin: 12px 0;
`
function UniconTooltip({ children, enabled }: PropsWithChildren<{ enabled?: boolean }>) {
return (
<MouseoverTooltip
timeout={ms`3s`}
offsetY={8}
disableHover={!enabled}
text={
// TODO(cartcrom): add Learn More link when unicon microsite is polished
<>
<ThemedText.SubHeaderSmall color="textPrimary" paddingTop="4px">
This is your Unicon
</ThemedText.SubHeaderSmall>
<Divider />
<ThemedText.Caption paddingBottom="4px">
Unicons are avatars for your wallet, generated from your address.
</ThemedText.Caption>
</>
}
placement="bottom"
>
<div>{children}</div>
</MouseoverTooltip>
)
}
const useIcon = (connectionType: ConnectionType, size?: number, enableInfotips?: boolean) => {
const { account, provider } = useWeb3React()
const { avatar } = useENSAvatar(account ?? undefined)
const isUniswapWallet = Boolean(provider && getWalletMeta(provider)?.name === 'Uniswap Wallet')
if (!account) return null
if (avatar || connectionType === ConnectionType.INJECTED) {
return <Identicon />
} else if (connectionType === ConnectionType.WALLET_CONNECT) {
return <img src={WalletConnectIcon} alt="WalletConnect" />
return isUniswapWallet ? (
<UniconTooltip enabled={enableInfotips}>
<Unicon address={account} size={size} />
</UniconTooltip>
) : (
<img src={WalletConnectIcon} alt="WalletConnect" />
)
} else if (connectionType === ConnectionType.COINBASE_WALLET) {
return <img src={CoinbaseWalletIcon} alt="Coinbase Wallet" />
}
......@@ -65,9 +110,17 @@ const useIcon = (connectionType: ConnectionType) => {
return undefined
}
export default function StatusIcon({ connectionType, size }: { connectionType: ConnectionType; size?: number }) {
export default function StatusIcon({
connectionType,
size,
enableInfotips,
}: {
connectionType: ConnectionType
size?: number
enableInfotips?: boolean
}) {
const hasSocks = useHasSocks()
const icon = useIcon(connectionType)
const icon = useIcon(connectionType, size, enableInfotips)
return (
<IconWrapper size={size ?? 16}>
......
This diff is collapsed.
This diff is collapsed.
import React, { memo, useMemo } from 'react'
import { useIsDarkMode } from 'state/user/hooks'
import { blurs, UniconAttributeData, UniconAttributes, UniconAttributesToIndices } from './types'
import { deriveUniconAttributeIndices, getUniconAttributeData, isEthAddress } from './utils'
const ORIGINAL_CONTAINER_SIZE = 36
const EMBLEM_XY_SHIFT = 10
function PathMask({
id,
paths,
scale,
shift = 0,
}: {
id: string
paths: React.SVGProps<SVGPathElement>[]
scale: number
shift?: number
}) {
return (
<mask id={id}>
<rect x="0" y="0" width="100%" height="100%" fill="white" />
<g transform={`scale(${scale}) \n translate(${shift}, ${shift})`}>
{paths.map((pathProps) => (
<path key={pathProps.d as string} {...pathProps} fill="black" />
))}
</g>
</mask>
)
}
type UniconMaskProps = { maskId: string; attributeData: UniconAttributeData; size: number }
function UniconMask({ maskId, attributeData, size }: UniconMaskProps) {
const shapeMaskId = `shape-${maskId}`
const containerMaskId = `container-${maskId}`
return (
<defs>
<PathMask
id={containerMaskId}
paths={attributeData[UniconAttributes.Container]}
scale={size / ORIGINAL_CONTAINER_SIZE}
/>
<PathMask
id={shapeMaskId}
paths={attributeData[UniconAttributes.Shape]}
scale={size / ORIGINAL_CONTAINER_SIZE}
shift={EMBLEM_XY_SHIFT}
/>
<mask id={maskId}>
<g fill="white">
<g mask={`url(#${shapeMaskId})`}>
<g transform={`scale(${size / ORIGINAL_CONTAINER_SIZE})`}>
{attributeData[UniconAttributes.Container].map((pathProps) => (
<path key={pathProps.d as string} {...pathProps} />
))}
</g>
</g>
<g mask={`url(#${containerMaskId})`}>
<g
transform={`scale(${size / ORIGINAL_CONTAINER_SIZE})
translate(10, 10)`}
>
{attributeData[UniconAttributes.Shape].map((pathProps) => (
<path key={pathProps.d as string} {...pathProps} />
))}
</g>
</g>
</g>
</mask>
</defs>
)
}
type UniconGradientProps = { gradientId: string; attributeData: UniconAttributeData }
function UniconGradient({ gradientId, attributeData }: UniconGradientProps) {
return (
<linearGradient id={gradientId}>
<stop offset="0%" stopColor={attributeData[UniconAttributes.GradientStart]} />
<stop offset="100%" stopColor={attributeData[UniconAttributes.GradientEnd]} />
</linearGradient>
)
}
function UniconBlur({ blurId, size }: { blurId: string; size: number }) {
return (
<filter id={blurId} x="-50%" y="-50%" height="200%" width="200%">
<feGaussianBlur in="SourceGraphic" stdDeviation={size / 3} />
</filter>
)
}
function UniconSvg({
attributeIndices,
size,
address,
}: {
attributeIndices: UniconAttributesToIndices
size: number
address: string
mobile?: boolean
}) {
const isDarkMode = useIsDarkMode()
const attributeData = useMemo(() => getUniconAttributeData(attributeIndices), [attributeIndices])
const gradientId = `gradient${address + size}`
const maskId = `mask${address + size}`
const blurId = `blur${address + size}`
const svgProps = {
viewBox: `0 0 ${size} ${size}`,
}
if (!attributeIndices || !attributeData) return null
return (
<svg {...svgProps}>
<defs>
<UniconMask maskId={maskId} attributeData={attributeData} size={size} />
<UniconGradient gradientId={gradientId} attributeData={attributeData} />
<UniconBlur blurId={blurId} size={size} />
</defs>
<g mask={`url(#${maskId})`}>
<rect x="0" y="0" width="100%" height="100%" fill={`url(#${gradientId})`} />
{!isDarkMode && <rect x="0" y="0" width="100%" height="100%" fill="black" opacity={0.08} />}
<ellipse
cx={size / 2}
cy={0}
rx={size / 2}
ry={size / 2}
fill={blurs[attributeIndices[UniconAttributes.GradientStart]]}
filter={`url(#${blurId})`}
/>
</g>
</svg>
)
}
interface Props {
address: string
size?: number
randomSeed?: number
border?: boolean
mobile?: boolean
}
function _Unicon({ address, size = 24, randomSeed = 0, mobile }: Props) {
const attributeIndices = useMemo(() => deriveUniconAttributeIndices(address, randomSeed), [address, randomSeed])
if (!address || !isEthAddress(address) || !attributeIndices) return null
return (
<div style={{ height: size, width: size, position: 'relative' }}>
<div style={{ height: size, width: size, overflow: 'visible', position: 'absolute' }}>
<UniconSvg attributeIndices={attributeIndices} size={size} address={address} mobile={mobile} />
</div>
</div>
)
}
export const Unicon = memo(_Unicon)
import { svgPaths as containerPaths } from './Container'
import { svgPaths as emblemPaths } from './Emblem'
export enum UniconAttributes {
GradientStart = 0,
GradientEnd = 1,
Container = 2,
Shape = 3,
}
export const UniconAttributesArray: UniconAttributes[] = [
UniconAttributes.GradientStart,
UniconAttributes.GradientEnd,
UniconAttributes.Container,
UniconAttributes.Shape,
]
export interface UniconAttributesToIndices {
[UniconAttributes.GradientStart]: number
[UniconAttributes.GradientEnd]: number
[UniconAttributes.Container]: number
[UniconAttributes.Shape]: number
}
export interface UniconAttributeData {
[UniconAttributes.GradientStart]: string
[UniconAttributes.GradientEnd]: string
[UniconAttributes.Container]: React.SVGProps<SVGPathElement>[]
[UniconAttributes.Shape]: React.SVGProps<SVGPathElement>[]
}
export const gradientStarts = [
'#6100FF',
'#5065FD',
'#36DBFF',
'#5CFE9D',
'#B1F13C',
'#F9F40B',
'#FF6F1E',
'#F14544',
'#FC72FF',
'#C0C0C0',
]
export const blurs = [
'#D3EBA3',
'#F06DF3',
'#9D99F5',
'#EDE590',
'#B0EDFE',
'#FBAA7F',
'#C8BB9B',
'#9D99F5',
'#A26AF3',
'#D3EBA3',
]
export const gradientEnds = [
'#D0B2F3',
'#BDB8FA',
'#63CDE8',
'#76D191',
'#9BCD46',
'#EDE590',
'#FBAA7F',
'#FEA79B',
'#F5A1F5',
'#B8C3B7',
]
export const UniconNumOptions = {
[UniconAttributes.GradientStart]: gradientStarts.length,
[UniconAttributes.GradientEnd]: gradientEnds.length,
[UniconAttributes.Container]: containerPaths.length,
[UniconAttributes.Shape]: emblemPaths.length,
}
import { isAddress } from 'ethers/lib/utils'
import { svgPaths as containerPaths } from './Container'
import { svgPaths as emblemPaths } from './Emblem'
import {
gradientEnds,
gradientStarts,
UniconAttributeData,
UniconAttributes,
UniconAttributesArray,
UniconAttributesToIndices,
UniconNumOptions,
} from './types'
const NUM_CHARS_TO_USE_PER_ATTRIBUTE = 2
export const isEthAddress = (address: string) => {
return address.startsWith('0x') && isAddress(address.toLowerCase())
}
export const deriveUniconAttributeIndices = (
address: string,
randomSeed = 0
): UniconAttributesToIndices | undefined => {
if (!isEthAddress(address)) return
const hexAddr = address.slice(-40)
const newIndices = {
[UniconAttributes.GradientStart]: 0,
[UniconAttributes.GradientEnd]: 0,
[UniconAttributes.Container]: 0,
[UniconAttributes.Shape]: 0,
} as UniconAttributesToIndices
for (const a of UniconAttributesArray) {
const optionHex = hexAddr.slice(NUM_CHARS_TO_USE_PER_ATTRIBUTE * a, NUM_CHARS_TO_USE_PER_ATTRIBUTE * (a + 1))
const optionDec = parseInt(optionHex, 16) + randomSeed
const optionIndex = optionDec % UniconNumOptions[a]
newIndices[a] = optionIndex
}
return newIndices
}
export const getUniconAttributeData = (attributeIndices: UniconAttributesToIndices): UniconAttributeData => {
return {
[UniconAttributes.GradientStart]: gradientStarts[attributeIndices[UniconAttributes.GradientStart]],
[UniconAttributes.GradientEnd]: gradientEnds[attributeIndices[UniconAttributes.GradientEnd]],
[UniconAttributes.Container]: containerPaths[attributeIndices[UniconAttributes.Container]],
[UniconAttributes.Shape]: emblemPaths[attributeIndices[UniconAttributes.Shape]],
} as UniconAttributeData
}
......@@ -251,7 +251,7 @@ function Web3StatusInner() {
pending={hasPendingTransactions}
isClaimAvailable={isClaimAvailable}
>
{!hasPendingTransactions && <StatusIcon size={24} connectionType={connectionType} />}
{!hasPendingTransactions && <StatusIcon enableInfotips={true} size={24} connectionType={connectionType} />}
{hasPendingTransactions ? (
<RowBetween>
<Text>
......
import { signTypedData } from '@uniswap/conedison/provider'
import { signTypedData } from '@uniswap/conedison/provider/signing'
import { AllowanceTransfer, MaxAllowanceTransferAmount, PERMIT2_ADDRESS, PermitSingle } from '@uniswap/permit2-sdk'
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
......
......@@ -5002,10 +5002,10 @@
react "^18.2.0"
react-dom "^18.2.0"
"@uniswap/conedison@^1.3.0":
version "1.3.0"
resolved "https://registry.yarnpkg.com/@uniswap/conedison/-/conedison-1.3.0.tgz#998aca2bad27f0780a05b40e4512acfcadfece79"
integrity sha512-zpZ52svBJ2btwl09mLOw7HlBxFDuYAjAZXLAR7WQZJeRgjD1yD2QuI3v7JliXvHzJh3ePYH6820EMp7xQbdAGQ==
"@uniswap/conedison@^1.3.0", "@uniswap/conedison@^1.4.0":
version "1.4.0"
resolved "https://registry.yarnpkg.com/@uniswap/conedison/-/conedison-1.4.0.tgz#44ad96333b92913a57be34bf5effcfee6534cba1"
integrity sha512-ZZMfPTjUiYpLvO0SuMPGNzkFrRpzf+bQYSL/CzaYKGSVdorRUj4XpeMdjlbuKUtHqUEunOWE8eDL3J1Hl4HOUg==
"@uniswap/default-token-list@^2.0.0":
version "2.2.0"
......
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