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.

The onboarding blockWebReact Native

1.What you build

No screenshot yet. Run the Next.js example (pnpm dev:example) to see it.

2.Add the components

Web: shadcn

terminal
npx shadcn add button card progress skeleton

Native: 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

terminal
npx shadcn add @better-supabase/onboarding-checklist

Native

terminal
npx shadcn add @better-supabase/onboarding-checklist

4.Write the component

Web: React and shadcn

View in the example
onboarding-card.tsx
"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 example
onboarding-checklist.tsx
import { 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>
  );
}