> ## Documentation Index
> Fetch the complete documentation index at: https://docs.claimr.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Integrate Claimr into a Next.js app

> Add the Claimr widget to a Next.js app with the App Router or Pages Router, keep it working across client-side navigation, and generate user tokens on the server.

This guide adds Claimr to a Next.js 13.4+ app. It covers the App Router first, then the [Pages Router](#pages-router). The widget always runs in the browser; Next.js server features are only used to keep your API token secret while generating user tokens.

| File | Runs on | Purpose |
| - | - | - |
| `lib/claimr.ts` | Browser | Framework-agnostic loader, readiness helpers, and TypeScript types. |
| `claimr/ClaimrProvider.tsx` | Browser (`'use client'`) | Loads the script once from the root layout and shares Claimr state through context. |
| `claimr/ClaimrWidget.tsx` | Browser (`'use client'`) | Renders the container the widget attaches to. |
| `claimr/useClaimr.ts` | Browser | Hook for Claimr state and SDK methods. |
| `lib/claimr-server.ts` | Server only | Generates user tokens with your secret API token. |
| `app/api/claimr/token/route.ts` | Server | Returns a user token to the signed-in user. Optional. |

Paths below assume the `@/` import alias points at your source root, which is the `create-next-app` default.

<Info>
  Read [How the loader works](/frameworks/overview#how-the-loader-works) for the reasoning behind this layout. In short: the script loads once in the root layout, which persists across client-side navigation, and the widget attaches to whichever page currently renders the container.
</Info>

## Set up the integration (App Router)

<Steps>
  <Step title="Add environment variables">
    ```bash .env.local theme={"dark"}
    # Public: inlined into the browser bundle
    NEXT_PUBLIC_CLAIMR_ORGANIZATION=your-organization-id
    NEXT_PUBLIC_CLAIMR_CAMPAIGN=your-campaign-id

    # Server only: never prefix these with NEXT_PUBLIC_
    CLAIMR_API_TOKEN=your-secret-api-token
    CLAIMR_PLATFORM=your-platform-name
    ```

    You only need `CLAIMR_API_TOKEN` and `CLAIMR_PLATFORM` if your users sign in through your app. `CLAIMR_PLATFORM` is a short name for your platform. It cannot be `claimr`.
  </Step>

  <Step title="Add the shared loader module">
    Create `lib/claimr.ts` with the contents of [the shared loader module](/frameworks/overview#the-shared-loader-module). It guards every `window` access, so importing it from a client component is safe during server rendering.
  </Step>

  <Step title="Create the provider">
    ```tsx claimr/ClaimrProvider.tsx expandable theme={"dark"}
    'use client';

    import { createContext, ReactNode, useEffect, useMemo, useState } from 'react';
    import { ClaimrCampaignInfo, ClaimrUser, getClaimr, loadClaimr } from '@/lib/claimr';

    export const CLAIMR_CONTAINER_ID = 'claimr-widget';

    export interface ClaimrState {
      loaded: boolean;
      error: Error | null;
      user: ClaimrUser | null;
      campaign: ClaimrCampaignInfo | null;
    }

    export const ClaimrContext = createContext<ClaimrState | null>(null);

    interface Props {
      children: ReactNode;
      /** User token generated on the server for the signed-in user. */
      userToken?: string;
      language?: string;
    }

    export function ClaimrProvider({ children, userToken, language }: Props) {
      const [loaded, setLoaded] = useState(false);
      const [error, setError] = useState<Error | null>(null);
      const [user, setUser] = useState<ClaimrUser | null>(null);
      const [campaign, setCampaign] = useState<ClaimrCampaignInfo | null>(null);

      useEffect(() => {
        // Effects only run in the browser, so window is available here.
        loadClaimr(
          {
            organization: process.env.NEXT_PUBLIC_CLAIMR_ORGANIZATION!,
            campaign: process.env.NEXT_PUBLIC_CLAIMR_CAMPAIGN!,
            container: CLAIMR_CONTAINER_ID,
            autoresize: true,
            language,
            userToken,
          },
          (claimr) => {
            claimr.on_user_info = (info) => setUser(info);
            claimr.on_campaign_info = (info) => setCampaign(info);
            claimr.on_logout = () => setUser(null);
          },
        )
          .then(() => setLoaded(true))
          .catch(setError);
        // Load once. Later prop changes are applied by the effect below.
        // eslint-disable-next-line react-hooks/exhaustive-deps
      }, []);

      useEffect(() => {
        if (loaded && userToken) getClaimr()?.set_user_token(userToken);
      }, [loaded, userToken]);

      const value = useMemo(() => ({ loaded, error, user, campaign }), [loaded, error, user, campaign]);

      return <ClaimrContext.Provider value={value}>{children}</ClaimrContext.Provider>;
    }
    ```
  </Step>

  <Step title="Create the widget component">
    ```tsx claimr/ClaimrWidget.tsx theme={"dark"}
    'use client';

    import { useEffect } from 'react';
    import { CLAIMR_CONTAINER_ID } from './ClaimrProvider';

    export function ClaimrWidget({ className }: { className?: string }) {
      useEffect(() => {
        return () => {
          // An open quest popup locks page scroll. Release it if the user navigates away mid-quest.
          document.body.style.overflow = '';
        };
      }, []);

      // Keep this element empty and do not bind an inline height: the loader owns both.
      return <div id={CLAIMR_CONTAINER_ID} className={className} />;
    }
    ```

    The empty `<div>` is rendered on the server too. That is intended: the script loads after hydration, so the loader adds its iframe to a node React has already hydrated, and there is no hydration mismatch.
  </Step>

  <Step title="Create the hook">
    ```ts claimr/useClaimr.ts theme={"dark"}
    'use client';

    import { useCallback, useContext } from 'react';
    import { getClaimr, withWidget } from '@/lib/claimr';
    import { ClaimrContext } from './ClaimrProvider';

    export function useClaimr() {
      const state = useContext(ClaimrContext);
      if (!state) throw new Error('useClaimr must be used inside <ClaimrProvider>');

      const openQuest = useCallback((questId: string) => getClaimr()?.open_quest(questId), []);
      const login = useCallback(() => getClaimr()?.login(), []);
      const logout = useCallback(() => getClaimr()?.logout(), []);
      const completeTask = useCallback((taskId: string) => withWidget((c) => c.complete_task(taskId)), []);
      const setLanguage = useCallback((code: string) => withWidget((c) => c.set_language(code)), []);
      const setTheme = useCallback((theme: string) => withWidget((c) => c.set_theme(theme)), []);

      return { ...state, openQuest, login, logout, completeTask, setLanguage, setTheme };
    }
    ```
  </Step>

  <Step title="Add the provider to the root layout">
    The root layout is a Server Component. It can render the client provider directly.

    ```tsx app/layout.tsx theme={"dark"}
    import type { ReactNode } from 'react';
    import { ClaimrProvider } from '@/claimr/ClaimrProvider';

    export default function RootLayout({ children }: { children: ReactNode }) {
      return (
        <html lang="en">
          <body>
            <ClaimrProvider>{children}</ClaimrProvider>
          </body>
        </html>
      );
    }
    ```

    Put the provider in the **root** layout, not in a nested layout or a page. The root layout never unmounts during client-side navigation, so the script loads exactly once.
  </Step>

  <Step title="Render the widget on a page">
    Pages stay Server Components. Only the widget itself is a client component.

    ```tsx app/quests/page.tsx theme={"dark"}
    import { ClaimrWidget } from '@/claimr/ClaimrWidget';

    export default function QuestsPage() {
      return (
        <main>
          <h1>Quests</h1>
          <ClaimrWidget className="claimr-widget" />
        </main>
      );
    }
    ```

    When the user navigates with `<Link>` to a page without `ClaimrWidget`, the loader detaches the widget. When they come back, it attaches it again.
  </Step>
</Steps>

<Note>
  Prefer `next/script`? You can load the script with `<Script id="claimr-script" src="https://widgets.claimr.io/claimr.min.js" strategy="afterInteractive" data-organization="..." data-campaign="..." data-container="claimr-widget" data-autoresize="true" onLoad={...} />` in the root layout instead of `loadClaimr`. Keep the ID exactly `claimr-script`, keep it in the root layout only, and assign callbacks in `onLoad`. The rest of this guide works the same way.
</Note>

## Use the SDK from components

Any client component inside the provider can call SDK methods through the hook:

```tsx components/OpenQuestButton.tsx theme={"dark"}
'use client';

import { useClaimr } from '@/claimr/useClaimr';

export function OpenQuestButton({ questId }: { questId: string }) {
  const { openQuest, user, login } = useClaimr();
  return user ? (
    <button onClick={() => openQuest(questId)}>Start quest</button>
  ) : (
    <button onClick={login}>Sign in to start</button>
  );
}
```

`openQuest` works on pages without the widget too: the quest is remembered and opens when the user reaches a page that renders `ClaimrWidget`. Methods such as `completeTask` and `setLanguage` wait for the widget internally.

## Sign users in with your own accounts

If your users already sign in to your app, generate a Claimr user token on the server and pass it to the provider.

<Steps>
  <Step title="Create the server helper">
    ```ts lib/claimr-server.ts theme={"dark"}
    import 'server-only';

    export async function getClaimrUserToken(account: string, name?: string): Promise<string> {
      const url = new URL('https://prod.claimr.io/api/v1/token');
      url.searchParams.set('account', account); // your stable user ID
      url.searchParams.set('platform', process.env.CLAIMR_PLATFORM!);
      if (name) url.searchParams.set('name', name);

      const response = await fetch(url, {
        headers: { Authorization: `Bearer ${process.env.CLAIMR_API_TOKEN}` },
        cache: 'no-store',
      });
      const body = await response.json();
      if (!response.ok || !body.success) throw new Error('Claimr token request failed');
      return body.data.token as string;
    }
    ```

    The `server-only` import makes the build fail if a client component ever imports this file. Install it with `npm install server-only`.
  </Step>

  <Step title="Pass the token from the root layout">
    ```tsx app/layout.tsx theme={"dark"}
    import type { ReactNode } from 'react';
    import { ClaimrProvider } from '@/claimr/ClaimrProvider';
    import { getClaimrUserToken } from '@/lib/claimr-server';
    import { auth } from '@/auth'; // your auth library, for example Auth.js

    export default async function RootLayout({ children }: { children: ReactNode }) {
      const session = await auth();
      const claimrToken = session?.user?.id
        ? await getClaimrUserToken(session.user.id, session.user.name ?? undefined)
        : undefined;

      return (
        <html lang="en">
          <body>
            <ClaimrProvider userToken={claimrToken}>{children}</ClaimrProvider>
          </body>
        </html>
      );
    }
    ```

    <Warning>
      Reading the session in the root layout makes every route dynamic, and this example calls the Claimr API on each request. The token never expires, so store it with your user record after the first request and read it from there.
    </Warning>
  </Step>

  <Step title="Or fetch the token from a route handler">
    If you want to keep pages static, or users sign in without a full page load, expose the token through a route handler and fetch it from the client.

    ```ts app/api/claimr/token/route.ts theme={"dark"}
    import { NextResponse } from 'next/server';
    import { getClaimrUserToken } from '@/lib/claimr-server';
    import { auth } from '@/auth';

    export async function GET() {
      const session = await auth();
      if (!session?.user?.id) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });

      const token = await getClaimrUserToken(session.user.id, session.user.name ?? undefined);
      return NextResponse.json({ token });
    }
    ```

    ```tsx claimr/ClaimrAuthProvider.tsx theme={"dark"}
    'use client';

    import { ReactNode, useEffect, useState } from 'react';
    import { useSession } from 'next-auth/react'; // or your auth library's client hook
    import { ClaimrProvider } from './ClaimrProvider';

    export function ClaimrAuthProvider({ children }: { children: ReactNode }) {
      const { status } = useSession();
      const [token, setToken] = useState<string>();

      useEffect(() => {
        if (status !== 'authenticated') return setToken(undefined);
        fetch('/api/claimr/token')
          .then((r) => r.json())
          .then((body) => setToken(body.token));
      }, [status]);

      return <ClaimrProvider userToken={token}>{children}</ClaimrProvider>;
    }
    ```

    Use `ClaimrAuthProvider` in the root layout in place of `ClaimrProvider`.
  </Step>

  <Step title="Log out of Claimr when the user logs out">
    ```ts theme={"dark"}
    'use client';

    import { signOut } from 'next-auth/react';
    import { getClaimr } from '@/lib/claimr';

    export async function handleSignOut() {
      getClaimr()?.logout();
      await signOut();
    }
    ```
  </Step>
</Steps>

Enable the matching sign-in option in the campaign under **Settings** > **Sign-in options**. See [User token](/api-guide/user-token).

## Pages Router

The same files work with the Pages Router. Drop the `'use client'` directives (they are ignored there), replace `server-only` with a plain module that you only import from API routes or `getServerSideProps`, and wire it up like this:

```tsx pages/_app.tsx theme={"dark"}
import type { AppProps } from 'next/app';
import { ClaimrProvider } from '@/claimr/ClaimrProvider';

export default function App({ Component, pageProps }: AppProps) {
  return (
    // claimrToken is optional: return it from getServerSideProps, or fetch it from the API route below.
    <ClaimrProvider userToken={pageProps.claimrToken}>
      <Component {...pageProps} />
    </ClaimrProvider>
  );
}
```

```ts pages/api/claimr-token.ts theme={"dark"}
import type { NextApiRequest, NextApiResponse } from 'next';
import { getClaimrUserToken } from '@/lib/claimr-server';
import { getUserFromRequest } from '@/auth'; // your auth helper

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  const user = await getUserFromRequest(req);
  if (!user) return res.status(401).json({ error: 'Unauthorized' });
  res.json({ token: await getClaimrUserToken(user.id, user.name) });
}
```

`_app` persists across client-side navigation in the Pages Router, just as the root layout does in the App Router.

## Wallets and Telegram

* **dApp with its own wallet**: add `platform: 'dapp'` to the `loadClaimr` config and render the `ClaimrWalletBridge` component from the [React guide](/frameworks/react#connect-the-users-wallet-dapps) inside your wagmi providers. Mark it `'use client'`.
* **Telegram Mini App**: add `platform: 'telegram'` to the config. See [Telegram Mini App](/how-to/integrating-claimr-widget-into-telegram-mini-app).

## Verify the integration

1. Run `npm run dev` and open the page with `ClaimrWidget`. The campaign renders, and the terminal shows no `window is not defined` errors.
2. In the browser console, `window.claimr.is_claimr_ready` returns `true`.
3. Navigate away with a `<Link>` and back. The widget comes back, and `document.querySelectorAll('#claimr-script').length` is still `1`.
4. Run `npm run build`. The build succeeds, and `CLAIMR_API_TOKEN` does not appear in `.next/static`.
5. If you use user tokens, sign in to your app and confirm the widget shows the same user without a second sign-in.
