flow / src /components /SignInGate.tsx
mfn
Share: permanent links (?w=) that open as your own copy; sign in with Hugging Face to share
aab2748
Raw History Blame Contribute Delete
5.16 kB
import { Button } from "@cloudflare/kumo";
import { ArrowSquareOutIcon, ArrowClockwiseIcon, SpinnerGapIcon } from "@phosphor-icons/react";
import flowMark from "../assets/flow-mark.svg";
import hfLogo from "../assets/hf-logo.svg";
import { useEffect, useState } from "react";
import { pendingShare } from "../lib/deepLink";
import { isEmbedded, newTabUrl } from "../lib/hfAuth";
import { fetchShare } from "../lib/share";
import { useAuthStore } from "../store/useAuthStore";
import { useSyncStore } from "../store/useSyncStore";
const LOGO = <img src={hfLogo} alt="" aria-hidden className="h-[18px] w-[18px]" draggable={false} />;
/** Someone opened a share link while signed out: say what it is before asking them to sign in. */
function SharePreview() {
const [info, setInfo] = useState<{ owner: string; title: string } | null>(null);
useEffect(() => {
const id = pendingShare();
if (!id) return;
let live = true;
void fetchShare(id)
.then((s) => live && setInfo({ owner: s.owner, title: s.title }))
.catch(() => undefined);
return () => {
live = false;
};
}, []);
if (!info) return null;
return (
<p className="gate__msg gate__share">
<strong>@{info.owner}</strong> shared “{info.title}” with you. Sign in to open your own copy.
</p>
);
}
/**
* Everything before the editor: signed out, signing in (embedded: in a new tab), and loading the
* account's storage. Flow keeps workflows only in the user's Hugging Face bucket, so there is no
* signed-out editor.
*/
export function SignInGate() {
const user = useAuthStore((s) => s.user);
const busy = useAuthStore((s) => s.busy);
const configured = useAuthStore((s) => s.configured);
const pending = useAuthStore((s) => s.pending);
const error = useAuthStore((s) => s.error);
const bootError = useSyncStore((s) => s.bootError);
const auth = useAuthStore.getState();
let body: React.ReactNode;
if (user && bootError) {
body = (
<>
<p className="gate__msg" role="alert">
{bootError}
</p>
<div className="gate__actions">
<Button variant="primary" icon={ArrowClockwiseIcon} onClick={() => void useSyncStore.getState().boot()}>
Retry
</Button>
<Button variant="secondary" onClick={() => auth.login()}>
Sign in again
</Button>
</div>
<button type="button" className="gate__link" onClick={() => auth.logout()}>
Sign out
</button>
</>
);
} else if (user || busy) {
body = (
<p className="gate__msg gate__loading" aria-live="polite">
<SpinnerGapIcon size={16} className="animate-spin" />
{user ? "Loading your workflows…" : "Signing in…"}
</p>
);
} else if (!configured) {
body = <p className="gate__msg">Sign-in isn't configured for this deployment (no OAuth client id).</p>;
} else if (pending === "tab") {
body = (
<>
<p className="gate__msg">
<strong>Signed in in the new tab?</strong> Continue there: it's the full app, and your work saves to your account either way.
</p>
<div className="gate__actions">
<Button variant="primary" icon={ArrowClockwiseIcon} onClick={() => void auth.finishLogin()}>
Use it here too
</Button>
<Button variant="secondary" icon={ArrowSquareOutIcon} onClick={() => auth.openApp()}>
Open Flow in a new tab
</Button>
</div>
{error && (
<p className="gate__err" role="alert">
{error}
</p>
)}
<p className="gate__fine">
Didn't see a tab?{" "}
<a className="gate__link" href={newTabUrl()} target="_blank" rel="noopener">
Open the sign-in tab
</a>{" "}
·{" "}
<button type="button" className="gate__link" onClick={() => auth.cancelLogin()}>
Cancel
</button>
</p>
</>
);
} else {
body = (
<>
<Button variant="primary" className="gate__cta" icon={LOGO} onClick={() => auth.login()}>
Sign in with Hugging Face
</Button>
{error && (
<p className="gate__err" role="alert">
{error}
</p>
)}
<p className="gate__fine">
{isEmbedded ? "Opens Flow in a new tab to sign in. " : ""}Flow is still in beta and hasn't launched yet. It's an independent tool, not made by Hugging Face. Runs use your own quota; workflows save to a private bucket on your account.
</p>
</>
);
}
return (
<div className="gate">
<div className="gate__card">
<div className="gate__brand">
<img src={flowMark} alt="" aria-hidden className="h-7 w-7" draggable={false} />
<h1 className="gate__title">Flow</h1>
<span className="beta-tag">Beta</span>
</div>
<p className="gate__lede">A visual editor for chaining Hugging Face Spaces into workflows and running them from one canvas.</p>
{!user && <SharePreview />}
{body}
</div>
</div>
);
}