fix(nip46): spec-correct connect params, full-handshake [NIP46] trace, visible handshake errors
This commit is contained in:
parent
2bc6321d58
commit
8f055f71bf
5 changed files with 672 additions and 51 deletions
|
|
@ -592,6 +592,11 @@ export function SignerModeScreen() {
|
|||
Waiting for the signer to scan… the connection appears automatically once
|
||||
approved. The code expires after a few minutes.
|
||||
</p>
|
||||
{nip46StatusState?.error && (
|
||||
<Alert tone="error" title="Pairing failed">
|
||||
{nip46StatusState.error}
|
||||
</Alert>
|
||||
)}
|
||||
{pairError && <ErrorText>{pairError}</ErrorText>}
|
||||
<div className="settings-inline" style={{ justifyContent: 'center' }}>
|
||||
<Button variant="ghost" onClick={() => void handlePairCancel()}>
|
||||
|
|
@ -636,6 +641,21 @@ export function SignerModeScreen() {
|
|||
</div>
|
||||
{error && <ErrorText>{error}</ErrorText>}
|
||||
{pairError && <ErrorText>{pairError}</ErrorText>}
|
||||
{/* A failed handshake must never look like an idle form:
|
||||
surface the backend's error so a timeout is visible. */}
|
||||
{nip46StatusState?.error && (
|
||||
<Alert tone="error" title="Connection failed">
|
||||
{nip46StatusState.error}
|
||||
</Alert>
|
||||
)}
|
||||
{/* A sent-but-unapproved connection request is in flight:
|
||||
say so instead of showing a blank form. */}
|
||||
{!nip46StatusState?.error && (nip46StatusState?.relays?.length ?? 0) > 0 && (
|
||||
<p className="hint">
|
||||
Connection request sent — approve it in Amber. This updates automatically; it
|
||||
can take up to a couple of minutes on a slow network.
|
||||
</p>
|
||||
)}
|
||||
<div className="settings-inline">
|
||||
{mode === 'nip46_client' && (
|
||||
<Button
|
||||
|
|
|
|||
72
frontend/src/test/SignerModeScreen.test.tsx
Normal file
72
frontend/src/test/SignerModeScreen.test.tsx
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import { screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { beforeEach, expect, vi } from 'vitest';
|
||||
import { SignerModeScreen } from '../screens/SignerModeScreen';
|
||||
import { renderWithApp } from './render';
|
||||
import { createFakeBackend, installFakeBackend } from './fakeBackend';
|
||||
import { makeState } from './apiMock';
|
||||
|
||||
vi.mock('qrcode', () => ({
|
||||
default: { toDataURL: vi.fn(async () => 'data:image/png;base64,QR') },
|
||||
}));
|
||||
|
||||
function installNip46Backend() {
|
||||
const backend = createFakeBackend(makeState({ signer_mode: 'nip46_client' }));
|
||||
installFakeBackend(backend);
|
||||
return backend;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('SignerModeScreen handshake states', () => {
|
||||
it('shows the QR waiting hint while a pairing is in flight', async () => {
|
||||
const backend = installNip46Backend();
|
||||
const user = userEvent.setup();
|
||||
renderWithApp(<SignerModeScreen />);
|
||||
|
||||
await screen.findByRole('button', { name: /Show QR/i });
|
||||
await user.click(screen.getByRole('button', { name: /Show QR/i }));
|
||||
|
||||
expect(await screen.findByText(/Waiting for the signer to scan/i)).toBeInTheDocument();
|
||||
expect(backend.requests.some((r) => r.method === 'nip46_pair_start')).toBe(true);
|
||||
});
|
||||
|
||||
it('shows a connecting hint after a paste-URI connect is sent but unapproved', async () => {
|
||||
const backend = installNip46Backend();
|
||||
backend.setNip46({
|
||||
type: 'nip46',
|
||||
connected: false,
|
||||
relays: ['wss://relay.test'],
|
||||
connected_relays: ['wss://relay.test'],
|
||||
pending_approvals: [],
|
||||
});
|
||||
renderWithApp(<SignerModeScreen />);
|
||||
|
||||
expect(await screen.findByText(/Connection request sent/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('surfaces a failed handshake as a visible error, not a silent idle form', async () => {
|
||||
const backend = installNip46Backend();
|
||||
backend.setNip46({
|
||||
type: 'nip46',
|
||||
connected: false,
|
||||
relays: ['wss://relay.test'],
|
||||
connected_relays: [],
|
||||
error:
|
||||
'The signer would not reveal its public key (timeout). Keep Amber open in the foreground with network access and try again.',
|
||||
pending_approvals: [],
|
||||
});
|
||||
renderWithApp(<SignerModeScreen />);
|
||||
|
||||
expect(await screen.findByText('Connection failed')).toBeInTheDocument();
|
||||
expect(
|
||||
await screen.findByText(/The signer would not reveal its public key/i),
|
||||
).toBeInTheDocument();
|
||||
// The failure must poll through the same status channel the screen reads.
|
||||
await waitFor(() =>
|
||||
expect(backend.requests.some((r) => r.method === 'nip46_status')).toBe(true),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -2,6 +2,7 @@ import type {
|
|||
AppState,
|
||||
BackendResponse,
|
||||
FeedItem,
|
||||
Nip46SignerStatus,
|
||||
ProfileSummary,
|
||||
PublishReport,
|
||||
RelayTestResult,
|
||||
|
|
@ -42,6 +43,9 @@ export interface FakeBackend {
|
|||
/** Current NIP-46 signer status. */
|
||||
signer: SignerStatus;
|
||||
setSigner: (next: SignerStatus) => void;
|
||||
/** NIP-46 client handshake status backing the nip46_* methods. */
|
||||
nip46: Nip46SignerStatus;
|
||||
setNip46: (next: Nip46SignerStatus) => void;
|
||||
/** Standing "always allow" grants returned by signer_grants_list. */
|
||||
signerGrants: SignerGrant[];
|
||||
/** Notes returned by `feed_get`. */
|
||||
|
|
@ -109,6 +113,16 @@ export function createFakeBackend(initial?: AppState): FakeBackend {
|
|||
setSigner(next) {
|
||||
backend.signer = next;
|
||||
},
|
||||
nip46: {
|
||||
type: 'nip46',
|
||||
connected: false,
|
||||
relays: [],
|
||||
connected_relays: [],
|
||||
pending_approvals: [],
|
||||
},
|
||||
setNip46(next) {
|
||||
backend.nip46 = next;
|
||||
},
|
||||
signerGrants: [],
|
||||
feedItems: [
|
||||
{
|
||||
|
|
@ -181,6 +195,38 @@ export function createFakeBackend(initial?: AppState): FakeBackend {
|
|||
case 'get_state':
|
||||
return state;
|
||||
|
||||
// NIP-46 client handshake surface used by SignerModeScreen. The fake
|
||||
// keeps a Nip46SignerStatus-shaped object so handshake-state tests
|
||||
// (pairing URI, connecting relays, failure errors) run without relays.
|
||||
case 'nip46_status':
|
||||
return backend.nip46;
|
||||
case 'nip46_pair_start': {
|
||||
const next = {
|
||||
...backend.nip46,
|
||||
pairing_uri: `nostrconnect://deadbeef?relay=${encodeURIComponent('wss://relay.test')}&secret=fake`,
|
||||
};
|
||||
backend.setNip46(next);
|
||||
return next;
|
||||
}
|
||||
case 'nip46_connect': {
|
||||
const next = { ...backend.nip46, relays: ['wss://relay.test'] };
|
||||
backend.setNip46(next);
|
||||
return next;
|
||||
}
|
||||
case 'nip46_disconnect': {
|
||||
const next: Nip46SignerStatus = {
|
||||
type: 'nip46',
|
||||
connected: false,
|
||||
relays: [],
|
||||
connected_relays: [],
|
||||
pending_approvals: [],
|
||||
};
|
||||
backend.setNip46(next);
|
||||
return next;
|
||||
}
|
||||
case 'nip46_approve':
|
||||
return backend.nip46;
|
||||
|
||||
case 'create_profile': {
|
||||
const label = String(params.label ?? '');
|
||||
const profile: ProfileSummary = {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue