> ## 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 React app

> Add the Claimr widget to a React app built with Vite, Create React App, or React Router, with a provider, a widget component, typed SDK access, user tokens, and wallet support.

This guide adds Claimr to a client-rendered React app (Vite, Create React App, or any React Router setup). For Next.js, follow the [Next.js guide](/frameworks/nextjs) instead, since it adds server-side rendering and server routes.

You will build four small pieces:

| File | Purpose |
| - | - |
| `src/lib/claimr.ts` | Framework-agnostic loader, readiness helpers, and TypeScript types. |
| `src/claimr/ClaimrProvider.tsx` | Loads the script once, assigns SDK callbacks, and shares user and campaign state through context. |
| `src/claimr/ClaimrWidget.tsx` | Renders the container the widget attaches to. Place it on any page or route. |
| `src/claimr/useClaimr.ts` | Hook for reading Claimr state and calling SDK methods from components. |

<Info>
  Read [How the loader works](/frameworks/overview#how-the-loader-works) first if you want to understand why the code is split this way. In short: the script loads once at the app root, and the widget attaches to whichever container is currently rendered.
</Info>

## Prerequisites

* React 18 or later.
* Your organization ID and campaign ID from **Publish** > **Get embed code** in the Claimr admin panel.

## Set up the integration

<Steps>
  <Step title="Add environment variables">
    Create or update `.env` in your project root. These values are public.

    ```bash .env theme={"dark"}
    VITE_CLAIMR_ORGANIZATION=your-organization-id
    VITE_CLAIMR_CAMPAIGN=your-campaign-id
    ```

    With Create React App, use the `REACT_APP_` prefix and read them from `process.env` instead of `import.meta.env`.
  </Step>

  <Step title="Add the shared loader module">
    Create `src/lib/claimr.ts` with the contents of [the shared loader module](/frameworks/overview#the-shared-loader-module). Copy it as is. It has no dependencies.
  </Step>

  <Step title="Create the provider">
    The provider is the only place that loads the script and assigns callbacks. It turns SDK callbacks into React state.

    ```tsx src/claimr/ClaimrProvider.tsx expandable theme={"dark"}
    import { createContext, ReactNode, useEffect, useMemo, useState } from 'react';
    import {
      ClaimrCampaignInfo,
      ClaimrConfig,
      ClaimrUser,
      getClaimr,
      loadClaimr,
    } from '../lib/claimr';

    export const CLAIMR_CONTAINER_ID = 'claimr-widget';

    export interface ClaimrState {
      /** The script has loaded and window.claimr exists. */
      loaded: boolean;
      /** The script failed to load, for example because an ad blocker blocked it. */
      error: Error | null;
      /** The user signed in to the widget, or null. */
      user: ClaimrUser | null;
      campaign: ClaimrCampaignInfo | null;
    }

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

    interface Props {
      children: ReactNode;
      /** Optional user token generated by your 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(() => {
        const config: ClaimrConfig = {
          organization: import.meta.env.VITE_CLAIMR_ORGANIZATION,
          campaign: import.meta.env.VITE_CLAIMR_CAMPAIGN,
          container: CLAIMR_CONTAINER_ID,
          autoresize: true,
          language,
          userToken,
        };

        // loadClaimr is idempotent, so Strict Mode's double effect is harmless.
        loadClaimr(config, (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 effects below.
        // eslint-disable-next-line react-hooks/exhaustive-deps
      }, []);

      // Keep the widget's user in sync with your app's user.
      useEffect(() => {
        if (!loaded) return;
        const claimr = getClaimr();
        if (userToken) claimr?.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">
    The widget component only renders the container. The loader notices it and attaches the widget. When the component unmounts, the loader removes the widget on its own.

    ```tsx src/claimr/ClaimrWidget.tsx theme={"dark"}
    import { useEffect } from 'react';
    import { CLAIMR_CONTAINER_ID } from './ClaimrProvider';

    interface Props {
      className?: string;
    }

    export function ClaimrWidget({ className }: Props) {
      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} />;
    }
    ```

    <Warning>
      Render at most one `ClaimrWidget` at a time. The loader attaches to the first element with the container ID.
    </Warning>
  </Step>

  <Step title="Create the hook">
    ```ts src/claimr/useClaimr.ts theme={"dark"}
    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="Wrap your app">
    Wrap the whole app once, above your router, so the script survives route changes.

    ```tsx src/main.tsx theme={"dark"}
    import { StrictMode } from 'react';
    import { createRoot } from 'react-dom/client';
    import { BrowserRouter } from 'react-router-dom';
    import { ClaimrProvider } from './claimr/ClaimrProvider';
    import { App } from './App';

    createRoot(document.getElementById('root')!).render(
      <StrictMode>
        <ClaimrProvider>
          <BrowserRouter>
            <App />
          </BrowserRouter>
        </ClaimrProvider>
      </StrictMode>,
    );
    ```
  </Step>

  <Step title="Render the widget">
    Place `ClaimrWidget` on the page or route where the campaign should appear.

    ```tsx src/pages/QuestsPage.tsx theme={"dark"}
    import { ClaimrWidget } from '../claimr/ClaimrWidget';
    import { useClaimr } from '../claimr/useClaimr';

    export function QuestsPage() {
      const { user, error } = useClaimr();

      return (
        <section>
          <h1>Quests</h1>
          {user && <p>Signed in to Claimr</p>}
          {error && <p>The quests could not be loaded. Disable your ad blocker and reload the page.</p>}
          <ClaimrWidget className="claimr-widget" />
        </section>
      );
    }
    ```

    With React Router, other routes simply do not render the container, and the loader detaches the widget when you leave this route.
  </Step>
</Steps>

## Use the SDK from components

Call SDK methods through the hook. Methods that need a loaded widget are wrapped in `withWidget`, so they are safe to call at any time.

```tsx theme={"dark"}
import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useClaimr } from '../claimr/useClaimr';

export function VideoLesson({ taskId }: { taskId: string }) {
  const { completeTask, setLanguage } = useClaimr();
  const { i18n } = useTranslation();

  // Follow the app's language.
  useEffect(() => {
    setLanguage(i18n.language);
  }, [i18n.language, setLanguage]);

  // Mark a front-end task as complete when the user finishes the video.
  return <video src="/lesson.mp4" controls onEnded={() => completeTask(taskId)} />;
}
```

`complete_task` only works for tasks configured as front-end tasks in the campaign. See [SDK tasks](/tasks/sdk-tasks).

To react to other widget events, add more callbacks in the provider's `setup` function, for example `claimr.on_leaderboard_open` or `claimr.on_analytics_event`, and expose them through context. Do not assign callbacks from other components: each callback has a single owner, so the last assignment wins.

## Sign users in with your own accounts

If your users already sign in to your app, give them a Claimr user token so they do not have to sign in again inside the widget.

<Steps>
  <Step title="Generate the token on your server">
    Your API token is secret. Call the token endpoint from your backend, never from React code.

    ```ts server/claimr-token.ts theme={"dark"}
    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!); // a short name for your platform, not "claimr"
      if (name) url.searchParams.set('name', name);

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

    Expose it through an authenticated endpoint of your own, for example `GET /api/claimr-token`, that reads the current user from the session. The token does not expire, so you can store it with the user record.
  </Step>

  <Step title="Pass the token to the provider">
    Wrap `ClaimrProvider` in a component that knows your signed-in user, and use it in `main.tsx` in place of the plain `<ClaimrProvider>`. It must render inside your auth provider.

    ```tsx src/claimr/ClaimrAuthProvider.tsx theme={"dark"}
    import { ReactNode, useEffect, useState } from 'react';
    import { ClaimrProvider } from './ClaimrProvider';
    import { useAuth } from '../auth'; // your app's auth hook

    export function ClaimrAuthProvider({ children }: { children: ReactNode }) {
      const { user } = useAuth();
      const [claimrToken, setClaimrToken] = useState<string>();

      useEffect(() => {
        if (!user) return setClaimrToken(undefined);
        fetch('/api/claimr-token', { credentials: 'include' })
          .then((r) => r.json())
          .then((body) => setClaimrToken(body.token));
      }, [user]);

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

  <Step title="Log out of Claimr when the user logs out of your app">
    ```ts theme={"dark"}
    import { getClaimr } from './lib/claimr';

    async function signOut() {
      await yourAuth.signOut();
      getClaimr()?.logout();
    }
    ```

    When a different user signs in without a page reload, call `logout()` before the new token reaches `set_user_token`.
  </Step>
</Steps>

Enable the matching sign-in option in the campaign under **Settings** > **Sign-in options**.

## Connect the user's wallet (dApps)

If your app already connects a wallet, let the widget use it instead of asking the user to connect again. Set `platform: 'dapp'` in the provider config and implement `on_wallet_request`. This example uses wagmi and RainbowKit, and must render inside `WagmiProvider` and `RainbowKitProvider`.

```tsx src/claimr/ClaimrWalletBridge.tsx expandable theme={"dark"}
import { useEffect, useRef } from 'react';
import { useConnectModal } from '@rainbow-me/rainbowkit';
import { useAccount, useChainId, useDisconnect, useSignMessage, useSwitchChain, useWriteContract } from 'wagmi';
import type { Abi, Address } from 'viem';
import { getClaimr, type ClaimrWalletRequest } from '../lib/claimr';

/** Answers the widget's wallet requests with the wallet your app already manages. Render once. */
export function ClaimrWalletBridge() {
  const { address } = useAccount();
  const chainId = useChainId();
  const { openConnectModal } = useConnectModal();
  const { signMessageAsync } = useSignMessage();
  const { writeContractAsync } = useWriteContract();
  const { switchChainAsync } = useSwitchChain();
  const { disconnect } = useDisconnect();

  useEffect(() => {
    const handler = async (request: ClaimrWalletRequest) => {
      switch (request.op) {
        case 'connect':
          if (address) return { address, chain: String(chainId) };
          openConnectModal?.();
          throw new Error('The user has not connected a wallet yet');
        case 'sign_message':
          return { signature: await signMessageAsync({ message: request.message }) };
        case 'send_transaction': {
          const target = Number(request.chain_id);
          if (target !== chainId) await switchChainAsync({ chainId: target });
          const hash = await writeContractAsync({
            abi: request.abi as Abi,
            address: request.contract as Address,
            functionName: request.method,
            args: request.args,
            chainId: target,
          });
          return { hash };
        }
        case 'disconnect':
          disconnect();
          return {};
      }
    };

    // The handler closes over the latest wallet state, so reassign it when that state changes.
    const claimr = getClaimr();
    if (claimr) claimr.on_wallet_request = handler;
  }, [address, chainId, openConnectModal, signMessageAsync, writeContractAsync, switchChainAsync, disconnect]);

  // Log the widget out when the wallet disconnects or changes. Comparing with the previous address
  // skips the page load, when wagmi briefly reports no address while it reconnects.
  const previousAddress = useRef(address);
  useEffect(() => {
    if (previousAddress.current && previousAddress.current !== address) getClaimr()?.logout();
    previousAddress.current = address;
  }, [address]);

  return null;
}
```

<Note>
  Render `ClaimrWalletBridge` inside `ClaimrProvider` and only after the provider reports `loaded`, for example `{loaded && <ClaimrWalletBridge />}`. That guarantees `window.claimr` exists when the handler is assigned.
</Note>

Each `op` must return a value or throw. Throwing reports a failure to the widget immediately, instead of leaving it waiting. See [`on_wallet_request`](/sdk-guide#on_wallet_request) for the full contract and [Integrate Claimr widget into your dApp](/how-to/integrate-claimr-widget-into-dapp) for the campaign settings a dApp needs.

## Verify the integration

1. Run the app and open the page with `ClaimrWidget`. The campaign renders inside the container.
2. In the browser console, run `window.claimr.is_claimr_ready`. It returns `true`.
3. Navigate to another route and back. The widget disappears and comes back, and `document.querySelectorAll('#claimr-script').length` is still `1`.
4. Open a quest, then press the browser back button. The page still scrolls.
5. If you use user tokens, sign in to your app and confirm the widget shows the same user without a second sign-in.

To see what the loader is doing, add `debug: true` to the provider config. The loader then logs its steps to the console.
