From a809a670239c9df678adc90d241518ef6b9645bc Mon Sep 17 00:00:00 2001 From: Avi Date: Fri, 25 Sep 2026 16:01:57 -0500 Subject: [PATCH] feat(ui): name the signer connection before the QR; prune stale vault rows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pairing path minted its profile under the backend's 'Remote Signer' default, so several test pairings left indistinguishable rows. The 'Sign in with a signer app (Amber)' button now shows a prefilled ('Amber') Connection name step first; the QR step starts only from there and the typed name reaches nip46_pair_start as the profile label. Enter submits directly (input focused, prefilled). Also cleaned the live vault (not in git): dropped 2 stale 'Remote Signer' connections and the secret-less 'Dev' remote stub tied to them, plus 13 orphaned connection_secrets entries; kept the live Amber pairing (npub1qn0w4a…, its connection + client key) and Testing123. frontend: 125 tests passed (label step covered: prefill, typed label reaches nip46_pair_start), typecheck/lint/format/electron:build/build green. Rust untouched. --- frontend/src/screens/CreateProfileModal.tsx | 59 +++++++++++++++++-- frontend/src/test/CreateProfileModal.test.tsx | 18 +++++- 2 files changed, 70 insertions(+), 7 deletions(-) diff --git a/frontend/src/screens/CreateProfileModal.tsx b/frontend/src/screens/CreateProfileModal.tsx index 457f471..30347d7 100644 --- a/frontend/src/screens/CreateProfileModal.tsx +++ b/frontend/src/screens/CreateProfileModal.tsx @@ -12,7 +12,7 @@ interface CreateProfileModalProps { onClose: () => void; } -type Phase = 'choice' | 'local' | 'creating' | 'success' | 'pairing' | 'paired'; +type Phase = 'choice' | 'pair-label' | 'pairing' | 'paired' | 'local' | 'creating' | 'success'; export function CreateProfileModal({ open, onClose }: CreateProfileModalProps) { const { state, createProfile, nip46PairStart, nip46Status, nip46Disconnect, refresh } = useApp(); @@ -39,7 +39,7 @@ export function CreateProfileModal({ open, onClose }: CreateProfileModalProps) { // Let the local-form input take focus once that step mounts. useEffect(() => { - if (phase === 'local') { + if (phase === 'local' || phase === 'pair-label') { const frame = requestAnimationFrame(() => inputRef.current?.focus()); return () => cancelAnimationFrame(frame); } @@ -133,7 +133,7 @@ export function CreateProfileModal({ open, onClose }: CreateProfileModalProps) { const startPairing = async () => { setError(null); try { - const status = await nip46PairStart(label.trim() || 'Remote Signer'); + const status = await nip46PairStart(label.trim() || 'Amber'); if (status.pairing_uri) setLivePairingUri(status.pairing_uri); setPhase('pairing'); } catch (err) { @@ -170,7 +170,13 @@ export function CreateProfileModal({ open, onClose }: CreateProfileModalProps) { {error && {error}}
- @@ -190,6 +196,51 @@ export function CreateProfileModal({ open, onClose }: CreateProfileModalProps) { ); } + if (phase === 'pair-label') { + return ( + +
{ + event.preventDefault(); + void startPairing(); + }} + noValidate + > +
+

+ Name this connection so you can tell it apart later — the profile it creates gets this + name, and Amber will show it when asking you to approve signatures. +

+
+
+ + setLabel(event.target.value)} + placeholder="e.g. Amber" + maxLength={60} + aria-describedby={error ? errorId : undefined} + aria-invalid={error ? true : undefined} + autoComplete="off" + /> + {error && {error}} +
+
+ + +
+
+
+ ); + } + if (phase === 'pairing') { return ( diff --git a/frontend/src/test/CreateProfileModal.test.tsx b/frontend/src/test/CreateProfileModal.test.tsx index 7cdca72..98b11f6 100644 --- a/frontend/src/test/CreateProfileModal.test.tsx +++ b/frontend/src/test/CreateProfileModal.test.tsx @@ -9,6 +9,11 @@ vi.mock('qrcode', () => ({ default: { toDataURL: vi.fn(async () => 'data:image/png;base64,QR') }, })); +async function startPairingFlow(user: ReturnType) { + await user.click(screen.getByRole('button', { name: /Sign in with a signer app \(Amber\)/ })); + await user.click(screen.getByRole('button', { name: 'Show QR code' })); +} + describe('CreateProfileModal', () => { it('offers the signer (Amber) and local-key choices first', async () => { const backend = createFakeBackend(makeEmptyState()); @@ -89,9 +94,16 @@ describe('CreateProfileModal', () => { const user = userEvent.setup(); await user.click(screen.getByRole('button', { name: /Sign in with a signer app \(Amber\)/ })); + // The label step comes first and is prefilled so Enter just works. + const nameInput = await screen.findByLabelText('Connection name'); + expect(nameInput).toHaveValue('Amber'); + await user.clear(nameInput); + await user.type(nameInput, 'My Phone'); + await user.click(screen.getByRole('button', { name: 'Show QR code' })); expect(await screen.findByText(/Waiting for the signer to scan/i)).toBeInTheDocument(); - expect(backend.requests.some((r) => r.method === 'nip46_pair_start')).toBe(true); + const start = backend.requests.find((r) => r.method === 'nip46_pair_start'); + expect(start?.params.label).toBe('My Phone'); expect(await screen.findByAltText('Pairing QR code')).toBeInTheDocument(); }); @@ -101,7 +113,7 @@ describe('CreateProfileModal', () => { renderWithApp(); const user = userEvent.setup(); - await user.click(screen.getByRole('button', { name: /Sign in with a signer app \(Amber\)/ })); + await startPairingFlow(user); await screen.findByText(/Waiting for the signer to scan/i); // Amber approves: the fake backend now reports the handshake done. The @@ -125,7 +137,7 @@ describe('CreateProfileModal', () => { renderWithApp(); const user = userEvent.setup(); - await user.click(screen.getByRole('button', { name: /Sign in with a signer app \(Amber\)/ })); + await startPairingFlow(user); await screen.findByText(/Waiting for the signer to scan/i); await user.click(screen.getByRole('button', { name: 'Cancel pairing' }));