[eric] signin: live port hand-off, stalled-wait retry, openswarm:// registered (ENG-381)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01En8dRGsJPLrJCQBEkTH4Mp
This commit is contained in:
ciregenz
2026-08-20 22:22:48 -07:00
co-authored by Claude Fable 5
parent 3c24c10cbd
commit fa9196c8af
5 changed files with 35 additions and 10 deletions
+8
View File
@@ -31,6 +31,14 @@
"electron-builder-squirrel-windows": "^26.8.1"
},
"build": {
"protocols": [
{
"name": "OpenSwarm",
"schemes": [
"openswarm"
]
}
],
"appId": "com.clusterlabs.openswarm",
"productName": "OpenSwarm",
"afterPack": "./build/after-pack.js",
@@ -5,7 +5,7 @@ import EmailIcon from '@mui/icons-material/Email';
import CheckRoundedIcon from '@mui/icons-material/CheckRounded';
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
import { fetchSettings } from '@/shared/state/settingsSlice';
import { OPENSWARM_DEFAULT_PROXY_URL } from '@/shared/config';
import { OPENSWARM_DEFAULT_PROXY_URL, getBackendPort } from '@/shared/config';
import { report } from '@/shared/serviceClient';
import type { ClaudeTokens } from '@/shared/styles/claudeTokens';
import SignInDialog from '@/app/components/overlays/SignInDialog';
@@ -17,6 +17,8 @@ import { googleStartUrl } from '@/shared/googleStartUrl';
// setup are tied to a real account. Google hands off through the external browser and lands out-of-band
// (the cloud page POSTs the bearer to the local backend), so we poll settings until user_id appears.
// Email reuses the proven SignInDialog (magic-link 6-digit code) on top of the beat.
const GOOGLE_STALL_MS = 60_000;
const BeatSignIn: React.FC<{
c: ClaudeTokens;
onNext: () => void;
@@ -28,6 +30,7 @@ const BeatSignIn: React.FC<{
const proxyUrl = useAppSelector((s) => s.settings.data.openswarm_proxy_url || OPENSWARM_DEFAULT_PROXY_URL);
const installId = useAppSelector((s) => s.settings.data.installation_id ?? '');
const [waitingGoogle, setWaitingGoogle] = useState(false);
const [stalled, setStalled] = useState(false);
const [notReady, setNotReady] = useState(false);
const [emailOpen, setEmailOpen] = useState(false);
const signedIn = !!userId;
@@ -38,11 +41,17 @@ const BeatSignIn: React.FC<{
return () => window.clearInterval(id);
}, [signedIn, waitingGoogle, dispatch]);
// The hand-off can die silently (browser blocked the localhost POST, wrong port, the tab was closed), and "Waiting for your browser" forever was the whole bug report. Past a minute, say so, offer the retry, and record it.
useEffect(() => {
if (signedIn || !waitingGoogle || stalled) return undefined;
const id = window.setTimeout(() => { setStalled(true); report('signin', 'google_handoff_stalled'); }, GOOGLE_STALL_MS);
return () => window.clearTimeout(id);
}, [signedIn, waitingGoogle, stalled]);
const onGoogle = (): void => {
if (signedIn) return;
report('signin', 'google_clicked');
const localPort = (window as unknown as { __OPENSWARM_PORT__?: number }).__OPENSWARM_PORT__ || 8324;
const startUrl = googleStartUrl(proxyUrl, installId, localPort);
report('signin', stalled ? 'google_retry_clicked' : 'google_clicked');
const startUrl = googleStartUrl(proxyUrl, installId, getBackendPort());
// First run is exactly when settings may not have landed yet, so this beat is the likeliest
// place to catch a not-ready install. Say so on the row instead of opening a broken page.
if (!startUrl) { setNotReady(true); return; }
@@ -50,11 +59,12 @@ const BeatSignIn: React.FC<{
const api = (window as unknown as { openswarm?: { openExternal?: (u: string) => void } }).openswarm;
if (api?.openExternal) api.openExternal(startUrl);
else window.open(startUrl, '_blank');
setStalled(false);
setWaitingGoogle(true);
};
const rows: Array<{ id: string; name: string; icon: React.ReactNode; onClick: () => void; hint?: string }> = [
{ id: 'google', name: 'Continue with Google', icon: <GoogleIcon sx={{ fontSize: 20, color: '#4285F4' }} />, onClick: onGoogle, hint: notReady ? 'Still starting up, try again in a second' : (waitingGoogle && !signedIn ? 'Waiting for your browser...' : undefined) },
{ id: 'google', name: 'Continue with Google', icon: <GoogleIcon sx={{ fontSize: 20, color: '#4285F4' }} />, onClick: onGoogle, hint: notReady ? 'Still starting up, try again in a second' : stalled ? 'Still waiting. Click to open the sign-in page again' : (waitingGoogle && !signedIn ? 'Waiting for your browser...' : undefined) },
{ id: 'email', name: 'Continue with email', icon: <EmailIcon sx={{ fontSize: 20, color: '#6f6e6a' }} />, onClick: () => { if (!signedIn) setEmailOpen(true); } },
];
@@ -17,7 +17,7 @@ import CloseIcon from '@mui/icons-material/Close';
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
import { activateSignin, fetchSettings } from '@/shared/state/settingsSlice';
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
import { OPENSWARM_DEFAULT_PROXY_URL } from '@/shared/config';
import { OPENSWARM_DEFAULT_PROXY_URL, getBackendPort } from '@/shared/config';
import { report } from '@/shared/serviceClient';
import { googleStartUrl } from '@/shared/googleStartUrl';
@@ -50,7 +50,7 @@ export default function SignInDialog({ onClose, initialStage = 'choose', mandato
const onGoogle = () => {
report('signin', 'google_clicked');
const localPort = (window as any).__OPENSWARM_PORT__ || 8324;
const localPort = getBackendPort();
const startUrl = googleStartUrl(cloudBase, installId, localPort);
if (!startUrl) {
setErrMsg('Still finishing startup. Give it a second and try again.');
@@ -116,7 +116,7 @@ export default function SignInDialog({ onClose, initialStage = 'choose', mandato
setBusy(true);
try {
report('signin', 'email_verify_submitted');
const localPort = (window as any).__OPENSWARM_PORT__ || 8324;
const localPort = getBackendPort();
const res = await fetch(`${cloudBase}/api/auth/email/verify`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -5,7 +5,7 @@ import Button from '@mui/material/Button';
import CircularProgress from '@mui/material/CircularProgress';
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
import { signOut } from '@/shared/state/settingsSlice';
import { OPENSWARM_DEFAULT_PROXY_URL } from '@/shared/config';
import { OPENSWARM_DEFAULT_PROXY_URL, getBackendPort } from '@/shared/config';
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
import SignInDialog from '@/app/components/overlays/SignInDialog';
import { googleStartUrl } from '@/shared/googleStartUrl';
@@ -46,7 +46,7 @@ const AccountCard: React.FC = () => {
const onSignIn = () => {
// Pass local_port so the bearer-handoff page POSTs to the right backend (Electron binds in 8324..8424).
const localPort = (window as any).__OPENSWARM_PORT__ || 8324;
const localPort = getBackendPort();
const startUrl = googleStartUrl(proxyUrl, installId, localPort);
// No error surface on this card, so the honest move is to do nothing visible rather than send
// them to a black cloud error page. Settings land within a second and the click then works.
+7
View File
@@ -10,6 +10,13 @@ const port =
8324;
const host = window.location.hostname || 'localhost';
/** The backend port as of NOW. Prefer this over the boot-time snapshot when the number leaves the app (the Google sign-in hand-off POSTs the bearer back to it): preload captures `__OPENSWARM_PORT__` once, and a slow first-run port scan leaves it null, which used to send the cloud to a bare 8324 nobody was listening on. */
export function getBackendPort(): number {
const ow = _w.openswarm;
const live = ow && typeof ow.getBackendPortLive === 'function' ? ow.getBackendPortLive() : null;
return typeof live === 'number' && live > 0 ? live : port;
}
export const API_BASE = `http://${host}:${port}/api`;
export const WS_BASE = `ws://${host}:${port}`;
// Must match openswarm-cloud's PUBLIC_BASE_URL (fly.toml) and the Google OAuth redirect URI.