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' }));