CI / Build & Quality Checks (push) Skipped
CI / Secret scan (gitleaks) (push) Skipped
CI / Playwright smoke (e2e) (push) Skipped
CI / Build & Quality Checks (pull_request) Successful in 3m50s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 12s
CI / Playwright smoke (e2e) (pull_request) Successful in 20m30s
On the desktop app with a working keychain (Windows Credential Manager), the tokens leave localStorage: it keeps a token-less shell blob (tokensIn: keychain) and the full session lives in memory, read from the keychain at boot before anything reads the session, so getFallbackSession stays synchronous. Web, Linux and older desktop builds are unchanged. - A new session (login, token refresh) is written to localStorage in full first; the mirror verifies the keychain copy and only then strips the tokens, so one durable store always holds the newest tokens. - Existing installs migrate on first boot the same way; a failing keychain write keeps the tokens in localStorage. - If a shell is there but the keychain entry is missing or unreadable, the app shows Retry / Sign in again instead of logging out, and the mirror is not started (it would clear the entry). Fixes #105 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
70 lines
2.1 KiB
TypeScript
70 lines
2.1 KiB
TypeScript
import React, { useState } from 'react';
|
||
import { Box, Button, Text, config } from 'folds';
|
||
import { removeFallbackSession } from '../state/sessions';
|
||
import type { KeychainBootResult } from '../state/keychainSession';
|
||
import type { KeychainInvoke } from '../state/keychainMirror';
|
||
|
||
/**
|
||
* [Gitea #105] Shown instead of the app when the desktop app's sign-in is kept
|
||
* in the OS keychain but couldn't be read this time. Never a silent logout:
|
||
* Retry boots again; signing in again is an explicit choice.
|
||
*/
|
||
export function KeychainUnavailable({
|
||
boot,
|
||
invoke,
|
||
}: {
|
||
boot: KeychainBootResult;
|
||
invoke: KeychainInvoke | undefined;
|
||
}) {
|
||
const [busy, setBusy] = useState(false);
|
||
const missing = boot.mode === 'missing';
|
||
const signInAgain = async () => {
|
||
setBusy(true);
|
||
removeFallbackSession();
|
||
await invoke?.('secure_session_clear').catch(() => undefined);
|
||
window.location.reload();
|
||
};
|
||
return (
|
||
<Box
|
||
direction="Column"
|
||
alignItems="Center"
|
||
justifyContent="Center"
|
||
gap="400"
|
||
style={{ minHeight: '100vh', padding: config.space.S700, textAlign: 'center' }}
|
||
>
|
||
<Text size="H3">Couldn't read your sign-in</Text>
|
||
<Text size="T300" style={{ maxWidth: 460 }}>
|
||
{missing
|
||
? 'Lotus Chat keeps your sign-in in Windows Credential Manager, but the entry is missing. If you removed it, sign in again.'
|
||
: 'Lotus Chat keeps your sign-in in Windows Credential Manager and couldn’t read it just now. This is usually temporary.'}
|
||
</Text>
|
||
{boot.mode === 'error' && (
|
||
<Text size="T200" priority="300">
|
||
{boot.error}
|
||
</Text>
|
||
)}
|
||
<Box gap="200">
|
||
<Button
|
||
variant="Primary"
|
||
size="400"
|
||
radii="300"
|
||
disabled={busy}
|
||
onClick={() => window.location.reload()}
|
||
>
|
||
<Text size="B400">Retry</Text>
|
||
</Button>
|
||
<Button
|
||
variant="Secondary"
|
||
fill="Soft"
|
||
size="400"
|
||
radii="300"
|
||
disabled={busy}
|
||
onClick={signInAgain}
|
||
>
|
||
<Text size="B400">Sign in again</Text>
|
||
</Button>
|
||
</Box>
|
||
</Box>
|
||
);
|
||
}
|