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

Commit 06d6b8e

Browse files
chore: update with recent changes
2 parents 2651ce5 + a6284be commit 06d6b8e

File tree

11 files changed

+191
-74
lines changed

11 files changed

+191
-74
lines changed

docs/languages/javascript/websocket-connection/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ If you're not familiar with WebSockets, please check out [our documentation](/do
1717
:::
1818

1919
### Set up a WebSocket connection
20+
2021
<!-- To create a websocket connection, we want to use the Deriv websocket URL with an `app_id`. You can create your own app_id within your [dashboard](/dashboard) or keep the default `1089` app_id for testing. Keep in mind that eventually, you should make your own app_id. Especially if you would like to monetize your application. -->
2122

2223
Next, we'll create a WebSocket connection to Deriv WebSocket Server as seen below:

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: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
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+
import TokenCreationDialogSuccess from '../../Dialogs/TokenCreationDialogSuccess';
7+
8+
type TCreateTokenField = {
9+
register: UseFormRegisterReturn;
10+
errors: Partial<
11+
FieldErrorsImpl<{
12+
read: boolean;
13+
trade: boolean;
14+
payments: boolean;
15+
trading_information: boolean;
16+
admin: boolean;
17+
name: string;
18+
}>
19+
>;
20+
form_is_cleared: boolean;
21+
setFormIsCleared: Dispatch<SetStateAction<boolean>>;
22+
is_toggle: boolean;
23+
setToggleModal: Dispatch<SetStateAction<boolean>>;
24+
};
25+
26+
const CreateTokenField = ({
27+
errors,
28+
register,
29+
form_is_cleared,
30+
setFormIsCleared,
31+
is_toggle,
32+
setToggleModal,
33+
}: TCreateTokenField) => {
34+
const { tokens } = useApiToken();
35+
const [input_value, setInputValue] = useState('');
36+
37+
useEffect(() => {
38+
if (form_is_cleared) {
39+
setInputValue('');
40+
setFormIsCleared(false);
41+
}
42+
}, [form_is_cleared]);
43+
44+
const getTokenNames = useMemo(() => {
45+
const token_names = [];
46+
for (const token_object of tokens) {
47+
const token_name = token_object.display_name.toLowerCase();
48+
token_names.push(token_name);
49+
}
50+
return token_names;
51+
}, [tokens]);
52+
53+
const token_name_exists = getTokenNames.includes(input_value.toLowerCase());
54+
const disable_button = token_name_exists || Object.keys(errors).length > 0 || input_value === '';
55+
const error_border_active = token_name_exists || errors.name;
56+
57+
return (
58+
<React.Fragment>
59+
<div className={styles.step_title}>
60+
<div className={`${styles.second_step} ${styles.step}`}>
61+
<Text as={'p'} type={'paragraph-1'} data-testid={'second-step-title'}>
62+
Name your token and click on Create to generate your token.
63+
</Text>
64+
</div>
65+
</div>
66+
<div
67+
onChange={(e) => setInputValue((e.target as HTMLInputElement).value)}
68+
className={`${styles.customTextInput} ${error_border_active ? 'error-border' : ''}`}
69+
>
70+
<input
71+
className={`${error_border_active ? 'error-border' : ''}`}
72+
type='text'
73+
name='name'
74+
{...register}
75+
placeholder='Token name'
76+
/>
77+
<Button disabled={disable_button} type='submit'>
78+
Create
79+
</Button>
80+
{is_toggle && <TokenCreationDialogSuccess setToggleModal={setToggleModal} />}
81+
</div>
82+
{errors && errors.name && (
83+
<Text as='span' type='paragraph-1' className='error-message'>
84+
{errors.name.message}
85+
</Text>
86+
)}
87+
{token_name_exists && (
88+
<div className='error-message'>
89+
<p>That name is taken. Choose another.</p>
90+
</div>
91+
)}
92+
</React.Fragment>
93+
);
94+
};
95+
96+
export default CreateTokenField;

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

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,32 @@ import { cleanup, fireEvent, 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

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

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

@@ -126,29 +161,16 @@ describe('Home Page', () => {
126161
expect(modal).toBeVisible();
127162
});
128163

129-
it('Should have create button disabled when input is empty or ends with whitespace', async () => {
164+
it('Should have create button disabled in case of empty input or error message', async () => {
130165
const submitButton = screen.getByRole('button', { name: /Create/i });
131166
expect(submitButton).toBeDisabled();
132167

133168
const nameInput = screen.getByRole('textbox');
134169

135-
await userEvent.type(nameInput, 'token text');
136-
expect(submitButton).not.toBeDisabled();
137-
138-
await userEvent.clear(nameInput);
170+
await userEvent.type(nameInput, 'token-text');
139171
expect(submitButton).toBeDisabled();
140172

141173
await userEvent.clear(nameInput);
142-
await userEvent.type(nameInput, 'token text ');
143174
expect(submitButton).toBeDisabled();
144175
});
145-
146-
it('Should disable user from entering whitespace in the beginning', async () => {
147-
const nameInput = screen.getByRole('textbox');
148-
149-
fireEvent.keyDown(nameInput, { code: 'Space', preventDefault });
150-
await userEvent.type(nameInput, '{space}');
151-
152-
expect(nameInput).toHaveValue('');
153-
});
154176
});

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: 38 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import React, { ChangeEvent, HTMLAttributes, useCallback, useState } from 'react';
2-
import { Button, Text } from '@deriv/ui';
1+
import React, { HTMLAttributes, useCallback, useState } from 'react';
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,7 +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 TokenCreationDialogSuccess from '../Dialogs/TokenCreationDialogSuccess';
11+
import CreateTokenField from './CreateTokenField';
1212

1313
const schema = yup
1414
.object({
@@ -17,7 +17,22 @@ const schema = yup
1717
payments: yup.boolean(),
1818
trading_information: yup.boolean(),
1919
admin: yup.boolean(),
20-
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+
),
2136
})
2237
.required();
2338

@@ -64,16 +79,21 @@ const scopes: TScope[] = [
6479

6580
const ApiTokenForm = (props: HTMLAttributes<HTMLFormElement>) => {
6681
const { createToken, isCreatingToken } = useCreateToken();
82+
const [form_is_cleared, setFormIsCleared] = useState(false);
83+
const [is_toggle, setToggleModal] = useState(false);
6784

68-
const { handleSubmit, register, setValue, getValues, reset } = useForm<TApiTokenForm>({
85+
const {
86+
handleSubmit,
87+
register,
88+
setValue,
89+
getValues,
90+
reset,
91+
formState: { errors },
92+
} = useForm<TApiTokenForm>({
6993
resolver: yupResolver(schema),
7094
mode: 'all',
7195
});
7296

73-
const [is_toggle, setToggleModal] = useState(false);
74-
const [is_disabled, setDisabled] = useState(true);
75-
const [is_empty, setEmpty] = useState(true);
76-
7797
const onSubmit = useCallback(
7898
(data: TApiTokenForm) => {
7999
const { name } = data;
@@ -85,6 +105,7 @@ const ApiTokenForm = (props: HTMLAttributes<HTMLFormElement>) => {
85105
trading_information: data.trading_information,
86106
});
87107
createToken(name, selectedTokenScope);
108+
setFormIsCleared(true);
88109
setToggleModal(!is_toggle);
89110
reset();
90111
},
@@ -99,20 +120,6 @@ const ApiTokenForm = (props: HTMLAttributes<HTMLFormElement>) => {
99120
[getValues, setValue],
100121
);
101122

102-
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
103-
const message = e.target.value;
104-
message.trim().length === 0 ? setEmpty(true) : setEmpty(false);
105-
/^\s.*/.test(message) || /\s$/.test(message) || message.trim().length === 0
106-
? setDisabled(true)
107-
: setDisabled(false);
108-
};
109-
110-
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
111-
if (e.code === 'Space' && is_empty) {
112-
e.preventDefault();
113-
}
114-
};
115-
116123
return (
117124
<form role={'form'} onSubmit={handleSubmit(onSubmit)} {...props}>
118125
<div className={styles.steps_line} />
@@ -147,30 +154,14 @@ const ApiTokenForm = (props: HTMLAttributes<HTMLFormElement>) => {
147154
/>
148155
))}
149156
</div>
150-
<div className={styles.step_title}>
151-
<div className={`${styles.second_step} ${styles.step}`}>
152-
<Text as={'p'} type={'paragraph-1'} data-testid={'second-step-title'}>
153-
Name your token and click on Create to generate your token.
154-
</Text>
155-
</div>
156-
</div>
157-
<div className={styles.customTextInput}>
158-
<input
159-
type='text'
160-
name='name'
161-
{...register('name')}
162-
placeholder='Token name'
163-
onChange={handleChange}
164-
onKeyDown={handleKeyDown}
165-
/>
166-
<Button type='submit' disabled={is_disabled}>
167-
Create
168-
</Button>
169-
{is_toggle && <TokenCreationDialogSuccess setToggleModal={setToggleModal} />}
170-
</div>
171-
<div className={styles.helperText}>
172-
<p>Length of token name must be between 2 and 32 characters.</p>
173-
</div>
157+
<CreateTokenField
158+
register={register('name')}
159+
errors={errors}
160+
form_is_cleared={form_is_cleared}
161+
setFormIsCleared={setFormIsCleared}
162+
is_toggle={is_toggle}
163+
setToggleModal={setToggleModal}
164+
/>
174165
<div className={styles.step_title}>
175166
<div className={`${styles.third_step} ${styles.step}`}>
176167
<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/AppForm/index.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,8 +163,7 @@ const AppForm = ({
163163
<label htmlFor='app_markup_percentage'>Markup percentage (optional)</label>
164164
</div>
165165
<Text as='p' type='paragraph-1' className={styles.helperText}>
166-
If you don&lsquo;t want to earn a markup, enter 0 here. Otherwise, enter a number
167-
up to 5. Maximum: 5.00%.
166+
Enter 0 if you don&lsquo;t want to earn a markup. Max markup: 5%
168167
</Text>
169168
{errors && errors?.app_markup_percentage && (
170169
<Text as='span' type='paragraph-1' className='error-message'>

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 {

0 commit comments

Comments
 (0)