Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds a ENS address selector to the Profile page #367

Merged
merged 25 commits into from
Apr 18, 2022
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b3769c1
add base ens name selector in Profile
ramirotw Dec 7, 2021
f7e62d0
fix styles
alongoni Dec 13, 2021
54b2683
fix codegen schema error
ramirotw Dec 16, 2021
e902cb6
Merge remote-tracking branch 'origin/develop' into ramirotw/issue-168…
ramirotw Dec 16, 2021
54dc4a7
handle error for xdai
ramirotw Dec 16, 2021
e1b5e33
shorten address in dropdown
ramirotw Dec 16, 2021
3361d68
Merge remote-tracking branch 'origin/develop' into ramirotw/issue-168…
ramirotw Feb 18, 2022
117545f
only show the address selector on mainnet
ramirotw Feb 18, 2022
828b2e1
Fix code style issues with Prettier
lint-action Feb 18, 2022
c8a3118
Merge remote-tracking branch 'origin/develop' into ramirotw/issue-168…
ramirotw Apr 1, 2022
0bc1579
handle account change
ramirotw Apr 1, 2022
97dce07
Merge remote-tracking branch 'origin/ramirotw/issue-1682-ens-affiliat…
ramirotw Apr 1, 2022
3837a52
parse ens name from qs
ramirotw Apr 1, 2022
49a1455
Merge remote-tracking branch 'origin/develop' into ramirotw/issue-168…
ramirotw Apr 8, 2022
db2b7bb
bump graphql-request
ramirotw Apr 8, 2022
8057369
show full address when there are no ens names
ramirotw Apr 11, 2022
13607b4
disable dropdown if there are no ens names
ramirotw Apr 12, 2022
5c0a805
add rinkeby subgraph
ramirotw Apr 12, 2022
7d529a9
remove dropdown when there's no ens names
ramirotw Apr 12, 2022
a804a4c
shorten always referral address
ramirotw Apr 13, 2022
571f64d
prevent returning an invalid state due to a loading variable
ramirotw Apr 13, 2022
1a95e1d
Update src/custom/hooks/useParseReferralQueryParam.ts
ramirotw Apr 14, 2022
93aa746
fix wrong utils path
ramirotw Apr 18, 2022
83092f2
merge useEffects hooks
ramirotw Apr 18, 2022
bcca83d
Merge remote-tracking branch 'origin/develop' into ramirotw/issue-168…
ramirotw Apr 18, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion codegen.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
overrideExisting: true
schema: 'https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3'
schema:
[
'https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3',
'https://api.thegraph.com/subgraphs/name/ensdomains/ens',
]
documents: 'src/**/!(*.d).{ts,tsx}'
generates:
./src/state/data/generated.ts:
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@
"eslint-plugin-simple-import-sort": "^7.0.0",
"ethers": "^5.4.6",
"graphql": "^15.5.0",
"graphql-request": "^3.4.0",
"graphql-request": "^4.2.0",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I introduced this change to force the CI script to run the postinstall npm script which runs the yarn graphql:generate script which generates the graphql TS types in src/state/data/generated.ts that is needed to import the EnsNamesQuery type.

"inter-ui": "^3.13.1",
"ipfs-deploy": "^8.0.1",
"jazzicon": "^1.5.0",
Expand Down
1 change: 0 additions & 1 deletion src/custom/components/AffiliateStatusCheck/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,6 @@ export default function AffiliateStatusCheck() {
return
}

setAffiliateState(null)
setError('')

if (!account) {
Expand Down
26 changes: 16 additions & 10 deletions src/custom/hooks/useParseReferralQueryParam.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { useMemo } from 'react'
import { isAddress } from '@ethersproject/address'
import { useMemo, useState } from 'react'
import useParsedQueryString from 'hooks/useParsedQueryString'
import { REFERRAL_QUERY_PARAM } from 'hooks/useReferralLink'
import useENS from 'hooks/useENS'
import { isAddress } from 'hook/utils'

type ReferralQueryValue = {
value: string
Expand All @@ -13,20 +14,25 @@ type ReferralQueryValue = {
*/
export default function useParseReferralQueryParam(): ReferralQueryValue {
const parsedQs = useParsedQueryString()
const referralAddress = parsedQs[REFERRAL_QUERY_PARAM] as string
const result = useENS(referralAddress)
const [loading, setLoading] = useState(isAddress(referralAddress) === false) // this is a hack to force a initial loading state to true in case of referralAddress is a ens name because the useENS hook returns loading as false when initialized

const referral = useMemo(() => {
const referralAddress = parsedQs[REFERRAL_QUERY_PARAM]
if (typeof referralAddress === 'string' && isAddress(referralAddress)) {
return { value: referralAddress, isValid: true }
if (loading || result.loading || !referralAddress) {
if (result.loading) {
setLoading(false)
}
return null
}

if (referralAddress) {
console.warn('Invalid referral address')
return { value: '', isValid: false }
if (result.address) {
return { value: result.address, isValid: true }
}

return null
}, [parsedQs])
console.warn('Invalid referral address')
return { value: '', isValid: false }
}, [result.loading, result.address, referralAddress, loading])

return referral
}
227 changes: 227 additions & 0 deletions src/custom/pages/Profile/AddressSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import styled, { css } from 'styled-components/macro'
import { Check, ChevronDown } from 'react-feather'
import { useOnClickOutside } from 'hooks/useOnClickOutside'
import { useActiveWeb3React } from 'hooks/web3'
import { ensNames } from './ens'
import { useAddress } from 'state/affiliate/hooks'
import { updateAddress } from 'state/affiliate/actions'
import { useAppDispatch } from 'state/hooks'
import { isAddress, shortenAddress } from 'utils'

type AddressSelectorProps = {
address: string
}

export default function AddressSelector(props: AddressSelectorProps) {
const { address } = props
const dispatch = useAppDispatch()
const selectedAddress = useAddress()
const { chainId, library } = useActiveWeb3React()
const [open, setOpen] = useState(false)
const [items, setItems] = useState<string[]>([address])
const toggle = useCallback(() => setOpen((open) => !open), [])
const node = useRef<HTMLDivElement>(null)
useOnClickOutside(node, open ? toggle : undefined)

const tryShortenAddress = useCallback((item?: string) => {
if (!item) {
return item
}

try {
return shortenAddress(item)
} catch (error) {
return item
}
}, [])

const handleSelectItem = useCallback(
(item: string) => {
dispatch(updateAddress(item))
toggle()
},
[dispatch, toggle]
)

useEffect(() => {
if (!chainId) {
return
}

ensNames(chainId, address).then((response) => {
if ('error' in response) {
console.info(response.error)
setItems([address])
return
}
setItems([...response, address])
})
}, [address, chainId])

useEffect(() => {
if (selectedAddress) {
return
}

dispatch(updateAddress(address))
}, [selectedAddress, address, dispatch])

useEffect(() => {
if (!selectedAddress) {
return
}

// if the user switches accounts, reset the selected address
if (isAddress(selectedAddress) && selectedAddress !== address) {
dispatch(updateAddress(address))
return
}

// the selected address is a ens name, verify that resolves to the correct address
const verify = async () => {
const resolvedAddress = await library?.resolveName(selectedAddress)
if (resolvedAddress !== address) {
dispatch(updateAddress(address))
}
}

verify()
}, [selectedAddress, address, dispatch, library])
ramirotw marked this conversation as resolved.
Show resolved Hide resolved

return (
<>
{items.length === 1 ? (
<strong>{tryShortenAddress(address)}</strong>
) : (
<Wrapper ref={node}>
<AddressInfo onClick={toggle}>
<span style={{ marginRight: '2px' }}>{tryShortenAddress(selectedAddress)}</span>
<ChevronDown size={16} style={{ marginTop: '2px' }} strokeWidth={2.5} />
</AddressInfo>
{open && (
<MenuFlyout>
{items.map((item) => (
<ButtonMenuItem key={item} $selected={item === ''} onClick={() => handleSelectItem(item)}>
<GreenCheck size={16} strokeWidth={2.5} $visible={item === selectedAddress} />{' '}
{tryShortenAddress(item)}
</ButtonMenuItem>
))}
</MenuFlyout>
)}
</Wrapper>
)}
</>
)
}

const Wrapper = styled.div`
position: relative;
display: inline;
margin-right: 0.4rem;
${({ theme }) => theme.mediaWidth.upToMedium`
justify-self: end;
`};

${({ theme }) => theme.mediaWidth.upToSmall`
margin: 0 0.5rem 0 0;
width: initial;
text-overflow: ellipsis;
flex-shrink: 1;
`};
`

const MenuFlyout = styled.span`
background-color: ${({ theme }) => theme.bg4};
border: 1px solid ${({ theme }) => theme.bg0};

box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.01), 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 16px 24px rgba(0, 0, 0, 0.04),
0px 24px 32px rgba(0, 0, 0, 0.01);
border-radius: 12px;
padding: 0.3rem;
display: flex;
flex-direction: column;
font-size: 1rem;
position: absolute;
left: 0;
top: 1.75rem;
z-index: 100;
min-width: 350px;
${({ theme }) => theme.mediaWidth.upToMedium`;
min-width: 145px
`};

> {
padding: 12px;
}
`
const MenuItem = css`
align-items: center;
background-color: transparent;
border-radius: 12px;
color: ${({ theme }) => theme.text2};
cursor: pointer;
display: flex;
flex: 1;
flex-direction: row;
font-size: 16px;
font-weight: 400;
justify-content: start;
:hover {
text-decoration: none;
}
`

export const AddressInfo = styled.button`
align-items: center;
background-color: ${({ theme }) => theme.bg4};
border-radius: 12px;
border: 1px solid ${({ theme }) => theme.bg0};
color: ${({ theme }) => theme.text1};
display: inline-flex;
flex-direction: row;
font-weight: 700;
font-size: 12px;
height: 100%;
margin: 0 0.4rem;
padding: 0.2rem 0.4rem;

:hover,
:focus {
cursor: pointer;
outline: none;
border: 1px solid ${({ theme }) => theme.bg3};
}
`
const ButtonMenuItem = styled.button<{ $selected?: boolean }>`
${MenuItem}
cursor: ${({ $selected }) => ($selected ? 'initial' : 'pointer')};
border: none;
box-shadow: none;
color: ${({ theme, $selected }) => ($selected ? theme.text2 : theme.text1)};
background-color: ${({ theme, $selected }) => $selected && theme.primary1};
outline: none;
font-weight: ${({ $selected }) => ($selected ? '700' : '500')};
font-size: 12px;
text-transform: lowercase;
padding: 6px 10px 6px 5px;

${({ $selected }) => $selected && `margin: 3px 0;`}

> ${AddressInfo} {
margin: 0 auto 0 8px;
}

&:hover {
color: ${({ theme, $selected }) => !$selected && theme.text1};
background: ${({ theme, $selected }) => !$selected && theme.bg4};
}

transition: background 0.13s ease-in-out;
`

const GreenCheck = styled(Check)<{ $visible: boolean }>`
margin-right: 5px;
color: ${({ theme }) => theme.success};
visibility: ${({ $visible }) => ($visible ? 'visible' : 'hidden')};
`
54 changes: 54 additions & 0 deletions src/custom/pages/Profile/ens.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { SupportedChainId } from 'constants/chains'
import { ClientError, gql, GraphQLClient } from 'graphql-request'
import { EnsNamesQuery } from 'state/data/generated'

const CHAIN_SUBGRAPH_URL: Record<number, string> = {
[SupportedChainId.MAINNET]: 'https://api.thegraph.com/subgraphs/name/ensdomains/ens',
[SupportedChainId.RINKEBY]: 'https://api.thegraph.com/subgraphs/name/ensdomains/ensrinkeby',
}

const DOMAINS_BY_ADDRESS_QUERY = gql`
query ensNames($resolvedAddress: String!) {
domains(where: { resolvedAddress_contains: $resolvedAddress }, orderBy: name) {
name
}
}
`

export async function ensNames(
chainId: SupportedChainId,
address: string
): Promise<
| {
error: { name: string; message: string; stack: string | undefined }
}
| string[]
> {
try {
const subgraphUrl = chainId ? CHAIN_SUBGRAPH_URL[chainId] : undefined

if (!subgraphUrl) {
return {
error: {
name: 'UnsupportedChainId',
message: `Subgraph queries against ChainId ${chainId} are not supported.`,
stack: '',
},
}
}

const data = await new GraphQLClient(subgraphUrl).request<EnsNamesQuery>(DOMAINS_BY_ADDRESS_QUERY, {
resolvedAddress: address.toLocaleLowerCase(),
})

return data.domains
.map((domain) => domain.name)
.filter((domainName): domainName is string => domainName !== null && domainName !== undefined)
} catch (error) {
if (error instanceof ClientError) {
const { name, message, stack } = error
return { error: { name, message, stack } }
}
throw error
}
}
Loading