Commit 09efb570 authored by tom's avatar tom

fix auth modal

parent e91e9ae1
......@@ -27,7 +27,7 @@ const RESTRICTED_MODULES = {
{ name: 'playwright/TestApp', message: 'Please use render() fixture from test() function of playwright/lib module' },
{
name: '@chakra-ui/react',
importNames: [ 'Menu', 'PinInput', 'useToast', 'useDisclosure' ],
importNames: [ 'Menu', 'useToast', 'useDisclosure' ],
message: 'Please use corresponding component or hook from ui/shared/chakra component instead',
},
{
......
......@@ -51,6 +51,7 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
{ ...(active ? { 'data-active': true } : {}) }
{ ...(selected ? { 'data-selected': true } : {}) }
{ ...(highlighted ? { 'data-highlighted': true } : {}) }
{ ...(loading ? { 'data-loading': true } : {}) }
disabled={ loading || disabled }
ref={ ref }
{ ...rest }
......
import { PinInput as ChakraPinInput, Group } from '@chakra-ui/react';
import * as React from 'react';
export interface PinInputProps extends ChakraPinInput.RootProps {
rootRef?: React.Ref<HTMLDivElement>;
count?: number;
inputProps?: React.InputHTMLAttributes<HTMLInputElement>;
attached?: boolean;
}
export const PinInput = React.forwardRef<HTMLInputElement, PinInputProps>(
function PinInput(props, ref) {
const { count = 6, inputProps, rootRef, attached, placeholder = ' ', bgColor, ...rest } = props;
return (
<ChakraPinInput.Root ref={ rootRef } placeholder={ placeholder } { ...rest }>
<ChakraPinInput.HiddenInput ref={ ref } { ...inputProps }/>
<ChakraPinInput.Control>
<Group attached={ attached }>
{ Array.from({ length: count }).map((_, index) => (
<ChakraPinInput.Input key={ index } index={ index } bgColor={ bgColor }/>
)) }
</Group>
</ChakraPinInput.Control>
</ChakraPinInput.Root>
);
},
);
export const keyframes = {
fromLeftToRight: {
from: {
left: '100%',
left: '0%',
transform: 'translateX(0%)',
},
to: {
left: '0%',
left: '100%',
transform: 'translateX(-100%)',
},
},
};
......@@ -171,6 +171,11 @@ const semanticTokens: ThemingConfig['semanticTokens'] = {
DEFAULT: { value: { _light: '{colors.blackAlpha.800}', _dark: '{colors.whiteAlpha.800}' } },
},
},
spinner: {
track: {
DEFAULT: { value: { _light: '{colors.blackAlpha.200}', _dark: '{colors.whiteAlpha.200}' } },
},
},
text: {
primary: { value: { _light: '{colors.blackAlpha.800}', _dark: '{colors.whiteAlpha.800}' } },
secondary: { value: { _light: '{colors.gray.500}', _dark: '{colors.gray.400}' } },
......
......@@ -14,13 +14,19 @@ export const recipe = defineRecipe({
variants: {
visual: {
solid: {
gap: 0,
bg: 'blue.600',
color: 'white',
_hover: {
bg: 'blue.400',
},
_active: { bg: 'blue.400' },
_loading: {
'& .chakra-spinner': {
borderColor: 'white',
borderBottomColor: 'spinner.track',
borderInlineStartColor: 'spinner.track',
},
},
},
outline: {
borderWidth: '2px',
......@@ -110,6 +116,8 @@ export const recipe = defineRecipe({
color: 'link.primary',
border: 'none',
fontWeight: '400',
px: 0,
h: 'auto',
_hover: {
bg: 'transparent',
color: 'link.primary.hovered',
......
......@@ -5,9 +5,11 @@ import { recipe as dialog } from './dialog.recipe';
import { recipe as field } from './field.recipe';
import { recipe as input } from './input.recipe';
import { recipe as link } from './link.recipe';
import { recipe as pinInput } from './pin-input.recipe';
import { recipe as popover } from './popover.recipe';
import { recipe as progressCircle } from './progress-circle.recipe';
import { recipe as skeleton } from './skeleton.recipe';
import { recipe as spinner } from './spinner.recipe';
import { recipe as switchRecipe } from './switch.recipe';
import { recipe as tabs } from './tabs.recipe';
import { recipe as textarea } from './textarea.recipe';
......@@ -20,6 +22,7 @@ export const recipes = {
input,
link,
skeleton,
spinner,
textarea,
};
......@@ -27,6 +30,7 @@ export const slotRecipes = {
alert,
dialog,
field,
pinInput,
popover,
progressCircle,
'switch': switchRecipe,
......
import { defineSlotRecipe } from '@chakra-ui/react';
import { mapEntries } from '../utils/entries';
import { recipe as inputRecipe } from './input.recipe';
const { variants } = inputRecipe;
export const recipe = defineSlotRecipe({
slots: [ 'input' ],
base: {
input: {
...inputRecipe.base,
textAlign: 'center',
width: 'var(--input-height)',
},
},
variants: {
size: {
md: {
input: {
boxSize: 10,
borderRadius: 'base',
},
},
},
variant: mapEntries(variants!.variant, (key, value) => [
key,
{ input: value },
]),
},
defaultVariants: {
size: 'md',
variant: 'outline',
},
});
import { defineRecipe } from '@chakra-ui/react';
export const recipe = defineRecipe({
base: {
display: 'inline-block',
borderColor: 'blue.500',
borderStyle: 'solid',
borderWidth: '2px',
borderRadius: 'full',
width: 'var(--spinner-size)',
height: 'var(--spinner-size)',
animation: 'spin',
animationDuration: 'slowest',
'--spinner-track-color': '{colors.spinner.track}',
borderBottomColor: 'var(--spinner-track-color)',
borderInlineStartColor: 'var(--spinner-track-color)',
},
variants: {
size: {
inherit: { '--spinner-size': '1em' },
xs: { '--spinner-size': 'sizes.3' },
sm: { '--spinner-size': 'sizes.4' },
md: { '--spinner-size': 'sizes.5' },
lg: { '--spinner-size': 'sizes.8' },
xl: { '--spinner-size': 'sizes.10' },
},
},
defaultVariants: {
size: 'md',
},
});
// https://github.com/chakra-ui/chakra-ui/blob/main/packages/react/src/utils/entries.ts#L1
export function mapEntries<A, B, K extends string | number | symbol>(
obj: { [key in K]: A },
f: (key: K, val: A) => [K, B],
): { [key in K]: B } {
const result: { [key in K]: B } = {} as unknown as { [key in K]: B };
for (const key in obj) {
const kv = f(key, obj[key]);
result[kv[0]] = kv[1];
}
return result;
}
/* eslint-disable max-len */
/* eslint-disable react/jsx-no-bind */
import { Heading, HStack, Link, Tabs, VStack } from '@chakra-ui/react';
import { Heading, HStack, Link, Spinner, Tabs, VStack } from '@chakra-ui/react';
import React from 'react';
import { Alert } from 'toolkit/chakra/alert';
......@@ -8,6 +8,7 @@ import { Button } from 'toolkit/chakra/button';
import { useColorMode } from 'toolkit/chakra/color-mode';
import { Field } from 'toolkit/chakra/field';
import { Input } from 'toolkit/chakra/input';
import { PinInput } from 'toolkit/chakra/pin-input';
import { ProgressCircleRing, ProgressCircleRoot } from 'toolkit/chakra/progress-circle';
import { Skeleton } from 'toolkit/chakra/skeleton';
import { Switch } from 'toolkit/chakra/switch';
......@@ -41,6 +42,8 @@ const ChakraShowcases = () => {
<Button visual="header" selected>Header selected</Button>
<Button visual="header" selected highlighted>Header highlighted</Button>
<Button visual="link">Link</Button>
<Button loading loadingText="Solid">Solid</Button>
<Button loading loadingText="Outline" visual="outline">Outline</Button>
</HStack>
</section>
......@@ -98,6 +101,10 @@ const ChakraShowcases = () => {
<Input type="email" value="me@example.com"/>
</Field>
</HStack>
<HStack mt={ 4 }>
<PinInput otp count={ 3 }/>
<PinInput otp count={ 3 } value={ [ '1', '2', '3' ] } disabled bgColor="dialog.bg"/>
</HStack>
</section>
<section>
......@@ -154,6 +161,7 @@ const ChakraShowcases = () => {
<span>Skeleton</span>
</Skeleton>
<ContentLoader/>
<Spinner/>
</HStack>
</section>
......
......@@ -19,7 +19,7 @@ const ContentLoader = ({ className, text }: Props) => {
width: '60px',
height: '6px',
animation: `fromLeftToRight 700ms ease-in-out infinite alternate`,
left: '100%',
left: '0%',
top: 0,
backgroundColor: 'blue.300',
borderRadius: 'full',
......
import { useDisclosure } from '@chakra-ui/react';
import React from 'react';
import { useDisclosure } from 'toolkit/hooks/useDisclosure';
import AuthModal from './AuthModal';
import useIsAuth from './useIsAuth';
......@@ -31,7 +32,7 @@ const AuthGuard = ({ children, onAuthSuccess }: Props) => {
return (
<>
{ children({ onClick: handleClick }) }
{ authModal.isOpen && <AuthModal onClose={ handleModalClose } initialScreen={{ type: 'select_method' }}/> }
{ authModal.open && <AuthModal onClose={ handleModalClose } initialScreen={{ type: 'select_method' }}/> }
</>
);
};
......
......@@ -8,7 +8,7 @@ import config from 'configs/app';
import { getResourceKey } from 'lib/api/useApiQuery';
import useGetCsrfToken from 'lib/hooks/useGetCsrfToken';
import * as mixpanel from 'lib/mixpanel';
import { DialogBackdrop, DialogBody, DialogContent, DialogHeader, DialogRoot } from 'toolkit/chakra/dialog';
import { DialogBody, DialogContent, DialogHeader, DialogRoot } from 'toolkit/chakra/dialog';
import ButtonBackTo from 'ui/shared/buttons/ButtonBackTo';
import AuthModalScreenConnectWallet from './screens/AuthModalScreenConnectWallet';
......@@ -99,8 +99,9 @@ const AuthModal = ({ initialScreen, onClose, mixpanelConfig, closeOnError }: Pro
!open && onClose();
}, [ onClose ]);
const currentStep = steps[steps.length - 1];
const header = (() => {
const currentStep = steps[steps.length - 1];
switch (currentStep.type) {
case 'select_method':
return 'Select a way to login';
......@@ -117,7 +118,6 @@ const AuthModal = ({ initialScreen, onClose, mixpanelConfig, closeOnError }: Pro
})();
const content = (() => {
const currentStep = steps[steps.length - 1];
switch (currentStep.type) {
case 'select_method':
return <AuthModalScreenSelectMethod onSelectMethod={ onNextStep }/>;
......@@ -168,8 +168,15 @@ const AuthModal = ({ initialScreen, onClose, mixpanelConfig, closeOnError }: Pro
}
return (
<DialogRoot open onOpenChange={ onModalOpenChange } size={{ base: 'full', lg: 'sm' }}>
<DialogBackdrop/>
<DialogRoot
open
onOpenChange={ onModalOpenChange }
size={{ base: 'full', lg: 'sm' }}
// we need to allow user interact with element outside of dialog otherwise they can't click on recaptcha
modal={ false }
// FIXME if we allow to close on interact outside, the dialog will be closed when user click on recaptcha
closeOnInteractOutside={ ![ 'email', 'otp_code' ].includes(currentStep.type) }
>
<DialogContent>
<DialogHeader
startElement={ steps.length > 1 && !steps[steps.length - 1].type.startsWith('success') && <ButtonBackTo onClick={ onPrevStep }/> }
......
import { HStack, PinInputField, Text } from '@chakra-ui/react';
import { HStack, Text } from '@chakra-ui/react';
import React from 'react';
import { useController, useFormContext } from 'react-hook-form';
import type { OtpCodeFormFields } from '../types';
import PinInput from 'ui/shared/chakra/PinInput';
import { PinInput } from 'toolkit/chakra/pin-input';
const CODE_LENGTH = 6;
......@@ -22,16 +22,24 @@ const AuthModalFieldOtpCode = ({ isDisabled: isDisabledProp }: Props) => {
const isDisabled = isDisabledProp || formState.isSubmitting;
const handleChange = React.useCallback(({ value }: { value: Array<string> }) => {
field.onChange(value);
}, [ field ]);
return (
<>
<HStack>
<PinInput otp placeholder="" { ...field } isDisabled={ isDisabled } isInvalid={ Boolean(fieldState.error) } bgColor="dialog_bg">
{ Array.from({ length: CODE_LENGTH }).map((_, index) => (
<PinInputField key={ index } borderRadius="base" borderWidth="2px" bgColor="dialog_bg"/>
)) }
</PinInput>
<PinInput
otp
name={ field.name }
value={ field.value }
onValueChange={ handleChange }
disabled={ isDisabled }
invalid={ Boolean(fieldState.error) }
bgColor="dialog.bg"
/>
</HStack>
{ fieldState.error?.message && <Text color="error" fontSize="xs" mt={ 1 }>{ fieldState.error.message }</Text> }
{ fieldState.error?.message && <Text color="text.error" textStyle="sm" mt={ 1 }>{ fieldState.error.message }</Text> }
</>
);
};
......
......@@ -35,7 +35,7 @@ const AuthModalScreenConnectWallet = ({ onSuccess, onError, isAuth, source }: Pr
}
}, [ start ]);
return <Center h="100px"><Spinner/></Center>;
return <Center h="100px"><Spinner size="xl"/></Center>;
};
export default React.memo(AuthModalScreenConnectWallet);
import { chakra, Box, Text, Button } from '@chakra-ui/react';
import { chakra, Box, Text } from '@chakra-ui/react';
import React from 'react';
import type { SubmitHandler } from 'react-hook-form';
import { FormProvider, useForm } from 'react-hook-form';
......@@ -6,10 +6,11 @@ import { FormProvider, useForm } from 'react-hook-form';
import type { OtpCodeFormFields, ScreenSuccess } from '../types';
import type { UserInfo } from 'types/api/account';
import { toaster } from 'toolkit/chakra/toaster';
import useApiFetch from 'lib/api/useApiFetch';
import getErrorMessage from 'lib/errors/getErrorMessage';
import getErrorObjPayload from 'lib/errors/getErrorObjPayload';
import { Button } from 'toolkit/chakra/button';
import { toaster } from 'toolkit/chakra/toaster';
import IconSvg from 'ui/shared/IconSvg';
import ReCaptcha from 'ui/shared/reCaptcha/ReCaptcha';
import useReCaptcha from 'ui/shared/reCaptcha/useReCaptcha';
......@@ -31,7 +32,7 @@ const AuthModalScreenOtpCode = ({ email, onSuccess, isAuth }: Props) => {
const formApi = useForm<OtpCodeFormFields>({
mode: 'onBlur',
defaultValues: {
code: '',
code: [],
},
});
......@@ -41,7 +42,7 @@ const AuthModalScreenOtpCode = ({ email, onSuccess, isAuth }: Props) => {
fetchParams: {
method: 'POST',
body: {
otp: formData.code,
otp: formData.code.join(''),
email,
},
},
......@@ -109,14 +110,10 @@ const AuthModalScreenOtpCode = ({ email, onSuccess, isAuth }: Props) => {
<AuthModalFieldOtpCode isDisabled={ isCodeSending }/>
<ReCaptcha ref={ recaptcha.ref }/>
<Button
variant="link"
display="flex"
alignItems="center"
visual="link"
columnGap={ 2 }
mt={ 3 }
fontWeight="400"
w="fit-content"
isDisabled={ isCodeSending }
disabled={ isCodeSending }
onClick={ handleResendCodeClick }
>
<IconSvg name="repeat" boxSize={ 5 }/>
......@@ -125,8 +122,8 @@ const AuthModalScreenOtpCode = ({ email, onSuccess, isAuth }: Props) => {
<Button
mt={ 6 }
type="submit"
isLoading={ formApi.formState.isSubmitting }
isDisabled={ formApi.formState.isSubmitting || isCodeSending }
loading={ formApi.formState.isSubmitting }
disabled={ formApi.formState.isSubmitting || isCodeSending }
loadingText="Submit"
onClick={ formApi.handleSubmit(onFormSubmit) }
>
......
......@@ -28,9 +28,9 @@ const AuthModalScreenSelectMethod = ({ onSelectMethod }: Props) => {
}, [ onSelectMethod ]);
return (
<VStack spacing={ 3 } mt={ 4 } align="stretch">
<Button variant="outline" onClick={ handleConnectWalletClick }>Continue with Web3 wallet</Button>
<Button variant="outline" onClick={ handleEmailClick }>Continue with email</Button>
<VStack gap={ 3 } mt={ 4 } align="stretch">
<Button visual="outline" onClick={ handleConnectWalletClick }>Continue with Web3 wallet</Button>
<Button visual="outline" onClick={ handleEmailClick }>Continue with email</Button>
</VStack>
);
};
......
......@@ -29,7 +29,7 @@ const AuthModalScreenSuccessEmail = ({ email, onConnectWallet, onClose, isAuth,
</Text>
<Button
mt={ 6 }
variant="outline"
visual="outline"
onClick={ onClose }
>
Got it!
......@@ -51,7 +51,7 @@ const AuthModalScreenSuccessEmail = ({ email, onConnectWallet, onClose, isAuth,
</>
) : (
<Button
variant="outline"
visual="outline"
mt={ 6 }
onClick={ onClose }
>
......
......@@ -31,7 +31,7 @@ const AuthModalScreenSuccessWallet = ({ address, onAddEmail, onClose, isAuth, pr
</Text>
<Button
mt={ 6 }
variant="outline"
visual="outline"
onClick={ onClose }
>
Got it!
......@@ -55,13 +55,13 @@ const AuthModalScreenSuccessWallet = ({ address, onAddEmail, onClose, isAuth, pr
</Text>
<Flex mt={ 6 } gap={ 2 }>
<Button onClick={ handleAddEmailClick }>Add email</Button>
<Button variant="simple" onClick={ onClose }>I{ apos }ll do it later</Button>
<Button visual="link" onClick={ onClose }>I{ apos }ll do it later</Button>
</Flex>
</>
) : (
<Button
mt={ 6 }
variant="outline"
visual="outline"
onClick={ onClose }
>
Got it!
......
......@@ -30,5 +30,5 @@ export interface EmailFormFields {
}
export interface OtpCodeFormFields {
code: string;
code: Array<string>;
}
......@@ -37,6 +37,7 @@ const DeFiDropdown = () => {
height: 5,
px: 1.5,
fontWeight: '500',
gap: 0,
};
const items = feature.items.map((item) => ({
......
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