Skip to content

Commit

Permalink
Easy flow to set an identity (#404)
Browse files Browse the repository at this point in the history
  • Loading branch information
Tbaut authored Oct 18, 2023
1 parent 2db0e8a commit 4142873
Show file tree
Hide file tree
Showing 15 changed files with 362 additions and 75 deletions.
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
228 changes: 228 additions & 0 deletions packages/ui/src/components/EasySetup/SetIdentity.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
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: '[email protected]',
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 [hasChangedAtLeastAField, setHasChangedAtLeastAField] = useState(false)
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 && hasChangedAtLeastAField) {
onSetErrorMessage('Display name is required')
return
}

onSetErrorMessage('')
}, [fieldtooLongError, hasChangedAtLeastAField, identityFields, 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) => {
setHasChangedAtLeastAField(true)
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 isDiplayNameError = fieldName === 'display' && !value && hasChangedAtLeastAField
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 || isDiplayNameError}
/>
</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

0 comments on commit 4142873

Please sign in to comment.