Skip to content

Commit f01ff30

Browse files
fix(ui): confirmation safeguard danger zone (freeCodeCamp#55724)
Co-authored-by: Tom <[email protected]>
1 parent cd9a708 commit f01ff30

File tree

5 files changed

+140
-7
lines changed

5 files changed

+140
-7
lines changed

client/i18n/locales/english/translations.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,10 @@
275275
"reset-p2": "You will effectively be set back to the very first day you signed up.",
276276
"reset-p3": "We won't be able to recover any of it for you later, even if you change your mind.",
277277
"nevermind-2": "Nevermind, I don't want to delete all of my progress",
278-
"reset-confirm": "Reset everything. I want to start from the beginning"
278+
"reset-confirm": "Reset everything. I want to start from the beginning",
279+
"verify-text": "To verify, type \"{{ verifyText }}\" below:",
280+
"verify-reset-text": "I agree that all progress will be lost",
281+
"verify-delete-text": "I agree to delete my account"
279282
},
280283
"email": {
281284
"missing": "You do not have an email associated with this account.",

client/src/components/settings/delete-modal.tsx

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
1-
import React from 'react';
1+
import React, { useState } from 'react';
22
import { Trans, useTranslation } from 'react-i18next';
3-
import { Button, Modal } from '@freecodecamp/ui';
3+
import {
4+
Button,
5+
ControlLabel,
6+
FormControl,
7+
FormGroup,
8+
Modal
9+
} from '@freecodecamp/ui';
410

511
import { Spacer } from '../helpers';
612

@@ -14,6 +20,14 @@ function DeleteModal(props: DeleteModalProps): JSX.Element {
1420
const { show, onHide } = props;
1521
const email = '[email protected]';
1622
const { t } = useTranslation();
23+
const [verifyText, setVerifyText] = useState('');
24+
25+
const handleVerifyTextChange = (
26+
event: React.ChangeEvent<HTMLInputElement>
27+
) => {
28+
setVerifyText(event.target.value);
29+
};
30+
1731
return (
1832
<Modal onClose={onHide} open={show} variant='danger' size='large'>
1933
<Modal.Header showCloseButton={true} closeButtonClassNames='close'>
@@ -41,11 +55,26 @@ function DeleteModal(props: DeleteModalProps): JSX.Element {
4155
{t('settings.danger.nevermind')}
4256
</Button>
4357
<Spacer size='small' />
58+
<FormGroup controlId='verify-delete'>
59+
<ControlLabel htmlFor='verify-delete-input'>
60+
{t('settings.danger.verify-text', {
61+
verifyText: t('settings.danger.verify-delete-text')
62+
})}
63+
</ControlLabel>
64+
<Spacer size='small' />
65+
<FormControl
66+
onChange={handleVerifyTextChange}
67+
value={verifyText}
68+
id='verify-delete-input'
69+
/>
70+
</FormGroup>
71+
<Spacer size='small' />
4472
<Button
4573
block={true}
4674
size='large'
4775
variant='danger'
4876
onClick={props.delete}
77+
disabled={verifyText !== t('settings.danger.verify-delete-text')}
4978
type='button'
5079
>
5180
{t('settings.danger.certain')}

client/src/components/settings/reset-modal.tsx

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
1-
import React from 'react';
1+
import React, { useState } from 'react';
22
import { useTranslation } from 'react-i18next';
3-
import { Button, Modal } from '@freecodecamp/ui';
3+
import {
4+
Button,
5+
ControlLabel,
6+
FormControl,
7+
FormGroup,
8+
Modal
9+
} from '@freecodecamp/ui';
410

511
import { Spacer } from '../helpers';
612

@@ -13,6 +19,13 @@ type ResetModalProps = {
1319
function ResetModal(props: ResetModalProps): JSX.Element {
1420
const { t } = useTranslation();
1521
const { show, onHide } = props;
22+
const [verifyText, setVerifyText] = useState('');
23+
24+
const handleVerifyTextChange = (
25+
event: React.ChangeEvent<HTMLInputElement>
26+
) => {
27+
setVerifyText(event.target.value);
28+
};
1629

1730
return (
1831
<Modal size='large' onClose={onHide} variant='danger' open={show}>
@@ -40,10 +53,25 @@ function ResetModal(props: ResetModalProps): JSX.Element {
4053
{t('settings.danger.nevermind-2')}
4154
</Button>
4255
<Spacer size='small' />
56+
<FormGroup controlId='verify-reset'>
57+
<ControlLabel htmlFor='verify-reset-input'>
58+
{t('settings.danger.verify-text', {
59+
verifyText: t('settings.danger.verify-reset-text')
60+
})}
61+
</ControlLabel>
62+
<Spacer size='small' />
63+
<FormControl
64+
onChange={handleVerifyTextChange}
65+
value={verifyText}
66+
id='verify-reset-input'
67+
/>
68+
</FormGroup>
69+
<Spacer size='small' />
4370
<Button
4471
block={true}
4572
size='large'
4673
variant='danger'
74+
disabled={verifyText !== t('settings.danger.verify-reset-text')}
4775
onClick={props.reset}
4876
type='button'
4977
>

e2e/delete-modal.spec.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ test.describe('Delete Modal component', () => {
5959
page.getByRole('button', { name: translations.settings.danger.certain })
6060
).toBeVisible();
6161

62+
await expect(
63+
page.getByRole('button', { name: translations.settings.danger.certain })
64+
).toBeDisabled();
65+
6266
await expect(
6367
page.getByRole('button', { name: translations.buttons.close })
6468
).toBeVisible();
@@ -88,7 +92,32 @@ test.describe('Delete Modal component', () => {
8892
).not.toBeVisible();
8993
});
9094

91-
test('should close the modal and redirect to /learn after the user clicks delete', async ({
95+
test('Delele button should be disabled if user incorrectly fills verify input text', async ({
96+
page
97+
}) => {
98+
await page
99+
.getByRole('button', { name: translations.settings.danger.delete })
100+
.click();
101+
102+
await expect(
103+
page.getByRole('dialog', {
104+
name: translations.settings.danger['delete-title']
105+
})
106+
).toBeVisible();
107+
108+
const verifyDeleteInput = page.getByRole('textbox', {
109+
exact: true
110+
});
111+
await verifyDeleteInput.fill('incorrect text');
112+
113+
await expect(
114+
page.getByRole('button', {
115+
name: translations.settings.danger.certain
116+
})
117+
).toBeDisabled();
118+
});
119+
120+
test('should close the modal and redirect to /learn after the user fills the verify input text and clicks delete', async ({
92121
page
93122
}) => {
94123
await page
@@ -101,6 +130,13 @@ test.describe('Delete Modal component', () => {
101130
})
102131
).toBeVisible();
103132

133+
const verifyDeleteText = translations.settings.danger['verify-delete-text'];
134+
135+
const verifyDeleteInput = page.getByRole('textbox', {
136+
exact: true
137+
});
138+
await verifyDeleteInput.fill(verifyDeleteText);
139+
104140
await page
105141
.getByRole('button', { name: translations.settings.danger.certain })
106142
.click();

e2e/progress-reset-modal.spec.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { exec } from 'child_process';
22
import { promisify } from 'util';
33
import { test, expect } from '@playwright/test';
4+
import translations from '../client/i18n/locales/english/translations.json';
45

56
const execP = promisify(exec);
67

@@ -69,6 +70,12 @@ test.describe('Progress reset modal', () => {
6970
name: 'Reset everything. I want to start from the beginning'
7071
})
7172
).toBeVisible();
73+
74+
await expect(
75+
page.getByRole('button', {
76+
name: 'Reset everything. I want to start from the beginning'
77+
})
78+
).toBeDisabled();
7279
});
7380

7481
test('should close the dialog if the user clicks the cancel button', async ({
@@ -93,7 +100,30 @@ test.describe('Progress reset modal', () => {
93100
).toBeHidden();
94101
});
95102

96-
test('should reset the progress if the user clicks the reset button', async ({
103+
test('Reset progress button should be disabled if user incorrectly fills verify input text', async ({
104+
page
105+
}) => {
106+
await page
107+
.getByRole('button', { name: 'Reset all of my progress' })
108+
.click();
109+
110+
await expect(
111+
page.getByRole('dialog', { name: 'Reset My Progress' })
112+
).toBeVisible();
113+
114+
const verifyResetInput = page.getByRole('textbox', {
115+
exact: true
116+
});
117+
await verifyResetInput.fill('incorrect text');
118+
119+
await expect(
120+
page.getByRole('button', {
121+
name: 'Reset everything. I want to start from the beginning'
122+
})
123+
).toBeDisabled();
124+
});
125+
126+
test('should reset the progress if the user fills the verify input text and clicks the reset button', async ({
97127
page
98128
}) => {
99129
await page
@@ -104,6 +134,13 @@ test.describe('Progress reset modal', () => {
104134
page.getByRole('dialog', { name: 'Reset My Progress' })
105135
).toBeVisible();
106136

137+
const verifyResetText = translations.settings.danger['verify-reset-text'];
138+
139+
const verifyResetInput = page.getByRole('textbox', {
140+
exact: true
141+
});
142+
await verifyResetInput.fill(verifyResetText);
143+
107144
await page
108145
.getByRole('button', {
109146
name: 'Reset everything. I want to start from the beginning'

0 commit comments

Comments
 (0)