Skip to content
This repository was archived by the owner on May 13, 2024. It is now read-only.
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@
@media (max-width: 1200px) {
position: fixed;
width: 100%;
top: calc(var(--nav-height) + rem(3.3));
top: calc(var(--nav-height) + rem(7));
left: 0;
right: 0;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import React, { Dispatch, SetStateAction, useEffect, useMemo, useState } from 'react';
import { Text, Button } from '@deriv/ui';
import styles from '../api-token.form.module.scss';
import useApiToken from '@site/src/hooks/useApiToken';
import { FieldErrorsImpl, UseFormRegisterReturn } from 'react-hook-form';

type TCreateTokenField = {
register: UseFormRegisterReturn;
errors: Partial<
FieldErrorsImpl<{
read: boolean;
trade: boolean;
payments: boolean;
trading_information: boolean;
admin: boolean;
name: string;
}>
>;
form_is_cleared: boolean;
setFormIsCleared: Dispatch<SetStateAction<boolean>>;
};

const CreateTokenField = ({
errors,
register,
form_is_cleared,
setFormIsCleared,
}: TCreateTokenField) => {
const { tokens } = useApiToken();
const [input_value, setInputValue] = useState('');

useEffect(() => {
if (form_is_cleared) {
setInputValue('');
setFormIsCleared(false);
}
}, [form_is_cleared]);

const getTokenNames = useMemo(() => {
const token_names = [];
for (const token_object of tokens) {
const token_name = token_object.display_name.toLowerCase();
token_names.push(token_name);
}
return token_names;
}, [tokens]);

const token_name_exists = getTokenNames.includes(input_value.toLowerCase());
const disable_button = token_name_exists || Object.keys(errors).length > 0 || input_value === '';
const error_border_active = token_name_exists || errors.name;

return (
<React.Fragment>
<div className={styles.step_title}>
<div className={`${styles.second_step} ${styles.step}`}>
<Text as={'p'} type={'paragraph-1'} data-testid={'second-step-title'}>
Name your token and click on Create to generate your token.
</Text>
</div>
</div>
<div
onChange={(e) => setInputValue((e.target as HTMLInputElement).value)}
className={`${styles.customTextInput} ${error_border_active ? 'error-border' : ''}`}
>
<input
className={`${error_border_active ? 'error-border' : ''}`}
type='text'
name='name'
{...register}
placeholder='Token name'
/>
<Button disabled={disable_button} type='submit'>
Create
</Button>
</div>
{errors && errors.name && (
<Text as='span' type='paragraph-1' className='error-message'>
{errors.name.message}
</Text>
)}
{token_name_exists && (
<div className='error-message'>
<p>That name is taken. Choose another.</p>
</div>
)}
</React.Fragment>
);
};

export default CreateTokenField;
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,32 @@ import { cleanup, render, screen, within } from '@site/src/test-utils';
import userEvent from '@testing-library/user-event';
import ApiTokenForm from '../api-token.form';
import useCreateToken from '../../../hooks/useCreateToken';
import useApiToken from '@site/src/hooks/useApiToken';

jest.mock('@site/src/hooks/useApiToken');

const mockUseApiToken = useApiToken as jest.MockedFunction<
() => Partial<ReturnType<typeof useApiToken>>
>;

mockUseApiToken.mockImplementation(() => ({
tokens: [
{
display_name: 'testtoken1',
last_used: '',
scopes: ['read', 'trade', 'payments', 'admin'],
token: 'asdf1234',
valid_for_ip: '',
},
{
display_name: 'testtoken2',
last_used: '',
scopes: ['read', 'trade', 'payments', 'admin'],
token: 'asdf1235',
valid_for_ip: '',
},
],
}));

jest.mock('@site/src/features/dashboard/hooks/useCreateToken');

Expand Down Expand Up @@ -103,6 +129,15 @@ describe('Home Page', () => {
expect(mockCreateToken).toHaveBeenCalledWith('test create token', []);
});

it('Should not be able to create a token if name already exists', async () => {
const nameInput = screen.getByRole('textbox');

await userEvent.type(nameInput, 'testtoken1');

const error = screen.getByText(/That name is taken. Choose another./i);
expect(error).toBeVisible;
});

it('Should not create token when name input is empty', async () => {
const nameInput = screen.getByRole('textbox');

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,19 +56,16 @@ form {
position: relative;
box-sizing: border-box;
margin: rem(1) 0;
&:focus-within {
border-color: var(--colors-blue400);
}
&:hover {
border: 1px solid var(--colors-greyLight600);
}
&:focus-within {
border-color: var(--colors-blue500);
}
button {
top: 0;
bottom: 0;
right: 0;
position: absolute;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
height: rem(3);
}
label {
position: absolute;
Expand Down Expand Up @@ -98,7 +95,7 @@ form {
}
&:focus {
outline-color: unset;
outline: 1px solid var(--colors-blue500);
outline: unset;
border-radius: rem(0.3);
& ~ label {
color: var(--colors-blue400);
Expand Down
51 changes: 34 additions & 17 deletions src/features/dashboard/components/ApiTokenForm/api-token.form.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { HTMLAttributes, useCallback, useState } from 'react';
import { Button, Text } from '@deriv/ui';
import { Text } from '@deriv/ui';
import { useForm } from 'react-hook-form';
import { Circles } from 'react-loader-spinner';
import { yupResolver } from '@hookform/resolvers/yup';
Expand All @@ -8,6 +8,7 @@ import ApiTokenCard from '../ApiTokenCard';
import useCreateToken from '@site/src/features/dashboard/hooks/useCreateToken';
import * as yup from 'yup';
import styles from './api-token.form.module.scss';
import CreateTokenField from './CreateTokenField';

const schema = yup
.object({
Expand All @@ -16,7 +17,22 @@ const schema = yup
payments: yup.boolean(),
trading_information: yup.boolean(),
admin: yup.boolean(),
name: yup.string().required(),
name: yup
.string()
.min(2, 'Your token name must be atleast 2 characters long.')
.max(32, 'Only up to 32 characters are allowed.')
.matches(/^(?=.*[a-zA-Z0-9])[a-zA-Z0-9_ ]*$/, {
message:
'Only alphanumeric characters with spaces and underscores are allowed. (Example: my_application)',
excludeEmptyString: true,
})
.matches(
/^(?!.*deriv|.*d3r1v|.*der1v|.*d3riv|.*b1nary|.*binary|.*b1n4ry|.*bin4ry|.*blnary|.*b\|nary).*$/i,
{
message: 'The name cannot contain “Binary”, “Deriv”, or similar words.',
excludeEmptyString: true,
},
),
})
.required();

Expand Down Expand Up @@ -63,8 +79,16 @@ const scopes: TScope[] = [

const ApiTokenForm = (props: HTMLAttributes<HTMLFormElement>) => {
const { createToken, isCreatingToken } = useCreateToken();
const [form_is_cleared, setFormIsCleared] = useState(false);

const { handleSubmit, register, setValue, getValues, reset } = useForm<TApiTokenForm>({
const {
handleSubmit,
register,
setValue,
getValues,
reset,
formState: { errors },
} = useForm<TApiTokenForm>({
resolver: yupResolver(schema),
mode: 'all',
});
Expand All @@ -80,6 +104,7 @@ const ApiTokenForm = (props: HTMLAttributes<HTMLFormElement>) => {
trading_information: data.trading_information,
});
createToken(name, selectedTokenScope);
setFormIsCleared(true);
reset();
},
[createToken, reset],
Expand Down Expand Up @@ -127,20 +152,12 @@ const ApiTokenForm = (props: HTMLAttributes<HTMLFormElement>) => {
/>
))}
</div>
<div className={styles.step_title}>
<div className={`${styles.second_step} ${styles.step}`}>
<Text as={'p'} type={'paragraph-1'} data-testid={'second-step-title'}>
Name your token and click on Create to generate your token.
</Text>
</div>
</div>
<div className={styles.customTextInput}>
<input type='text' name='name' {...register('name')} placeholder='Token name' />
<Button type='submit'>Create</Button>
</div>
<div className={styles.helperText}>
<p>Length of token name must be between 2 and 32 characters.</p>
</div>
<CreateTokenField
register={register('name')}
errors={errors}
form_is_cleared={form_is_cleared}
setFormIsCleared={setFormIsCleared}
/>
<div className={styles.step_title}>
<div className={`${styles.third_step} ${styles.step}`}>
<Text as={'p'} type={'paragraph-1'} data-testid={'third-step-title'}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ const DeleteTokenDialog = ({ onDelete, setToggleModal }: TDeleteTokendialog) =>
text: 'Yes, delete',
color: 'primary',
onClick: () => {
setToggleModal(false);
onDelete();
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const ApiLastUsedCell = ({
const onDelete = () => {
const values = row.original;
deleteToken(values.token);
setToggleModal(false);
};

return (
Expand Down
1 change: 1 addition & 0 deletions src/features/dashboard/components/Table/table.scss
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
td:first-child,
th:first-child {
padding-left: rem(3.2);
white-space: break-spaces;
}

tbody tr {
Expand Down
10 changes: 10 additions & 0 deletions src/styles/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,16 @@ h6 {
color: var(--colors-coral500) !important;
}

.error-border {
border-color: var(--colors-coral500) !important;
&:focus-within {
border-color: var(--colors-coral500) !important;
}
&:focus {
outline: var(--colors-coral500) !important;
}
}

/* reset */
button {
all: unset;
Expand Down