Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions desktop/src/features/onboarding/ui/BackupStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,29 @@ export function BackupStep({
<Eye className="h-6 w-6" aria-hidden="true" />
)}
</Button>
<Button
aria-label={
copyState === "copied"
? "Private key copied"
: "Copy private key"
}
className="h-10 w-10 shrink-0 text-muted-foreground hover:text-foreground"
data-testid="backup-key-copy"
disabled={copyState === "copying"}
onClick={() => void copyKeyToClipboard()}
size="icon"
type="button"
variant="ghost"
>
{copyState === "copied" ? (
<Check
className="h-6 w-6 text-primary"
aria-hidden="true"
/>
) : (
<Copy className="h-6 w-6" aria-hidden="true" />
)}
</Button>
</div>
</Card>

Expand Down
21 changes: 15 additions & 6 deletions desktop/tests/e2e/onboarding-backup.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,11 @@ test("backup step appears on fresh-key path after profile submit", async ({
});

// ---------------------------------------------------------------------------
// 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.
// Key-created view: masked key with reveal and copy controls. Backup options
// open the dark security view; the raw key is fetched only on explicit action.
// ---------------------------------------------------------------------------

test("key view reveals explicitly; options copy explicitly", async ({
page,
}) => {
test("key view reveals and copies explicitly", async ({ page }) => {
await page.context().grantPermissions(["clipboard-read", "clipboard-write"]);
await enterMachineBackup(page);

Expand All @@ -88,6 +86,17 @@ test("key view reveals explicitly; options copy explicitly", async ({
await expect(key).not.toContainText("nsec1");
expect(await invokedCommands(page)).not.toContain("get_nsec");

// Copy is available directly on the key card and provides confirmation.
const copyButton = page.getByTestId("backup-key-copy");
await expect(copyButton).toHaveAccessibleName("Copy private key");
await expect(copyButton).toHaveText("");
await copyButton.click();
await expect(copyButton).toHaveAccessibleName("Private key copied");
await expect
.poll(async () => invokedCommands(page))
.toContain("copy_text_to_clipboard");
expect(await invokedCommands(page)).toContain("get_nsec");

// Reveal fetches the key; box must not reflow (same-length monospace mask).
await page.getByTestId("backup-key-reveal-toggle").click();
await expect(key).toContainText("nsec1mock");
Expand All @@ -100,7 +109,7 @@ test("key view reveals explicitly; options copy explicitly", async ({
await page.getByTestId("backup-key-reveal-toggle").click();
await expect(key).not.toContainText("nsec1");

// Copy is available only after opening the dark backup-options view.
// The deeper backup-options view retains its existing copy action.
await page.getByTestId("backup-options-link").click();
await expect(
page.getByTestId("onboarding-page-backup-options"),
Expand Down