> For the complete documentation index, see [llms.txt](https://docs.humanity.org/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.humanity.org/developer-guides-and-tutorials/sdk-api-guides/react-sdk/verified-dashboard-with-the-react-sdk.md).

# Verified Dashboard with the React SDK

{% hint style="info" %}
This guide uses the sandbox environment.
{% endhint %}

The [SDK QuickStart](/build-with-humanity/build-with-the-sdk-api/sdk-quickstart.md) gets auth working on one page. This adds routing, a protected dashboard gated on real preset data, and a profile page listing every `identity:read` preset's status.

Full list is under [SDK OAuth Scopes and Presets](/build-with-humanity/build-with-the-sdk-api/sdk-oauth-scopes-and-presets.md).

***

### Before you start

You need:

* [Node 18](https://nodejs.org/en) or higher
* [Bun](https://bun.sh)
* A Humanity Developer Account — register at [developers.humanity.org](https://developers.humanity.org)
* A sandbox **Palm Print** credential — generate one at the [Sandbox Credential Generator](https://app.sandbox.humanity.org/sandbox)

New to this? See the Generating Mock Credentials guide.

***

### 01. Create the project

```bash
bun create vite verified-dashboard-react-sdk --template react-ts
cd verified-dashboard-react-sdk
bun add @humanity-org/react-sdk react-router-dom
bun add -d vite-plugin-node-polyfills @types/node
```

Update `vite.config.ts`:

```ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { nodePolyfills } from 'vite-plugin-node-polyfills';

export default defineConfig({
  plugins: [react(), nodePolyfills()],
});
```

***

### 02. Configure the Developer Portal

Go to [developers.humanity.org](https://developers.humanity.org), open your application, and switch to the **Sandbox** tab in Settings.

* Add `http://localhost:5173/` as a redirect URI
* Under scopes, enable:
  * **OpenID Connect** (`openid`)
  * **Identity Information** (`identity:read`)

> The SDK matches the callback by origin + path only, ignoring the query string — so your redirect URI can point straight at `/`. No dedicated callback route needed.

***

### 03. Set up environment variables

Create `.env.local` at the project root:

```
VITE_HUMANITY_CLIENT_ID=your_sandbox_client_id
VITE_HUMANITY_REDIRECT_URI=http://localhost:5173/
```

Get your `client_id` from the Developer Portal under your application's **Sandbox** tab.

***

### 04. Set up the folder structure

```bash
mkdir -p src/components src/pages
```

***

### 05. Wire up HumanityProvider and routing

`HumanityProvider` must sit inside `BrowserRouter`.

Replace `src/main.tsx`:

```tsx
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import { HumanityProvider } from '@humanity-org/react-sdk';
import '@humanity-org/react-sdk/styles.css';
import App from './App';
import './index.css';

ReactDOM.createRoot(document.getElementById('root')!).render(
  <BrowserRouter>
    <HumanityProvider
      clientId={import.meta.env.VITE_HUMANITY_CLIENT_ID}
      redirectUri={import.meta.env.VITE_HUMANITY_REDIRECT_URI}
      environment="sandbox"
      storage="sessionStorage"
    >
      <App />
    </HumanityProvider>
  </BrowserRouter>
);
```

***

### 06. Add the protected route

Create `src/components/ProtectedRoute.tsx`:

```tsx
import { Navigate, Outlet } from 'react-router-dom';
import { useAuth } from '@humanity-org/react-sdk';

export function ProtectedRoute() {
  const { isAuthenticated, isLoading } = useAuth();

  if (isLoading) return <div className="loading">Loading...</div>;
  if (!isAuthenticated) return <Navigate to="/" replace />;

  return <Outlet />;
}
```

***

### 07. Build the app routes

Replace `src/App.tsx`:

```tsx
import { Routes, Route } from 'react-router-dom';
import { Home } from './pages/Home';
import { Dashboard } from './pages/Dashboard';
import { Profile } from './pages/Profile';
import { ProtectedRoute } from './components/ProtectedRoute';

export default function App() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route element={<ProtectedRoute />}>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/profile" element={<Profile />} />
      </Route>
    </Routes>
  );
}
```

***

### 08. Build the Home page

Create `src/pages/Home.tsx`:

```tsx
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { HumanityConnect, useAuth } from '@humanity-org/react-sdk';

export function Home() {
  const { isAuthenticated, error } = useAuth();
  const navigate = useNavigate();

  useEffect(() => {
    if (isAuthenticated) navigate('/dashboard');
  }, [isAuthenticated, navigate]);

  return (
    <div className="home">
      <h1>Verified Dashboard</h1>
      <p>Sign in with Humanity to access your dashboard.</p>
      <HumanityConnect
        scopes={['openid', 'identity:read']}
        mode="redirect"
        onError={(error) => console.error('Login failed:', error)}
      />
      {error && <p className="error">{error.message}</p>}
    </div>
  );
}
```

The `useEffect` covers the case where an already-authenticated user lands on `/` — they skip the login screen and go straight to the dashboard.

***

### 09. List the identity presets

`identity:read` covers 12 presets — Profile lists all of them. Both pages also share a couple of small helpers for working with batch results, so this is worth its own file instead of duplicating them.

Create `src/presets.ts`:

```ts
// All presets available under the identity:read scope.
export const IDENTITY_PRESETS = [
  'humanity_uuid',
  'humanity_score',
  'is_human',
  'country_of_residence',
  'nationality',
  'residency_region',
  'email',
  'phone',
  'social_accounts',
  'primary_wallet_address',
  'palm_verified',
  'proof_of_residency',
] as const;

export interface PresetResult {
  preset: string;
  verified: boolean;
  value?: unknown;
}

export const PRESET_BATCH_LIMIT = 10;

export function chunk<T>(items: readonly T[], size: number): T[][] {
  const chunks: T[][] = [];
  for (let i = 0; i < items.length; i += size) {
    chunks.push(items.slice(i, i + size));
  }
  return chunks;
}

export function verifyAllBatched(
  verifyAll: (presets: string[]) => Promise<unknown[]>,
  presets: readonly string[]
): Promise<PresetResult[]> {
  return Promise.all(chunk(presets, PRESET_BATCH_LIMIT).map((batch) => verifyAll(batch))).then((batches) =>
    batches.flat()
  ) as Promise<PresetResult[]>;
}

export function findPresetResult(results: PresetResult[] | null, preset: string): PresetResult | undefined {
  return results?.find((r) => r.preset === preset || (r as { presetName?: string }).presetName === preset);
}

export function isPassed(result: PresetResult | undefined): boolean {
  return result?.verified === true && Boolean(result.value);
}
```

{% hint style="info" %}
B**atch limit**: `verifyAll()` caps at 10 presets per call — requesting all 12 fails. `verifyAllBatched()` splits into batches of **PRESET\_BATCH\_LIMIT**, runs them in parallel, and flattens the results.
{% endhint %}

### 10. Build the Dashboard

Create `src/pages/Dashboard.tsx`:

Each social card asks two separate questions: is this platform linked at all, and if it is, does the link actually carry any data?.&#x20;

Three states instead of two — not-linked, linked-empty, linked — a linked-but-empty account is a real case, not an edge case then `humanVerified` runs through `isPassed()` .

Once it flips true, the heading drops "Linked accounts" and welcomes the user by their `humanity_uuid` instead, falling back to "Verified human" only if that specific preset didn't come back verified.

```tsx
import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { usePresets, useAuth } from '@humanity-org/react-sdk';
import { verifyAllBatched, findPresetResult, isPassed, type PresetResult } from '../presets';

interface SocialAccount {
  provider: string;
  username?: string;
  displayName?: string;
  emails?: string[];
}

const SOCIAL_PLATFORMS = [
  { id: 'twitter', label: 'Twitter' },
  { id: 'discord', label: 'Discord' },
  { id: 'telegram', label: 'Telegram' },
  { id: 'github', label: 'GitHub' },
  { id: 'google', label: 'Google' },
  { id: 'linkedin', label: 'LinkedIn' },
] as const;

function SocialCard({ label, account }: { label: string; account?: SocialAccount }) {
  const hasValue = Boolean(account?.username || account?.displayName);
  const state = !account ? 'not-linked' : hasValue ? 'linked' : 'linked-empty';

  return (
    <div className={`social-card social-card--${state}`}>
      <h3>{label}</h3>
      {state === 'not-linked' && <p>Not linked</p>}
      {state === 'linked-empty' && <p>Linked, no data returned</p>}
      {state === 'linked' && (
        <p>
          under username: <span className="username-value">{account?.displayName ?? account?.username}</span>
        </p>
      )}
    </div>
  );
}

export function Dashboard() {
  const { logout } = useAuth();
  const { verifyAll } = usePresets();
  const [results, setResults] = useState<PresetResult[] | null>(null);

  useEffect(() => {
    verifyAllBatched(verifyAll, ['is_human', 'social_accounts', 'humanity_uuid']).then(setResults);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  const humanVerified = results !== null && isPassed(findPresetResult(results, 'is_human'));
  const socialResult = findPresetResult(results, 'social_accounts');
  const accounts = (
    socialResult?.verified && Array.isArray(socialResult.value) ? socialResult.value : []
  ) as SocialAccount[];
  const accountFor = (id: string) => accounts.find((a) => a.provider?.toLowerCase() === id);

  const uuidResult = findPresetResult(results, 'humanity_uuid');
  const uuidValue = uuidResult?.verified ? String(uuidResult.value ?? '') : null;

  return (
    <div className="dashboard">
      <header>
        <h1>Dashboard</h1>
        <nav>
          <Link to="/profile">Profile</Link>
          <button onClick={logout}>Sign out</button>
        </nav>
      </header>

      <section>
        <h2>Public content</h2>
        <p>
          Lorem ipsum dolor sit amet, consectetur adipiscing elit. Visible to any authenticated user, verified or
          not.
        </p>
      </section>

      <section>
        {results === null ? (
          <>
            <h2>Linked accounts</h2>
            <p>Checking verification...</p>
          </>
        ) : !humanVerified ? (
          <>
            <h2>Linked accounts</h2>
            <p>Human verification required to see linked accounts.</p>
          </>
        ) : (
          <>
            <h2>Welcome {uuidValue ?? 'Verified human'}</h2>
            <p className="caption">You can prove your humanity across the following social profiles</p>
            <div className="social-grid">
              {SOCIAL_PLATFORMS.map((platform) => (
                <SocialCard key={platform.id} label={platform.label} account={accountFor(platform.id)} />
              ))}
            </div>
          </>
        )}
      </section>
    </div>
  );
}
```

`verifyAll` returns the preset field in camelCase — request `is_human` and the response comes back as `isHuman`. Because of this, looking up a result using the exact string you requested won't match through usePresets's built-in `isVerified()` / `getResult()`.\
\
`findPresetResult()` handles this by matching on **presetName** instead, which preserves the original request string.

***

### 11. Build the Profile page

Create `src/pages/Profile.tsx`:

`resultFor` reuses the Dashboard's `findPresetResult()`, so the lookup logic stays in one place. Most values are rendered with a plain JSON.stringify. `social_accounts` gets its own table instead — one row per account beats scanning a JSON blob for who's linked.

```tsx
import { Link } from 'react-router-dom';
import { HumanityProfile } from '@humanity-org/react-sdk';
import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { usePresets } from '@humanity-org/react-sdk';
import { IDENTITY_PRESETS, verifyAllBatched, findPresetResult, type PresetResult } from '../presets';

interface SocialAccount {
  provider: string;
  username?: string;
  displayName?: string;
  emails?: string[];
}

function SocialAccountsTable({ accounts }: { accounts: SocialAccount[] }) {
  if (accounts.length === 0) return <span>—</span>;

  return (
    <table className="sub-table">
      <thead>
        <tr>
          <th>Provider</th>
          <th>Username</th>
          <th>Display name</th>
          <th>Emails</th>
        </tr>
      </thead>
      <tbody>
        {accounts.map((account, i) => (
          <tr key={`${account.provider}-${i}`}>
            <td>{account.provider}</td>
            <td>{account.username ?? '—'}</td>
            <td>{account.displayName ?? '—'}</td>
            <td>{account.emails?.length ? account.emails.join(', ') : '—'}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}

export function Profile() {
  const { verifyAll } = usePresets();
  const [results, setResults] = useState<PresetResult[] | null>(null);

  useEffect(() => {
    verifyAllBatched(verifyAll, IDENTITY_PRESETS).then(setResults);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  const resultFor = (preset: string) => findPresetResult(results, preset);

  const formatValue = (value: unknown) => {
    if (value === undefined || value === null) return '—';
    if (typeof value === 'object') return JSON.stringify(value);
    return String(value);
  };

  const renderValue = (preset: string, result: PresetResult | undefined) => {
    if (!result?.verified) return '—';
    if (preset === 'social_accounts' && Array.isArray(result.value)) {
      return <SocialAccountsTable accounts={result.value as SocialAccount[]} />;
    }
    return formatValue(result.value);
  };

  return (
    <div className="profile">
      <header>
        <Link to="/dashboard">← Dashboard</Link>
      </header>

      <h1>Profile</h1>

      <section>
        {results === null ? (
          <p>Checking verification...</p>
        ) : (
          <table className="preset-table">
            <thead>
              <tr>
                <th>Preset</th>
                <th>Value</th>
                <th>Status</th>
              </tr>
            </thead>
            <tbody>
              {IDENTITY_PRESETS.map((preset) => {
                const result = resultFor(preset);
                return (
                  <tr key={preset}>
                    <td>{preset}</td>
                    <td>{renderValue(preset, result)}</td>
                    <td>{result?.verified ? '✅' : '❌'}</td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        )}
      </section>
    </div>
  );
}
```

***

### 12. Add styles

Replace `src/index.css`:

```css
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

body {
  font-family: system-ui, sans-serif;
  background: #0f0f0f;
  color: #f0f0f0;
  min-height: 100vh;
}

.home {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
  gap: 1.5rem;
  text-align: center;
}

.home h1 { font-size: 2rem; }
.home p { color: #999; }
.error { color: #f87171; }

.dashboard, .profile {
  max-width: 800px;
  margin: 0 auto;
  padding: 2rem;
}

header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 2rem;
  padding-bottom: 1rem;
  border-bottom: 1px solid #2a2a2a;
}

nav { display: flex; gap: 1rem; align-items: center; }

section {
  background: #1a1a1a;
  border: 1px solid #2a2a2a;
  border-radius: 8px;
  padding: 1.5rem;
  margin-bottom: 1rem;
}
section h2 { margin-bottom: 1rem; }

button {
  background: #1a1a1a;
  color: #f0f0f0;
  border: 1px solid #333;
  border-radius: 6px;
  padding: 0.4rem 0.8rem;
  cursor: pointer;
}

button:hover { border-color: #555; }

a { color: #888; text-decoration: none; }
a:hover { color: #f0f0f0; }

.loading { display: flex; justify-content: center; padding: 2rem; color: #888; }

.profile h1 { font-size: 1.5rem; margin-bottom: 1rem; }

.caption { color: #888; font-size: 0.85rem; margin-bottom: 1rem; }

.social-grid {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}
.social-card {
  display: flex;
  justify-content: space-between;
  align-items: center;
  width: 100%;
  border: 1px solid #2a2a2a;
  border-radius: 8px;
  padding: 1rem 1.25rem;
}
.social-card h3 { font-size: 1rem; }
.social-card p { font-size: 0.85rem; color: #999; }
.social-card--not-linked { opacity: 0.4; }
.social-card--linked-empty { border-color: #666; }
.social-card--linked { border-color: #4ade80; background: rgba(74, 222, 128, 0.08); }
.social-card--linked p { color: #f0f0f0; }
.username-value { color: #4ade80; }

.preset-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.9rem;
}
.preset-table th {
  text-align: left;
  color: #888;
  font-weight: 500;
  padding: 0.5rem;
  border-bottom: 1px solid #2a2a2a;
}
.preset-table td {
  padding: 0.5rem;
  border-bottom: 1px solid #2a2a2a;
  font-family: ui-monospace, monospace;
}
.preset-table tr:last-child td { border-bottom: none; }

.sub-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.8rem;
  margin: 0.25rem 0;
}
.sub-table th {
  text-align: left;
  color: #666;
  font-weight: 500;
  padding: 0.25rem 0.5rem;
  border-bottom: 1px solid #333;
}
.sub-table td {
  padding: 0.25rem 0.5rem;
  border-bottom: 1px solid #222;
}
.sub-table tr:last-child td { border-bottom: none; }
```

***

### 12. Run it

```bash
bun dev
```

Open `http://localhost:5173`. Click Sign in with Humanity, complete consent, and you'll land on the Dashboard.

* Once `is_human` passes, the heading swaps to your `humanity_uuid` and each platform gets its own card, color-coded by link status
* `/profile` lists all 12 identity:read presets — status, value, and the full `social_accounts` breakdown
* Sign out clears the session and drops you back at `/`

<figure><img src="/files/6H4FeyerVb0NvirtCbJi" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/OWz4Z5HRRvEkUcGSW01y" alt=""><figcaption></figcaption></figure>

> ⚠️ Firefox and Safari users: both can clear sessionStorage mid-redirect during cross-domain OAuth. The SDK always stores the PKCE verifier and OAuth state there, regardless of the storage prop — so a clear mid-flow breaks the callback with a state/PKCE mismatch.&#x20;
>
> Switch to Chrome or Brave if you hit this in testing.

***

### How it works

The SDK owns the full OAuth flow internally — PKCE, token exchange, storage. On your side: `isAuthenticated` for route protection, `user` for profile data, `usePresets().verifyAll()` for a batch check across every preset you care about, and a simple count or per-preset lookup for gating.

Compare with the [Frontend OAuth with the Connect SDK](/developer-guides-and-tutorials/sdk-api-guides/connect-sdk/frontend-oauth-with-the-connect-sdk.md) guide — there you call `buildAuthUrl()`, `exchangeCodeForToken()`, and `verifyPresets()` directly. Here you drop in components.

### Next steps

The [Human First Content Platform App](/developer-guides-and-tutorials/sdk-api-guides/react-sdk/human-first-content-platform-app.md) builds on this same project — same `HumanityProvider` setup, same `usePresets` pattern — but swaps the single `is_human` gate for four content tiers, each unlocked by its own preset.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.humanity.org/developer-guides-and-tutorials/sdk-api-guides/react-sdk/verified-dashboard-with-the-react-sdk.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
