Commit a4276b6d authored by tom's avatar tom

refactor form and check address on first step

parent 05e7fe9e
{
"typescript.tsdk": "node_modules/typescript/lib"
}
\ No newline at end of file
...@@ -88,8 +88,8 @@ export const RESOURCES = { ...@@ -88,8 +88,8 @@ export const RESOURCES = {
// ACCOUNT: ADDRESS VERIFICATION & TOKEN INFO // ACCOUNT: ADDRESS VERIFICATION & TOKEN INFO
address_verification: { address_verification: {
path: '/api/v1/chains/:chainId/verified-addresses\\:verify', path: '/api/v1/chains/:chainId/verified-addresses:type',
pathParams: [ 'chainId' as const ], pathParams: [ 'chainId' as const, 'type' as const ],
endpoint: appConfig.contractInfoApi.endpoint, endpoint: appConfig.contractInfoApi.endpoint,
basePath: appConfig.contractInfoApi.basePath, basePath: appConfig.contractInfoApi.basePath,
}, },
......
import { Icon, Modal, ModalBody, ModalCloseButton, ModalContent, ModalHeader, ModalOverlay, Alert, Link } from '@chakra-ui/react'; import { Icon, Modal, ModalBody, ModalCloseButton, ModalContent, ModalHeader, ModalOverlay, Link } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { SubmitHandler } from 'react-hook-form';
import { useForm, FormProvider } from 'react-hook-form';
import type { AddressVerificationFormFields } from './types'; import type { AddressVerificationFormFirstStepFields, AddressCheckStatusSuccess } from './types';
import appConfig from 'configs/app/config';
import eastArrowIcon from 'icons/arrows/east.svg'; import eastArrowIcon from 'icons/arrows/east.svg';
import type { ResourceError } from 'lib/api/resources';
import useApiFetch from 'lib/api/useApiFetch';
import Web3ModalProvider from 'ui/shared/Web3ModalProvider'; import Web3ModalProvider from 'ui/shared/Web3ModalProvider';
import AddressVerificationStepAddress from './steps/AddressVerificationStepAddress'; import AddressVerificationStepAddress from './steps/AddressVerificationStepAddress';
...@@ -22,15 +17,10 @@ interface Props { ...@@ -22,15 +17,10 @@ interface Props {
const AddressVerificationModal = ({ isOpen, onClose }: Props) => { const AddressVerificationModal = ({ isOpen, onClose }: Props) => {
const [ stepIndex, setStepIndex ] = React.useState(0); const [ stepIndex, setStepIndex ] = React.useState(0);
const [ error, setError ] = React.useState(''); const [ data, setData ] = React.useState<AddressVerificationFormFirstStepFields & AddressCheckStatusSuccess>({ address: '', signingMessage: '' });
const apiFetch = useApiFetch(); const handleGoToSecondStep = React.useCallback((firstStepResult: typeof data) => {
const formApi = useForm<AddressVerificationFormFields>({ setData(firstStepResult);
mode: 'onBlur',
});
const { handleSubmit } = formApi;
const handleGoToNextStep = React.useCallback(() => {
setStepIndex((prev) => prev + 1); setStepIndex((prev) => prev + 1);
}, []); }, []);
...@@ -41,39 +31,11 @@ const AddressVerificationModal = ({ isOpen, onClose }: Props) => { ...@@ -41,39 +31,11 @@ const AddressVerificationModal = ({ isOpen, onClose }: Props) => {
const handleClose = React.useCallback(() => { const handleClose = React.useCallback(() => {
onClose(); onClose();
setStepIndex(0); setStepIndex(0);
setError(''); }, [ onClose ]);
formApi.reset();
}, [ formApi, onClose ]);
const handleSignClick = React.useCallback(() => {
setError('');
}, []);
const onFormSubmit: SubmitHandler<AddressVerificationFormFields> = React.useCallback(async(data) => {
// eslint-disable-next-line no-console
console.log('__>__', data);
const body = {
contractAddress: data.address,
message: data.message,
signature: data.signature,
};
try {
await apiFetch('address_verification', {
fetchParams: { method: 'POST', body },
pathParams: { chainId: appConfig.network.id },
});
} catch (error: unknown) {
const _error = error as ResourceError<{message: string}>;
setError(_error.payload?.message || 'Oops! Something went wrong');
}
}, [ apiFetch ]);
const onSubmit = handleSubmit(onFormSubmit);
const steps = [ const steps = [
{ title: 'Verify new address ownership', content: <AddressVerificationStepAddress onContinue={ handleGoToNextStep }/> }, { title: 'Verify new address ownership', content: <AddressVerificationStepAddress onContinue={ handleGoToSecondStep }/> },
{ title: 'Sign message', content: <AddressVerificationStepSignature onSubmit={ onSubmit } onSign={ handleSignClick }/> }, { title: 'Sign message', content: <AddressVerificationStepSignature { ...data }/> },
{ title: 'Congrats! Address is verified.', content: <AddressVerificationStepSuccess onShowListClick={ handleClose } onAddTokenClick={ handleClose }/> }, { title: 'Congrats! Address is verified.', content: <AddressVerificationStepSuccess onShowListClick={ handleClose } onAddTokenClick={ handleClose }/> },
]; ];
const step = steps[stepIndex]; const step = steps[stepIndex];
...@@ -93,12 +55,7 @@ const AddressVerificationModal = ({ isOpen, onClose }: Props) => { ...@@ -93,12 +55,7 @@ const AddressVerificationModal = ({ isOpen, onClose }: Props) => {
<ModalCloseButton/> <ModalCloseButton/>
<ModalBody mb={ 0 }> <ModalBody mb={ 0 }>
<Web3ModalProvider> <Web3ModalProvider>
<FormProvider { ...formApi }>
<form noValidate onSubmit={ onSubmit }>
{ error && <Alert status="warning" mb={ 6 }>{ error }</Alert> }
{ step.content } { step.content }
</form>
</FormProvider>
</Web3ModalProvider> </Web3ModalProvider>
</ModalBody> </ModalBody>
</ModalContent> </ModalContent>
......
import { FormControl, Input } from '@chakra-ui/react'; import { FormControl, Input } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { ControllerRenderProps } from 'react-hook-form'; import type { Control, ControllerRenderProps, FormState } from 'react-hook-form';
import { Controller, useFormContext } from 'react-hook-form'; import { Controller } from 'react-hook-form';
import type { AddressVerificationFormFields } from '../types'; import type { AddressVerificationFormFirstStepFields, RootFields } from '../types';
import { ADDRESS_REGEXP, ADDRESS_LENGTH } from 'lib/validations/address'; import { ADDRESS_REGEXP, ADDRESS_LENGTH } from 'lib/validations/address';
import InputPlaceholder from 'ui/shared/InputPlaceholder'; import InputPlaceholder from 'ui/shared/InputPlaceholder';
type Fields = RootFields & AddressVerificationFormFirstStepFields;
interface Props { interface Props {
isDisabled?: boolean; formState: FormState<Fields>;
control: Control<Fields>;
} }
const AddressVerificationFieldAddress = ({ isDisabled }: Props) => { const AddressVerificationFieldAddress = ({ formState, control }: Props) => {
const { formState, control } = useFormContext<AddressVerificationFormFields>(); const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<Fields, 'address'>}) => {
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<AddressVerificationFormFields, 'address'>}) => {
const error = 'address' in formState.errors ? formState.errors.address : undefined; const error = 'address' in formState.errors ? formState.errors.address : undefined;
return ( return (
...@@ -25,13 +25,13 @@ const AddressVerificationFieldAddress = ({ isDisabled }: Props) => { ...@@ -25,13 +25,13 @@ const AddressVerificationFieldAddress = ({ isDisabled }: Props) => {
required required
isInvalid={ Boolean(error) } isInvalid={ Boolean(error) }
maxLength={ ADDRESS_LENGTH } maxLength={ ADDRESS_LENGTH }
isDisabled={ isDisabled || formState.isSubmitting } isDisabled={ formState.isSubmitting }
autoComplete="off" autoComplete="off"
/> />
<InputPlaceholder text="Smart contract address (0x...)" error={ error }/> <InputPlaceholder text="Smart contract address (0x...)" error={ error }/>
</FormControl> </FormControl>
); );
}, [ formState.errors, formState.isSubmitting, isDisabled ]); }, [ formState.errors, formState.isSubmitting ]);
return ( return (
<Controller <Controller
......
import { FormControl, Textarea } from '@chakra-ui/react'; import { FormControl, Textarea } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { ControllerRenderProps } from 'react-hook-form'; import type { Control, ControllerRenderProps, FormState } from 'react-hook-form';
import { Controller, useFormContext } from 'react-hook-form'; import { Controller } from 'react-hook-form';
import type { AddressVerificationFormFields } from '../types'; import type { AddressVerificationFormSecondStepFields } from '../types';
import dayjs from 'lib/date/dayjs';
import InputPlaceholder from 'ui/shared/InputPlaceholder'; import InputPlaceholder from 'ui/shared/InputPlaceholder';
interface Props { interface Props {
isDisabled?: boolean; formState: FormState<AddressVerificationFormSecondStepFields>;
control: Control<AddressVerificationFormSecondStepFields>;
} }
const AddressVerificationFieldMessage = ({ isDisabled }: Props) => { const AddressVerificationFieldMessage = ({ formState, control }: Props) => {
const { formState, control, getValues } = useFormContext<AddressVerificationFormFields>(); const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<AddressVerificationFormSecondStepFields, 'message'>}) => {
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<AddressVerificationFormFields, 'message'>}) => {
const error = 'message' in formState.errors ? formState.errors.message : undefined; const error = 'message' in formState.errors ? formState.errors.message : undefined;
return ( return (
...@@ -24,22 +22,18 @@ const AddressVerificationFieldMessage = ({ isDisabled }: Props) => { ...@@ -24,22 +22,18 @@ const AddressVerificationFieldMessage = ({ isDisabled }: Props) => {
{ ...field } { ...field }
required required
isInvalid={ Boolean(error) } isInvalid={ Boolean(error) }
isDisabled={ isDisabled || formState.isSubmitting } isDisabled={ formState.isSubmitting }
autoComplete="off" autoComplete="off"
maxH="105px" maxH="105px"
/> />
<InputPlaceholder text="Message to sign" error={ error }/> <InputPlaceholder text="Message to sign" error={ error }/>
</FormControl> </FormControl>
); );
}, [ formState.errors, formState.isSubmitting, isDisabled ]); }, [ formState.errors, formState.isSubmitting ]);
const address = getValues('address');
// eslint-disable-next-line max-len
const defaultValue = `[Blockscout.com] [${ dayjs().format('YYYY-MM-DD HH:mm:ss') }] I, hereby verify that I am the owner/creator of the address [${ address }]`;
return ( return (
<Controller <Controller
defaultValue={ defaultValue } defaultValue="some value"
name="message" name="message"
control={ control } control={ control }
render={ renderControl } render={ renderControl }
......
import { FormControl, Input } from '@chakra-ui/react'; import { FormControl, Input } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { ControllerRenderProps } from 'react-hook-form'; import type { Control, ControllerRenderProps, FormState } from 'react-hook-form';
import { Controller, useFormContext } from 'react-hook-form'; import { Controller } from 'react-hook-form';
import type { AddressVerificationFormFields } from '../types'; import type { AddressVerificationFormSecondStepFields } from '../types';
import { SIGNATURE_REGEXP } from 'lib/validations/signature'; import { SIGNATURE_REGEXP } from 'lib/validations/signature';
import InputPlaceholder from 'ui/shared/InputPlaceholder'; import InputPlaceholder from 'ui/shared/InputPlaceholder';
const AddressVerificationFieldSignature = () => { interface Props {
const { formState, control } = useFormContext<AddressVerificationFormFields>(); formState: FormState<AddressVerificationFormSecondStepFields>;
control: Control<AddressVerificationFormSecondStepFields>;
}
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<AddressVerificationFormFields, 'signature'>}) => { const AddressVerificationFieldSignature = ({ formState, control }: Props) => {
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<AddressVerificationFormSecondStepFields, 'signature'>}) => {
const error = 'signature' in formState.errors ? formState.errors.signature : undefined; const error = 'signature' in formState.errors ? formState.errors.signature : undefined;
return ( return (
......
import { Box, Button, Flex, Link } from '@chakra-ui/react'; import { Alert, Box, Button, Flex, Link } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import { useFormContext } from 'react-hook-form'; import type { SubmitHandler } from 'react-hook-form';
import { useForm } from 'react-hook-form';
import type { AddressVerificationFormFields } from '../types'; import type {
AddressCheckResponseError,
AddressCheckResponseSuccess,
AddressCheckStatusSuccess,
AddressVerificationFormFirstStepFields,
RootFields,
} from '../types';
import appConfig from 'configs/app/config';
import useApiFetch from 'lib/api/useApiFetch';
import AddressVerificationFieldAddress from '../fields/AddressVerificationFieldAddress'; import AddressVerificationFieldAddress from '../fields/AddressVerificationFieldAddress';
type Fields = RootFields & AddressVerificationFormFirstStepFields;
interface Props { interface Props {
onContinue: () => void; onContinue: (data: AddressVerificationFormFirstStepFields & AddressCheckStatusSuccess) => void;
} }
const AddressVerificationStepAddress = ({ onContinue }: Props) => { const AddressVerificationStepAddress = ({ onContinue }: Props) => {
const { formState, trigger } = useFormContext<AddressVerificationFormFields>(); const formApi = useForm<Fields>({
mode: 'onBlur',
});
const { handleSubmit, formState, control, setError, clearErrors, watch } = formApi;
const apiFetch = useApiFetch();
const address = watch('address');
React.useEffect(() => {
clearErrors('root');
}, [ address, clearErrors ]);
const onFormSubmit: SubmitHandler<Fields> = React.useCallback(async(data) => {
const body = {
contractAddress: data.address,
};
const response = await apiFetch<'address_verification', AddressCheckResponseSuccess, AddressCheckResponseError>('address_verification', {
fetchParams: { method: 'POST', body },
pathParams: { chainId: appConfig.network.id, type: ':prepare' },
});
const handleButtonClick = React.useCallback(() => { if (response.status !== 'SUCCESS') {
if (formState.errors.address) { switch (response.status) {
trigger('address'); case 'INVALID_ADDRESS_ERROR': {
return; return setError('root', { type: 'manual', message: 'Specified address either does not exist or is EOA' });
}
case 'IS_OWNER_ERROR': {
return setError('root', { type: 'manual', message: 'User is already an owner of the address' });
}
case 'OWNERSHIP_VERIFIED_ERROR': {
return setError('root', { type: 'manual', message: 'Address ownership has been verified by another account' });
}
case 'SOURCE_CODE_NOT_VERIFIED_ERROR': {
return setError('root', { type: 'manual', message: 'Contract source code has not been verified' });
} }
onContinue();
}, [ formState, onContinue, trigger ]); default: {
return setError('root', { type: 'manual', message: response.payload?.message || 'Oops! Something went wrong' });
}
}
}
onContinue({ ...response.result, address: data.address });
}, [ apiFetch, onContinue, setError ]);
const onSubmit = handleSubmit(onFormSubmit);
return ( return (
<Box> <form noValidate onSubmit={ onSubmit }>
{ formState.errors.root?.type === 'manual' && <Alert status="warning" mb={ 6 }>{ formState.errors.root?.message }</Alert> }
<Box mb={ 8 }>Let’s check your address...</Box> <Box mb={ 8 }>Let’s check your address...</Box>
<AddressVerificationFieldAddress/> <AddressVerificationFieldAddress formState={ formState } control={ control }/>
<Flex alignItems="center" mt={ 8 } columnGap={ 5 }> <Flex alignItems="center" mt={ 8 } columnGap={ 5 }>
<Button size="lg" onClick={ handleButtonClick }> <Button size="lg" type="submit" isDisabled={ formState.isSubmitting }>
Continue Continue
</Button> </Button>
<Box> <Box>
...@@ -34,7 +83,7 @@ const AddressVerificationStepAddress = ({ onContinue }: Props) => { ...@@ -34,7 +83,7 @@ const AddressVerificationStepAddress = ({ onContinue }: Props) => {
<Link>support@blockscout.com</Link> <Link>support@blockscout.com</Link>
</Box> </Box>
</Flex> </Flex>
</Box> </form>
); );
}; };
......
import { Alert, Box, Button, Flex, Radio, RadioGroup } from '@chakra-ui/react'; import { Alert, Box, Button, Flex, Radio, RadioGroup } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import { useFormContext } from 'react-hook-form'; import type { SubmitHandler } from 'react-hook-form';
import { useForm } from 'react-hook-form';
import { useSignMessage } from 'wagmi'; import { useSignMessage } from 'wagmi';
import type { AddressVerificationFormFields } from '../types'; import type { AddressVerificationFormSecondStepFields, AddressCheckStatusSuccess, AddressVerificationFormFirstStepFields } from '../types';
import appConfig from 'configs/app/config';
import type { ResourceError } from 'lib/api/resources';
import useApiFetch from 'lib/api/useApiFetch';
import AddressVerificationFieldMessage from '../fields/AddressVerificationFieldMessage'; import AddressVerificationFieldMessage from '../fields/AddressVerificationFieldMessage';
import AddressVerificationFieldSignature from '../fields/AddressVerificationFieldSignature'; import AddressVerificationFieldSignature from '../fields/AddressVerificationFieldSignature';
interface Props { interface Props extends AddressVerificationFormFirstStepFields, AddressCheckStatusSuccess{}
onSubmit: () => void;
onSign: () => void;
}
const AddressVerificationStepSignature = ({ onSubmit, onSign }: Props) => { const AddressVerificationStepSignature = ({ address, signingMessage }: Props) => {
const [ signMethod, setSignMethod ] = React.useState<'wallet' | 'manually'>('wallet'); const [ signMethod, setSignMethod ] = React.useState<'wallet' | 'manually'>('wallet');
const [ error, setError ] = React.useState(''); const [ error, setError ] = React.useState('');
const { getValues, setValue, formState } = useFormContext<AddressVerificationFormFields>(); const formApi = useForm<AddressVerificationFormSecondStepFields>({
mode: 'onBlur',
defaultValues: {
message: signingMessage,
},
});
const { handleSubmit, formState, control, setValue, getValues } = formApi;
const apiFetch = useApiFetch();
const { signMessage, isLoading: isSigning } = useSignMessage({ const { signMessage, isLoading: isSigning } = useSignMessage({
onSuccess: (data) => { onSuccess: (data) => {
setValue('signature', data); setValue('signature', data);
onSubmit();
}, },
onError: (error) => { onError: (error) => {
setError((error as Error)?.message || 'Something went wrong'); setError((error as Error)?.message || 'Something went wrong');
...@@ -34,29 +44,46 @@ const AddressVerificationStepSignature = ({ onSubmit, onSign }: Props) => { ...@@ -34,29 +44,46 @@ const AddressVerificationStepSignature = ({ onSubmit, onSign }: Props) => {
}, []); }, []);
const handleWeb3SignClick = React.useCallback(() => { const handleWeb3SignClick = React.useCallback(() => {
onSign();
const message = getValues('message'); const message = getValues('message');
signMessage({ message }); signMessage({ message });
}, [ getValues, onSign, signMessage ]); }, [ getValues, signMessage ]);
const handleManualSignClick = React.useCallback(() => { const handleManualSignClick = React.useCallback(() => {
onSign(); }, []);
onSubmit();
}, [ onSign, onSubmit ]); const onFormSubmit: SubmitHandler<AddressVerificationFormSecondStepFields> = React.useCallback(async(data) => {
const body = {
contractAddress: address,
message: data.message,
signature: data.signature,
};
try {
await apiFetch('address_verification', {
fetchParams: { method: 'POST', body },
pathParams: { chainId: appConfig.network.id, type: ':verify' },
});
} catch (error: unknown) {
const _error = error as ResourceError<{message: string}>;
setError(_error.payload?.message || 'Oops! Something went wrong');
}
}, [ address, apiFetch ]);
const onSubmit = handleSubmit(onFormSubmit);
return ( return (
<Box> <form noValidate onSubmit={ onSubmit }>
{ error && <Alert status="warning" mb={ 6 }>{ error }</Alert> } { error && <Alert status="warning" mb={ 6 }>{ error }</Alert> }
<Box mb={ 8 }> <Box mb={ 8 }>
Please select the address to sign and copy the message below and sign it using the Blockscout sign message provider of your choice... Please select the address to sign and copy the message below and sign it using the Blockscout sign message provider of your choice...
</Box> </Box>
<Flex rowGap={ 5 } flexDir="column"> <Flex rowGap={ 5 } flexDir="column">
<AddressVerificationFieldMessage isDisabled/> <AddressVerificationFieldMessage formState={ formState } control={ control }/>
<RadioGroup onChange={ handleSignMethodChange } value={ signMethod } display="flex" flexDir="column" rowGap={ 4 }> <RadioGroup onChange={ handleSignMethodChange } value={ signMethod } display="flex" flexDir="column" rowGap={ 4 }>
<Radio value="wallet">Sign via Web3 wallet</Radio> <Radio value="wallet">Sign via Web3 wallet</Radio>
<Radio value="manually">Sign manually</Radio> <Radio value="manually">Sign manually</Radio>
</RadioGroup> </RadioGroup>
{ signMethod === 'manually' && <AddressVerificationFieldSignature/> } { signMethod === 'manually' && <AddressVerificationFieldSignature formState={ formState } control={ control }/> }
</Flex> </Flex>
<Flex alignItems="center" mt={ 8 } columnGap={ 5 }> <Flex alignItems="center" mt={ 8 } columnGap={ 5 }>
<Button <Button
...@@ -68,7 +95,7 @@ const AddressVerificationStepSignature = ({ onSubmit, onSign }: Props) => { ...@@ -68,7 +95,7 @@ const AddressVerificationStepSignature = ({ onSubmit, onSign }: Props) => {
{ signMethod === 'manually' ? 'Verify' : 'Sign and verify' } { signMethod === 'manually' ? 'Verify' : 'Sign and verify' }
</Button> </Button>
</Flex> </Flex>
</Box> </form>
); );
}; };
......
export interface AddressVerificationFormFields { export interface AddressVerificationFormFirstStepFields {
address: string; address: string;
}
export interface AddressVerificationFormSecondStepFields {
signature: string; signature: string;
message: string; message: string;
} }
export interface RootFields {
root: string;
}
export interface AddressCheckStatusSuccess {
contractCreator?: string;
contractOwner?: string;
signingMessage: string;
}
export type AddressCheckResponseSuccess = {
status: 'SUCCESS';
result: AddressCheckStatusSuccess;
} |
{ status: 'IS_OWNER_ERROR' } |
{ status: 'OWNERSHIP_VERIFIED_ERROR' } |
{ status: 'SOURCE_CODE_NOT_VERIFIED_ERROR' } |
{ status: 'INVALID_ADDRESS_ERROR' };
export interface AddressCheckResponseError {
code: number;
message: string;
}
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