All recipes
Recipe
Onboarding checklist
The organization's getting-started steps with progress, hidden once every step is done. Web and native call useOnboarding from better-supabase/blocks/onboarding/react.
1.What you build
No screenshot yet. Run the Next.js example (pnpm dev:example) to see it.
2.Add the components
Web: shadcn
npx shadcn add button card progress skeletonNative: React Native
The snippet uses ActivityIndicator, Text, View from react-native, so it needs no styling library. For shadcn-style native components, React Native Reusables serves them from a registry the shadcn CLI reads.
3.Install the glue
Install the block with npx better-supabase sql add onboarding. The glue items are copied into your app, so edit them as you need.
Web
npx shadcn add @better-supabase/onboarding-checklistNative
npx shadcn add @better-supabase/onboarding-checklist4.Write the component
Web: React and shadcn
View in the example"use client";
import { useOnboarding } from "better-supabase/blocks/onboarding/react";
import { tenantOf } from "better-supabase/react";
import { CheckCircle2Icon, CircleIcon } from "lucide-react";
import { useExtracted } from "next-intl";
import { buttonVariants } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Progress } from "@/components/ui/progress";
import { Skeleton } from "@/components/ui/skeleton";
import { gettingStarted } from "@/features/onboarding/checklist";
import { Link } from "@/i18n/navigation";
import { useAuth } from "@/lib/hooks";
/** `blocks.onboarding.api` in better-supabase.config.ts. */
const API_SCHEMA = "api";
/** The organization's getting-started checklist; hidden once every step is done. */
export function OnboardingCard() {
const t = useExtracted("dashboard");
const organizationId = tenantOf(useAuth()) ?? null;
const { progress } = useOnboarding(gettingStarted, {
organizationId,
schema: API_SCHEMA,
});
if (!organizationId) return null;
if (progress === undefined) return <OnboardingCardSkeleton />;
if (progress.done) return null;
const titles = {
customer: t("Add your first customer"),
invite: t("Invite a teammate"),
"api-key": t("Create an API key"),
plan: t("Review your plan"),
};
return (
<Card data-testid="onboarding">
<CardHeader>
<CardTitle>{t("Get started")}</CardTitle>
<CardDescription>
{t("{completed, number} of {total, number} steps done", {
completed: progress.completed,
total: progress.total,
})}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<Progress value={(progress.completed / progress.total) * 100} />
<ul className="divide-y">
{progress.steps.map((step) => (
<li key={step.id} className="flex items-center gap-3 py-2">
{step.completed ? (
<CheckCircle2Icon className="text-primary size-4" />
) : (
<CircleIcon className="text-muted-foreground size-4" />
)}
<span
className={
step.completed
? "text-muted-foreground flex-1 line-through"
: "flex-1"
}
>
{titles[step.id]}
</span>
{step.completed ? null : (
<Link
href={step.href ?? "/"}
className={buttonVariants({ variant: "outline", size: "sm" })}
>
{t("Start")}
</Link>
)}
</li>
))}
</ul>
</CardContent>
</Card>
);
}
export function OnboardingCardSkeleton() {
return <Skeleton className="h-64 rounded-xl" aria-busy="true" />;
}
Native: React Native
View in the exampleimport { useOnboarding } from "better-supabase/blocks/onboarding/react";
import { tenantOf, useAuth } from "better-supabase/react";
import { ActivityIndicator, Text, View } from "react-native";
import { API_SCHEMA } from "../lib/blocks";
import { gettingStarted } from "../lib/onboarding";
/** The organization's getting-started checklist; hidden once every step is done. */
export function OnboardingChecklist() {
const organizationId = tenantOf(useAuth()) ?? null;
const { progress } = useOnboarding(gettingStarted, {
organizationId,
schema: API_SCHEMA,
});
if (!organizationId) return null;
if (progress === undefined) return <ActivityIndicator />;
if (progress.done) return null;
return (
<View style={{ gap: 8, padding: 16, borderWidth: 1, borderRadius: 12 }}>
<Text style={{ fontSize: 17, fontWeight: "600" }}>Get started</Text>
<Text style={{ color: "#737373" }}>
{progress.completed} of {progress.total} steps done
</Text>
<View style={{ height: 6, borderRadius: 3, backgroundColor: "#e5e5e5" }}>
<View
style={{
height: 6,
borderRadius: 3,
backgroundColor: "#111",
width: `${(progress.completed / progress.total) * 100}%`,
}}
/>
</View>
{progress.steps.map((step) => (
<Text
key={step.id}
style={
step.completed
? { color: "#737373", textDecorationLine: "line-through" }
: undefined
}
>
{step.completed ? "✓" : "○"} {step.title}
</Text>
))}
</View>
);
}