fix: surface expired Composio auth state (#1893)

Co-authored-by: honor2030 <19909783+honor2030@users.noreply.github.com>
Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
This commit is contained in:
이민재
2026-05-16 20:41:20 -07:00
committed by GitHub
co-authored by honor2030 Steven Enamakel
parent c8cd15be91
commit 9400f77364
9 changed files with 152 additions and 14 deletions
@@ -223,6 +223,23 @@ describe('<ComposioConnectModal>', () => {
expect(screen.queryByText('(oxox)')).not.toBeInTheDocument();
});
it('shows an expired-auth recovery state with a reconnect CTA', () => {
const connection: ComposioConnection = {
id: 'ca_expired',
toolkit: 'gmail',
status: 'EXPIRED',
};
render(
<ComposioConnectModal toolkit={mockToolkit} connection={connection} onClose={() => {}} />
);
expect(screen.getByText(/Gmail authorization expired/i)).toBeInTheDocument();
expect(screen.getByText(/Reconnect to re-enable Gmail tools/i)).toBeInTheDocument();
expect(screen.getByRole('button', { name: /Reconnect Gmail/i })).toBeInTheDocument();
expect(screen.queryByText(/ca_expired/)).not.toBeInTheDocument();
});
// ── Connect flow → openUrl(connectUrl) ───────────────────────────
//
// Verifies the end-to-end OAuth handoff plumbing for #1710:
@@ -127,6 +127,7 @@ type Phase =
| 'authorizing'
| 'waiting'
| 'connected'
| 'expired'
| 'disconnecting'
| 'error';
@@ -156,8 +157,15 @@ export default function ComposioConnectModal({
const initialState = deriveComposioState(connection);
const initiallyConnected = initialState === 'connected';
const initiallyExpired = initialState === 'expired';
const [phase, setPhase] = useState<Phase>(
initiallyConnected ? 'connected' : initialState === 'pending' ? 'waiting' : 'idle'
initiallyConnected
? 'connected'
: initiallyExpired
? 'expired'
: initialState === 'pending'
? 'waiting'
: 'idle'
);
const [error, setError] = useState<string | null>(null);
const [connectUrl, setConnectUrl] = useState<string | null>(null);
@@ -255,6 +263,12 @@ export default function ComposioConnectModal({
setError(`Connection failed (status: ${hit.status}).`);
return;
}
if (state === 'expired') {
stopPolling();
setPhase('expired');
setError(null);
return;
}
}
} catch (err) {
// Swallow transient errors during polling — we'll retry on next tick.
@@ -481,7 +495,12 @@ export default function ComposioConnectModal({
if (e.target === e.currentTarget) onClose();
};
const headerTitle = phase === 'connected' ? `Manage ${toolkit.name}` : `Connect ${toolkit.name}`;
const headerTitle =
phase === 'connected'
? `Manage ${toolkit.name}`
: phase === 'expired'
? `Reconnect ${toolkit.name}`
: `Connect ${toolkit.name}`;
const modalContent = (
<div
@@ -653,6 +672,27 @@ export default function ComposioConnectModal({
</>
)}
{phase === 'expired' && (
<>
<div className="rounded-xl border border-coral-200 bg-coral-50 p-3">
<div className="flex items-center gap-2 text-sm font-medium text-coral-800">
<div className="w-2 h-2 rounded-full bg-coral-500" />
{toolkit.name} authorization expired
</div>
<p className="mt-2 text-xs leading-relaxed text-coral-700">
Reconnect to re-enable {toolkit.name} tools. OpenHuman will keep this integration
unavailable until you refresh OAuth access.
</p>
</div>
<button
type="button"
onClick={() => void handleConnect()}
className="w-full rounded-xl bg-primary-500 text-white text-sm font-medium py-2.5 hover:bg-primary-600 transition-colors">
Reconnect {toolkit.name}
</button>
</>
)}
{phase === 'connected' && (
<>
<div className="flex items-center gap-2 text-sm text-sage-700">
@@ -706,7 +746,9 @@ export default function ComposioConnectModal({
<button
type="button"
onClick={() => {
setPhase(initiallyConnected ? 'connected' : 'idle');
setPhase(
initiallyConnected ? 'connected' : initiallyExpired ? 'expired' : 'idle'
);
setError(null);
}}
className="w-full rounded-xl border border-stone-200 bg-white text-stone-700 text-sm font-medium py-2 hover:bg-stone-50 transition-colors">