feat(onboarding): first-login wizard for incremental YouTube consent

After the clean name/email sign-in, a wizard walks the user through granting
YouTube read (then optionally write) one step at a time, each with a plain
rationale and an up-front heads-up about Google's "unverified app" screen.

The visible step is derived from the granted scopes (can_read/can_write) so the
flow resumes correctly across the full-page consent redirect; it's dismissible
and reopenable from Settings -> Account, which now lists read and write as
separate, individually-grantable access rows.
This commit is contained in:
npeter83 2026-06-14 01:11:29 +02:00
parent 580d7f0e0d
commit 5215765b51
5 changed files with 302 additions and 29 deletions

View file

@ -24,6 +24,8 @@ import Feed from "./components/Feed";
import Channels from "./components/Channels";
import Stats from "./components/Stats";
import SettingsPanel from "./components/SettingsPanel";
import OnboardingWizard from "./components/OnboardingWizard";
import { shouldAutoOpenOnboarding } from "./lib/onboarding";
import Toaster from "./components/Toaster";
const DEFAULT_FILTERS: FeedFilters = {
@ -61,6 +63,7 @@ export default function App() {
const [sidebarLayout, setSidebarLayoutState] = useState<SidebarLayout>(loadLayout);
const [page, setPageState] = useState<Page>(readPage);
const [settingsOpen, setSettingsOpen] = useState(false);
const [wizardOpen, setWizardOpen] = useState(false);
function setFilters(next: FeedFilters) {
setFiltersState(next);
@ -86,6 +89,13 @@ export default function App() {
const meQuery = useQuery({ queryKey: ["me"], queryFn: api.me });
// First-login onboarding: prompt the user to connect YouTube (and resume the flow after
// each consent redirect). Derived from granted scopes + storage flags so it's stable
// across the full-page OAuth round-trip; dismissible and reopenable from Settings.
useEffect(() => {
if (meQuery.data && shouldAutoOpenOnboarding(meQuery.data)) setWizardOpen(true);
}, [meQuery.data?.id, meQuery.data?.can_read, meQuery.data?.can_write]);
// On login, adopt server-stored preferences.
useEffect(() => {
const prefs = meQuery.data?.preferences;
@ -181,8 +191,15 @@ export default function App() {
view={view}
setView={changeView}
onClose={() => setSettingsOpen(false)}
onOpenWizard={() => {
setSettingsOpen(false);
setWizardOpen(true);
}}
/>
)}
{wizardOpen && meQuery.data && (
<OnboardingWizard me={meQuery.data} onClose={() => setWizardOpen(false)} />
)}
<Toaster />
</div>
);