feat(desktop): onboarding step dots, identity/key page restyle, overflow scroll (#1993)

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
Wes
2026-07-16 15:28:05 -06:00
committed by GitHub
parent db57bc8a2c
commit f2522995cb
9 changed files with 230 additions and 240 deletions

View File

@@ -1,10 +1,9 @@
import { AlertTriangle, RefreshCw } from "lucide-react";
import { AlertTriangle, Info, RefreshCw } from "lucide-react";
import * as React from "react";
import { getNsec } from "@/shared/api/tauriIdentity";
import { Button } from "@/shared/ui/button";
import { Spinner } from "@/shared/ui/spinner";
import { StepProgress } from "@/shared/ui/step-progress";
import {
type OnboardingTransitionDirection,
OnboardingSlideTransition,
@@ -14,51 +13,33 @@ import { NsecMaskedDisplay } from "./NsecMaskedDisplay";
/**
* Pure helper so the disabled logic can be unit-tested without a DOM.
*
* Disabled when:
* - still loading (key not fetched yet)
* - load failed (only the explicit "Skip for now" ghost advances past an error)
* - key loaded AND checkbox not yet checked
*
* Enabled when key is null after a clean (non-error) load — backend returned
* nothing, so there is nothing to acknowledge and the user may proceed.
* Disabled while loading (key not fetched yet) or after a failed load (only
* the explicit "Skip for now" ghost advances past an error).
*/
export function backupNextDisabled({
isLoading,
loadError,
nsec,
hasAcknowledged,
}: {
isLoading: boolean;
loadError: string | null;
nsec: string | null;
hasAcknowledged: boolean;
}): boolean {
return isLoading || loadError !== null || (nsec !== null && !hasAcknowledged);
return isLoading || loadError !== null;
}
type BackupStepProps = {
currentStep: number;
direction: OnboardingTransitionDirection;
totalSteps: number;
onBack: () => void;
onNext: () => void;
};
/**
* Onboarding backup step — shows the user their nsec and requires
* acknowledgement before advancing. Only shown on the fresh-key path.
* Onboarding backup step — shows the user their freshly created key so they
* can save it somewhere safe. Only shown on the fresh-key path.
*/
export function BackupStep({
currentStep,
direction,
totalSteps,
onBack,
onNext,
}: BackupStepProps) {
export function BackupStep({ direction, onBack, onNext }: BackupStepProps) {
const [nsec, setNsec] = React.useState<string | null>(null);
const [isLoading, setIsLoading] = React.useState(true);
const [loadError, setLoadError] = React.useState<string | null>(null);
const [hasAcknowledged, setHasAcknowledged] = React.useState(false);
const cancelledRef = React.useRef(false);
const loadNsec = React.useCallback(async () => {
@@ -92,30 +73,29 @@ export function BackupStep({
return (
<OnboardingSlideTransition
className="flex w-full flex-col items-center pb-36 lg:pb-0"
className="flex w-full flex-col items-center"
data-testid="onboarding-page-backup"
direction={direction}
transitionKey={`backup-${direction}`}
>
<div className="w-full max-w-[500px] text-center">
<h1 className="text-3xl font-semibold tracking-tight text-foreground">
Save your private key
Your unique identity has been created
</h1>
<p className="mt-3 text-sm leading-6 text-muted-foreground">
Buzz generated a Nostr private key for you. This key is stored in your
system keychain, but you should also save it somewhere safe in case
you ever need to restore your account on another device.
<p className="mt-3 text-sm leading-6 text-foreground/80">
This key is stored in your system keychain, but save it some place
safe in case you ever need to restore your account.
</p>
</div>
<div className="mt-8 w-full max-w-[500px] space-y-4 text-left">
<div className="mt-10 w-full max-w-[640px]">
{isLoading ? (
<div className="flex items-center gap-2 rounded-lg border border-border/70 bg-muted/30 px-4 py-6 text-sm text-muted-foreground">
<div className="flex items-center justify-center gap-2 py-6 text-sm text-foreground/70">
<Spinner className="h-4 w-4 border-2" />
Loading your private key…
</div>
) : loadError ? (
<div className="space-y-3">
<div className="mx-auto max-w-[500px] space-y-3 text-left">
<div
className="flex items-start gap-3 rounded-lg border border-destructive/30 bg-destructive/10 px-4 py-3 text-sm text-destructive"
data-testid="backup-load-error"
@@ -140,46 +120,31 @@ export function BackupStep({
</Button>
</div>
) : nsec ? (
<NsecMaskedDisplay nsec={nsec} />
<div className="rounded-3xl bg-white/85 px-6 py-5 shadow-[0_0_70px_45px_rgba(255,255,255,0.85)]">
<NsecMaskedDisplay nsec={nsec} variant="bare" />
</div>
) : (
<p className="text-sm text-muted-foreground">
<p className="text-center text-sm text-foreground/70">
No key available to back up.
</p>
)}
{nsec ? (
<div className="rounded-lg border border-amber-500/30 bg-amber-500/10 px-4 py-3 text-sm leading-5 text-amber-700 dark:text-amber-400">
<strong>Never share your private key.</strong> Anyone with it can
impersonate you and access everything in your account.
</div>
) : null}
{!isLoading && !loadError && nsec ? (
<label className="flex cursor-pointer items-start gap-3">
<input
checked={hasAcknowledged}
className="mt-0.5 h-4 w-4 shrink-0 cursor-pointer accent-primary"
data-testid="backup-acknowledge"
onChange={(e) => setHasAcknowledged(e.target.checked)}
type="checkbox"
/>
<span className="text-sm leading-5">
I've saved my private key in a safe place
<p className="mx-auto mt-6 flex max-w-[440px] items-start justify-center gap-1.5 text-center text-xs leading-5 text-foreground/70">
<Info className="mt-0.5 h-3.5 w-3.5 shrink-0" />
<span>
Never share your private key. Anyone can impersonate you and
access everything in your account.
</span>
</label>
</p>
) : null}
</div>
<div className="mt-8 flex w-full max-w-[500px] flex-col gap-3 max-lg:pointer-events-none max-lg:fixed max-lg:inset-x-0 max-lg:bottom-0 max-lg:z-40 max-lg:mt-0 max-lg:max-w-none max-lg:border-t max-lg:border-border max-lg:bg-background max-lg:p-4 max-lg:pb-[max(1rem,env(safe-area-inset-bottom))]">
<div className="mt-12 flex flex-col items-center gap-3">
<Button
className="h-10 w-full max-lg:pointer-events-auto"
className="h-10 rounded-full px-8"
data-testid="onboarding-next"
disabled={backupNextDisabled({
isLoading,
loadError,
nsec,
hasAcknowledged,
})}
disabled={backupNextDisabled({ isLoading, loadError })}
onClick={onNext}
type="button"
>
@@ -188,7 +153,7 @@ export function BackupStep({
{loadError ? (
<Button
className="h-10 w-full text-muted-foreground hover:text-accent-foreground max-lg:pointer-events-auto"
className="h-9 rounded-full px-5 text-muted-foreground hover:text-accent-foreground"
data-testid="backup-skip"
onClick={onNext}
type="button"
@@ -199,7 +164,7 @@ export function BackupStep({
) : null}
<Button
className="h-10 w-full text-muted-foreground hover:text-accent-foreground max-lg:pointer-events-auto"
className="h-9 rounded-full bg-foreground/10 px-6 hover:bg-foreground/15"
data-testid="onboarding-back"
onClick={onBack}
type="button"
@@ -207,15 +172,6 @@ export function BackupStep({
>
Back
</Button>
<StepProgress
activeSegmentClassName="bg-primary"
className="mt-1 max-lg:pointer-events-auto lg:hidden"
completeSegmentClassName="bg-primary/35"
currentStep={currentStep}
inactiveSegmentClassName="bg-muted-foreground/25"
totalSteps={totalSteps}
/>
</div>
</OnboardingSlideTransition>
);

View File

@@ -12,6 +12,7 @@ import { BackupStep } from "./BackupStep";
import { LandingBees } from "./LandingBees";
import { NostrKeyImportForm } from "./NostrKeyImportForm";
import { OnboardingSlideTransition } from "./OnboardingSlideTransition";
import { OnboardingStepDots } from "./OnboardingStepDots";
import { SetupStep } from "./SetupStep";
type MachinePage = "identity" | "key-import" | "backup" | "setup";
@@ -87,14 +88,17 @@ export function MachineOnboardingFlow({
return (
<div
className={`buzz-onboarding-neutral-theme buzz-startup-shell flex items-center justify-center px-4 py-8 text-foreground ${
page === "identity" ? "buzz-onboarding-welcome" : ""
}`}
className={`buzz-onboarding-neutral-theme buzz-startup-shell flex max-h-dvh items-start justify-center overflow-y-auto px-4 text-foreground ${
page === "setup" ? "py-24" : "py-8"
} ${page === "identity" ? "buzz-onboarding-welcome" : ""}`}
data-testid="machine-onboarding-gate"
>
<StartupWindowDragRegion />
{page === "identity" ? <LandingBees /> : null}
<div className="relative flex w-full max-w-[920px] flex-col items-center text-center">
{page !== "identity" ? (
<OnboardingStepDots current={page === "setup" ? 3 : 2} />
) : null}
<div className="relative my-auto flex w-full max-w-[920px] flex-col items-center text-center">
{page === "identity" ? (
<OnboardingSlideTransition
className="flex w-full max-w-[720px] flex-col items-center text-center"
@@ -135,17 +139,17 @@ export function MachineOnboardingFlow({
</OnboardingSlideTransition>
) : page === "key-import" ? (
<OnboardingSlideTransition
className="flex w-full max-w-[500px] flex-col items-center text-center"
className="flex w-full max-w-[640px] flex-col items-center text-center"
direction="forward"
transitionKey="machine-key-import"
>
<h1 className="text-3xl font-semibold tracking-tight">
{identityLost ? "Re-import your key" : "Use your existing key"}
{identityLost ? "Re-import your key" : "Enter your private key"}
</h1>
<p className="mt-3 text-sm leading-6 text-muted-foreground">
<p className="mt-3 max-w-[440px] text-sm leading-6 text-foreground/80">
{identityLost
? "Your identity is no longer in the system keyring. Re-import your nsec to restore it."
: "Import your Nostr private key to use that identity with Buzz."}
: "If you already have a Nostr account, enter your private key below to get started."}
</p>
<NostrKeyImportForm
backLabel={identityLost ? "Start new identity" : "Back"}
@@ -155,15 +159,14 @@ export function MachineOnboardingFlow({
: () => setPage("identity")
}
onImport={importExistingIdentity}
variant="spotlight"
/>
</OnboardingSlideTransition>
) : page === "backup" ? (
<BackupStep
currentStep={2}
direction="forward"
onBack={() => setPage("identity")}
onNext={() => setPage("setup")}
totalSteps={3}
/>
) : (
<SetupStep

View File

@@ -15,6 +15,8 @@ type NostrKeyImportFormProps = {
errorMessage?: string | null;
onBack: () => void;
onImport: (nsec: string) => Promise<void>;
/** "spotlight" is the first-launch treatment: glowy centered input, no drop zone, pill buttons. */
variant?: "default" | "spotlight";
};
/**
@@ -30,6 +32,7 @@ export function NostrKeyImportForm({
errorMessage: externalErrorMessage = null,
onBack,
onImport,
variant = "default",
}: NostrKeyImportFormProps) {
const [nsecInput, setNsecInput] = React.useState("");
const [isImporting, setIsImporting] = React.useState(false);
@@ -114,23 +117,31 @@ export function NostrKeyImportForm({
}}
>
<div className="space-y-1.5 text-left">
<label
className="text-sm font-medium text-foreground"
htmlFor="nostr-private-key"
>
Private key
</label>
{variant === "spotlight" ? null : (
<label
className="text-sm font-medium text-foreground"
htmlFor="nostr-private-key"
>
Private key
</label>
)}
<Input
autoComplete="off"
autoCorrect="off"
className="h-10 bg-background"
className={
variant === "spotlight"
? "h-16 rounded-2xl border-0 bg-white/85 text-center font-mono !text-xl shadow-[0_0_70px_45px_rgba(255,255,255,0.85)] placeholder:text-foreground/30 focus-visible:ring-0"
: "h-10 bg-background"
}
data-testid="nostr-import-nsec-input"
id="nostr-private-key"
onChange={(event) => {
setNsecInput(event.target.value);
setImportError(null);
}}
placeholder="nsec1..."
placeholder={
variant === "spotlight" ? "Enter your key here" : "nsec1..."
}
ref={inputRef}
spellCheck={false}
type="password"
@@ -138,86 +149,90 @@ export function NostrKeyImportForm({
/>
</div>
<input
accept=".key,text/plain"
className="sr-only"
disabled={isInteractionDisabled}
onChange={(event) => {
void handleFiles(event.currentTarget.files);
event.currentTarget.value = "";
}}
ref={fileInputRef}
tabIndex={-1}
type="file"
/>
{variant === "spotlight" ? null : (
<>
<input
accept=".key,text/plain"
className="sr-only"
disabled={isInteractionDisabled}
onChange={(event) => {
void handleFiles(event.currentTarget.files);
event.currentTarget.value = "";
}}
ref={fileInputRef}
tabIndex={-1}
type="file"
/>
<button
className={cn(
"relative flex h-[120px] flex-col items-center justify-center gap-3 overflow-hidden rounded-xl border border-transparent bg-muted text-foreground transition-[background-color,border-color,box-shadow,color] duration-[250ms] ease-out hover:bg-muted/80 disabled:opacity-60",
isDragging &&
"border-primary bg-primary/10 text-primary ring-1 ring-primary/35 hover:bg-primary/10",
)}
data-dragging={isDragging ? "true" : undefined}
data-testid="nostr-import-drop"
disabled={isInteractionDisabled}
onClick={openFilePicker}
onDragEnter={(event) => {
event.preventDefault();
event.stopPropagation();
if (!isInteractionDisabled) {
setIsDragging(true);
}
}}
onDragLeave={(event) => {
event.preventDefault();
event.stopPropagation();
if (
event.currentTarget.contains(event.relatedTarget as Node | null)
) {
return;
}
setIsDragging(false);
}}
onDragOver={(event) => {
event.preventDefault();
event.stopPropagation();
if (!isInteractionDisabled) {
event.dataTransfer.dropEffect = "copy";
}
}}
onDrop={(event) => {
event.preventDefault();
event.stopPropagation();
setIsDragging(false);
if (isInteractionDisabled) {
return;
}
void handleFiles(event.dataTransfer.files);
}}
type="button"
>
<span
aria-hidden="true"
className={cn(
"pointer-events-none absolute inset-0 rounded-[inherit] bg-primary/10 opacity-0 transition-opacity duration-[250ms] ease-out",
isDragging && "opacity-100",
)}
/>
<KeyRound
className={cn(
"relative h-8 w-8 text-muted-foreground transition-colors duration-[250ms] ease-out",
isDragging && "text-primary",
)}
/>
<span
className={cn(
"relative text-sm font-medium text-muted-foreground transition-colors duration-[250ms] ease-out",
isDragging && "text-primary",
)}
>
Drop a key here
</span>
</button>
<button
className={cn(
"relative flex h-[120px] flex-col items-center justify-center gap-3 overflow-hidden rounded-xl border border-transparent bg-muted text-foreground transition-[background-color,border-color,box-shadow,color] duration-[250ms] ease-out hover:bg-muted/80 disabled:opacity-60",
isDragging &&
"border-primary bg-primary/10 text-primary ring-1 ring-primary/35 hover:bg-primary/10",
)}
data-dragging={isDragging ? "true" : undefined}
data-testid="nostr-import-drop"
disabled={isInteractionDisabled}
onClick={openFilePicker}
onDragEnter={(event) => {
event.preventDefault();
event.stopPropagation();
if (!isInteractionDisabled) {
setIsDragging(true);
}
}}
onDragLeave={(event) => {
event.preventDefault();
event.stopPropagation();
if (
event.currentTarget.contains(event.relatedTarget as Node | null)
) {
return;
}
setIsDragging(false);
}}
onDragOver={(event) => {
event.preventDefault();
event.stopPropagation();
if (!isInteractionDisabled) {
event.dataTransfer.dropEffect = "copy";
}
}}
onDrop={(event) => {
event.preventDefault();
event.stopPropagation();
setIsDragging(false);
if (isInteractionDisabled) {
return;
}
void handleFiles(event.dataTransfer.files);
}}
type="button"
>
<span
aria-hidden="true"
className={cn(
"pointer-events-none absolute inset-0 rounded-[inherit] bg-primary/10 opacity-0 transition-opacity duration-[250ms] ease-out",
isDragging && "opacity-100",
)}
/>
<KeyRound
className={cn(
"relative h-8 w-8 text-muted-foreground transition-colors duration-[250ms] ease-out",
isDragging && "text-primary",
)}
/>
<span
className={cn(
"relative text-sm font-medium text-muted-foreground transition-colors duration-[250ms] ease-out",
isDragging && "text-primary",
)}
>
Drop a key here
</span>
</button>
</>
)}
{previewNpub ? (
<div
@@ -246,22 +261,36 @@ export function NostrKeyImportForm({
<p className="text-center text-sm text-destructive">{errorMessage}</p>
) : null}
<div className="flex w-full flex-col gap-3 pt-1">
<div
className={
variant === "spotlight"
? "mt-4 flex w-full flex-col items-center gap-3"
: "flex w-full flex-col gap-3 pt-1"
}
>
<Button
className="h-10 w-full"
className={
variant === "spotlight" ? "h-10 rounded-full px-8" : "h-10 w-full"
}
data-testid="nostr-import-submit"
disabled={!isValid || isInteractionDisabled}
type="submit"
>
{isImporting ? (
<Spinner aria-label="Importing key" className="h-4 w-4 border-2" />
) : variant === "spotlight" ? (
"Next"
) : (
"Continue with this key"
)}
</Button>
<Button
className="h-10 w-full text-muted-foreground hover:text-accent-foreground"
className={
variant === "spotlight"
? "h-9 rounded-full bg-foreground/10 px-6 hover:bg-foreground/15"
: "h-10 w-full text-muted-foreground hover:text-accent-foreground"
}
disabled={isImporting}
onClick={onBack}
type="button"

View File

@@ -4,6 +4,8 @@ import { Button } from "@/shared/ui/button";
type NsecMaskedDisplayProps = {
nsec: string;
/** "bare" drops the boxed chrome for the onboarding spotlight treatment. */
variant?: "boxed" | "bare";
};
/**
@@ -14,7 +16,10 @@ type NsecMaskedDisplayProps = {
* - select is disabled while masked (user-select: none)
* - state is cleared when the component unmounts
*/
export function NsecMaskedDisplay({ nsec }: NsecMaskedDisplayProps) {
export function NsecMaskedDisplay({
nsec,
variant = "boxed",
}: NsecMaskedDisplayProps) {
const [isRevealed, setIsRevealed] = React.useState(false);
const [isCopied, setIsCopied] = React.useState(false);
const copyTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
@@ -40,10 +45,18 @@ export function NsecMaskedDisplay({ nsec }: NsecMaskedDisplayProps) {
}
return (
<div className="overflow-hidden rounded-lg border border-border/70 bg-muted/30">
<div
className={
variant === "boxed"
? "overflow-hidden rounded-lg border border-border/70 bg-muted/30"
: ""
}
>
<div className="flex items-center gap-2 px-3 py-2">
<p
className={`min-w-0 flex-1 break-all font-mono text-xs leading-5 ${
className={`min-w-0 flex-1 break-all font-mono leading-5 ${
variant === "bare" ? "text-base" : "text-xs"
} ${
isRevealed
? "select-text text-foreground"
: "select-none text-muted-foreground blur-[4px]"

View File

@@ -0,0 +1,40 @@
import { BuzzMark } from "@/shared/ui/buzz-logo/BuzzMark";
/**
* Positions in the first-launch flow: landing, identity/key, provider setup,
* community choice, community profile, meet the team.
*/
export const TOTAL_ONBOARDING_PAGES = 6;
/**
* Top-left paging indicator for the first-launch flow. The bee marks the
* current page on a dot track; shown on every page after the landing screen.
*/
export function OnboardingStepDots({
current,
total = TOTAL_ONBOARDING_PAGES,
}: {
current: number;
total?: number;
}) {
return (
<div
aria-hidden
className="pointer-events-none fixed left-6 top-12 z-10 flex items-center gap-2.5 text-foreground"
data-testid="onboarding-step-dots"
>
{Array.from({ length: total }, (_, i) => i + 1).map((position) =>
position === current ? (
<span className="block w-9" key={position}>
<BuzzMark className="h-auto w-full" />
</span>
) : (
<span
className="block h-[7px] w-[7px] rounded-full bg-foreground"
key={position}
/>
),
)}
</div>
);
}

View File

@@ -59,66 +59,21 @@ test("currentStep_falls_back_to_1_for_pages_outside_the_step_list", () => {
test("backup_next_disabled_while_loading", () => {
// During a slow keychain read, Next must be blocked — user cannot race past
// the key display before it is shown.
assert.equal(
backupNextDisabled({
isLoading: true,
loadError: null,
nsec: null,
hasAcknowledged: false,
}),
true,
);
assert.equal(backupNextDisabled({ isLoading: true, loadError: null }), true);
});
test("backup_next_disabled_on_load_error", () => {
// Error state: only the explicit "Skip for now" ghost advances; Next blocked.
assert.equal(
backupNextDisabled({
isLoading: false,
loadError: "IPC error",
nsec: null,
hasAcknowledged: false,
}),
backupNextDisabled({ isLoading: false, loadError: "IPC error" }),
true,
);
});
test("backup_next_disabled_when_nsec_loaded_and_not_acknowledged", () => {
// Key shown but checkbox unchecked.
test("backup_next_enabled_after_clean_load", () => {
// Key shown (or backend cleanly returned none) — user may proceed.
assert.equal(
backupNextDisabled({
isLoading: false,
loadError: null,
nsec: "nsec1test",
hasAcknowledged: false,
}),
true,
);
});
test("backup_next_enabled_when_nsec_loaded_and_acknowledged", () => {
// Key shown and checkbox checked — the normal happy path.
assert.equal(
backupNextDisabled({
isLoading: false,
loadError: null,
nsec: "nsec1test",
hasAcknowledged: true,
}),
false,
);
});
test("backup_next_enabled_when_backend_returned_null_key_cleanly", () => {
// Backend returned no key without an error (edge case): nothing to acknowledge,
// allow forward progress so onboarding is never bricked.
assert.equal(
backupNextDisabled({
isLoading: false,
loadError: null,
nsec: null,
hasAcknowledged: false,
}),
backupNextDisabled({ isLoading: false, loadError: null }),
false,
);
});

View File

@@ -26,7 +26,9 @@ test("normal first launch uses the already-persisted identity", async ({
await page.getByRole("button", { name: "Get started" }).click();
await expect(
page.getByRole("heading", { name: "Save your private key" }),
page.getByRole("heading", {
name: "Your unique identity has been created",
}),
).toBeVisible();
await expect(gate).toHaveCSS(
"background-image",

View File

@@ -20,7 +20,9 @@ test("backup step appears on fresh-key path after profile submit", async ({
await expect(page.getByTestId("onboarding-page-backup")).toBeVisible();
await expect(
page.getByRole("heading", { name: "Save your private key" }),
page.getByRole("heading", {
name: "Your unique identity has been created",
}),
).toBeVisible();
});
@@ -55,19 +57,11 @@ test("backup step shows masked nsec from mock bridge", async ({ page }) => {
});
});
test("backup step Next is disabled until checkbox is checked", async ({
page,
}) => {
test("backup step Next is enabled once the key is shown", async ({ page }) => {
await enterMachineBackup(page);
await expect(page.getByTestId("onboarding-page-backup")).toBeVisible();
await expect(page.getByTestId("nsec-value")).toBeVisible();
// Next is disabled while checkbox is unchecked.
await expect(page.getByTestId("onboarding-next")).toBeDisabled();
// Check the checkbox → Next enables.
await page.getByTestId("backup-acknowledge").check();
await expect(page.getByTestId("onboarding-next")).toBeEnabled();
});
@@ -78,7 +72,6 @@ test("backup step advances to machine setup on Next click", async ({
await expect(page.getByTestId("onboarding-page-backup")).toBeVisible();
await expect(page.getByTestId("nsec-value")).toBeVisible();
await page.getByTestId("backup-acknowledge").check();
await page.getByTestId("onboarding-next").click();
await expect(page.getByTestId("onboarding-page-2")).toBeVisible();

View File

@@ -20,6 +20,5 @@ export async function seedActiveIdentity(
export async function passThroughBackupStep(page: Page) {
await expect(page.getByTestId("onboarding-page-backup")).toBeVisible();
await expect(page.getByTestId("nsec-value")).toBeVisible();
await page.getByTestId("backup-acknowledge").check();
await page.getByTestId("onboarding-next").click();
}