diff --git a/desktop/src/features/communities/ui/WelcomeSetup.tsx b/desktop/src/features/communities/ui/WelcomeSetup.tsx index 71bd382db..bde145822 100644 --- a/desktop/src/features/communities/ui/WelcomeSetup.tsx +++ b/desktop/src/features/communities/ui/WelcomeSetup.tsx @@ -103,7 +103,7 @@ export function WelcomeSetup({ data-system-color-scheme={systemColorScheme} > - +
{page === "welcome" ? ( diff --git a/desktop/src/features/onboarding/ui/BackupStep.tsx b/desktop/src/features/onboarding/ui/BackupStep.tsx index 1d1b1d0bc..5b9b6c7a0 100644 --- a/desktop/src/features/onboarding/ui/BackupStep.tsx +++ b/desktop/src/features/onboarding/ui/BackupStep.tsx @@ -1,4 +1,12 @@ -import { Check, Copy, Eye, EyeOff, Info } from "lucide-react"; +import { + Check, + ChevronDown, + Copy, + Eye, + EyeOff, + Info, + ShieldCheck, +} from "lucide-react"; import * as React from "react"; import { getNsec } from "@/shared/api/tauriIdentity"; @@ -8,7 +16,6 @@ import { Button } from "@/shared/ui/button"; import { FuzzyLogo } from "@/shared/ui/buzz-logo/FuzzyLogo"; import { Card } from "@/shared/ui/card"; import { Spinner } from "@/shared/ui/spinner"; -import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip"; import { ONBOARDING_PRIMARY_CTA_CLASS } from "./OnboardingChrome"; import { OnboardingFooter } from "./OnboardingFooter"; import { @@ -42,18 +49,28 @@ export function backupNextDisabled(): boolean { type BackupStepProps = { direction: OnboardingTransitionDirection; onBack: () => void; - /** Advances to the dedicated "Download your key" onboarding step. */ - onDownload: () => void; + onNext: () => void; + onOpenPasswordBackup: () => void; + onShowOptions: () => void; + optionsExpanded: boolean; + returningFromSecurity: boolean; }; /** - * Onboarding backup step — shows the freshly created key and offers a direct - * clipboard copy destined for a password manager. Next leads into the - * encrypted download step (its own onboarding page, via `onDownload`), which - * is skippable there. The raw key is fetched only when the user explicitly - * clicks Copy or Reveal, and is never held in state before that. + * Onboarding identity-key step — shows the freshly created key, then opens a + * dark backup-options state. Copy fetches the raw key only after an explicit + * click; password backup opens the separate security flow. Neither method + * blocks Next. */ -export function BackupStep({ direction, onBack, onDownload }: BackupStepProps) { +export function BackupStep({ + direction, + onBack, + onNext, + onOpenPasswordBackup, + onShowOptions, + optionsExpanded, + returningFromSecurity, +}: BackupStepProps) { const [created, setCreated] = React.useState(introPlayed); const [copyState, setCopyState] = React.useState< "idle" | "copying" | "copied" @@ -136,12 +153,116 @@ export function BackupStep({ direction, onBack, onDownload }: BackupStepProps) { [nsec], ); + if (optionsExpanded) { + return ( + +
+

+ Backup options +

+

+ Choose how you want to keep a restorable copy of your identity key. +

+
+ +
+
+
+
+ +
+
+
+ + {copyError ? ( +

+ Could not retrieve your private key: {copyError}. You can continue + and find it later in Settings > Profile > Identity. +

+ ) : null} +
+
+ ); + } + return (
{/* Plain string concat: cn()'s tailwind-merge misreads the custom @@ -204,55 +325,37 @@ export function BackupStep({ direction, onBack, onDownload }: BackupStepProps) { {isRevealed && nsec ? nsec : maskedKey}

-
- - - - - - - Copy your key and save it somewhere safe — a password - manager is a great place for it. - - -
+
+ + +
+ {copyError ? (

) : null} - +

-

+

Never share your private key. Anyone with this key can @@ -282,7 +385,7 @@ export function BackupStep({ direction, onBack, onDownload }: BackupStepProps) { className={ONBOARDING_PRIMARY_CTA_CLASS} data-testid="onboarding-next" disabled={backupNextDisabled()} - onClick={onDownload} + onClick={onNext} type="button" > Next diff --git a/desktop/src/features/onboarding/ui/CommunityOnboardingFlow.tsx b/desktop/src/features/onboarding/ui/CommunityOnboardingFlow.tsx index 805d19661..4b729ab33 100644 --- a/desktop/src/features/onboarding/ui/CommunityOnboardingFlow.tsx +++ b/desktop/src/features/onboarding/ui/CommunityOnboardingFlow.tsx @@ -448,7 +448,7 @@ export function CommunityOnboardingFlow({ > {isProfileStage || isTeamStage ? ( - + ) : null}

void; onNext: () => void; }; /** - * Onboarding download step — the password-first encrypted key download - * flow, promoted to its own page in the machine onboarding flow. + * Password-backup security subview within the identity-key onboarding step. * The raw key never enters this component: Rust builds the NIP-49 payload * locally and the native save dialog produces the user-owned file. */ export function DownloadKeyStep({ direction, session, - onBack, onNext, }: DownloadKeyStepProps) { const reduceMotion = useReducedMotion() ?? false; @@ -79,7 +75,7 @@ export function DownloadKeyStep({ initial={reduceMotion ? false : { opacity: 0, y: 12 }} transition={{ delay: 0.12, duration: 0.4, ease: "easeOut" }} > - +
)} - - {hasCreated ? null : (

You can back up your key anytime in Settings → Profile → diff --git a/desktop/src/features/onboarding/ui/EncryptedBackupCreator.tsx b/desktop/src/features/onboarding/ui/EncryptedBackupCreator.tsx index b080f6808..ab02838cf 100644 --- a/desktop/src/features/onboarding/ui/EncryptedBackupCreator.tsx +++ b/desktop/src/features/onboarding/ui/EncryptedBackupCreator.tsx @@ -255,10 +255,12 @@ function PassphraseGeneratorPopover({ disabled = false, onRequestGenerate, onGenerated, + securityTheme = false, }: { disabled?: boolean; onRequestGenerate?: () => void; onGenerated: (value: string) => void; + securityTheme?: boolean; }) { const [open, setOpen] = React.useState(false); const [words, setWords] = React.useState(DEFAULT_GENERATED_WORDS); @@ -335,7 +337,10 @@ function PassphraseGeneratorPopover({ { // Clicking the anchor icon is "outside" the content — keep the // popover open so that click re-rolls instead of closing. @@ -677,6 +682,7 @@ export function EncryptedBackupCreator({ // A generated password must be visible so the user can save it. setIsRevealed(true); }} + securityTheme={variant === "spotlight"} /> {issue ? (

- + Create a new backup password? diff --git a/desktop/src/features/onboarding/ui/MachineOnboardingFlow.tsx b/desktop/src/features/onboarding/ui/MachineOnboardingFlow.tsx index 2576f71e1..c9ac67b7b 100644 --- a/desktop/src/features/onboarding/ui/MachineOnboardingFlow.tsx +++ b/desktop/src/features/onboarding/ui/MachineOnboardingFlow.tsx @@ -1,5 +1,6 @@ import * as React from "react"; import type { QueryClient } from "@tanstack/react-query"; +import { ArrowUp } from "lucide-react"; import { getIdentity, @@ -30,10 +31,11 @@ export type MachineOnboardingPage = | "identity" | "key-import" | "backup" - | "download" | "setup" | "config"; +type BackupSubview = "created" | "options" | "password"; + /** A pending navigation the parent should execute after RouterProvider mounts. */ export type PostOnboardingNavigation = { to: string; @@ -71,9 +73,17 @@ export function MachineOnboardingFlow({ null, ); const [readyRuntimeIds, setReadyRuntimeIds] = React.useState([]); - // Owned here (not by DownloadKeyStep) so Back navigation — which unmounts - // the step — keeps the created backup, entered password, and test progress. + const [backupSubview, setBackupSubview] = + React.useState("created"); + const [backupDirection, setBackupDirection] = React.useState< + "forward" | "backward" + >("forward"); + const [returningFromSecurity, setReturningFromSecurity] = + React.useState(false); + // Owned here so switching between the yellow onboarding view and the dark + // security subview keeps the created backup, password, and test progress. const backupSession = useEncryptedBackupSession(); + const isSecuritySubview = page === "backup" && backupSubview !== "created"; const handleReadyRuntimeIdsChange = React.useCallback( (runtimeIds: readonly string[]) => { setReadyRuntimeIds(Array.from(new Set(runtimeIds))); @@ -88,6 +98,9 @@ export function MachineOnboardingFlow({ const identity = await getIdentity(); queryClient.setQueryData(["identity"], identity); setSelectedPubkey(identity.pubkey); + setBackupDirection("forward"); + setReturningFromSecurity(false); + setBackupSubview("created"); setPage("backup"); } catch (cause) { setError( @@ -110,6 +123,9 @@ export function MachineOnboardingFlow({ const identity = await persistCurrentIdentity(); queryClient.setQueryData(["identity"], identity); setSelectedPubkey(identity.pubkey); + setBackupDirection("forward"); + setReturningFromSecurity(false); + setBackupSubview("created"); setPage("backup"); } catch (cause) { setError( @@ -135,6 +151,8 @@ export function MachineOnboardingFlow({ return (

{page === "identity" ? : null} - {page !== "identity" ? ( + {isSecuritySubview ? ( +
+ +
+ ) : page !== "identity" ? ( ) : null} @@ -241,31 +268,47 @@ export function MachineOnboardingFlow({
) : page === "backup" ? ( - setPage("identity")} - onDownload={() => setPage("download")} - /> - ) : page === "download" ? ( - setPage("backup")} - onNext={() => setPage("setup")} - session={backupSession} - /> + backupSubview === "password" ? ( + setPage("setup")} + session={backupSession} + /> + ) : ( + setPage("identity")} + onNext={() => setPage("setup")} + onOpenPasswordBackup={() => { + setBackupDirection("forward"); + setReturningFromSecurity(false); + setBackupSubview("password"); + }} + onShowOptions={() => { + setBackupDirection("forward"); + setReturningFromSecurity(false); + setBackupSubview("options"); + }} + optionsExpanded={backupSubview === "options"} + returningFromSecurity={returningFromSecurity} + /> + ) ) : page === "setup" ? ( { if (identityWasImported) { setPage("key-import"); return; } - backupSessionToPasswordEntry(backupSession); - setPage("download"); + if (backupSubview === "password") { + backupSessionToPasswordEntry(backupSession); + } + setBackupDirection("backward"); + setReturningFromSecurity(false); + setPage("backup"); }, next: (runtimeIds) => { const ids = Array.from(runtimeIds); diff --git a/desktop/src/features/onboarding/ui/OnboardingChrome.tsx b/desktop/src/features/onboarding/ui/OnboardingChrome.tsx index 24f020a09..345d3cce4 100644 --- a/desktop/src/features/onboarding/ui/OnboardingChrome.tsx +++ b/desktop/src/features/onboarding/ui/OnboardingChrome.tsx @@ -1,12 +1,11 @@ import { BuzzMark } from "@/shared/ui/buzz-logo/BuzzMark"; /** - * Positions in the first-launch flow: landing, identity/key, key download, - * harness setup, default config, community choice, community profile, meet - * the team. Used as the default pagination length when a flow doesn't pass an - * explicit total. + * Positions in the first-launch flow: landing, identity/key, harness setup, + * default config, community choice, community profile, meet the team. Password + * backup is an optional subview of identity/key, not another position. */ -export const TOTAL_ONBOARDING_PAGES = 8; +export const TOTAL_ONBOARDING_PAGES = 7; /** Shared pill shape (38px tall) for every onboarding primary CTA. */ const ONBOARDING_CTA_SHAPE = "h-[2.375rem] rounded-full px-6"; diff --git a/desktop/src/features/onboarding/ui/OnboardingSlideTransition.tsx b/desktop/src/features/onboarding/ui/OnboardingSlideTransition.tsx index 82d9a0213..ba5d8b2c8 100644 --- a/desktop/src/features/onboarding/ui/OnboardingSlideTransition.tsx +++ b/desktop/src/features/onboarding/ui/OnboardingSlideTransition.tsx @@ -14,6 +14,7 @@ export type OnboardingTransitionDirection = "forward" | "backward"; export type OnboardingTransitionEffect = | "fade" | "line-slide" + | "mask-reveal-down" | "mask-reveal-up" | "none"; diff --git a/desktop/src/shared/styles/globals/components.css b/desktop/src/shared/styles/globals/components.css index c21e4a617..036cf925e 100644 --- a/desktop/src/shared/styles/globals/components.css +++ b/desktop/src/shared/styles/globals/components.css @@ -241,6 +241,54 @@ --buzz-onboarding-avatar-dialog-shadow: 0 0 0; } + /* + * Backup options and password backup intentionally leave the bright + * onboarding world for a dark security-focused subview. Keep these semantic + * overrides on a reusable class as well as the shell so portaled + * popovers/dialogs can opt into the same treatment. + */ + .buzz-onboarding-security-theme { + color-scheme: dark; + --buzz-onboarding-shell-bottom: #082b49; + --buzz-onboarding-cta-label: #f5f5f5; + --background: 222 45% 4%; + --foreground: 0 0% 96%; + --card: 220 14% 11%; + --card-foreground: 0 0% 96%; + --popover: 220 14% 9%; + --popover-foreground: 0 0% 96%; + --primary: 0 0% 16%; + --primary-foreground: 0 0% 96%; + --secondary: 0 0% 12%; + --secondary-foreground: 0 0% 96%; + --muted: 0 0% 12%; + --muted-foreground: 0 0% 72%; + --accent: 0 0% 16%; + --accent-foreground: 0 0% 96%; + --destructive: 0 84% 70%; + --destructive-foreground: 0 0% 4%; + --border: 0 0% 22%; + --input: 0 0% 22%; + --ring: 0 0% 84%; + } + + .buzz-onboarding-neutral-theme.buzz-startup-shell.buzz-onboarding-security-theme { + background-color: #010103; + background-image: + radial-gradient(circle, rgb(143 211 255 / 0.11) 1px, transparent 1px), + radial-gradient( + ellipse at 50% 58%, + rgb(28 112 196 / 0.24) 0%, + rgb(18 76 138 / 0.1) 38%, + transparent 66% + ), + linear-gradient(to bottom, #010103 0%, #040914 46%, #082b49 100%); + background-size: + 24px 24px, + auto, + auto; + } + .buzz-onboarding-key-text { @apply w-full break-all [overflow-wrap:anywhere] font-mono text-nsec-key; @@ -376,10 +424,13 @@ animation-name: buzz-onboarding-line-slide-backward; } + .buzz-onboarding-transition-line[data-onboarding-effect="mask-reveal-down"], .buzz-onboarding-transition-line[data-onboarding-effect="mask-reveal-up"] { overflow: visible; } + .buzz-onboarding-transition-line[data-onboarding-effect="mask-reveal-down"] + > .buzz-onboarding-transition-content, .buzz-onboarding-transition-line[data-onboarding-effect="mask-reveal-up"] > .buzz-onboarding-transition-content { /* `backwards` (not `both`): the reveal ends on an identity transform, so @@ -387,12 +438,23 @@ that establishes a containing block and traps `position: fixed` descendants (the bottom-docked onboarding footer). Reverting to no transform at rest looks identical and frees fixed positioning. */ - animation: buzz-onboarding-mask-reveal-up 760ms - cubic-bezier(0.22, 1, 0.36, 1) backwards; + animation-duration: 760ms; + animation-fill-mode: backwards; + animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1); animation-delay: var(--buzz-onboarding-transition-delay, 0ms); transform-origin: 50% 70%; } + .buzz-onboarding-transition-line[data-onboarding-effect="mask-reveal-down"] + > .buzz-onboarding-transition-content { + animation-name: buzz-onboarding-mask-reveal-down; + } + + .buzz-onboarding-transition-line[data-onboarding-effect="mask-reveal-up"] + > .buzz-onboarding-transition-content { + animation-name: buzz-onboarding-mask-reveal-up; + } + .buzz-onboarding-name-placeholder-caret { animation: buzz-onboarding-caret-blink 1.1s steps(1, end) infinite; } @@ -543,6 +605,25 @@ } } + @keyframes buzz-onboarding-mask-reveal-down { + from { + filter: blur(8px); + opacity: 0; + transform: translate3d(0, -30px, 0) scale(0.985); + } + + 56% { + filter: blur(2px); + opacity: 0.86; + } + + to { + filter: blur(0); + opacity: 1; + transform: translate3d(0, 0, 0) scale(1); + } + } + @media (prefers-reduced-motion: reduce) { .buzz-onboarding-runtime-check, .buzz-onboarding-runtime-checkmark { diff --git a/desktop/tests/e2e/onboarding-backup.spec.ts b/desktop/tests/e2e/onboarding-backup.spec.ts index dff9b012b..56c280757 100644 --- a/desktop/tests/e2e/onboarding-backup.spec.ts +++ b/desktop/tests/e2e/onboarding-backup.spec.ts @@ -16,6 +16,20 @@ async function enterMachineBackup(page: import("@playwright/test").Page) { await page.getByRole("button", { name: "Create a new identity key" }).click(); } +async function openBackupOptions(page: import("@playwright/test").Page) { + await expect(page.getByTestId("backup-intro-logo")).toHaveCount(0); + await page.getByTestId("backup-options-toggle").click(); + await expect( + page.getByTestId("onboarding-page-backup-options"), + ).toBeVisible(); +} + +async function openPasswordBackup(page: import("@playwright/test").Page) { + await openBackupOptions(page); + await page.getByTestId("backup-option-password").click(); + await expect(page.getByTestId("onboarding-page-download")).toBeVisible(); +} + async function invokedCommands(page: import("@playwright/test").Page) { return page.evaluate( () => @@ -55,11 +69,11 @@ test("backup step appears on fresh-key path after profile submit", async ({ }); // --------------------------------------------------------------------------- -// Chooser: masked key with reveal toggle and inline copy. The raw key is -// fetched only on explicit reveal/copy, and Next is never blocked. +// Key-created view: masked key with reveal toggle. Backup options open the +// dark security view; the raw key is fetched only on explicit reveal/copy. // --------------------------------------------------------------------------- -test("chooser shows masked key; reveal and copy fetch it explicitly", async ({ +test("key view reveals explicitly; options copy explicitly", async ({ page, }) => { await page.context().grantPermissions(["clipboard-read", "clipboard-write"]); @@ -86,18 +100,24 @@ test("chooser shows masked key; reveal and copy fetch it explicitly", async ({ await page.getByTestId("backup-key-reveal-toggle").click(); await expect(key).not.toContainText("nsec1"); - // Inline copy goes straight to the clipboard. + // Copy is available only after opening the dark backup-options view. + await page.getByTestId("backup-options-toggle").click(); + await expect( + page.getByTestId("onboarding-page-backup-options"), + ).toBeVisible(); + await expect(page.getByTestId("onboarding-next")).toHaveCount(0); await page.getByTestId("backup-copy-key").click(); await expect .poll(async () => invokedCommands(page)) .toContain("copy_text_to_clipboard"); expect(await invokedCommands(page)).toContain("get_nsec"); - // Next leads into the download step, where backup stays skippable. + // Return restores the yellow key-created view; its Next skips backup and + // continues directly to setup. + await page.getByTestId("backup-return-to-onboarding").click(); + await expect(page.getByTestId("onboarding-page-backup")).toBeVisible(); await expect(page.getByTestId("onboarding-next")).toBeEnabled(); await page.getByTestId("onboarding-next").click(); - await expect(page.getByTestId("onboarding-page-download")).toBeVisible(); - await page.getByTestId("onboarding-skip").click(); await expect(page.getByTestId("onboarding-page-2")).toBeVisible(); }); @@ -112,10 +132,9 @@ test("download happy path: generated password, encrypt, native save, Next", asyn }) => { await enterMachineBackup(page); - // The download flow is its own onboarding step behind the footer's Next. - await expect(page.getByTestId("backup-intro-logo")).toHaveCount(0); - await page.getByTestId("onboarding-next").click(); - await expect(page.getByTestId("onboarding-page-download")).toBeVisible(); + // Password backup opens from the dark options state without adding an + // onboarding progress step. + await openPasswordBackup(page); // The password field starts empty; the create button sits in the footer's // primary slot and stays disabled until a valid password exists. @@ -239,28 +258,27 @@ test("download happy path: generated password, encrypt, native save, Next", asyn await expect(page.getByTestId("onboarding-page-2")).toBeVisible(); }); -test("download step Back returns to the backup chooser", async ({ page }) => { +test("security view returns to the yellow onboarding view", async ({ + page, +}) => { await enterMachineBackup(page); + await openPasswordBackup(page); - await page.getByTestId("onboarding-next").click(); - await expect(page.getByTestId("onboarding-page-download")).toBeVisible(); await expect(page.getByTestId("backup-passphrase-input")).toBeVisible(); - // The chooser's footer Next belongs to the previous step; the download - // step only mounts its own Next once the backup exists. + await expect(page.getByTestId("onboarding-step-dots")).toHaveCount(0); await expect(page.getByTestId("onboarding-next")).toHaveCount(0); - await page.getByTestId("onboarding-back").click(); + await page.getByTestId("backup-return-to-onboarding").click(); await expect(page.getByTestId("onboarding-page-backup")).toBeVisible(); await expect(page.getByTestId("backup-key-value")).toBeVisible(); await expect(page.getByTestId("onboarding-next")).toBeVisible(); }); -test("test-view Back returns to a secure saved-password placeholder", async ({ +test("returning to onboarding preserves password-backup progress", async ({ page, }) => { await enterMachineBackup(page); - await page.getByTestId("onboarding-next").click(); - await expect(page.getByTestId("onboarding-page-download")).toBeVisible(); + await openPasswordBackup(page); const input = page.getByTestId("backup-passphrase-input"); await input.fill("mock-horse-battery-staple"); @@ -269,30 +287,20 @@ test("test-view Back returns to a secure saved-password placeholder", async ({ page.getByRole("heading", { name: "Now, test your backup" }), ).toBeVisible(); - // Back retains only the encrypted blob and renders a fixed visual mask over - // an empty readonly input. - await page.getByTestId("onboarding-back").click(); - await expect( - page.getByRole("heading", { name: "Backup your key with a password" }), - ).toBeVisible(); - await expect(input).toHaveValue(""); - await expect(input).toHaveAttribute("readonly", ""); - await expect(page.getByTestId("backup-saved-password-mask")).toBeVisible(); + await page.getByTestId("backup-return-to-onboarding").click(); + await expect(page.getByTestId("onboarding-page-backup")).toBeVisible(); - // Re-downloading needs no password or re-encryption. - await page.getByTestId("encrypted-backup-create").click(); + // Re-entering the security flow restores the in-progress backup test. + await openPasswordBackup(page); await expect( page.getByRole("heading", { name: "Now, test your backup" }), ).toBeVisible(); - - await page.getByTestId("onboarding-back").click(); - await page.getByTestId("onboarding-back").click(); - await expect(page.getByTestId("onboarding-page-backup")).toBeVisible(); + await expect(page.getByTestId("backup-test-dropzone")).toBeVisible(); }); test("typed password requires 12 characters", async ({ page }) => { await enterMachineBackup(page); - await page.getByTestId("onboarding-next").click(); + await openPasswordBackup(page); const create = page.getByTestId("encrypted-backup-create"); await expect(create).toBeDisabled(); // empty field @@ -345,12 +353,10 @@ test("reveal shows inline error when get_nsec fails and Next still advances", as await page.getByTestId("backup-key-reveal-toggle").click(); await expect(page.getByTestId("backup-copy-error")).toBeVisible(); - // Keychain failure does not trap the user: Next still advances into the - // download step, and Skip there continues to setup. + // Keychain failure does not trap the user: Next still skips backup and + // advances directly to setup. await expect(page.getByTestId("onboarding-next")).toBeEnabled(); await page.getByTestId("onboarding-next").click(); - await expect(page.getByTestId("onboarding-page-download")).toBeVisible(); - await page.getByTestId("onboarding-skip").click(); await expect(page.getByTestId("onboarding-page-2")).toBeVisible(); }); diff --git a/desktop/tests/e2e/onboarding-docked-cta-screenshots.spec.ts b/desktop/tests/e2e/onboarding-docked-cta-screenshots.spec.ts index 13aaf6e85..0e535ffb1 100644 --- a/desktop/tests/e2e/onboarding-docked-cta-screenshots.spec.ts +++ b/desktop/tests/e2e/onboarding-docked-cta-screenshots.spec.ts @@ -68,8 +68,17 @@ test("machine onboarding: landing, backup, setup docked CTAs", async ({ await waitForAnimations(page); await page.screenshot({ path: `${SHOT_DIR}/02b-backup-revealed.png` }); - // Next leads into the download step; Skip there continues to setup. - await page.getByTestId("onboarding-next").click(); + // Backup options leave the yellow flow for the dark security view without + // adding a progress step or a generic Next action. + await page.getByTestId("backup-options-toggle").click(); + await expect( + page.getByTestId("onboarding-page-backup-options"), + ).toBeVisible(); + await expect(page.getByTestId("onboarding-next")).toHaveCount(0); + await waitForAnimations(page); + await page.screenshot({ path: `${SHOT_DIR}/02c-backup-options.png` }); + + await page.getByTestId("backup-option-password").click(); await expect(page.getByTestId("onboarding-page-download")).toBeVisible(); await page.getByTestId("onboarding-skip").click(); await expect(