import { useEffect, useRef } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { AlertTriangle, Check, Loader2, RefreshCw, ShieldCheck, X, Youtube } from "lucide-react"; import { api, type Me } from "../lib/api"; import { beginGrant, endOnboarding } from "../lib/onboarding"; // A first-login wizard that walks the user through granting YouTube access one scope at a // time, each with a plain explanation and an up-front heads-up about Google's "unverified // app" screen. Sign-in itself only asks for name/email (no warning); YouTube access is // always an explicit, informed opt-in here. // // The visible step is derived from what's already granted, so the wizard resumes correctly // after each redirect: no read -> "read", read but no write -> "write", both -> "done". // Right after read is granted, it auto-imports the user's subscriptions so they land on a // populated feed (channels already in the shared catalog appear instantly). // A small reusable heads-up so the Google consent warning never feels like a surprise. function ConsentHeadsUp() { return (

Google will show a "Google hasn't verified this app"{" "} screen — that's expected, because Siftlode hasn't gone through Google's full review. It's safe to continue: click Advanced →{" "} Go to Siftlode. You can revoke access anytime from your{" "} Google Account .

); } function Dots({ index, total }: { index: number; total: number }) { return (
{Array.from({ length: total }).map((_, i) => ( ))}
); } export default function OnboardingWizard({ me, onClose }: { me: Me; onClose: () => void }) { const qc = useQueryClient(); const importTriggered = useRef(false); // Once we have read access, find out whether any subscriptions are imported yet. const myStatus = useQuery({ queryKey: ["my-status"], queryFn: api.myStatus, enabled: me.can_read, }); const importSubs = useMutation({ mutationFn: () => api.syncSubscriptions(), onSuccess: () => { // Feed/channels/status now have content — refresh the app behind the wizard. qc.invalidateQueries({ queryKey: ["feed"] }); qc.invalidateQueries({ queryKey: ["feed-count"] }); qc.invalidateQueries({ queryKey: ["channels"] }); qc.invalidateQueries({ queryKey: ["my-status"] }); }, }); // First time we see read access with zero imported channels, pull the user's // subscriptions automatically so they don't have to hunt for a button. useEffect(() => { if ( me.can_read && myStatus.data && myStatus.data.channels_total === 0 && !importTriggered.current ) { importTriggered.current = true; importSubs.mutate(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [me.can_read, myStatus.data?.channels_total]); // Closing without finishing the essential read step suppresses future auto-popups // (it stays reachable from Settings). Once read is granted, closing is just "done". const close = () => { endOnboarding(!me.can_read); onClose(); }; useEffect(() => { function onKey(e: KeyboardEvent) { if (e.key === "Escape") close(); } document.addEventListener("keydown", onKey); return () => document.removeEventListener("keydown", onKey); // eslint-disable-next-line react-hooks/exhaustive-deps }, [me.can_read]); // Are we currently importing (or about to)? Show a progress screen instead of the // "you're connected" step until the first import settles. const importing = me.can_read && !importSubs.isError && (importSubs.isPending || (importTriggered.current && !importSubs.isSuccess) || (myStatus.data?.channels_total === 0 && !importSubs.isSuccess)); const step = !me.can_read ? "read" : !me.can_write ? "write" : "done"; const stepIndex = step === "read" ? 0 : step === "write" ? 1 : 2; return (
{step === "read" && ( <>

Connect your YouTube subscriptions

You're signed in. To build your feed, Siftlode needs{" "} read-only access to the channels you're subscribed to on YouTube. It never posts, deletes, or changes anything with this.

)} {/* Read granted: show import progress before the optional write step. */} {me.can_read && importing && ( <>

Building your feed…

Importing your YouTube subscriptions. Channels already in Siftlode show up instantly; any new ones fill in automatically in the background.

)} {step === "write" && !importing && ( <>

You're connected

{importSubs.isError ? ( <>Your YouTube account is connected, but importing subscriptions failed — you can retry from Settings → Sync. ) : ( <> Your feed is ready {myStatus.data ? ` (${myStatus.data.channels_total} channels)` : ""}. Optionally, let Siftlode{" "} )} {!importSubs.isError && ( <> unsubscribe from channels on YouTube for you — this needs an extra write permission. Skip it to stay read-only; you can always enable it later in Settings. )}

{importSubs.isError && ( )}
)} {step === "done" && !importing && ( <>

All set

Read and unsubscribe access are both granted. You can review or revoke either one anytime in Settings → Account, or from your Google Account.

)}
); }