Every response already funnels through runtimeFetch, so a classifier there spots 401s, confirms them against /auth/session (a proxied provider 401 must not read as a logout), and flips a small auth-session store. The web and hosted surfaces show a frosted banner under the header whose Log in button hands off to the session gate's existing unlock flow; sends are paused while expired, the session-load error screen explains the auth case and retries itself after login, and returning to a long-idle window revalidates once via visibility/focus. Native mobile feeds the same signal into its connection re-probe instead of showing the banner; VS Code is exempt.
42 lines
1.7 KiB
TypeScript
42 lines
1.7 KiB
TypeScript
import React from 'react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Icon } from '@/components/icon/Icon';
|
|
import { useI18n } from '@/lib/i18n';
|
|
import { useAuthSessionStore } from '@/lib/runtime-auth-expiry';
|
|
|
|
/**
|
|
* Non-blocking notice that the OpenChamber session expired mid-work. It never
|
|
* takes the screen on its own: work stays visible and interactive, and only
|
|
* the explicit "Log in" click hands control to the session gate's full login
|
|
* flow (password, passkey, desktop shell — all already there).
|
|
*/
|
|
export const AuthExpiredBanner: React.FC = () => {
|
|
const { t } = useI18n();
|
|
const authState = useAuthSessionStore((store) => store.state);
|
|
const markReauthenticating = useAuthSessionStore((store) => store.markReauthenticating);
|
|
|
|
if (authState !== 'expired') {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
// Below the header on purpose: the header row can be a window-drag region
|
|
// on desktop, where nothing under the cursor is clickable.
|
|
<div
|
|
className="pointer-events-none fixed inset-x-0 z-[200] flex justify-center px-4"
|
|
style={{ top: 'calc(var(--oc-header-height, 56px) + 8px)' }}
|
|
>
|
|
<div
|
|
role="alert"
|
|
className="oc-glass-popover oc-glass-floating pointer-events-auto flex items-center gap-3 rounded-lg px-3 py-2"
|
|
>
|
|
<Icon name="lock" className="size-4 flex-shrink-0" style={{ color: 'var(--status-error)' }} />
|
|
<span className="typography-ui-label text-foreground">{t('sessionAuth.expired.banner')}</span>
|
|
<Button size="xs" variant="outline" onClick={markReauthenticating} className="normal-case">
|
|
{t('sessionAuth.expired.loginAction')}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|