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

Easy flow to set an identity #404

Merged
merged 10 commits into from
Oct 18, 2023
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
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
45 changes: 14 additions & 31 deletions packages/ui/src/components/AccountDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import { AccountBadge, IconSizeVariant } from '../types'
import { getDisplayAddress } from '../utils'
import IdenticonBadge from './IdenticonBadge'
import { useApi } from '../contexts/ApiContext'
import { DeriveAccountInfo, DeriveAccountRegistration } from '@polkadot/api-derive/types'
import IdentityIcon from './IdentityIcon'
import Balance from './library/Balance'
import { useGetEncodedAddress } from '../hooks/useGetEncodedAddress'
import { useIdentity } from '../hooks/useIdentity'

interface Props {
address: string
Expand All @@ -30,44 +30,27 @@ const AccountDisplay = ({
}: Props) => {
const { getNamesWithExtension } = useAccountNames()
const localName = useMemo(() => getNamesWithExtension(address), [address, getNamesWithExtension])
const [identity, setIdentity] = useState<DeriveAccountRegistration | null>(null)
const { api } = useApi()
const [mainDisplay, setMainDisplay] = useState<string>('')
const [sub, setSub] = useState<string | null>(null)
const getEncodedAddress = useGetEncodedAddress()
const encodedAddress = useMemo(() => getEncodedAddress(address), [address, getEncodedAddress])
const identity = useIdentity(address)

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

let unsubscribe: () => void

api.derive.accounts
.info(address, (info: DeriveAccountInfo) => {
setIdentity(info.identity)
if (!identity) return

if (info.identity.displayParent && info.identity.display) {
// when an identity is a sub identity `displayParent` is set
// and `display` get the sub identity
setMainDisplay(info.identity.displayParent)
setSub(info.identity.display)
} else {
// There should not be a `displayParent` without a `display`
// but we can't be too sure.
setMainDisplay(
info.identity.displayParent || info.identity.display || info.nickname || ''
)
}
})
.then((unsub) => {
unsubscribe = unsub
})
.catch((e) => console.error(e))

return () => unsubscribe && unsubscribe()
}, [address, api])
if (identity.displayParent && identity.display) {
// when an identity is a sub identity `displayParent` is set
// and `display` get the sub identity
setMainDisplay(identity.displayParent)
setSub(identity.display)
} else {
// There should not be a `displayParent` without a `display`
// but we can't be too sure.
setMainDisplay(identity.displayParent || identity.display || '')
}
}, [address, api, identity])

return (
<div className={className}>
Expand Down
6 changes: 3 additions & 3 deletions packages/ui/src/components/EasySetup/BalancesTransfer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import { styled } from '@mui/material/styles'
import { SubmittableExtrinsic } from '@polkadot/api/types'
import { ISubmittableResult } from '@polkadot/types/types'
import GenericAccountSelection, { AccountBaseInfo } from '../select/GenericAccountSelection'
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useApi } from '../../contexts/ApiContext'
import { useCheckBalance } from '../../hooks/useCheckBalance'
import BN from 'bn.js'
import { getGlobalMaxValue, inputToBn } from '../../utils'
import { TextFieldStyled } from '../library'
import { TextField } from '../library'
import { getOptionLabel } from '../../utils/getOptionLabel'
import { useAccountBaseFromAccountList } from '../../hooks/useAccountBaseFromAccountList'

Expand Down Expand Up @@ -123,7 +123,7 @@ const BalancesTransfer = ({ className, onSetExtrinsic, onSetErrorMessage, from }
accountList={accountBase}
testId="field-to"
/>
<TextFieldStyled
<TextField
data-cy="field-amount"
label={`Amount`}
onChange={onAmountChange}
Expand Down
4 changes: 2 additions & 2 deletions packages/ui/src/components/EasySetup/FromCallData.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { SubmittableExtrinsic } from '@polkadot/api/types'
import { ISubmittableResult } from '@polkadot/types/types'
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { useApi } from '../../contexts/ApiContext'
import { TextFieldStyled } from '../library'
import { TextField } from '../library'
import CallInfo from '../CallInfo'
import { useCallInfoFromCallData } from '../../hooks/useCallInfoFromCallData'
import { HexString } from '../../types'
Expand Down Expand Up @@ -96,7 +96,7 @@ const FromCallData = ({ className, onSetExtrinsic, isProxySelected, onSetErrorMe
Multix will override the proxy.proxy call with the proxy you have selected
</AlertStyled>
)}
<TextFieldStyled
<TextField
label={`Call data`}
onChange={onCallDataChange}
value={pastedCallData || ''}
Expand Down
1 change: 0 additions & 1 deletion packages/ui/src/components/EasySetup/ManualExtrinsic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,6 @@ const ManualExtrinsic = ({
const isValidAmountString = useCallback(
(value: any) => {
if (!value.match(/^[0-9]+([.][0-9]+)?$/)) {
console.log('wrong boom')
onSetErrorMessage('Only numbers and "." are accepted.')
return false
}
Expand Down
226 changes: 226 additions & 0 deletions packages/ui/src/components/EasySetup/SetIdentity.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
import { Grid } from '@mui/material'
import { styled } from '@mui/material/styles'
import { SubmittableExtrinsic } from '@polkadot/api/types'
import { ISubmittableResult } from '@polkadot/types/types'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useApi } from '../../contexts/ApiContext'
import { TextField } from '../library'
import { useIdentity } from '../../hooks/useIdentity'

interface Props {
className?: string
from: string
onSetExtrinsic: (ext?: SubmittableExtrinsic<'promise', ISubmittableResult>) => void
onSetErrorMessage: React.Dispatch<React.SetStateAction<string>>
}

type IdentityFields = {
display: string | undefined
legal: string | undefined
web: string | undefined
riot: string | undefined
email: string | undefined
twitter: string | undefined
}

const getRawOrNone = (val: string | undefined) => {
return val
? {
Raw: val
}
: { none: null }
}
const getExtrinsicsArgs = ({ legal, display, email, riot, twitter, web }: IdentityFields) => {
return {
additional: [],
display: getRawOrNone(display),
legal: getRawOrNone(legal),
web: getRawOrNone(web),
riot: getRawOrNone(riot),
email: getRawOrNone(email),
pgpFingerprint: null,
twitter: getRawOrNone(twitter)
}
}

const fieldNameAndPlaceholder = (fieldName: keyof IdentityFields) => {
switch (fieldName) {
case 'display':
return {
field: 'Display name',
placeholder: 'Luke',
required: true
}

case 'legal':
return {
field: 'Legal name',
placeholder: 'Luke Skylwalker',
required: false
}

case 'riot':
return {
field: 'Element handle',
placeholder: '@luke:matrix.org',
required: false
}

case 'web':
return {
field: 'Website',
placeholder: 'https://luke.sky',
required: false
}

case 'twitter':
return {
field: 'Twitter/X handle',
placeholder: '@luke',
required: false
}

case 'email':
return {
field: 'Email',
placeholder: 'hello@luke.sky',
required: false
}

default:
return {
field: fieldName,
placeholder: '',
required: false
}
}
}

const MAX_ALLOWED_VAL_LENGTH = 32

const SetIdentity = ({ className, onSetExtrinsic, from, onSetErrorMessage }: Props) => {
const { api, chainInfo } = useApi()
const [identityFields, setIdentityFields] = useState<IdentityFields | undefined>()
const chainIdentity = useIdentity(from)
const fieldtooLongError = useMemo(() => {
const res: (keyof IdentityFields)[] = []
identityFields &&
Object.entries(identityFields).forEach(([field, value]) => {
if (typeof value === 'string' && value.length >= MAX_ALLOWED_VAL_LENGTH) {
res.push(field as keyof IdentityFields)
}
})

return res
}, [identityFields])

useEffect(() => {
if (fieldtooLongError.length > 0) {
onSetErrorMessage(`A field exceeds the ${MAX_ALLOWED_VAL_LENGTH} character limit`)
return
}

if (!identityFields?.display) {
onSetErrorMessage('Display name is required')
return
}

onSetErrorMessage('')
}, [fieldtooLongError, identityFields?.display, onSetErrorMessage])

useEffect(() => {
if (chainIdentity) {
const { display, email, legal, web, riot, twitter } = chainIdentity
setIdentityFields({
display,
legal,
web,
riot,
email,
twitter
})
} else {
setIdentityFields({
display: undefined,
legal: undefined,
web: undefined,
riot: undefined,
email: undefined,
twitter: undefined
})
}
}, [chainIdentity])

useEffect(() => {
if (!api) {
onSetExtrinsic(undefined)
return
}

if (!identityFields) {
onSetExtrinsic(undefined)
return
}

if (fieldtooLongError.length > 0) {
onSetExtrinsic(undefined)
return
}

const extrinsicsArgs = getExtrinsicsArgs(identityFields)
onSetExtrinsic(api.tx.identity.setIdentity(extrinsicsArgs))
}, [api, chainInfo, fieldtooLongError, identityFields, onSetErrorMessage, onSetExtrinsic])

const onChangeField = useCallback((field: keyof IdentityFields, value: string) => {
setIdentityFields((prev) => (prev ? { ...prev, [field]: value } : undefined))
}, [])

return (
<Grid
className={className}
container
spacing={1}
>
{identityFields &&
Object.entries(identityFields).map(([fieldName, value]) => {
const { field, placeholder, required } = fieldNameAndPlaceholder(
fieldName as keyof IdentityFields
)
const isFieldError = fieldtooLongError.includes(fieldName as keyof IdentityFields)
const isDiplayAndEmpty = fieldName === 'display' && !value
return (
<Grid
item
xs={12}
sm={6}
md={6}
alignItems="center"
>
<TextFieldStyled
data-cy={`${fieldName}-identity`}
label={field}
placeholder={placeholder}
onChange={(val) =>
onChangeField(fieldName as keyof IdentityFields, val.target.value)
}
value={value || ''}
required={required}
helperText={isFieldError && `Field has more than ${MAX_ALLOWED_VAL_LENGTH} chars`}
error={isFieldError || isDiplayAndEmpty}
/>
</Grid>
)
})}
</Grid>
)
}

const TextFieldStyled = styled(TextField)`
.MuiFormHelperText-root.Mui-error {
position: initial;
}
`

export default styled(SetIdentity)`
margin-top: 0.5rem;
`
4 changes: 2 additions & 2 deletions packages/ui/src/components/library/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Button, ButtonWithIcon } from './Button'
import { Link, RouterLink, NavLink } from './Link'
import { InputField } from './InputField'
import TextFieldStyled from './TextFieldStyled'
import TextField from './TextFieldStyled'
import TextFieldLargeStyled from './TextFieldLargeStyled'
import Select from './Select'
import Autocomplete from './Autocomplete'
Expand All @@ -16,7 +16,7 @@ export {
NavLink,
RouterLink,
InputField,
TextFieldStyled,
TextField,
TextFieldLargeStyled,
Select
}
4 changes: 2 additions & 2 deletions packages/ui/src/components/modals/ProposalSigning.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Alert, CircularProgress, Dialog, DialogContent, DialogTitle, Grid } from '@mui/material'
import { Button, TextFieldStyled } from '../library'
import { Button, TextField } from '../library'
import { ChangeEvent, useCallback, useEffect, useMemo, useState } from 'react'
import { styled } from '@mui/material/styles'
import { useAccounts } from '../../contexts/AccountsContext'
Expand Down Expand Up @@ -311,7 +311,7 @@ const ProposalSigning = ({
xs={12}
md={6}
>
<TextFieldStyled
<TextField
className="addedCallData"
label={`Call data ${mustSubmitCallData ? '' : '(optional)'}`}
onChange={onAddedCallDataChange}
Expand Down
Loading
Loading