mirror of
https://github.com/lightninglabs/lightning-terminal.git
synced 2026-08-13 12:33:36 +02:00
app: fix brittle LNC reconnect flow — fail fast, clear stale sessions
- Reconnect timeout reduced from 30s to 12s - On any reconnect failure, automatically clear the saved session flag so the user isn't stuck in a broken reconnect loop on next visit - Error message simplified to "Session expired. Please connect with a new pairing phrase." instead of vague troubleshooting advice - When reconnect fails, UI auto-switches to the new pairing phrase form - "Use a new pairing phrase" now explicitly clears old LNC storage before attempting the fresh connection Made-with: Cursor
This commit is contained in:
parent
f6f5df1155
commit
65747216b7
2 changed files with 136 additions and 52 deletions
|
|
@ -18,6 +18,7 @@ import {
|
|||
ChevronDown,
|
||||
ChevronRight,
|
||||
HelpCircle,
|
||||
Server,
|
||||
} from 'lucide-react';
|
||||
|
||||
const fadeUp = keyframes`
|
||||
|
|
@ -25,6 +26,11 @@ const fadeUp = keyframes`
|
|||
to { opacity: 1; transform: translateY(0); }
|
||||
`;
|
||||
|
||||
const spin = keyframes`
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
`;
|
||||
|
||||
const S = {
|
||||
Wrapper: styled.div`
|
||||
display: flex;
|
||||
|
|
@ -430,35 +436,42 @@ const S = {
|
|||
}
|
||||
`,
|
||||
NewSessionBtn: styled.button`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: rgba(255, 255, 255, 0.25);
|
||||
font-family: inherit;
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
padding: 6px 0;
|
||||
margin-top: 4px;
|
||||
&:hover {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
`,
|
||||
Footer: styled.div`
|
||||
margin-top: 12px;
|
||||
text-align: center;
|
||||
animation: ${fadeUp} 0.5s ease both;
|
||||
animation-delay: 0.15s;
|
||||
`,
|
||||
FooterLink: styled.button`
|
||||
background: none;
|
||||
border: none;
|
||||
color: rgba(255, 255, 255, 0.2);
|
||||
font-size: 13px;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
margin-top: 8px;
|
||||
transition: color 0.15s ease;
|
||||
&:hover {
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
}
|
||||
`,
|
||||
FooterLink: styled.button`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: rgba(255, 255, 255, 0.25);
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
padding: 6px 0;
|
||||
margin-top: 8px;
|
||||
transition: color 0.15s ease;
|
||||
&:hover {
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
}
|
||||
`,
|
||||
Spinner: styled(Loader2)`
|
||||
animation: ${spin} 1s linear infinite;
|
||||
`,
|
||||
};
|
||||
|
||||
const CMD = 'litcli sessions add --label="Lightning Terminal Web" --type admin';
|
||||
|
|
@ -535,7 +548,7 @@ const providerHelp: {
|
|||
{
|
||||
id: 'self',
|
||||
name: 'Self-Hosted (litd)',
|
||||
logo: '/icons/Lit.png',
|
||||
logo: '/icons/Lit-dark.png',
|
||||
steps: null,
|
||||
},
|
||||
];
|
||||
|
|
@ -583,13 +596,21 @@ const ConnectNodePage: React.FC = () => {
|
|||
setLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
if (hasSavedSession && !pairingPhrase.trim()) {
|
||||
if (hasSavedSession && !showNewSession && !pairingPhrase.trim()) {
|
||||
await store.authStore.reconnectLnc(lncPassword);
|
||||
} else {
|
||||
if (showNewSession) {
|
||||
LncApi.clearPaired();
|
||||
}
|
||||
await store.authStore.loginWithLnc(pairingPhrase, lncPassword);
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err?.message || 'Connection failed');
|
||||
const msg = err?.message || 'Connection failed';
|
||||
setError(msg);
|
||||
if (msg.includes('expired') || msg.includes('Session')) {
|
||||
setShowNewSession(true);
|
||||
setLncPassword('');
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
|
@ -675,10 +696,7 @@ const ConnectNodePage: React.FC = () => {
|
|||
<S.Btn type="submit" disabled={loading || !lncPassword.trim()}>
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2
|
||||
size={16}
|
||||
style={{ animation: 'spin 1s linear infinite' }}
|
||||
/>
|
||||
<S.Spinner size={16} />
|
||||
Connecting...
|
||||
</>
|
||||
) : (
|
||||
|
|
@ -687,7 +705,9 @@ const ConnectNodePage: React.FC = () => {
|
|||
</S.Btn>
|
||||
</S.Form>
|
||||
<S.NewSessionBtn onClick={() => setShowNewSession(true)}>
|
||||
<Key size={13} />
|
||||
Use a new pairing phrase instead
|
||||
<ChevronRight size={13} style={{ marginLeft: 2 }} />
|
||||
</S.NewSessionBtn>
|
||||
</S.ReconnectPanel>
|
||||
) : (
|
||||
|
|
@ -723,10 +743,7 @@ const ConnectNodePage: React.FC = () => {
|
|||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2
|
||||
size={16}
|
||||
style={{ animation: 'spin 1s linear infinite' }}
|
||||
/>
|
||||
<S.Spinner size={16} />
|
||||
Connecting...
|
||||
</>
|
||||
) : (
|
||||
|
|
@ -736,15 +753,8 @@ const ConnectNodePage: React.FC = () => {
|
|||
</S.Form>
|
||||
)}
|
||||
|
||||
<S.SecurityNote>
|
||||
<Shield size={12} />
|
||||
End-to-end encrypted. No keys or credentials leave your node.
|
||||
</S.SecurityNote>
|
||||
|
||||
{showLncForm && (
|
||||
<>
|
||||
<S.Divider>need a pairing phrase?</S.Divider>
|
||||
|
||||
<S.HelpToggle onClick={() => setHelpOpen(!helpOpen)}>
|
||||
<HelpCircle size={13} />
|
||||
How to get your pairing phrase
|
||||
|
|
@ -810,8 +820,8 @@ const ConnectNodePage: React.FC = () => {
|
|||
)}
|
||||
</S.SmallCopyBtn>
|
||||
</S.CommandBlock>
|
||||
Copy the <strong>10-word pairing phrase</strong> from the
|
||||
output.
|
||||
Copy the <strong>10-word pairing phrase</strong> from
|
||||
the output.
|
||||
</>
|
||||
)}
|
||||
</S.AccordionBody>
|
||||
|
|
@ -848,7 +858,7 @@ const ConnectNodePage: React.FC = () => {
|
|||
<S.Btn type="submit" disabled={loading || !password.trim()}>
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2 size={16} style={{ animation: 'spin 1s linear infinite' }} />
|
||||
<S.Spinner size={16} />
|
||||
Connecting...
|
||||
</>
|
||||
) : (
|
||||
|
|
@ -862,13 +872,12 @@ const ConnectNodePage: React.FC = () => {
|
|||
</S.SecurityNote>
|
||||
</>
|
||||
)}
|
||||
</S.Content>
|
||||
|
||||
<S.Footer>
|
||||
<S.FooterLink onClick={() => store.appView.goTo('/get-node')}>
|
||||
Don't have a node? Get one →
|
||||
<Server size={13} />
|
||||
Don't have a node? Get one
|
||||
<ChevronRight size={13} style={{ marginLeft: 2 }} />
|
||||
</S.FooterLink>
|
||||
</S.Footer>
|
||||
</S.Content>
|
||||
</S.Wrapper>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -143,6 +143,25 @@ export default class AuthStore {
|
|||
});
|
||||
}
|
||||
|
||||
private _withTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise<T> {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
const timer = setTimeout(
|
||||
() => reject(new Error(`${label} timed out after ${ms / 1000}s`)),
|
||||
ms,
|
||||
);
|
||||
promise.then(
|
||||
v => {
|
||||
clearTimeout(timer);
|
||||
resolve(v);
|
||||
},
|
||||
e => {
|
||||
clearTimeout(timer);
|
||||
reject(e);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect via LNC pairing phrase (first time)
|
||||
*/
|
||||
|
|
@ -156,9 +175,35 @@ export default class AuthStore {
|
|||
password,
|
||||
});
|
||||
|
||||
await lnc.connect();
|
||||
try {
|
||||
await this._withTimeout(lnc.connect(), 30_000, 'Connection');
|
||||
} catch (err: any) {
|
||||
const msg = err?.message || '';
|
||||
if (msg.includes('timed out')) {
|
||||
try {
|
||||
lnc.disconnect();
|
||||
} catch {
|
||||
/* best-effort cleanup */
|
||||
}
|
||||
throw new Error(
|
||||
'Connection timed out. Make sure litd is running on your node and try a fresh pairing phrase.',
|
||||
);
|
||||
}
|
||||
if (
|
||||
msg.includes('WASM') ||
|
||||
msg.includes('proxy') ||
|
||||
msg.includes('stream not found')
|
||||
) {
|
||||
throw new Error(
|
||||
'Could not reach your node. Make sure litd is running and the pairing phrase hasn\u2019t already been used. Generate a new one with: litcli sessions add --label="web" --type admin',
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
if (!lnc.isConnected) {
|
||||
throw new Error('Failed to establish LNC connection');
|
||||
throw new Error(
|
||||
'Connection failed. Verify litd is running and reachable, then try a fresh pairing phrase.',
|
||||
);
|
||||
}
|
||||
|
||||
const lncApi = new LncApi(lnc);
|
||||
|
|
@ -168,13 +213,18 @@ export default class AuthStore {
|
|||
});
|
||||
|
||||
try {
|
||||
await this.validate();
|
||||
await this._withTimeout(this.validate(), 15_000, 'Validation');
|
||||
LncApi.markPaired();
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
lnc.disconnect();
|
||||
runInAction(() => {
|
||||
this.isLnc = false;
|
||||
});
|
||||
if (error?.message?.includes('timed out')) {
|
||||
throw new Error(
|
||||
'Connected to node but verification timed out. Your node may be slow to respond — try again.',
|
||||
);
|
||||
}
|
||||
throw new Error(
|
||||
'Connected but could not verify node access. Check your pairing phrase.',
|
||||
);
|
||||
|
|
@ -190,9 +240,34 @@ export default class AuthStore {
|
|||
|
||||
const lnc = new LNC({ password });
|
||||
|
||||
await lnc.connect();
|
||||
try {
|
||||
await this._withTimeout(lnc.connect(), 12_000, 'Reconnection');
|
||||
} catch (err: any) {
|
||||
try {
|
||||
lnc.disconnect();
|
||||
} catch {
|
||||
/* best-effort cleanup */
|
||||
}
|
||||
LncApi.clearPaired();
|
||||
const msg = err?.message || '';
|
||||
if (
|
||||
msg.includes('timed out') ||
|
||||
msg.includes('stream not found') ||
|
||||
msg.includes('WASM') ||
|
||||
msg.includes('proxy') ||
|
||||
msg.includes('closed network')
|
||||
) {
|
||||
throw new Error(
|
||||
'Session expired. Please connect with a new pairing phrase.',
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
if (!lnc.isConnected) {
|
||||
throw new Error('Failed to reconnect via LNC');
|
||||
LncApi.clearPaired();
|
||||
throw new Error(
|
||||
'Session expired. Please connect with a new pairing phrase.',
|
||||
);
|
||||
}
|
||||
|
||||
const lncApi = new LncApi(lnc);
|
||||
|
|
@ -201,7 +276,7 @@ export default class AuthStore {
|
|||
this.isLnc = true;
|
||||
});
|
||||
|
||||
await this.validate();
|
||||
await this._withTimeout(this.validate(), 15_000, 'Validation');
|
||||
}
|
||||
|
||||
/** Disconnect LNC and clear pairing data */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue