# @anlyonhq/react components (beta)

Prebuilt screens for end users: approve what the agent wants to do, and connect accounts. React 18 or 19.

```bash
npm install @anlyonhq/react @anlyonhq/sdk
```

## How it fits

- The server mints a **session token** for the signed-in user with the API key. The key never reaches the browser.
- The provider fetches the token from your route, keeps it in memory, refreshes it before expiry.
- A session token can list, connect and disconnect that user's accounts and, when the key has `end-users:approvals`, list and decide that user's approvals. It cannot call actions, read secrets, see other users or confirm a connection. 15 minutes by default, at most 1 hour.
- In a browser it works from the origins of the environment's **allowed redirect URLs** (otherwise `403 ORIGIN_NOT_ALLOWED`). Add `http://localhost:<port>/...` to a separate, non-production environment for local work.

## 1. Session route

```typescript
// app/api/anlyon/session/route.ts
import { Client } from '@anlyonhq/sdk';
import { createSessionHandler } from '@anlyonhq/react/server';
import { auth } from '@/lib/auth';

const anlyon = new Client({ apiKey: process.env.ANLYON_API_KEY! });

export const POST = createSessionHandler({
  client: anlyon,
  getEndUserId: async () => (await auth())?.user.id,
});
```

Answers `{ token, expiresAt }` with `Cache-Control: no-store`, and `401` when nobody is signed in. Never log the token.

## 2. Connect callback

```typescript
// app/api/anlyon/connected/route.ts
import { createConnectCallbackHandler } from '@anlyonhq/react/server';

export const GET = createConnectCallbackHandler({
  client: anlyon,
  getEndUserId: async () => (await auth())?.user.id,
  redirectTo: '/settings/connections',
});
```

It confirms the flow for the session's user, then closes the popup or redirects.

## 3. Provider

```tsx
'use client';
import { AnlyonProvider } from '@anlyonhq/react';
import '@anlyonhq/react/styles.css';

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <AnlyonProvider
      getToken={() => fetch('/api/anlyon/session', { method: 'POST' }).then((r) => r.json())}
      connect={{ redirectUrl: 'https://app.example.com/api/anlyon/connected' }}
    >
      {children}
    </AnlyonProvider>
  );
}
```

Props: `getToken` (required), `baseUrl` (default `https://api.anlyon.com`), `connect` (`{ mode: 'popup' | 'redirect', redirectUrl }`), `pollInterval` (ms, default `30000`, `0` off), `locale`. When the signed-in user changes without a page load, remount the provider with the user id as its `key`.

## 4. Components

| Component | Use |
| --- | --- |
| `<ApprovalInbox />` | Every approval waiting for this user. `status="all"` includes decided ones |
| `<ApprovalPrompt approvalId={result.approvalId} onDecided={…} />` | One approval inline, under the chat message whose tool call waits |
| `<ConnectButton integration="github" />` | Connect, or Reconnect when the connection stopped working |
| `<Connections integrations={['github', 'slack']} />` | The user's accounts with status, reconnect and disconnect |
| `<ConnectPrompt result={part.output} />` | Renders a connect card for an `@anlyonhq/ai-sdk` result with `connect_required` or `reconnect_required`; nothing otherwise |

With the AI SDK:

```tsx
{message.parts.map((part) =>
  part.type.startsWith('tool-') && part.state === 'output-available' ? (
    <div key={part.toolCallId}>
      <ConnectPrompt result={part.output} onConnected={() => sendMessage({ text: 'I connected it.' })} />
      {(part.output as { status?: string }).status === 'pending_approval' && (
        <ApprovalPrompt approvalId={(part.output as { approvalId: string }).approvalId} />
      )}
    </div>
  ) : null,
)}
```

Hooks for custom markup: `useApprovals`, `useApproval`, `useConnections`, `useConnect`. Errors are `AnlyonUIError` with `status`, `code` and `message`.

## Theming

Follows shadcn/ui tokens (`--background`, `--primary`, `--radius`…) with no setup. Override with `--anlyon-*` variables (`--anlyon-primary`, `--anlyon-radius`, …). Every component takes `className`; `unstyled` drops the bundled styles.

## Session errors

| Code | Status | Meaning |
| --- | --- | --- |
| `END_USER_SESSION_REQUIRED` | 401 | No token sent |
| `END_USER_SESSION_EXPIRED` | 401 | Expired; the provider fetches a new one and retries once |
| `END_USER_SESSION_INVALID` | 401 | Altered, its key revoked, or the user erased |
| `END_USER_SESSION_SCOPE` | 403 | The minting key lacks `end-users:approvals` |
| `ORIGIN_NOT_ALLOWED` | 403 | Page origin is not an allowed redirect URL's origin |
| `END_USER_SESSION_NOT_ALLOWED` | 403 | Session token sent to a route it cannot use |
| `END_USER_SESSION_API_KEY_REQUIRED` | 403 | Sessions are minted with an API key |
