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;
|
||||
}
|
||||
|
||||
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) {
|
|||
</div>
|
||||
{error && <ErrorText id={errorId}>{error}</ErrorText>}
|
||||
<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} />
|
||||
Sign in with a signer app (Amber)
|
||||
</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') {
|
||||
return (
|
||||
<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') },
|
||||
}));
|
||||
|
||||
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', () => {
|
||||
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(<CreateProfileModal open onClose={vi.fn()} />);
|
||||
|
||||
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(<CreateProfileModal open onClose={vi.fn()} />);
|
||||
|
||||
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' }));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue