feat(ui): name the signer connection before the QR; prune stale vault rows
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.
This commit is contained in:
parent
2ef2cd1181
commit
a809a67023
2 changed files with 70 additions and 7 deletions
|
|
@ -12,7 +12,7 @@ interface CreateProfileModalProps {
|
||||||
onClose: () => void;
|
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) {
|
export function CreateProfileModal({ open, onClose }: CreateProfileModalProps) {
|
||||||
const { state, createProfile, nip46PairStart, nip46Status, nip46Disconnect, refresh } = useApp();
|
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.
|
// Let the local-form input take focus once that step mounts.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (phase === 'local') {
|
if (phase === 'local' || phase === 'pair-label') {
|
||||||
const frame = requestAnimationFrame(() => inputRef.current?.focus());
|
const frame = requestAnimationFrame(() => inputRef.current?.focus());
|
||||||
return () => cancelAnimationFrame(frame);
|
return () => cancelAnimationFrame(frame);
|
||||||
}
|
}
|
||||||
|
|
@ -133,7 +133,7 @@ export function CreateProfileModal({ open, onClose }: CreateProfileModalProps) {
|
||||||
const startPairing = async () => {
|
const startPairing = async () => {
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const status = await nip46PairStart(label.trim() || 'Remote Signer');
|
const status = await nip46PairStart(label.trim() || 'Amber');
|
||||||
if (status.pairing_uri) setLivePairingUri(status.pairing_uri);
|
if (status.pairing_uri) setLivePairingUri(status.pairing_uri);
|
||||||
setPhase('pairing');
|
setPhase('pairing');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -170,7 +170,13 @@ export function CreateProfileModal({ open, onClose }: CreateProfileModalProps) {
|
||||||
</div>
|
</div>
|
||||||
{error && <ErrorText id={errorId}>{error}</ErrorText>}
|
{error && <ErrorText id={errorId}>{error}</ErrorText>}
|
||||||
<div style={{ display: 'grid', gap: 12 }}>
|
<div style={{ display: 'grid', gap: 12 }}>
|
||||||
<Button variant="primary" onClick={() => void startPairing()}>
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
onClick={() => {
|
||||||
|
if (!label.trim()) setLabel('Amber');
|
||||||
|
setPhase('pair-label');
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Icon name="key" size={16} />
|
<Icon name="key" size={16} />
|
||||||
Sign in with a signer app (Amber)
|
Sign in with a signer app (Amber)
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -190,6 +196,51 @@ export function CreateProfileModal({ open, onClose }: CreateProfileModalProps) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (phase === 'pair-label') {
|
||||||
|
return (
|
||||||
|
<Modal open={open} title="Sign in with a signer" onClose={handleClose}>
|
||||||
|
<form
|
||||||
|
onSubmit={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
void startPairing();
|
||||||
|
}}
|
||||||
|
noValidate
|
||||||
|
>
|
||||||
|
<div className="create-explainer">
|
||||||
|
<p>
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="field">
|
||||||
|
<label htmlFor="pair-label-input">Connection name</label>
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
id="pair-label-input"
|
||||||
|
type="text"
|
||||||
|
value={label}
|
||||||
|
onChange={(event) => setLabel(event.target.value)}
|
||||||
|
placeholder="e.g. Amber"
|
||||||
|
maxLength={60}
|
||||||
|
aria-describedby={error ? errorId : undefined}
|
||||||
|
aria-invalid={error ? true : undefined}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
{error && <ErrorText id={errorId}>{error}</ErrorText>}
|
||||||
|
</div>
|
||||||
|
<div className="modal-actions">
|
||||||
|
<Button variant="ghost" onClick={() => setPhase('choice')}>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
<Button variant="primary" type="submit" disabled={!canSubmit}>
|
||||||
|
Show QR code
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (phase === 'pairing') {
|
if (phase === 'pairing') {
|
||||||
return (
|
return (
|
||||||
<Modal open={open} title="Sign in with Amber" onClose={handleClose}>
|
<Modal open={open} title="Sign in with Amber" onClose={handleClose}>
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,11 @@ vi.mock('qrcode', () => ({
|
||||||
default: { toDataURL: vi.fn(async () => 'data:image/png;base64,QR') },
|
default: { toDataURL: vi.fn(async () => 'data:image/png;base64,QR') },
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
async function startPairingFlow(user: ReturnType<typeof userEvent.setup>) {
|
||||||
|
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', () => {
|
describe('CreateProfileModal', () => {
|
||||||
it('offers the signer (Amber) and local-key choices first', async () => {
|
it('offers the signer (Amber) and local-key choices first', async () => {
|
||||||
const backend = createFakeBackend(makeEmptyState());
|
const backend = createFakeBackend(makeEmptyState());
|
||||||
|
|
@ -89,9 +94,16 @@ describe('CreateProfileModal', () => {
|
||||||
|
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
await user.click(screen.getByRole('button', { name: /Sign in with a signer app \(Amber\)/ }));
|
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(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();
|
expect(await screen.findByAltText('Pairing QR code')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -101,7 +113,7 @@ describe('CreateProfileModal', () => {
|
||||||
renderWithApp(<CreateProfileModal open onClose={vi.fn()} />);
|
renderWithApp(<CreateProfileModal open onClose={vi.fn()} />);
|
||||||
|
|
||||||
const user = userEvent.setup();
|
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 screen.findByText(/Waiting for the signer to scan/i);
|
||||||
|
|
||||||
// Amber approves: the fake backend now reports the handshake done. The
|
// Amber approves: the fake backend now reports the handshake done. The
|
||||||
|
|
@ -125,7 +137,7 @@ describe('CreateProfileModal', () => {
|
||||||
renderWithApp(<CreateProfileModal open onClose={vi.fn()} />);
|
renderWithApp(<CreateProfileModal open onClose={vi.fn()} />);
|
||||||
|
|
||||||
const user = userEvent.setup();
|
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 screen.findByText(/Waiting for the signer to scan/i);
|
||||||
|
|
||||||
await user.click(screen.getByRole('button', { name: 'Cancel pairing' }));
|
await user.click(screen.getByRole('button', { name: 'Cancel pairing' }));
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue