Skip to content

Commit 6be0f2a

Browse files
committed
frontend/RecoveryDataComponent: be more penetrant
Be more penetrant to force the user to download their recovery file fix #225
1 parent e489c7a commit 6be0f2a

File tree

4 files changed

+82
-20
lines changed

4 files changed

+82
-20
lines changed

frontend/src/components/RecoveryDataComponent.tsx

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Signal, useSignal } from "@preact/signals";
22
import { Base64 } from "js-base64";
3-
import { Button, Modal } from "react-bootstrap";
3+
import { Button, Modal, Form } from "react-bootstrap";
44
import { useTranslation } from "react-i18next";
55

66
interface RecoveryDataProps {
@@ -36,27 +36,56 @@ export async function saveRecoveryData(secret: Uint8Array, email: string) {
3636
export function RecoveryDataComponent(props: RecoveryDataProps) {
3737
const {t} = useTranslation("", {useSuspense: false, keyPrefix: "register"});
3838
const saved = useSignal(false);
39+
const confirmed = useSignal(false);
3940

4041
return <Modal show={props.show.value} onHide={() => {
41-
props.show.value = false;
42-
window.location.replace("/");
42+
// Only allow closing if user has saved and confirmed
43+
if (saved.value && confirmed.value) {
44+
props.show.value = false;
45+
window.location.replace("/");
46+
}
4347
}}>
44-
<Modal.Header closeButton>
48+
<Modal.Header closeButton={saved.value && confirmed.value}>
4549
<Modal.Title>{t("save_recovery_data")}</Modal.Title>
4650
</Modal.Header>
4751

4852
<Modal.Body>
4953
<p className="mb-3">{t("save_recovery_data_text")}</p>
50-
<Button variant="primary" onClick={() => {
51-
saveRecoveryData(props.secret, props.email);
52-
saved.value = true;
53-
}}>{t("save")}</Button>
54+
<div className="mb-3">
55+
<Button
56+
variant="primary"
57+
size="lg"
58+
className="w-100"
59+
onClick={() => {
60+
saveRecoveryData(props.secret, props.email);
61+
saved.value = true;
62+
}}>
63+
{t("save")}
64+
</Button>
65+
</div>
66+
{saved.value && (
67+
<Form.Check
68+
type="checkbox"
69+
id="recovery-confirmation"
70+
label={t("save_recovery_data_confirmation")}
71+
checked={confirmed.value}
72+
onChange={(e: any) => { confirmed.value = e.target.checked; }}
73+
className="mt-3"
74+
/>
75+
)}
5476
</Modal.Body>
5577

5678
<Modal.Footer>
57-
<Button variant={saved.value ? "primary" : "danger"} onClick={() => {
58-
props.show.value = false;
59-
}}>{t("close")}</Button>
79+
<Button
80+
variant={saved.value && confirmed.value ? "primary" : "secondary"}
81+
disabled={!saved.value || !confirmed.value}
82+
onClick={() => {
83+
if (saved.value && confirmed.value) {
84+
props.show.value = false;
85+
}
86+
}}>
87+
{t("close")}
88+
</Button>
6089
</Modal.Footer>
6190
</Modal>
6291
}

frontend/src/components/__tests__/RecoveryDataComponent.test.tsx

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ describe('RecoveryDataComponent', () => {
1010
vi.clearAllMocks();
1111
});
1212

13-
it('renders modal content when shown and triggers save', async () => {
13+
it('renders modal content when shown and triggers save with confirmation', async () => {
1414
const mod = await importComponent();
1515
const { RecoveryDataComponent } = mod;
1616

@@ -25,34 +25,65 @@ describe('RecoveryDataComponent', () => {
2525
const footer = screen.getByTestId('modal-footer');
2626
const closeButton = within(footer).getByRole('button', { name: 'close' });
2727

28-
expect(closeButton.className).toContain('btn-danger');
28+
// Initially close button should be disabled
29+
expect(closeButton).toBeDisabled();
30+
expect(closeButton.className).toContain('btn-secondary');
2931

3032
const saveButton = screen.getByRole('button', { name: 'save' });
3133
fireEvent.click(saveButton);
3234

35+
// After saving, confirmation checkbox should appear
3336
await waitFor(() => {
37+
expect(screen.getByLabelText('save_recovery_data_confirmation')).toBeInTheDocument();
38+
});
39+
40+
// Close button should still be disabled until checkbox is checked
41+
expect(closeButton).toBeDisabled();
42+
43+
// Check the confirmation checkbox
44+
const confirmationCheckbox = screen.getByLabelText('save_recovery_data_confirmation');
45+
fireEvent.click(confirmationCheckbox);
46+
47+
await waitFor(() => {
48+
expect(closeButton).not.toBeDisabled();
3449
expect(closeButton.className).toContain('btn-primary');
3550
});
3651

3752
fireEvent.click(closeButton);
3853
expect(show.value).toBe(false);
3954
});
4055

41-
it('calls onHide and navigates on modal close', async () => {
42-
const { RecoveryDataComponent } = await importComponent();
56+
it('prevents closing modal until file is saved and confirmed', async () => {
57+
const { RecoveryDataComponent } = await importComponent();
4358

4459
const show = signal(true);
4560

4661
render(<RecoveryDataComponent email={'[email protected]'} secret={new Uint8Array()} show={show} />);
4762

48-
const closeButtons = screen.getAllByTestId('modal-close');
49-
const bottomClose = closeButtons[closeButtons.length - 1];
50-
fireEvent.click(bottomClose);
63+
// Try to close the modal without saving/confirming - should not close
64+
const closeButton = screen.getByRole('button', { name: 'close' });
65+
fireEvent.click(closeButton);
66+
67+
// Modal should still be open
68+
expect(show.value).toBe(true);
5169

70+
// Save the file first
71+
const saveButton = screen.getByRole('button', { name: 'save' });
72+
fireEvent.click(saveButton);
73+
74+
// Check the confirmation checkbox
5275
await waitFor(() => {
53-
expect(show.value).toBe(false);
54-
expect(window.location.replace).toHaveBeenCalledWith('/');
76+
const confirmationCheckbox = screen.getByLabelText('save_recovery_data_confirmation');
77+
fireEvent.click(confirmationCheckbox);
5578
});
79+
80+
// Now try to close - should work
81+
await waitFor(() => {
82+
expect(closeButton).not.toBeDisabled();
83+
});
84+
85+
fireEvent.click(closeButton);
86+
expect(show.value).toBe(false);
5687
});
5788
});
5889

frontend/src/locales/de.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ export const de ={
130130
"save_recovery_data": "Wiederhestellungsdatei speichern",
131131
"save": "Speichern",
132132
"save_recovery_data_text": "Da die Zugangsdaten für die Geräte nur mithilfe des korrekten Passworts entschlüsselt werden können brauchst du, falls du dein Passwort vergessen solltest, diese Datei um den Zugang zu deinen Geräten wiederherzustellen. Bewahre diese Datei sicher und für niemanden sonst zugänglich auf, da sie mit deinem Passwort gleichzustellen ist.",
133+
"save_recovery_data_confirmation": "Ich habe die Wiederherstellungsdatei heruntergeladen und sicher gespeichert",
133134
"close": "Schließen",
134135
"registration_successful": "Die Registrierung war erfolgreich. Du solltest innerhalb der nächsten paar Minuten eine Email mit einem Bestätigungslink erhalten"
135136
,"resend_verification": "Bestätigungs-E-Mail erneut senden"

frontend/src/locales/en.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ export const en = {
130130
"save_recovery_data": "Save recovery file",
131131
"save": "Save",
132132
"save_recovery_data_text": "Since the access data for the devices can only be decrypted with the correct password, you need this file to restore access to your devices if you forget your password. Keep this file safe and inaccessible to others, as it is equivalent to your password.",
133+
"save_recovery_data_confirmation": "I have downloaded and safely stored the recovery file",
133134
"close": "Close",
134135
"registration_successful": "Registration was successful. You should receive an email with a confirmation link within the next few minutes."
135136
,"resend_verification": "Resend verification email"

0 commit comments

Comments
 (0)