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:
parent
580d7f0e0d
commit
5215765b51
5 changed files with 302 additions and 29 deletions
|
|
@ -29,6 +29,7 @@ export default function SettingsPanel({
|
|||
view,
|
||||
setView,
|
||||
onClose,
|
||||
onOpenWizard,
|
||||
}: {
|
||||
me: Me;
|
||||
theme: ThemePrefs;
|
||||
|
|
@ -36,6 +37,7 @@ export default function SettingsPanel({
|
|||
view: "grid" | "list";
|
||||
setView: (v: "grid" | "list") => void;
|
||||
onClose: () => void;
|
||||
onOpenWizard: () => void;
|
||||
}) {
|
||||
const [tab, setTab] = useState<TabId>("appearance");
|
||||
const [closing, setClosing] = useState(false);
|
||||
|
|
@ -115,7 +117,7 @@ export default function SettingsPanel({
|
|||
)}
|
||||
{t.id === "notifications" && <Notifications />}
|
||||
{t.id === "sync" && <Sync me={me} />}
|
||||
{t.id === "account" && <Account me={me} />}
|
||||
{t.id === "account" && <Account me={me} onOpenWizard={onOpenWizard} />}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -464,7 +466,46 @@ function Sync({ me }: { me: Me }) {
|
|||
);
|
||||
}
|
||||
|
||||
function Account({ me }: { me: Me }) {
|
||||
function AccessRow({
|
||||
title,
|
||||
granted,
|
||||
grantedHint,
|
||||
enableHint,
|
||||
onEnable,
|
||||
}: {
|
||||
title: string;
|
||||
granted: boolean;
|
||||
grantedHint: string;
|
||||
enableHint: string;
|
||||
onEnable: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-start justify-between gap-3 py-2">
|
||||
<div className="min-w-0">
|
||||
<div className="text-sm font-medium">{title}</div>
|
||||
<p className="text-xs text-muted leading-relaxed mt-0.5">
|
||||
{granted ? grantedHint : enableHint}
|
||||
</p>
|
||||
</div>
|
||||
{granted ? (
|
||||
<span className="shrink-0 text-[11px] px-2 py-1 rounded-full border border-accent/40 text-accent">
|
||||
Granted
|
||||
</span>
|
||||
) : (
|
||||
<Tooltip hint="Redirects to Google to grant the permission. Google shows an 'unverified app' notice — that's expected; click Advanced → Go to Subfeed.">
|
||||
<button
|
||||
onClick={onEnable}
|
||||
className="shrink-0 glass-card glass-hover px-3 py-1.5 rounded-xl text-sm transition"
|
||||
>
|
||||
Enable
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Account({ me, onOpenWizard }: { me: Me; onOpenWizard: () => void }) {
|
||||
return (
|
||||
<>
|
||||
<Section title="Account">
|
||||
|
|
@ -480,34 +521,39 @@ function Account({ me }: { me: Me }) {
|
|||
<div className="text-xs text-muted capitalize">{me.role}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-1 pt-3 border-t border-border">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<div className="text-sm font-medium">Playlist editing & YouTube export</div>
|
||||
<p className="text-xs text-muted leading-relaxed mt-0.5">
|
||||
{me.can_write
|
||||
? "Granted. You can unsubscribe from channels on YouTube (and export playlists once that ships). Subfeed only writes when you ask it to."
|
||||
: "Subfeed is read-only by default — it can browse your subscriptions but not change your YouTube account. Enable this to unsubscribe from channels (and, later, export playlists). You'll re-consent with Google."}
|
||||
</p>
|
||||
</div>
|
||||
{me.can_write ? (
|
||||
<span className="shrink-0 text-[11px] px-2 py-1 rounded-full border border-accent/40 text-accent">
|
||||
Enabled
|
||||
</span>
|
||||
) : (
|
||||
<Tooltip hint="Redirects to Google to grant YouTube write access (unsubscribe / export). You can keep using Subfeed read-only if you skip it.">
|
||||
<button
|
||||
onClick={() => {
|
||||
window.location.href = "/auth/upgrade";
|
||||
}}
|
||||
className="shrink-0 glass-card glass-hover px-3 py-1.5 rounded-xl text-sm transition"
|
||||
>
|
||||
Enable
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section title="YouTube access">
|
||||
<p className="text-xs text-muted leading-relaxed mb-1">
|
||||
Sign-in only shares your name and email. YouTube access is granted separately, below —
|
||||
each is optional and revocable anytime in your Google Account.
|
||||
</p>
|
||||
<div className="divide-y divide-border">
|
||||
<AccessRow
|
||||
title="Read subscriptions (your feed)"
|
||||
granted={me.can_read}
|
||||
grantedHint="Granted. Subfeed reads the channels you follow to build your feed. It never changes your YouTube account with this."
|
||||
enableHint="Read-only access to your subscriptions — required to build your feed. Subfeed can't modify anything with it."
|
||||
onEnable={() => {
|
||||
window.location.href = "/auth/upgrade?access=read";
|
||||
}}
|
||||
/>
|
||||
<AccessRow
|
||||
title="Unsubscribe (write)"
|
||||
granted={me.can_write}
|
||||
grantedHint="Granted. You can unsubscribe from channels on YouTube from inside Subfeed. It only writes when you ask it to."
|
||||
enableHint="Lets Subfeed unsubscribe from channels on YouTube for you. Optional — skip it to stay read-only."
|
||||
onEnable={() => {
|
||||
window.location.href = "/auth/upgrade?access=write";
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={onOpenWizard}
|
||||
className="mt-2 text-sm text-accent hover:underline"
|
||||
>
|
||||
Walk me through setup
|
||||
</button>
|
||||
</Section>
|
||||
{me.role === "admin" && <AdminInvites />}
|
||||
</>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue