Skip to content

Commit a095ed1

Browse files
authored
Add remaining retry and reset views to recovery phrase creation wizard. (#3511)
Add remaining retry and reset views to recovery phrase creation wizard. # Changes - Implement `Retry` view. - Move state above methods that mutate it in `Acknowledge`. - Update `Reset` view according to design. - Improve rendering of long words and accessibility in `Write` and `Verify` views. - Show toast on recovery phrase activation. - Update auth store if we just replaced the auth method currently in use. - Warn user if they're leaving in the middle of a recovery phrase set-up. # Tests The e2e tests will be in a later follow-up PR. <!-- SCREENSHOTS REPORT START --> <!-- SCREENSHOTS REPORT STOP -->
1 parent a567741 commit a095ed1

File tree

8 files changed

+179
-33
lines changed

8 files changed

+179
-33
lines changed

src/frontend/src/lib/components/wizards/createRecoveryPhrase/CreateRecoveryPhraseWizard.svelte

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import { generateMnemonic } from "$lib/utils/recoveryPhrase";
66
import Reset from "$lib/components/wizards/createRecoveryPhrase/views/Reset.svelte";
77
import { waitFor } from "$lib/utils/utils";
8+
import Retry from "$lib/components/wizards/createRecoveryPhrase/views/Retry.svelte";
89
910
interface Props {
1011
onCreate: (recoveryPhrase: string[]) => Promise<void>;
@@ -24,6 +25,7 @@
2425
2526
let recoveryPhrase = $state(unverifiedRecoveryPhrase);
2627
let isWritten = $state(unverifiedRecoveryPhrase !== undefined);
28+
let isIncorrect = $state(false);
2729
2830
const createRecoveryPhrase = async () => {
2931
const generated = generateMnemonic();
@@ -35,11 +37,15 @@
3537
// after the user spent some time on selecting words one after another.
3638
await waitFor(2000);
3739
if (recoveryPhrase?.join(" ") !== entered.join(" ")) {
38-
// TODO: add error view for this with retry action
39-
throw new Error("Invalid recovery phrase");
40+
isIncorrect = true;
41+
return;
4042
}
4143
onVerified();
4244
};
45+
const retryVerification = () => {
46+
isWritten = false;
47+
isIncorrect = false;
48+
};
4349
</script>
4450

4551
{#if recoveryPhrase === undefined}
@@ -50,6 +56,8 @@
5056
{/if}
5157
{:else if !isWritten}
5258
<Write {recoveryPhrase} onWritten={() => (isWritten = true)} />
59+
{:else if isIncorrect}
60+
<Retry onRetry={retryVerification} {onCancel} />
5361
{:else}
5462
<Verify {recoveryPhrase} onCompleted={verifyRecoveryPhrase} />
5563
{/if}

src/frontend/src/lib/components/wizards/createRecoveryPhrase/views/Acknowledge.svelte

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
1313
const { onAcknowledged }: Props = $props();
1414
15+
let isAcknowledged = $state(false);
16+
let isGeneratingPhrase = $state(false);
17+
1518
const handleAcknowledge = async () => {
1619
try {
1720
isGeneratingPhrase = true;
@@ -20,9 +23,6 @@
2023
isGeneratingPhrase = false;
2124
}
2225
};
23-
24-
let isAcknowledged = $state(false);
25-
let isGeneratingPhrase = $state(false);
2626
</script>
2727

2828
<Steps total={3} current={1} class="my-10" />
Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,64 @@
11
<script lang="ts">
22
import { t } from "$lib/stores/locale.store";
33
import Button from "$lib/components/ui/Button.svelte";
4+
import { Trans } from "$lib/components/locale";
5+
import FeaturedIcon from "$lib/components/ui/FeaturedIcon.svelte";
6+
import { TriangleAlertIcon } from "@lucide/svelte";
7+
import ProgressRing from "$lib/components/ui/ProgressRing.svelte";
48
59
interface Props {
610
onReset: () => Promise<void>;
711
onCancel: () => void;
812
}
913
1014
const { onReset, onCancel }: Props = $props();
15+
16+
let isGeneratingPhrase = $state(false);
17+
18+
const handleReset = async () => {
19+
try {
20+
isGeneratingPhrase = true;
21+
await onReset();
22+
} finally {
23+
isGeneratingPhrase = false;
24+
}
25+
};
1126
</script>
1227

13-
<h2>{$t`Reset your recovery phrase?`}</h2>
14-
<Button onclick={onReset}>
15-
{$t`Reset`}
28+
<FeaturedIcon variant="warning" size="lg" class="mb-4">
29+
<TriangleAlertIcon class="size-6" />
30+
</FeaturedIcon>
31+
<h2 class="text-text-primary mb-3 text-2xl font-medium">
32+
{$t`Reset your recovery phrase?`}
33+
</h2>
34+
<p class="text-text-tertiary mb-3 text-base font-medium">
35+
<Trans>
36+
Resetting will invalidate your current recovery phrase, leaving only the new
37+
one usable.
38+
</Trans>
39+
</p>
40+
<p class="text-text-tertiary mb-8 text-base font-medium">
41+
<Trans>Do you want to continue?</Trans>
42+
</p>
43+
<Button
44+
onclick={handleReset}
45+
size="lg"
46+
danger
47+
class="mb-3"
48+
disabled={isGeneratingPhrase}
49+
>
50+
{#if isGeneratingPhrase}
51+
<ProgressRing />
52+
<span>{$t`Resetting recovery phrase...`}</span>
53+
{:else}
54+
<span>{$t`Reset`}</span>
55+
{/if}
1656
</Button>
17-
<Button onclick={onCancel} variant="tertiary">
57+
<Button
58+
onclick={onCancel}
59+
variant="tertiary"
60+
size="lg"
61+
disabled={isGeneratingPhrase}
62+
>
1863
{$t`Cancel`}
1964
</Button>
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<script lang="ts">
2+
import { t } from "$lib/stores/locale.store";
3+
import { Trans } from "$lib/components/locale";
4+
import Button from "$lib/components/ui/Button.svelte";
5+
import FeaturedIcon from "$lib/components/ui/FeaturedIcon.svelte";
6+
import { MessageSquareXIcon, TriangleAlertIcon } from "@lucide/svelte";
7+
8+
interface Props {
9+
onRetry: () => void;
10+
onCancel: () => void;
11+
}
12+
13+
const { onRetry, onCancel }: Props = $props();
14+
</script>
15+
16+
<FeaturedIcon variant="error" size="lg" class="mb-4">
17+
<MessageSquareXIcon class="size-6" />
18+
</FeaturedIcon>
19+
<h2 class="text-text-primary mb-3 text-2xl font-medium">
20+
{$t`Something is wrong!`}
21+
</h2>
22+
<p class="text-text-tertiary mb-8 text-base font-medium">
23+
<Trans>Incorrect word order. Review and try again.</Trans>
24+
</p>
25+
<Button onclick={onRetry} size="lg" class="mb-1.5">
26+
{$t`Retry`}
27+
</Button>
28+
<Button onclick={onCancel} variant="tertiary" size="lg">
29+
{$t`Cancel`}
30+
</Button>

src/frontend/src/lib/components/wizards/createRecoveryPhrase/views/Verify.svelte

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { Trans } from "$lib/components/locale";
44
import Steps from "$lib/components/wizards/createRecoveryPhrase/components/Steps.svelte";
55
import Button from "$lib/components/ui/Button.svelte";
6-
import ProgressSteps from "$lib/components/wizards/createRecoveryPhrase/components/ProgressSteps.svelte";
6+
import StepsProgressBar from "$lib/components/wizards/createRecoveryPhrase/components/StepsProgressBar.svelte";
77
88
interface Props {
99
recoveryPhrase: string[];
@@ -33,11 +33,13 @@
3333
});
3434
</script>
3535

36-
{#if isCheckingOrder}
37-
<ProgressSteps total={3} class="my-10" />
38-
{:else}
39-
<Steps total={3} current={3} class="my-10" />
40-
{/if}
36+
<div class="limited-height my-10">
37+
{#if isCheckingOrder}
38+
<StepsProgressBar total={3} />
39+
{:else}
40+
<Steps total={3} current={3} />
41+
{/if}
42+
</div>
4143
<h2 class="text-text-primary mb-3 text-2xl font-medium">
4244
{#if isCheckingOrder}
4345
{$t`Checking your order`}
@@ -73,13 +75,13 @@
7375
<span
7476
class={[
7577
"text-text-secondary w-4 text-center text-xs font-semibold tabular-nums select-none",
76-
isSelected && "!text-text-primary",
78+
isSelected ? "!text-text-primary" : "-translate-y-0.25",
7779
isCheckingOrder && "!text-text-disabled",
7880
]}
7981
>
8082
{isSelected || isCheckingOrder
8183
? `${selectedPosition + 1}`.padStart(2, "0")
82-
: "__"}
84+
: "_"}
8385
</span>
8486
<span
8587
class={[
@@ -90,8 +92,8 @@
9092
></span>
9193
<span
9294
class={[
93-
"text-text-secondary -translate-y-0.25 select-none",
94-
word.length > 8 ? "text-sm" : "text-base",
95+
"text-text-secondary -translate-y-0.25 text-base select-none",
96+
word.length > 7 && "tracking-tight",
9597
isSelected && "!text-text-primary font-semibold",
9698
isCheckingOrder && "!text-text-disabled",
9799
]}
@@ -109,3 +111,11 @@
109111
>
110112
{$t`Clear all`}
111113
</Button>
114+
115+
<style>
116+
@media (max-height: 700px) {
117+
.limited-height {
118+
display: none !important;
119+
}
120+
}
121+
</style>

src/frontend/src/lib/components/wizards/createRecoveryPhrase/views/Write.svelte

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,59 +15,78 @@
1515
let isRevealed = $state(false);
1616
</script>
1717

18-
<Steps total={3} current={2} class="my-10" />
19-
<h2 class="text-text-primary mb-3 text-2xl font-medium">
18+
<div class="limited-height my-10">
19+
<Steps total={3} current={2} />
20+
</div>
21+
<h2 class="text-text-primary text-2xl font-medium">
2022
{$t`Save your recovery phrase`}
2123
</h2>
22-
<p class="text-text-tertiary mb-8 text-base font-medium">
24+
<p class="text-text-tertiary limited-height mt-3 text-base font-medium">
2325
<Trans>
2426
Write down your recovery phrase in order and verify it afterwards. Store it
2527
safely and do not share it. Losing it means you will lose access.
2628
</Trans>
2729
</p>
2830
<div class="relative">
29-
<div class={["mb-8 grid grid-cols-3 gap-3", !isRevealed && "blur-md"]}>
31+
<ol
32+
class={["my-8 grid grid-cols-3 gap-3", !isRevealed && "blur-md"]}
33+
aria-hidden={isRevealed ? "false" : "true"}
34+
>
3035
{#each recoveryPhrase as word, index}
31-
<div
36+
<li
3237
class="border-border-primary flex h-7 flex-row items-center rounded-full border px-1.5"
3338
>
3439
<div
3540
class="text-text-secondary pointer-events-none w-4 text-center text-xs font-semibold tabular-nums select-none"
41+
aria-hidden="true"
3642
>
3743
{`${index + 1}`.padStart(2, "0")}
3844
</div>
3945
<div class="border-border-secondary mx-1 h-full border-r"></div>
4046
<div
4147
class={[
4248
"text-text-secondary -translate-y-0.25 text-base",
43-
word.length > 8 ? "text-sm" : "text-base",
49+
word.length > 7 && "tracking-tight",
4450
]}
4551
>
4652
{word}
4753
</div>
48-
</div>
54+
</li>
4955
{/each}
50-
</div>
56+
</ol>
5157
<button
5258
onclick={() => (isRevealed = true)}
5359
class={[
5460
"group absolute inset-0 flex flex-col items-center justify-center outline-none",
5561
isRevealed && "hidden",
5662
]}
5763
>
58-
<EyeOffIcon class="text-fg-primary mb-1.5 size-5" />
64+
<EyeOffIcon class="text-fg-primary mb-1.5 size-5" aria-hidden />
5965
<span
6066
class="text-text-primary mb-4 text-sm font-bold group-focus-visible:underline"
6167
>
6268
{$t`Click to reveal`}
6369
</span>
64-
<span class="text-text-primary text-sm text-balance">
70+
<span class="text-text-primary text-sm text-balance" aria-hidden="true">
6571
<Trans>
6672
Make sure no one can see what's on your screen or what you are writing.
6773
</Trans>
6874
</span>
6975
</button>
7076
</div>
71-
<Button onclick={onWritten} disabled={!isRevealed} size="lg">
77+
<Button
78+
onclick={onWritten}
79+
disabled={!isRevealed}
80+
size="lg"
81+
aria-hidden={isRevealed ? "false" : "true"}
82+
>
7283
{$t`I have written it down`}
7384
</Button>
85+
86+
<style>
87+
@media (max-height: 700px) {
88+
.limited-height {
89+
display: none !important;
90+
}
91+
}
92+
</style>

0 commit comments

Comments
 (0)