feat(ui): drop the pairing label step — profile names come from the account

Sep 25 feedback: the prefilled 'Amber' name had to be cleared letter by
letter before the QR would appear (the input fight), and naming should
be automatic anyway. One click on 'Sign in with a signer app (Amber)'
now goes straight to the QR with the seed label 'Amber'; the existing
adopt_identity background enrichment fetches the account's kind-0 after
the handshake and upgrades the profile row to the account's REAL
display name whenever it still carries our seed (a manual rename in
Profiles always wins and is never overwritten).

frontend: 125 tests green (pairing test asserts the one-click path and
the 'Amber' seed label), typecheck/lint/format/electron:build/build
green. Rust untouched — the auto-naming enrichment already shipped at
a76d8df.
This commit is contained in:
Avi 2026-09-25 16:30:18 -05:00
commit fc2fe93161
2 changed files with 13 additions and 62 deletions

View file

@ -12,7 +12,7 @@ interface CreateProfileModalProps {
onClose: () => void;
}
type Phase = 'choice' | 'pair-label' | 'pairing' | 'paired' | 'local' | 'creating' | 'success';
type Phase = 'choice' | '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' || phase === 'pair-label') {
if (phase === 'local') {
const frame = requestAnimationFrame(() => inputRef.current?.focus());
return () => cancelAnimationFrame(frame);
}
@ -133,6 +133,12 @@ export function CreateProfileModal({ open, onClose }: CreateProfileModalProps) {
const startPairing = async () => {
setError(null);
try {
// No user-typed label: the profile is named automatically. The
// backend fetches the account's kind-0 after the handshake and
// upgrades this seed label to the account's real display name
// (adopt_identity background enrichment); a nameless account keeps
// 'Amber', which beats a manual step the user must fight with a
// backspace key (Sep 25 feedback).
const status = await nip46PairStart(label.trim() || 'Amber');
if (status.pairing_uri) setLivePairingUri(status.pairing_uri);
setPhase('pairing');
@ -170,13 +176,7 @@ export function CreateProfileModal({ open, onClose }: CreateProfileModalProps) {
</div>
{error && <ErrorText id={errorId}>{error}</ErrorText>}
<div style={{ display: 'grid', gap: 12 }}>
<Button
variant="primary"
onClick={() => {
if (!label.trim()) setLabel('Amber');
setPhase('pair-label');
}}
>
<Button variant="primary" onClick={() => void startPairing()}>
<Icon name="key" size={16} />
Sign in with a signer app (Amber)
</Button>
@ -196,51 +196,6 @@ 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}>

View file

@ -11,7 +11,6 @@ vi.mock('qrcode', () => ({
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', () => {
@ -93,17 +92,14 @@ describe('CreateProfileModal', () => {
renderWithApp(<CreateProfileModal open onClose={vi.fn()} />);
const user = userEvent.setup();
// No label step (Sep 25 feedback: typing/fighting a prefilled name was
// friction). One click mints the QR with the 'Amber' seed label; the
// backend upgrades it to the account's real kind-0 display name.
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();
const start = backend.requests.find((r) => r.method === 'nip46_pair_start');
expect(start?.params.label).toBe('My Phone');
expect(start?.params.label).toBe('Amber');
expect(await screen.findByAltText('Pairing QR code')).toBeInTheDocument();
});