mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
fix(desktop): make saved .ncryptsec backups importable from file
Max's clean-room pass found the wipe -> import-from-file leg unreachable: the file input accepted only .key,text/plain (macOS greys out the identity.ncryptsec our own save dialog emits), and the first-launch spotlight variant rendered no file input at all, leaving paste-only recovery exactly where a returning user holds a backup file. Accept .ncryptsec in the shared hidden file input, hoist it out of the default-variant branch, and give the spotlight variant an 'Import from a file' button wired to the same picker. Drop-zone behavior in the default variant is unchanged. E2E covers the spotlight file path end to end (accept attribute asserted explicitly, since setInputFiles bypasses it) through decrypt-and-continue. Co-authored-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
This commit is contained in:
co-authored by
Tyler Longwell
parent
51ea1b18f3
commit
72f00afa89
@@ -96,7 +96,7 @@ export function NostrKeyImportForm({
|
|||||||
|
|
||||||
if (file.size > NOSTR_KEY_FILE_MAX_BYTES) {
|
if (file.size > NOSTR_KEY_FILE_MAX_BYTES) {
|
||||||
setImportError(
|
setImportError(
|
||||||
"That file is too large to be a key. Drop a .key file or paste your nsec.",
|
"That file is too large to be a key. Choose a .key or .ncryptsec backup file, or paste your key.",
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -243,11 +243,13 @@ export function NostrKeyImportForm({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{variant === "spotlight" ? null : (
|
{/* Hidden file input shared by both variants: the default drop zone and
|
||||||
<>
|
the spotlight "Import from a file" button both open it. Accepts the
|
||||||
|
.ncryptsec archives our own save flow emits alongside raw .key files. */}
|
||||||
<input
|
<input
|
||||||
accept=".key,text/plain"
|
accept=".key,.ncryptsec,text/plain"
|
||||||
className="sr-only"
|
className="sr-only"
|
||||||
|
data-testid="nostr-import-file-input"
|
||||||
disabled={isInteractionDisabled}
|
disabled={isInteractionDisabled}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
void handleFiles(event.currentTarget.files);
|
void handleFiles(event.currentTarget.files);
|
||||||
@@ -258,6 +260,23 @@ export function NostrKeyImportForm({
|
|||||||
type="file"
|
type="file"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{variant === "spotlight" ? (
|
||||||
|
// First-launch/wiped-identity treatment: no drop zone, but the file
|
||||||
|
// path must still exist — a backup saved through the OS dialog is
|
||||||
|
// exactly what a wiped user returns with.
|
||||||
|
<div className="mt-2 text-center">
|
||||||
|
<Button
|
||||||
|
className="h-9 rounded-full bg-foreground/10 px-6 hover:bg-foreground/15"
|
||||||
|
data-testid="nostr-import-file-button"
|
||||||
|
disabled={isInteractionDisabled}
|
||||||
|
onClick={openFilePicker}
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
>
|
||||||
|
Import from a file
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<button
|
<button
|
||||||
className={cn(
|
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",
|
"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",
|
||||||
@@ -325,7 +344,6 @@ export function NostrKeyImportForm({
|
|||||||
Drop a key here
|
Drop a key here
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isEncryptedInput ? (
|
{isEncryptedInput ? (
|
||||||
|
|||||||
@@ -657,6 +657,48 @@ test("first-launch encrypted backup import asks for a passphrase and continues",
|
|||||||
await expect(page.getByTestId("machine-onboarding-gate")).toBeVisible();
|
await expect(page.getByTestId("machine-onboarding-gate")).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("first-launch import accepts an .ncryptsec backup file", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await installMockBridge(page, undefined, {
|
||||||
|
skipCommunitySeed: true,
|
||||||
|
skipOnboardingSeed: true,
|
||||||
|
});
|
||||||
|
await page.goto("/");
|
||||||
|
|
||||||
|
await page.getByRole("button", { name: "Use an existing key" }).click();
|
||||||
|
|
||||||
|
// The spotlight variant must expose a file path: a wiped user returns with
|
||||||
|
// exactly the identity.ncryptsec our own save dialog produced. The accept
|
||||||
|
// attribute is asserted explicitly because setInputFiles bypasses it — the
|
||||||
|
// OS picker is what filters on it in real use.
|
||||||
|
await expect(page.getByTestId("nostr-import-file-button")).toBeVisible();
|
||||||
|
const fileInput = page.getByTestId("nostr-import-file-input");
|
||||||
|
await expect(fileInput).toHaveAttribute(
|
||||||
|
"accept",
|
||||||
|
".key,.ncryptsec,text/plain",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Spec-vector blob the mock bridge accepts with the mock passphrase.
|
||||||
|
const mockNcryptsec =
|
||||||
|
"ncryptsec1qgg9947rlpvqu76pj5ecreduf9jxhselq2nae2kghhvd5g7dgjtcxfqtd67p9m0w57lspw8gsq6yphnm8623nsl8xn9j4jdzz84zm3frztj3z7s35vpzmqf6ksu8r89qk5z2zxfmu5gv8th8wclt0h4p";
|
||||||
|
await fileInput.setInputFiles({
|
||||||
|
buffer: Buffer.from(`${mockNcryptsec}\n`),
|
||||||
|
mimeType: "text/plain",
|
||||||
|
name: "identity.ncryptsec",
|
||||||
|
});
|
||||||
|
|
||||||
|
// File contents land in the key field and switch the form to encrypted mode.
|
||||||
|
await expect(page.getByTestId("nostr-import-encrypted-badge")).toBeVisible();
|
||||||
|
await page
|
||||||
|
.getByTestId("nostr-import-passphrase")
|
||||||
|
.fill("mock horse battery staple lake orbit");
|
||||||
|
await page.getByTestId("nostr-import-submit").click();
|
||||||
|
|
||||||
|
await expect(page.getByTestId("onboarding-page-2")).toBeVisible();
|
||||||
|
await expect(page.getByTestId("machine-onboarding-gate")).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
test("non-local runtime override keeps community selection without release flag", async ({
|
test("non-local runtime override keeps community selection without release flag", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user