npm package: @kleroai/shopify
Description: Klero feedback board for Shopify embedded apps using Polaris web components
Latest version: 0.2.0

# @kleroai/shopify

Embed Klero feedback, roadmap, and changelog inside your Shopify embedded app. Renders using [Shopify Polaris web components](https://shopify.dev/docs/api/app-home/polaris-web-components) so the UI feels native to the Shopify admin.

## Requirements

- A Klero project (get your slug from the [Klero dashboard](https://app.klero.ai))
- A Shopify embedded app that loads Polaris web components:

  ```html
  <script src="https://cdn.shopify.com/shopifycloud/polaris.js"></script>
  ```

- React >= 18

## Installation

```bash
npm install @kleroai/shopify
```

## Quick Start

### 1. Add environment variables

```env
KLERO_SLUG=your-project-slug
KLERO_JWT_SECRET=your-jwt-secret   # from Klero dashboard → Settings → JWT Secret
KLERO_BASE_URL=                    # optional: custom domain
```

### 2. Add a Feedback route

```tsx
// app/routes/app.feedback.tsx
import type { LoaderFunctionArgs } from 'react-router';
import { useLoaderData } from 'react-router';
import { KleroFeedbackPage, signKleroJwtFromShopifySession } from '@kleroai/shopify';
import { authenticate } from '../shopify.server';

export const loader = async ({ request }: LoaderFunctionArgs) => {
  const { session } = await authenticate.admin(request);

  return {
    slug: process.env.KLERO_SLUG || '',
    baseUrl: process.env.KLERO_BASE_URL || undefined,
    customerToken: await signKleroJwtFromShopifySession(
      process.env.KLERO_JWT_SECRET || '',
      session
    ),
  };
};

export default function FeedbackPage() {
  const { slug, baseUrl, customerToken } = useLoaderData<typeof loader>();
  return <KleroFeedbackPage slug={slug} baseUrl={baseUrl} customerToken={customerToken ?? undefined} />;
}
```

Repeat the same pattern for Roadmap (`KleroRoadmapPage`) and Changelog (`KleroChangelogPage`).

## Page Components

These are the recommended way to add Klero to your app. They handle the `<s-page>` wrapper, heading updates, and header button injection automatically.

### KleroFeedbackPage

```tsx
<KleroFeedbackPage slug="my-project" customerToken={token} />
```

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `slug` | `string` | Yes | Your Klero project slug |
| `baseUrl` | `string` | No | Override API base URL (for custom domains) |
| `customerToken` | `string` | No | JWT for the logged-in Shopify user |

### KleroRoadmapPage

```tsx
<KleroRoadmapPage slug="my-project" customerToken={token} />
```

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `slug` | `string` | Yes | Your Klero project slug |
| `baseUrl` | `string` | No | Override API base URL |
| `customerToken` | `string` | No | JWT for the logged-in user |
| `roadmapSlug` | `string` | No | Show a specific roadmap by slug |

### KleroChangelogPage

```tsx
<KleroChangelogPage slug="my-project" customerToken={token} />
```

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `slug` | `string` | Yes | Your Klero project slug |
| `baseUrl` | `string` | No | Override API base URL |
| `customerToken` | `string` | No | JWT for the logged-in user |

## Low-level Widget Components

Use these if you need to render widgets inside your own `<s-page>` and manage heading/view state yourself.

### KleroFeedbackWidget

```tsx
<KleroFeedbackWidget
  slug="my-project"
  customerToken={token}
  pageEl={pageEl}
  onDetailChange={(title) => setHeading(title ?? 'Feedback')}
/>
```

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `slug` | `string` | Yes | Your Klero project slug |
| `pageEl` | `HTMLElement` | No | The `<s-page>` DOM element — used to inject action buttons into the page header |
| `baseUrl` | `string` | No | Override API base URL (for custom domains) |
| `customerToken` | `string` | No | JWT for the logged-in Shopify user |
| `onDetailChange` | `(title: string \| null) => void` | No | Called when drilling into/out of a feedback item (use to update page heading) |
| `onViewChange` | `(view: 'list' \| 'detail') => void` | No | Called on view transitions |

### KleroRoadmapWidget

```tsx
<KleroRoadmapWidget
  slug="my-project"
  customerToken={token}
  pageEl={pageEl}
  onDetailChange={(title) => setHeading(title ?? 'Roadmap')}
/>
```

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `slug` | `string` | Yes | Your Klero project slug |
| `pageEl` | `HTMLElement` | No | The `<s-page>` DOM element |
| `roadmapSlug` | `string` | No | Show a specific roadmap by slug |
| `baseUrl` | `string` | No | Override API base URL |
| `customerToken` | `string` | No | JWT for the logged-in user |
| `onDetailChange` | `(title: string \| null) => void` | No | Called when entering/leaving item detail |
| `onViewChange` | `(view: 'list' \| 'detail' \| 'create') => void` | No | Called on view transitions |

### KleroChangelogWidget

```tsx
<KleroChangelogWidget
  slug="my-project"
  customerToken={token}
  pageEl={pageEl}
  onDetailChange={(title) => setHeading(title ?? 'Changelog')}
/>
```

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `slug` | `string` | Yes | Your Klero project slug |
| `pageEl` | `HTMLElement` | No | The `<s-page>` DOM element |
| `baseUrl` | `string` | No | Override API base URL |
| `customerToken` | `string` | No | JWT for the logged-in user |
| `onDetailChange` | `(title: string \| null) => void` | No | Called when entering/leaving an entry |

## Server-side authentication

Use `signKleroJwtFromShopifySession` in a loader to issue a customer JWT from the Shopify admin session. This maps the logged-in merchant/staff user to a Klero customer so their votes and comments are attributed correctly.

```ts
// In a React Router loader
import { signKleroJwtFromShopifySession } from '@kleroai/shopify';

export const loader = async ({ request }: LoaderFunctionArgs) => {
  const { session } = await authenticate.admin(request);

  return {
    customerToken: await signKleroJwtFromShopifySession(
      process.env.KLERO_JWT_SECRET || '',
      session
    ),
  };
};
```

Returns `null` if the session has no associated user (e.g. offline token). The component accepts `null` / `undefined` and falls back to anonymous mode.

> **Important:** `signKleroJwtFromShopifySession` requires an **online** Shopify session to read the associated user. If your `shopify.server.ts` was generated from the default Shopify CLI template it uses offline tokens by default, and users will appear anonymous. Add `useOnlineTokens: true` to your `shopifyApp()` config:
>
> ```ts
> // app/shopify.server.ts
> const shopify = shopifyApp({
>   // ...
>   useOnlineTokens: true,
> });
> ```
>
> After adding this, re-authenticate your app (reinstall or go through the OAuth flow again) so Shopify creates online sessions.

For lower-level control, use `signKleroJwt` directly:

```ts
import { signKleroJwt } from '@kleroai/shopify';

const token = await signKleroJwt(process.env.KLERO_JWT_SECRET, {
  id: 'user-123',
  email: 'user@example.com',
  name: 'Jane Smith',
});
```

## Imperative API

For non-React usage, mount widgets imperatively:

```ts
import { KleroFeedback, KleroRoadmap, KleroChangelog } from '@kleroai/shopify';

const feedback = await KleroFeedback.init({
  slug: 'my-project',
  el: '#feedback-container',
  customerToken: token,
});

// Clean up
feedback.destroy();
```

> **Note:** `init()` is async — it loads the Klero UI bundle from the server on first call, then resolves with the widget instance.

## License

Proprietary.