Scope desktop onboarding state by account

This commit is contained in:
Sreeram Sreedhar 2026-06-23 21:26:24 -07:00
parent 65238152da
commit c9bf702272
4 changed files with 88 additions and 45 deletions

View file

@ -51,8 +51,8 @@ export default function LoginPage() {
setSubmittedEmail(null)
setIsSubmitting(false)
try {
await getSession()
router.replace(postAuthRedirectPath())
const session = await getSession()
router.replace(postAuthRedirectPath(session))
} catch (err) {
setError(formatError(err, "Could not validate browser sign-in"))
}

View file

@ -79,7 +79,7 @@ export default function DesktopOnboardingPage() {
getSession()
.then((nextSession) => {
if (cancelled) return
const status = getDesktopOnboardingStatus()
const status = getDesktopOnboardingStatus(nextSession)
setSession(nextSession)
setStep(status.lastStep === "done" ? "welcome" : status.lastStep)
setConnectedTools(status.connectedTools)
@ -94,23 +94,26 @@ export default function DesktopOnboardingPage() {
}
}, [router])
const goToStep = useCallback((nextStep: DesktopOnboardingStep) => {
setStep(nextStep)
updateDesktopOnboardingStatus({ lastStep: nextStep })
}, [])
const goToStep = useCallback(
(nextStep: DesktopOnboardingStep) => {
setStep(nextStep)
updateDesktopOnboardingStatus({ lastStep: nextStep }, session)
},
[session],
)
const complete = useCallback(
(nextConnectedTools = connectedTools) => {
completeDesktopOnboarding(nextConnectedTools)
completeDesktopOnboarding(nextConnectedTools, session)
router.replace("/")
},
[connectedTools, router],
[connectedTools, router, session],
)
const skip = useCallback(() => {
skipDesktopOnboarding(step)
skipDesktopOnboarding(step, session)
router.replace("/")
}, [router, step])
}, [router, session, step])
if (!authChecked || !session) {
return (
@ -148,6 +151,7 @@ export default function DesktopOnboardingPage() {
) : null}
{step === "tools" ? (
<ToolsStep
session={session}
connectedTools={connectedTools}
onConnectedToolsChange={setConnectedTools}
onBack={() => goToStep("welcome")}
@ -233,11 +237,13 @@ function WelcomeStep({
}
function ToolsStep({
session,
connectedTools,
onConnectedToolsChange,
onBack,
onContinue,
}: {
session: AuthSession
connectedTools: DesktopToolId[]
onConnectedToolsChange: (tools: DesktopToolId[]) => void
onBack: () => void
@ -263,13 +269,13 @@ function ToolsStep({
.filter((tool) => tool.connected)
.map((tool) => tool.id)
onConnectedToolsChange(nextConnected)
updateDesktopOnboardingStatus({ connectedTools: nextConnected })
updateDesktopOnboardingStatus({ connectedTools: nextConnected }, session)
} catch (err) {
setError(formatUnknownError(err, "Could not scan local tools"))
} finally {
setLoading(false)
}
}, [onConnectedToolsChange])
}, [onConnectedToolsChange, session])
useEffect(() => {
refreshTools()

View file

@ -2,23 +2,33 @@
import { useRouter } from "next/navigation"
import { type ReactNode, useEffect, useState } from "react"
import { getSession } from "@/lib/auth"
import { getSession, type AuthSession } from "@/lib/auth"
import { shouldShowDesktopOnboarding } from "@/lib/onboarding"
type AuthStatus = "loading" | "authenticated" | "unauthenticated"
function useAuthStatus(): AuthStatus {
function useAuthStatus(): {
status: AuthStatus
session: AuthSession | null
} {
const [status, setStatus] = useState<AuthStatus>("loading")
const [session, setSession] = useState<AuthSession | null>(null)
useEffect(() => {
let cancelled = false
getSession()
.then(() => {
if (!cancelled) setStatus("authenticated")
.then((nextSession) => {
if (!cancelled) {
setSession(nextSession)
setStatus("authenticated")
}
})
.catch(() => {
if (!cancelled) setStatus("unauthenticated")
if (!cancelled) {
setSession(null)
setStatus("unauthenticated")
}
})
return () => {
@ -26,11 +36,11 @@ function useAuthStatus(): AuthStatus {
}
}, [])
return status
return { status, session }
}
export function AuthGuard({ children }: { children: ReactNode }) {
const status = useAuthStatus()
const { status, session } = useAuthStatus()
const router = useRouter()
useEffect(() => {
@ -39,10 +49,10 @@ export function AuthGuard({ children }: { children: ReactNode }) {
return
}
if (status === "authenticated" && shouldShowDesktopOnboarding()) {
if (status === "authenticated" && shouldShowDesktopOnboarding(session)) {
router.replace("/onboarding")
}
}, [status, router])
}, [status, session, router])
if (status !== "authenticated") {
return (

View file

@ -4,6 +4,11 @@ import type { DesktopToolId } from "@/lib/tools"
export type DesktopOnboardingStep = "welcome" | "tools" | "filesystem" | "done"
export type DesktopOnboardingOwner = {
userId?: string | null
email?: string | null
}
export type DesktopOnboardingStatus = {
version: "v1"
completed: boolean
@ -30,11 +35,19 @@ function storageAvailable() {
)
}
export function getDesktopOnboardingStatus(): DesktopOnboardingStatus {
function storageKey(owner?: DesktopOnboardingOwner | null) {
const accountId = owner?.userId || owner?.email
if (!accountId) return STORAGE_KEY
return `${STORAGE_KEY}:${encodeURIComponent(accountId)}`
}
export function getDesktopOnboardingStatus(
owner?: DesktopOnboardingOwner | null,
): DesktopOnboardingStatus {
if (!storageAvailable()) return DEFAULT_STATUS
try {
const raw = window.localStorage.getItem(STORAGE_KEY)
const raw = window.localStorage.getItem(storageKey(owner))
if (!raw) return DEFAULT_STATUS
const parsed = JSON.parse(raw) as Partial<DesktopOnboardingStatus>
if (parsed.version !== "v1") return DEFAULT_STATUS
@ -52,52 +65,66 @@ export function getDesktopOnboardingStatus(): DesktopOnboardingStatus {
export function saveDesktopOnboardingStatus(
status: DesktopOnboardingStatus,
owner?: DesktopOnboardingOwner | null,
): void {
if (!storageAvailable()) return
try {
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(status))
window.localStorage.setItem(storageKey(owner), JSON.stringify(status))
} catch {}
}
export function updateDesktopOnboardingStatus(
patch: Partial<DesktopOnboardingStatus>,
owner?: DesktopOnboardingOwner | null,
): DesktopOnboardingStatus {
const next: DesktopOnboardingStatus = {
...getDesktopOnboardingStatus(),
...getDesktopOnboardingStatus(owner),
...patch,
version: "v1",
}
saveDesktopOnboardingStatus(next)
saveDesktopOnboardingStatus(next, owner)
return next
}
export function shouldShowDesktopOnboarding() {
const status = getDesktopOnboardingStatus()
export function shouldShowDesktopOnboarding(
owner?: DesktopOnboardingOwner | null,
) {
const status = getDesktopOnboardingStatus(owner)
return !status.completed && !status.skipped
}
export function postAuthRedirectPath() {
return shouldShowDesktopOnboarding() ? "/onboarding" : "/"
export function postAuthRedirectPath(owner?: DesktopOnboardingOwner | null) {
return shouldShowDesktopOnboarding(owner) ? "/onboarding" : "/"
}
export function completeDesktopOnboarding(
connectedTools: DesktopToolId[] = [],
owner?: DesktopOnboardingOwner | null,
) {
return updateDesktopOnboardingStatus({
completed: true,
skipped: false,
completedAt: new Date().toISOString(),
lastStep: "done",
connectedTools,
})
return updateDesktopOnboardingStatus(
{
completed: true,
skipped: false,
completedAt: new Date().toISOString(),
lastStep: "done",
connectedTools,
},
owner,
)
}
export function skipDesktopOnboarding(lastStep: DesktopOnboardingStep) {
return updateDesktopOnboardingStatus({
completed: false,
skipped: true,
skippedAt: new Date().toISOString(),
lastStep,
})
export function skipDesktopOnboarding(
lastStep: DesktopOnboardingStep,
owner?: DesktopOnboardingOwner | null,
) {
return updateDesktopOnboardingStatus(
{
completed: false,
skipped: true,
skippedAt: new Date().toISOString(),
lastStep,
},
owner,
)
}