# ProductOnboarding

> The first-run flow every Nasaq product shares: workspace → business basics → sample data → connect your AI → invite your team → done. Skippable and resumable from server state.

Source: https://docs.nasaqui.com/components/product-onboarding

## Install

```bash
npx shadcn@latest add https://docs.nasaqui.com/r/product-onboarding.json
```

The same first-run flow in every Nasaq product. It runs these steps in a fixed order: confirm the workspace, the
business basics, load sample data, connect an AI app over MCP, and invite the team. The first two are required and
the rest can be skipped. Progress lives on the server, so people resume where they left off, and "Skip setup" leaves
at any point. It is built on `SetupWizard`; the sample-data, connect and invite steps come with their own bodies.

## When to use

- The `/onboarding` page a product opens after "Create business/store".

## When not to use

- A product that is not a business workspace: use `SetupWizard` with your own steps.
- The dashboard reminder: use `OnboardingChecklist` with `onboardingOpen(state)`.

## Import

```tsx
import { ProductOnboarding } from "@fadymondy/nasaq/web";
// inside this monorepo: "@nasaq/web"
```

## Quick start

```tsx
"use client";
import { Field, FieldLabel, Input, ProductOnboarding, type ProductOnboardingState } from "@fadymondy/nasaq/web";

declare const state: ProductOnboardingState;
declare const api: { post(path: string, body?: unknown): Promise<void> };

export function Onboarding({ onExit }: { onExit: () => void }) {
  return (
    <ProductOnboarding
      product="Matjar"
      state={state}
      workspace={{ content: <Field><FieldLabel>Store name</FieldLabel><Input defaultValue="3x1" /></Field> }}
      business={{ content: <Field><FieldLabel>Currency</FieldLabel><Input defaultValue="EGP" /></Field> }}
      onLoadSampleData={() => api.post("/api/onboarding/sample-data")}
      mcp={{ server: { name: "matjar", url: "https://3x1.matjar.app/api/mcp" } }}
      onInvite={(emails, role) => api.post("/api/team/invites", { emails, role })}
      roles={[{ value: "staff", label: "Staff" }]}
      onAction={(step, action) => api.post("/api/onboarding", { step, action })}
      onExit={onExit}
    />
  );
}
```

## Anatomy

```
ProductOnboarding                 data-slot="product-onboarding"
├─ SetupWizard                    data-step="workspace|business|sample-data|connect-mcp|invite"
│  ├─ workspace (required)        your form
│  ├─ business (required)         your form
│  ├─ sample-data (optional)      built-in: Load sample data → success alert
│  ├─ connect-mcp (optional)      built-in: McpConnectApps
│  ├─ invite (optional)           built-in: emails + role → onInvite
│  └─ done screen                 Open dashboard
└─ Skip setup                     dismiss + onExit
```

## API

**ProductOnboarding**

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `product` | `string` | required | Product name in the page language. |
| `state` | `ProductOnboardingState` | required | `{ completed, skipped?, dismissed?, finishedAt? }` from `GET /api/onboarding`. |
| `workspace` / `business` | `ProductOnboardingStepBody` | required | `{ content, ready?, title?, description? }`. |
| `onLoadSampleData` | `() => AuthSubmitResult \| Promise` | | Omit to drop the step. |
| `mcp` | `Omit<McpConnectAppsProps, "product">` | | Omit to drop the step. |
| `onInvite` | `(emails, role) => AuthSubmitResult \| Promise` | | Omit to drop the step. |
| `roles` | `{ value, label }[]` | | Invite roles; the first is the default. |
| `onAction` | `(step, action) => AuthSubmitResult \| Promise` | required | `action` is `complete`, `skip`, `dismiss` or `finish`. `{ error }` keeps the step. |
| `onExit` | `() => void` | required | Open dashboard / after Skip setup. |
| `labels` / `wizardLabels` | partial labels | built-in EN/AR | |

Helpers (pure): `onboardingResumeIndex(stepIds, state)`, `onboardingOpen(state)`, `applyOnboardingAction(state, step, action, now?)` (the server's rules, for optimistic updates and tests), `onboardingProgress(stepIds, state)`, `isOptionalStep(id)`. Constants: `PRODUCT_ONBOARDING_STEPS`, `REQUIRED_ONBOARDING_STEPS`.

The server contract every product implements: `GET /api/onboarding` → state, and `POST /api/onboarding { step?, action }` → state. `skip` never overrides `complete`. `finish` sets `finishedAt` once.

## Examples

Show the dashboard card while setup is open:

```tsx
import { OnboardingChecklist, PRODUCT_ONBOARDING_STEPS, onboardingOpen, type ProductOnboardingState } from "@fadymondy/nasaq/web";

declare const state: ProductOnboardingState;
export const Resume = () =>
  onboardingOpen(state) ? (
    <OnboardingChecklist items={PRODUCT_ONBOARDING_STEPS.map((id) => ({ id, title: id, done: state.completed.includes(id) || (state.skipped ?? []).includes(id) }))} />
  ) : null;
```

## Accessibility

| Key | Action |
| --- | --- |
| `Tab` | Moves through the step body, then Back · Skip · Continue. |
| `Enter` / `Space` | Activates the focused button. |

The step heading takes focus on every step change. Errors render as `role="alert"`.

## RTL & i18n

Every built-in string has EN and AR copy. Email addresses are entered `dir="ltr"`. Arrows mirror in RTL (from `SetupWizard`).

## Styling & tokens

Inherits `SetupWizard` and `McpConnectApps`. Target `[data-slot=product-onboarding]` and `[data-step]`.

## Do / Don't

- Do mark `workspace` complete on the server when the console creates the business, so people start at step 2.
- Do keep business basics to six fields or fewer.
- Don't add steps; put product extras on the dashboard checklist.

## Related

- [setup-wizard](https://docs.nasaqui.com/components/setup-wizard)
- [onboarding-checklist](https://docs.nasaqui.com/components/onboarding-checklist)
- [mcp-connect](https://docs.nasaqui.com/components/mcp-connect)
- [product-shell](https://docs.nasaqui.com/components/product-shell)

## Lab

`https://docs.nasaqui.com/?path=/docs/components-onboarding-pages-product-onboarding--docs`

## Code

### React

```tsx
"use client";
import { Field, FieldLabel, Input, ProductOnboarding, type ProductOnboardingState } from "@fadymondy/nasaq/web";

declare const state: ProductOnboardingState;
declare const api: { post(path: string, body?: unknown): Promise<void> };

export function Onboarding({ onExit }: { onExit: () => void }) {
  return (
    <ProductOnboarding
      product="Matjar"
      state={state}
      workspace={{ content: <Field><FieldLabel>Store name</FieldLabel><Input defaultValue="3x1" /></Field> }}
      business={{ content: <Field><FieldLabel>Currency</FieldLabel><Input defaultValue="EGP" /></Field> }}
      onLoadSampleData={() => api.post("/api/onboarding/sample-data")}
      mcp={{ server: { name: "matjar", url: "https://3x1.matjar.app/api/mcp" } }}
      onInvite={(emails, role) => api.post("/api/team/invites", { emails, role })}
      roles={[{ value: "staff", label: "Staff" }]}
      onAction={(step, action) => api.post("/api/onboarding", { step, action })}
      onExit={onExit}
    />
  );
}
```

### shadcn

```tsx
"use client";
import { Field, FieldLabel, Input } from "@/components/ui/field";
import { ProductOnboarding, type ProductOnboardingState } from "@/components/ui/product-onboarding";

declare const state: ProductOnboardingState;
declare const api: { post(path: string, body?: unknown): Promise<void> };

export function Onboarding({ onExit }: { onExit: () => void }) {
  return (
    <ProductOnboarding
      product="Matjar"
      state={state}
      workspace={{ content: <Field><FieldLabel>Store name</FieldLabel><Input defaultValue="3x1" /></Field> }}
      business={{ content: <Field><FieldLabel>Currency</FieldLabel><Input defaultValue="EGP" /></Field> }}
      onLoadSampleData={() => api.post("/api/onboarding/sample-data")}
      mcp={{ server: { name: "matjar", url: "https://3x1.matjar.app/api/mcp" } }}
      onInvite={(emails, role) => api.post("/api/team/invites", { emails, role })}
      roles={[{ value: "staff", label: "Staff" }]}
      onAction={(step, action) => api.post("/api/onboarding", { step, action })}
      onExit={onExit}
    />
  );
}
```

### Vue

```vue
<script setup lang="ts">
// The product onboarding steps with the Vue wizard: workspace → business → sample data → connect your AI → invite → done.
import { NqSetupWizard } from "@fadymondy/nasaq/vue";

const state = { completed: ["workspace"], skipped: [] as string[] };
const steps = [
  { id: "workspace", title: "Your workspace" },
  { id: "business", title: "Business basics" },
  { id: "sample-data", title: "Sample data", optional: true },
  { id: "connect-mcp", title: "Connect your AI", optional: true },
  { id: "invite", title: "Invite your team", optional: true },
];
const resumeAt = Math.max(0, steps.findIndex((s) => !state.completed.includes(s.id) && !state.skipped.includes(s.id)));
const record = async (_step: string) => undefined;
const finish = async () => undefined;
</script>

<template>
  <NqSetupWizard title="Set up Matjar" :steps="steps" :default-current="resumeAt" :completed="state.completed" :on-step-complete="record" :on-finish="finish">
    <template #step-workspace><input aria-label="Store name" value="3x1" /></template>
    <template #step-business><input aria-label="Currency" value="EGP" /></template>
    <template #step-sample-data><button type="button">Load sample data</button></template>
    <template #step-connect-mcp><p>Connect Claude, ChatGPT or your editor.</p></template>
    <template #step-invite><textarea aria-label="Email addresses" dir="ltr" /></template>
  </NqSetupWizard>
</template>
```
