Skip to content
This repository was archived by the owner on May 13, 2024. It is now read-only.

Commit a6284be

Browse files
authored
Merge pull request #198 from hubert-deriv/error_messages_invalid_token
2 parents 9bc76e4 + 9dd089d commit a6284be

File tree

9 files changed

+177
-27
lines changed

9 files changed

+177
-27
lines changed

src/components/ApiTokenNavbarItem/api_token_switcher.module.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@
121121
@media (max-width: 1200px) {
122122
position: fixed;
123123
width: 100%;
124-
top: calc(var(--nav-height) + rem(3.3));
124+
top: calc(var(--nav-height) + rem(7));
125125
left: 0;
126126
right: 0;
127127
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import React, { Dispatch, SetStateAction, useEffect, useMemo, useState } from 'react';
2+
import { Text, Button } from '@deriv/ui';
3+
import styles from '../api-token.form.module.scss';
4+
import useApiToken from '@site/src/hooks/useApiToken';
5+
import { FieldErrorsImpl, UseFormRegisterReturn } from 'react-hook-form';
6+
7+
type TCreateTokenField = {
8+
register: UseFormRegisterReturn;
9+
errors: Partial<
10+
FieldErrorsImpl<{
11+
read: boolean;
12+
trade: boolean;
13+
payments: boolean;
14+
trading_information: boolean;
15+
admin: boolean;
16+
name: string;
17+
}>
18+
>;
19+
form_is_cleared: boolean;
20+
setFormIsCleared: Dispatch<SetStateAction<boolean>>;
21+
};
22+
23+
const CreateTokenField = ({
24+
errors,
25+
register,
26+
form_is_cleared,
27+
setFormIsCleared,
28+
}: TCreateTokenField) => {
29+
const { tokens } = useApiToken();
30+
const [input_value, setInputValue] = useState('');
31+
32+
useEffect(() => {
33+
if (form_is_cleared) {
34+
setInputValue('');
35+
setFormIsCleared(false);
36+
}
37+
}, [form_is_cleared]);
38+
39+
const getTokenNames = useMemo(() => {
40+
const token_names = [];
41+
for (const token_object of tokens) {
42+
const token_name = token_object.display_name.toLowerCase();
43+
token_names.push(token_name);
44+
}
45+
return token_names;
46+
}, [tokens]);
47+
48+
const token_name_exists = getTokenNames.includes(input_value.toLowerCase());
49+
const disable_button = token_name_exists || Object.keys(errors).length > 0 || input_value === '';
50+
const error_border_active = token_name_exists || errors.name;
51+
52+
return (
53+
<React.Fragment>
54+
<div className={styles.step_title}>
55+
<div className={`${styles.second_step} ${styles.step}`}>
56+
<Text as={'p'} type={'paragraph-1'} data-testid={'second-step-title'}>
57+
Name your token and click on Create to generate your token.
58+
</Text>
59+
</div>
60+
</div>
61+
<div
62+
onChange={(e) => setInputValue((e.target as HTMLInputElement).value)}
63+
className={`${styles.customTextInput} ${error_border_active ? 'error-border' : ''}`}
64+
>
65+
<input
66+
className={`${error_border_active ? 'error-border' : ''}`}
67+
type='text'
68+
name='name'
69+
{...register}
70+
placeholder='Token name'
71+
/>
72+
<Button disabled={disable_button} type='submit'>
73+
Create
74+
</Button>
75+
</div>
76+
{errors && errors.name && (
77+
<Text as='span' type='paragraph-1' className='error-message'>
78+
{errors.name.message}
79+
</Text>
80+
)}
81+
{token_name_exists && (
82+
<div className='error-message'>
83+
<p>That name is taken. Choose another.</p>
84+
</div>
85+
)}
86+
</React.Fragment>
87+
);
88+
};
89+
90+
export default CreateTokenField;

src/features/dashboard/components/ApiTokenForm/__tests__/api-token.form.test.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,32 @@ import { cleanup, render, screen, within } from '@site/src/test-utils';
33
import userEvent from '@testing-library/user-event';
44
import ApiTokenForm from '../api-token.form';
55
import useCreateToken from '../../../hooks/useCreateToken';
6+
import useApiToken from '@site/src/hooks/useApiToken';
7+
8+
jest.mock('@site/src/hooks/useApiToken');
9+
10+
const mockUseApiToken = useApiToken as jest.MockedFunction<
11+
() => Partial<ReturnType<typeof useApiToken>>
12+
>;
13+
14+
mockUseApiToken.mockImplementation(() => ({
15+
tokens: [
16+
{
17+
display_name: 'testtoken1',
18+
last_used: '',
19+
scopes: ['read', 'trade', 'payments', 'admin'],
20+
token: 'asdf1234',
21+
valid_for_ip: '',
22+
},
23+
{
24+
display_name: 'testtoken2',
25+
last_used: '',
26+
scopes: ['read', 'trade', 'payments', 'admin'],
27+
token: 'asdf1235',
28+
valid_for_ip: '',
29+
},
30+
],
31+
}));
632

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

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

132+
it('Should not be able to create a token if name already exists', async () => {
133+
const nameInput = screen.getByRole('textbox');
134+
135+
await userEvent.type(nameInput, 'testtoken1');
136+
137+
const error = screen.getByText(/That name is taken. Choose another./i);
138+
expect(error).toBeVisible;
139+
});
140+
106141
it('Should not create token when name input is empty', async () => {
107142
const nameInput = screen.getByRole('textbox');
108143

src/features/dashboard/components/ApiTokenForm/api-token.form.module.scss

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -56,19 +56,16 @@ form {
5656
position: relative;
5757
box-sizing: border-box;
5858
margin: rem(1) 0;
59-
&:focus-within {
60-
border-color: var(--colors-blue400);
61-
}
6259
&:hover {
6360
border: 1px solid var(--colors-greyLight600);
6461
}
62+
&:focus-within {
63+
border-color: var(--colors-blue500);
64+
}
6565
button {
66-
top: 0;
67-
bottom: 0;
68-
right: 0;
69-
position: absolute;
7066
border-top-left-radius: 0;
7167
border-bottom-left-radius: 0;
68+
height: rem(3);
7269
}
7370
label {
7471
position: absolute;
@@ -98,7 +95,7 @@ form {
9895
}
9996
&:focus {
10097
outline-color: unset;
101-
outline: 1px solid var(--colors-blue500);
98+
outline: unset;
10299
border-radius: rem(0.3);
103100
& ~ label {
104101
color: var(--colors-blue400);

src/features/dashboard/components/ApiTokenForm/api-token.form.tsx

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { HTMLAttributes, useCallback, useState } from 'react';
2-
import { Button, Text } from '@deriv/ui';
2+
import { Text } from '@deriv/ui';
33
import { useForm } from 'react-hook-form';
44
import { Circles } from 'react-loader-spinner';
55
import { yupResolver } from '@hookform/resolvers/yup';
@@ -8,6 +8,7 @@ import ApiTokenCard from '../ApiTokenCard';
88
import useCreateToken from '@site/src/features/dashboard/hooks/useCreateToken';
99
import * as yup from 'yup';
1010
import styles from './api-token.form.module.scss';
11+
import CreateTokenField from './CreateTokenField';
1112

1213
const schema = yup
1314
.object({
@@ -16,7 +17,22 @@ const schema = yup
1617
payments: yup.boolean(),
1718
trading_information: yup.boolean(),
1819
admin: yup.boolean(),
19-
name: yup.string().required(),
20+
name: yup
21+
.string()
22+
.min(2, 'Your token name must be atleast 2 characters long.')
23+
.max(32, 'Only up to 32 characters are allowed.')
24+
.matches(/^(?=.*[a-zA-Z0-9])[a-zA-Z0-9_ ]*$/, {
25+
message:
26+
'Only alphanumeric characters with spaces and underscores are allowed. (Example: my_application)',
27+
excludeEmptyString: true,
28+
})
29+
.matches(
30+
/^(?!.*deriv|.*d3r1v|.*der1v|.*d3riv|.*b1nary|.*binary|.*b1n4ry|.*bin4ry|.*blnary|.*b\|nary).*$/i,
31+
{
32+
message: 'The name cannot contain “Binary”, “Deriv”, or similar words.',
33+
excludeEmptyString: true,
34+
},
35+
),
2036
})
2137
.required();
2238

@@ -63,8 +79,16 @@ const scopes: TScope[] = [
6379

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

67-
const { handleSubmit, register, setValue, getValues, reset } = useForm<TApiTokenForm>({
84+
const {
85+
handleSubmit,
86+
register,
87+
setValue,
88+
getValues,
89+
reset,
90+
formState: { errors },
91+
} = useForm<TApiTokenForm>({
6892
resolver: yupResolver(schema),
6993
mode: 'all',
7094
});
@@ -80,6 +104,7 @@ const ApiTokenForm = (props: HTMLAttributes<HTMLFormElement>) => {
80104
trading_information: data.trading_information,
81105
});
82106
createToken(name, selectedTokenScope);
107+
setFormIsCleared(true);
83108
reset();
84109
},
85110
[createToken, reset],
@@ -127,20 +152,12 @@ const ApiTokenForm = (props: HTMLAttributes<HTMLFormElement>) => {
127152
/>
128153
))}
129154
</div>
130-
<div className={styles.step_title}>
131-
<div className={`${styles.second_step} ${styles.step}`}>
132-
<Text as={'p'} type={'paragraph-1'} data-testid={'second-step-title'}>
133-
Name your token and click on Create to generate your token.
134-
</Text>
135-
</div>
136-
</div>
137-
<div className={styles.customTextInput}>
138-
<input type='text' name='name' {...register('name')} placeholder='Token name' />
139-
<Button type='submit'>Create</Button>
140-
</div>
141-
<div className={styles.helperText}>
142-
<p>Length of token name must be between 2 and 32 characters.</p>
143-
</div>
155+
<CreateTokenField
156+
register={register('name')}
157+
errors={errors}
158+
form_is_cleared={form_is_cleared}
159+
setFormIsCleared={setFormIsCleared}
160+
/>
144161
<div className={styles.step_title}>
145162
<div className={`${styles.third_step} ${styles.step}`}>
146163
<Text as={'p'} type={'paragraph-1'} data-testid={'third-step-title'}>

src/features/dashboard/components/ApiTokenTable/DeleteTokenDialog/index.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ const DeleteTokenDialog = ({ onDelete, setToggleModal }: TDeleteTokendialog) =>
3232
text: 'Yes, delete',
3333
color: 'primary',
3434
onClick: () => {
35-
setToggleModal(false);
3635
onDelete();
3736
},
3837
},

src/features/dashboard/components/ApiTokenTable/table.lastused.cell.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const ApiLastUsedCell = ({
1616
const onDelete = () => {
1717
const values = row.original;
1818
deleteToken(values.token);
19+
setToggleModal(false);
1920
};
2021

2122
return (

src/features/dashboard/components/Table/table.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
td:first-child,
4040
th:first-child {
4141
padding-left: rem(3.2);
42+
white-space: break-spaces;
4243
}
4344

4445
tbody tr {

src/styles/index.scss

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,16 @@ h6 {
8989
color: var(--colors-coral500) !important;
9090
}
9191

92+
.error-border {
93+
border-color: var(--colors-coral500) !important;
94+
&:focus-within {
95+
border-color: var(--colors-coral500) !important;
96+
}
97+
&:focus {
98+
outline: var(--colors-coral500) !important;
99+
}
100+
}
101+
92102
/* reset */
93103
button {
94104
all: unset;

0 commit comments

Comments
 (0)