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 = {
// ACCOUNT: ADDRESS VERIFICATION & TOKEN INFO
address_verification: {
path: '/api/v1/chains/:chainId/verified-addresses\\:verify',
pathParams: [ 'chainId' as const ],
path: '/api/v1/chains/:chainId/verified-addresses:type',
pathParams: [ 'chainId' as const, 'type' as const ],
endpoint: appConfig.contractInfoApi.endpoint,
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 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 type { ResourceError } from 'lib/api/resources';
import useApiFetch from 'lib/api/useApiFetch';
import Web3ModalProvider from 'ui/shared/Web3ModalProvider';
import AddressVerificationStepAddress from './steps/AddressVerificationStepAddress';
......@@ -22,15 +17,10 @@ interface Props {
const AddressVerificationModal = ({ isOpen, onClose }: Props) => {
const [ stepIndex, setStepIndex ] = React.useState(0);
const [ error, setError ] = React.useState('');
const [ data, setData ] = React.useState<AddressVerificationFormFirstStepFields & AddressCheckStatusSuccess>({ address: '', signingMessage: '' });
const apiFetch = useApiFetch();
const formApi = useForm<AddressVerificationFormFields>({
mode: 'onBlur',
});
const { handleSubmit } = formApi;
const handleGoToNextStep = React.useCallback(() => {
const handleGoToSecondStep = React.useCallback((firstStepResult: typeof data) => {
setData(firstStepResult);
setStepIndex((prev) => prev + 1);
}, []);
......@@ -41,39 +31,11 @@ const AddressVerificationModal = ({ isOpen, onClose }: Props) => {
const handleClose = React.useCallback(() => {
onClose();
setStepIndex(0);
setError('');
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);
}, [ onClose ]);
const steps = [
{ title: 'Verify new address ownership', content: <AddressVerificationStepAddress onContinue={ handleGoToNextStep }/> },
{ title: 'Sign message', content: <AddressVerificationStepSignature onSubmit={ onSubmit } onSign={ handleSignClick }/> },
{ title: 'Verify new address ownership', content: <AddressVerificationStepAddress onContinue={ handleGoToSecondStep }/> },
{ title: 'Sign message', content: <AddressVerificationStepSignature { ...data }/> },
{ title: 'Congrats! Address is verified.', content: <AddressVerificationStepSuccess onShowListClick={ handleClose } onAddTokenClick={ handleClose }/> },
];
const step = steps[stepIndex];
......@@ -93,12 +55,7 @@ const AddressVerificationModal = ({ isOpen, onClose }: Props) => {
<ModalCloseButton/>
<ModalBody mb={ 0 }>
<Web3ModalProvider>
<FormProvider { ...formApi }>
<form noValidate onSubmit={ onSubmit }>
{ error && <Alert status="warning" mb={ 6 }>{ error }</Alert> }
{ step.content }
</form>
</FormProvider>
{ step.content }
</Web3ModalProvider>
</ModalBody>
</ModalContent>
......
import { FormControl, Input } from '@chakra-ui/react';
import React from 'react';
import type { ControllerRenderProps } from 'react-hook-form';
import { Controller, useFormContext } from 'react-hook-form';
import type { Control, ControllerRenderProps, FormState } 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 InputPlaceholder from 'ui/shared/InputPlaceholder';
type Fields = RootFields & AddressVerificationFormFirstStepFields;
interface Props {
isDisabled?: boolean;
formState: FormState<Fields>;
control: Control<Fields>;
}
const AddressVerificationFieldAddress = ({ isDisabled }: Props) => {
const { formState, control } = useFormContext<AddressVerificationFormFields>();
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<AddressVerificationFormFields, 'address'>}) => {
const AddressVerificationFieldAddress = ({ formState, control }: Props) => {
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<Fields, 'address'>}) => {
const error = 'address' in formState.errors ? formState.errors.address : undefined;
return (
......@@ -25,13 +25,13 @@ const AddressVerificationFieldAddress = ({ isDisabled }: Props) => {
required
isInvalid={ Boolean(error) }
maxLength={ ADDRESS_LENGTH }
isDisabled={ isDisabled || formState.isSubmitting }
isDisabled={ formState.isSubmitting }
autoComplete="off"
/>
<InputPlaceholder text="Smart contract address (0x...)" error={ error }/>
</FormControl>
);
}, [ formState.errors, formState.isSubmitting, isDisabled ]);
}, [ formState.errors, formState.isSubmitting ]);
return (
<Controller
......
import { FormControl, Textarea } from '@chakra-ui/react';
import React from 'react';
import type { ControllerRenderProps } from 'react-hook-form';
import { Controller, useFormContext } from 'react-hook-form';
import type { Control, ControllerRenderProps, FormState } 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';
interface Props {
isDisabled?: boolean;
formState: FormState<AddressVerificationFormSecondStepFields>;
control: Control<AddressVerificationFormSecondStepFields>;
}
const AddressVerificationFieldMessage = ({ isDisabled }: Props) => {
const { formState, control, getValues } = useFormContext<AddressVerificationFormFields>();
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<AddressVerificationFormFields, 'message'>}) => {
const AddressVerificationFieldMessage = ({ formState, control }: Props) => {
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<AddressVerificationFormSecondStepFields, 'message'>}) => {
const error = 'message' in formState.errors ? formState.errors.message : undefined;
return (
......@@ -24,22 +22,18 @@ const AddressVerificationFieldMessage = ({ isDisabled }: Props) => {
{ ...field }
required
isInvalid={ Boolean(error) }
isDisabled={ isDisabled || formState.isSubmitting }
isDisabled={ formState.isSubmitting }
autoComplete="off"
maxH="105px"
/>
<InputPlaceholder text="Message to sign" error={ error }/>
</FormControl>
);
}, [ formState.errors, formState.isSubmitting, isDisabled ]);
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 }]`;
}, [ formState.errors, formState.isSubmitting ]);
return (
<Controller
defaultValue={ defaultValue }
defaultValue="some value"
name="message"
control={ control }
render={ renderControl }
......
import { FormControl, Input } from '@chakra-ui/react';
import React from 'react';
import type { ControllerRenderProps } from 'react-hook-form';
import { Controller, useFormContext } from 'react-hook-form';
import type { Control, ControllerRenderProps, FormState } 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 InputPlaceholder from 'ui/shared/InputPlaceholder';
const AddressVerificationFieldSignature = () => {
const { formState, control } = useFormContext<AddressVerificationFormFields>();
interface Props {
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;
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 { 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';
type Fields = RootFields & AddressVerificationFormFirstStepFields;
interface Props {
onContinue: () => void;
onContinue: (data: AddressVerificationFormFirstStepFields & AddressCheckStatusSuccess) => void;
}
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 (formState.errors.address) {
trigger('address');
return;
if (response.status !== 'SUCCESS') {
switch (response.status) {
case 'INVALID_ADDRESS_ERROR': {
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' });
}
default: {
return setError('root', { type: 'manual', message: response.payload?.message || 'Oops! Something went wrong' });
}
}
}
onContinue();
}, [ formState, onContinue, trigger ]);
onContinue({ ...response.result, address: data.address });
}, [ apiFetch, onContinue, setError ]);
const onSubmit = handleSubmit(onFormSubmit);
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>
<AddressVerificationFieldAddress/>
<AddressVerificationFieldAddress formState={ formState } control={ control }/>
<Flex alignItems="center" mt={ 8 } columnGap={ 5 }>
<Button size="lg" onClick={ handleButtonClick }>
<Button size="lg" type="submit" isDisabled={ formState.isSubmitting }>
Continue
</Button>
<Box>
......@@ -34,7 +83,7 @@ const AddressVerificationStepAddress = ({ onContinue }: Props) => {
<Link>support@blockscout.com</Link>
</Box>
</Flex>
</Box>
</form>
);
};
......
import { Alert, Box, Button, Flex, Radio, RadioGroup } from '@chakra-ui/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 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 AddressVerificationFieldSignature from '../fields/AddressVerificationFieldSignature';
interface Props {
onSubmit: () => void;
onSign: () => void;
}
interface Props extends AddressVerificationFormFirstStepFields, AddressCheckStatusSuccess{}
const AddressVerificationStepSignature = ({ onSubmit, onSign }: Props) => {
const AddressVerificationStepSignature = ({ address, signingMessage }: Props) => {
const [ signMethod, setSignMethod ] = React.useState<'wallet' | 'manually'>('wallet');
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({
onSuccess: (data) => {
setValue('signature', data);
onSubmit();
},
onError: (error) => {
setError((error as Error)?.message || 'Something went wrong');
......@@ -34,29 +44,46 @@ const AddressVerificationStepSignature = ({ onSubmit, onSign }: Props) => {
}, []);
const handleWeb3SignClick = React.useCallback(() => {
onSign();
const message = getValues('message');
signMessage({ message });
}, [ getValues, onSign, signMessage ]);
}, [ getValues, signMessage ]);
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 (
<Box>
<form noValidate onSubmit={ onSubmit }>
{ error && <Alert status="warning" mb={ 6 }>{ error }</Alert> }
<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...
</Box>
<Flex rowGap={ 5 } flexDir="column">
<AddressVerificationFieldMessage isDisabled/>
<AddressVerificationFieldMessage formState={ formState } control={ control }/>
<RadioGroup onChange={ handleSignMethodChange } value={ signMethod } display="flex" flexDir="column" rowGap={ 4 }>
<Radio value="wallet">Sign via Web3 wallet</Radio>
<Radio value="manually">Sign manually</Radio>
</RadioGroup>
{ signMethod === 'manually' && <AddressVerificationFieldSignature/> }
{ signMethod === 'manually' && <AddressVerificationFieldSignature formState={ formState } control={ control }/> }
</Flex>
<Flex alignItems="center" mt={ 8 } columnGap={ 5 }>
<Button
......@@ -68,7 +95,7 @@ const AddressVerificationStepSignature = ({ onSubmit, onSign }: Props) => {
{ signMethod === 'manually' ? 'Verify' : 'Sign and verify' }
</Button>
</Flex>
</Box>
</form>
);
};
......
export interface AddressVerificationFormFields {
export interface AddressVerificationFormFirstStepFields {
address: string;
}
export interface AddressVerificationFormSecondStepFields {
signature: 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