npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

cilantro-react

v0.1.12

Published

React SDK/UI for Cilantro Smart Wallet - providers, hooks, and headless components

Readme

cilantro-react

React SDK/UI for Cilantro Smart Wallet. Provides a single context provider, hooks, and UI components for authentication, wallets, signers, and transactions.

Table of Contents


Installation

npm install cilantro-react cilantro-sdk
# or
yarn add cilantro-react cilantro-sdk
# or
pnpm add cilantro-react cilantro-sdk

Requirements:

  • React 18+
  • cilantro-sdk (peer dependency)
  • Tailwind CSS for component styling (components use Tailwind-compatible class names)

Polyfills (Browser)

Import polyfills at the very top of your entry file (before any other imports):

import 'cilantro-sdk/polyfills';

Quick Start

import 'cilantro-sdk/polyfills';
import { CilantroProvider, LoginForm, AuthGuard, useAuth, useWallet } from 'cilantro-react';

function App() {
  return (
    <CilantroProvider
      apiKey={import.meta.env.VITE_API_KEY}
      baseURL="https://api.cilantro.gg"
    >
      <AuthGuard>
        <Dashboard />
      </AuthGuard>
    </CilantroProvider>
  );
}

function Dashboard() {
  const { user, logout } = useAuth();
  const { wallet, wallets } = useWallet();

  return (
    <div>
      <p>Welcome, {user?.username}</p>
      <p>Wallet: {wallet?.walletName}</p>
      <button onClick={logout}>Logout</button>
    </div>
  );
}

CilantroProvider

The root provider that initializes configuration, storage, authentication, and wallet state.

import { CilantroProvider, createIndexedDBAdapter } from 'cilantro-react';

const storage = createIndexedDBAdapter();

<CilantroProvider
  apiKey="your-platform-api-key"
  baseURL="https://api.cilantro.gg"
  storageAdapter={storage}
>
  <App />
</CilantroProvider>

Props

| Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | apiKey | string | No* | — | Platform API key for server-side operations | | baseURL | string | No | https://api.cilantro.gg | API base URL | | storageAdapter | DeviceKeyStorage | No | IndexedDB | Storage adapter for device keys | | syncJwtToCookie | boolean | No | false | When true, sync JWT to a cookie for Next.js/middleware | | jwtCookieName | string | No | cilantro_jwt | Cookie name when syncJwtToCookie is true | | children | ReactNode | Yes | — | App content |

*Either apiKey or JWT (obtained via login) is required for authenticated requests.

SDK Auth (automatic)

CilantroProvider configures the underlying cilantro-sdk authentication when the user is logged in. Once you have a JWT in context (from login or session restore), all cilantro-sdk calls in the same app use that auth automatically. You do not need to call setSdkAuth or similar before each request; the provider handles it.

Storage Adapters

For email and phone signers, device keys must be stored. Choose an adapter:

| Adapter | Use Case | |---------|----------| | createIndexedDBAdapter() | Production – persistent, large capacity | | createLocalStorageAdapter() | Development – simple, limited capacity | | createMemoryAdapter() | Testing – not persisted |

import { createIndexedDBAdapter, createLocalStorageAdapter, createMemoryAdapter } from 'cilantro-react';

// Production
const storage = createIndexedDBAdapter();

// Development
const storage = createLocalStorageAdapter();

// Testing
const storage = createMemoryAdapter();

Next.js / Middleware

For Next.js apps, middleware runs on the server and cannot access localStorage. To protect routes or read the JWT in middleware:

  1. Enable JWT cookie sync: <CilantroProvider syncJwtToCookie>
  2. In middleware, read the cookie (default name: cilantro_jwt) to check auth.
  3. Optionally redirect unauthenticated users. Example:
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const token = request.cookies.get('cilantro_jwt')?.value;
  if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', request.url));
  }
  return NextResponse.next();
}

Hooks

useAuth

Authentication state and actions.

import { useAuth } from 'cilantro-react';

function MyComponent() {
  const { user, jwt, isLoading, isAuthenticated, login, register, logout, clearSessionDueToAuthError } = useAuth();

  const handleLogin = async () => {
    await login({ usernameOrEmail: '[email protected]', password: 'password123' });
  };

  const handleRegister = async () => {
    await register('johndoe', '[email protected]', 'password123');
  };

  return (
    <div>
      {isLoading && <p>Loading...</p>}
      {isAuthenticated ? (
        <>
          <p>Hello, {user?.username}</p>
          <button onClick={logout}>Logout</button>
        </>
      ) : (
        <button onClick={handleLogin}>Login</button>
      )}
    </div>
  );
}

Returns: UseAuthResult

| Property | Type | Description | |----------|------|-------------| | user | User \| null | Current user object ({ username?, email?, userType? }) | | jwt | string \| null | JWT token (null when not authenticated). Prefer this name. | | token | string \| null | Alias for jwt. Same value. | | isLoading | boolean | True while restoring session from storage | | isAuthenticated | boolean | True when user has valid JWT | | login | (params: { usernameOrEmail: string; password: string }) => Promise<void> | Log in; throws on error | | loginWithSocial | (params: { provider: "google" \| "apple"; id_token: string }) => Promise<void> | Sign in with Google or Apple OAuth | | register | (username: string, email: string, password: string, isActive?: boolean) => Promise<void> | Register and auto-login | | logout | () => void | Clear session and token | | clearSessionDueToAuthError | () => void | Clear session when API returns 401/403; calls onSessionExpired |


useReturnUrl

Read returnUrl from URL search params for redirect-after-auth flows.

import { useReturnUrl } from 'cilantro-react';

function LoginPage() {
  const { returnPath, redirect, rawReturnUrl } = useReturnUrl({ param: 'returnUrl', defaultPath: '/' });

  return (
    <LoginForm
      redirectAfterSuccess={returnPath}
      onRedirect={(path) => router.replace(path)}
    />
  );
}

useWallet

Wallet state and actions.

import { useWallet } from 'cilantro-react';

function WalletManager() {
  const { wallet, wallets, createWallet, selectWallet, refreshWallets, isLoading, error } = useWallet();

  const handleCreate = async () => {
    const result = await createWallet({ name: 'My New Wallet' });
    console.log('Created:', result.data.id);
  };

  return (
    <div>
      {isLoading && <p>Loading wallets...</p>}
      {error && <p className="text-red-500">{error}</p>}
      <p>Current wallet: {wallet?.walletName ?? 'None selected'}</p>
      <ul>
        {wallets.map((w) => (
          <li key={w.id} onClick={() => selectWallet(w.id)}>
            {w.walletName}
          </li>
        ))}
      </ul>
      <button onClick={handleCreate}>Create Wallet</button>
      <button onClick={refreshWallets}>Refresh</button>
    </div>
  );
}

Returns: UseWalletResult

| Property | Type | Description | |----------|------|-------------| | wallet | WalletData \| null | Currently selected wallet | | wallets | WalletData[] | All wallets for the authenticated user | | createWallet | (params: { name: string; userId?: string }) => Promise<WalletControllerCreateResult> | Create a new wallet | | selectWallet | (walletId: string) => void | Select a wallet by ID | | refreshWallets | () => Promise<void> | Refresh wallet list from API | | isLoading | boolean | True while loading wallets | | error | string \| null | Error from wallet operations (e.g. fetch failed) |


useSigners

Signers for a specific wallet.

import { useSigners } from 'cilantro-react';

function SignerManager({ walletId }: { walletId: string }) {
  const {
    signers,
    isLoading,
    error,
    refresh,
    createEmailSigner,
    createPhoneSigner,
    revokeSigner,
  } = useSigners(walletId);

  const handleCreateEmail = async () => {
    const signer = await createEmailSigner({ email: '[email protected]' });
    console.log('Created signer:', signer.signerId);
  };

  const handleCreatePhone = async () => {
    const signer = await createPhoneSigner({ phone: '+1234567890' });
    console.log('Created signer:', signer.signerId);
  };

  return (
    <div>
      {isLoading && <p>Loading signers...</p>}
      {error && <p className="text-red-500">{error}</p>}
      <ul>
        {signers.map((s) => (
          <li key={s.id}>
            {s.email || s.phone || s.id} ({s.signerType})
            <button onClick={() => revokeSigner(s.id)}>Revoke</button>
          </li>
        ))}
      </ul>
      <button onClick={handleCreateEmail}>Add Email Signer</button>
      <button onClick={handleCreatePhone}>Add Phone Signer</button>
      <button onClick={refresh}>Refresh</button>
    </div>
  );
}

Parameters:

  • walletId: string | null | { walletId?: string | null } — Wallet ID to load signers for

Returns: UseSignersResult

| Property | Type | Description | |----------|------|-------------| | signers | SignerData[] | List of signers for the wallet | | isLoading | boolean | True while loading | | error | string \| null | Error message if load failed | | refresh | () => Promise<void> | Reload signers | | createEmailSigner | (params: { email: string }) => Promise<SignerInfo> | Create email signer | | createPhoneSigner | (params: { phone: string }) => Promise<SignerInfo> | Create phone signer | | revokeSigner | (signerId: string) => Promise<void> | Revoke a signer |


usePasskey

Passkey (WebAuthn) registration and authentication.

import { usePasskey } from 'cilantro-react';

function PasskeyManager({ walletId }: { walletId: string }) {
  const { isSupported, register, authenticate, isLoading } = usePasskey(walletId);

  if (!isSupported) {
    return <p>Passkeys not supported in this browser.</p>;
  }

  const handleRegister = async () => {
    const result = await register();
    console.log('Passkey registered:', result.signerId);
  };

  const handleAuthenticate = async () => {
    const result = await authenticate();
    console.log('Authenticated with passkey');
  };

  return (
    <div>
      <button onClick={handleRegister} disabled={isLoading}>
        {isLoading ? 'Registering...' : 'Register Passkey'}
      </button>
      <button onClick={handleAuthenticate} disabled={isLoading}>
        {isLoading ? 'Authenticating...' : 'Authenticate'}
      </button>
    </div>
  );
}

Parameters:

  • walletId: string | undefined — Wallet ID to register passkey for

Returns: UsePasskeyResult

| Property | Type | Description | |----------|------|-------------| | isSupported | boolean | True if WebAuthn is available | | register | () => Promise<{ signerId: string; isNew: boolean }> | Register a new passkey | | authenticate | (signerId?: string) => Promise<{ credential: unknown }> | Authenticate with passkey | | isLoading | boolean | True during operation |


useExternalWallet

Connect to external Solana wallets (Phantom, Solflare, etc.).

import { useExternalWallet } from 'cilantro-react';

function ExternalWalletConnect() {
  const { wallets, connectedWallet, connect, disconnect } = useExternalWallet();

  return (
    <div>
      {connectedWallet ? (
        <div>
          <p>Connected: {connectedWallet.name}</p>
          <p>Public Key: {connectedWallet.publicKey?.toBase58()}</p>
          <button onClick={disconnect}>Disconnect</button>
        </div>
      ) : (
        <div>
          {wallets.map((wallet) => (
            <button key={wallet.name} onClick={() => connect(wallet)}>
              Connect {wallet.name}
            </button>
          ))}
        </div>
      )}
    </div>
  );
}

Returns: UseExternalWalletResult

| Property | Type | Description | |----------|------|-------------| | wallets | SolanaWallet[] | Detected Solana wallets (Phantom, Solflare, etc.) | | connectedWallet | SolanaWallet \| null | Currently connected wallet | | connect | (wallet: SolanaWallet) => Promise<void> | Connect to a wallet | | disconnect | () => void | Disconnect current wallet |


useSendTransaction

Send SOL and SPL tokens.

import { useSendTransaction } from 'cilantro-react';

function SendTransaction({ walletId, signerId }: { walletId: string; signerId: string }) {
  const { sendSOL, sendSPL, isPending, error } = useSendTransaction(walletId, signerId, 'email');

  const handleSendSOL = async () => {
    const result = await sendSOL({
      recipientAddress: '7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU',
      amountLamports: 100000000, // 0.1 SOL
    });
    console.log('Transaction signature:', result.data.signature);
  };

  const handleSendSPL = async () => {
    const result = await sendSPL({
      mintAddress: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', // USDC
      recipientAddress: '7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU',
      amount: 1000000, // 1 USDC (6 decimals)
    });
    console.log('Transaction signature:', result.data.signature);
  };

  return (
    <div>
      {error && <p className="text-red-500">{error}</p>}
      <button onClick={handleSendSOL} disabled={isPending}>
        {isPending ? 'Sending...' : 'Send SOL'}
      </button>
      <button onClick={handleSendSPL} disabled={isPending}>
        {isPending ? 'Sending...' : 'Send USDC'}
      </button>
    </div>
  );
}

Parameters:

  • walletId: string | undefined — Wallet to send from
  • signerId?: string — Signer ID (optional)
  • signerType?: 'email' | 'phone' | 'passkey' | 'external' — Signer type (optional)

Returns: UseSendTransactionResult

| Property | Type | Description | |----------|------|-------------| | sendSOL | (params: { recipientAddress: string; amountLamports: number }) => Promise<SubmitTransactionResult> | Send SOL | | sendSPL | (params: { mintAddress: string; recipientAddress: string; amount: number }) => Promise<SubmitTransactionResult> | Send SPL tokens | | isPending | boolean | True while sending | | error | string \| null | Error message if send failed |


useCilantroConfig

Access SDK configuration and storage adapter.

import { useCilantroConfig } from 'cilantro-react';

function ConfigDisplay() {
  const { config, storage } = useCilantroConfig();

  return (
    <div>
      <p>API Key: {config.apiKey ? '***' : 'Not set'}</p>
      <p>Base URL: {config.baseURL}</p>
      <p>Storage: {storage ? 'Configured' : 'Not configured'}</p>
    </div>
  );
}

Returns: UseCilantroConfigResult

| Property | Type | Description | |----------|------|-------------| | config | { apiKey?: string; baseURL?: string; jwt?: string } | Current configuration | | storage | DeviceKeyStorage \| null | Storage adapter instance |


Components

Auth Components

LoginForm

Username/email + password login form with loading and error states.

import { LoginForm } from 'cilantro-react';

<LoginForm
  onSuccess={() => console.log('Logged in!')}
  onError={(err) => console.error(err.message)}
  redirectAfterSuccess="/dashboard"
  onRedirect={(path) => router.replace(path)}
  onGoogleSignIn={async () => (await getGoogleIdToken())}
  onAppleSignIn={async () => (await getAppleIdToken())}
  title="Welcome back"
  description="Sign in to your account"
  submitLabel="Sign in"
  renderSwitchToRegister={() => <a href="/register">Create account</a>}
  className="max-w-sm"
/>

Props:

| Prop | Type | Default | Description | |------|------|---------|-------------| | onSuccess | () => void | — | Called after successful login | | onError | (error: Error) => void | — | Called on login error | | redirectAfterSuccess | string | — | Redirect to this path after login. If unset, reads from URL ?returnUrl= | | onRedirect | (path: string) => void | — | Custom redirect for SPA routers | | returnUrlParam | string | "returnUrl" | Query param to read return URL from | | title | string | "Sign in" | Form title | | description | string | — | Form description | | submitLabel | string | "Sign in" | Submit button label | | rememberMe | boolean | — | Show remember me option | | renderSwitchToRegister | () => ReactNode | — | Render link to register page | | onGoogleSignIn | () => Promise<string> | — | Enable Google sign-in. Return id_token from OAuth. | | onAppleSignIn | () => Promise<string> | — | Enable Apple sign-in. Return id_token from OAuth. | | className | string | — | Root element class | | classNames | LoginFormClassNames | — | Class names for inner elements |


SocialLoginButtons

Standalone social login buttons (Google, Apple). Use with loginWithSocial from useAuth(). Provide OAuth callbacks that return the provider's id_token.

import { SocialLoginButtons } from 'cilantro-react';

<SocialLoginButtons
  onGoogleSignIn={async () => {
    // Use @react-oauth/google or Google Identity Services
    const credential = await getGoogleCredential();
    return credential;  // id_token
  }}
  onAppleSignIn={async () => {
    // Use Apple JS SDK
    const idToken = await getAppleIdToken();
    return idToken;
  }}
/>

LogoutButton

Logout button with optional confirmation dialog.

import { LogoutButton } from 'cilantro-react';

<LogoutButton
  onLogout={() => console.log('Logged out')}
  redirectAfterLogout="/login"
  onRedirect={(path) => router.replace(path)}
  confirmBeforeLogout={true}
>
  Sign out
</LogoutButton>

Props:

| Prop | Type | Default | Description | |------|------|---------|-------------| | onLogout | () => void | — | Called after logout | | redirectAfterLogout | string | — | Redirect to this path after logout | | onRedirect | (path: string) => void | — | Custom redirect for SPA routers | | confirmBeforeLogout | boolean | false | Show confirmation dialog | | className | string | — | Button class | | children | ReactNode | "Log out" | Button content |


AuthGuard

Protect content behind authentication. Shows fallback (default: LoginForm) when not authenticated.

import { AuthGuard, LoginForm } from 'cilantro-react';

// Basic usage - shows LoginForm when not authenticated
<AuthGuard>
  <ProtectedContent />
</AuthGuard>

// Custom fallback
<AuthGuard fallback={<CustomLoginPage />}>
  <ProtectedContent />
</AuthGuard>

// Redirect to login page (appends ?returnUrl=currentPath for redirect-after-login)
<AuthGuard redirectTo="/login">
  <ProtectedContent />
</AuthGuard>

// Next.js / React Router: use onRedirect for client-side navigation (no full page reload)
import { useRouter } from 'next/navigation';
const router = useRouter();
<AuthGuard redirectTo="/login" onRedirect={(path) => router.replace(path)}>
  <ProtectedContent />
</AuthGuard>

// Polished UX: centered or fullscreen layout for auth fallback
<AuthGuard layout="centered">  {/* or "fullscreen" */}
  <ProtectedContent />
</AuthGuard>

// Hide content when not authenticated (no fallback)
<AuthGuard showFallback={false}>
  <ProtectedContent />
</AuthGuard>

// Show skeleton while loading
<AuthGuard useSkeleton={true}>
  <ProtectedContent />
</AuthGuard>

Props:

| Prop | Type | Default | Description | |------|------|---------|-------------| | children | ReactNode | — | Content to show when authenticated | | fallback | ReactNode | <LoginForm /> | Content when not authenticated | | redirectTo | string | — | Redirect unauthenticated users to this path (appends ?returnUrl=) | | onRedirect | (path: string) => void | — | Custom redirect for SPA routers (e.g. router.replace) | | returnUrlParam | string | "returnUrl" | Query param name for return URL | | layout | "inline" \| "centered" \| "fullscreen" | "inline" | Layout for fallback content | | showFallback | boolean | true | Whether to show fallback or null | | useSkeleton | boolean | false | Show skeleton while loading auth state | | className | string | — | Root element class | | classNames | AuthGuardClassNames | — | Class names for inner elements |


RegisterForm

Registration form with username, email, and password. Auto-login after success.

import { RegisterForm } from 'cilantro-react';

<RegisterForm
  onSuccess={() => console.log('Registered!')}
  redirectAfterSuccess="/dashboard"
  onRedirect={(path) => router.replace(path)}  // For Next.js/React Router
  renderSwitchToLogin={() => <a href="/login">Already have an account? Sign in</a>}
/>

AuthShell

Full-page centered layout for auth screens. Use for standalone login/register pages.

import { AuthShell, LoginForm } from 'cilantro-react';

<AuthShell logo={<img src="/logo.svg" alt="App" />} maxWidth="sm">
  <LoginForm redirectAfterSuccess="/dashboard" />
</AuthShell>

NextAuthGuard (Next.js)

Drop-in AuthGuard for Next.js App Router. Uses router.replace() for client-side redirects. Requires next to be installed.

import { NextAuthGuard } from 'cilantro-react/next';

<NextAuthGuard redirectTo="/login" layout="fullscreen">
  <ProtectedContent />
</NextAuthGuard>

Wallet Components

WalletSelector

Dropdown to select from available wallets.

import { WalletSelector } from 'cilantro-react';

// Controlled
const [walletId, setWalletId] = useState('');
<WalletSelector value={walletId} onChange={setWalletId} />

// With callback
<WalletSelector
  onWalletChange={(id, wallet) => console.log('Selected:', wallet?.walletName)}
  placeholder="Choose a wallet"
/>

// Headless (custom rendering)
<WalletSelector>
  {({ wallets, selectedWallet, selectWallet, isLoading }) => (
    <div>
      {wallets.map((w) => (
        <button key={w.id} onClick={() => selectWallet(w.id)}>
          {w.walletName}
        </button>
      ))}
    </div>
  )}
</WalletSelector>

Props:

| Prop | Type | Default | Description | |------|------|---------|-------------| | value | string | — | Controlled wallet ID | | onChange | (walletId: string) => void | — | Called when wallet changes | | onWalletChange | (walletId: string, wallet: WalletData \| null) => void | — | Called with wallet object | | placeholder | string | "Select a wallet" | Placeholder text | | useSkeleton | boolean | true | Show skeleton while loading | | className | string | — | Root element class | | classNames | WalletSelectorClassNames | — | Class names for inner elements | | children | (props) => ReactNode | — | Headless render function | | renderTrigger | (props) => ReactNode | — | Custom trigger render | | renderList | (props) => ReactNode | — | Custom list render |


CreateWalletForm

Form to create a new wallet.

import { CreateWalletForm } from 'cilantro-react';

<CreateWalletForm
  onCreated={(result) => console.log('Created wallet:', result.data.id)}
  onError={(err) => console.error(err.message)}
  userId="optional-user-id"
/>

Props:

| Prop | Type | Default | Description | |------|------|---------|-------------| | userId | string | — | Optional user ID to associate wallet with | | onCreated | (wallet: WalletControllerCreateResult) => void | — | Called after wallet is created | | onError | (error: Error) => void | — | Called on error | | className | string | — | Form class |


WalletAddress

Display a wallet address with copy functionality.

import { WalletAddress } from 'cilantro-react';

<WalletAddress
  address="7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU"
  short={true}
  copyable={true}
/>

Props:

| Prop | Type | Default | Description | |------|------|---------|-------------| | address | string | — | Wallet address to display | | short | boolean | false | Truncate address for display | | copyable | boolean | false | Show copy button | | className | string | — | Element class |


WalletBalance

Display wallet SOL and token balances.

import { WalletBalance } from 'cilantro-react';

<WalletBalance walletId={walletId} showTokens={true} />

Props:

| Prop | Type | Default | Description | |------|------|---------|-------------| | walletId | string | — | Wallet ID to show balance for | | showTokens | boolean | false | Show SPL token balances | | className | string | — | Element class |


ConnectWalletButton

Connect to external Solana wallets (Phantom, Solflare).

import { ConnectWalletButton } from 'cilantro-react';

<ConnectWalletButton
  onConnect={(publicKey, wallet) => console.log('Connected:', publicKey)}
  onDisconnect={() => console.log('Disconnected')}
>
  Connect External Wallet
</ConnectWalletButton>

Props:

| Prop | Type | Default | Description | |------|------|---------|-------------| | onConnect | (publicKey: string, wallet: SolanaWallet) => void | — | Called when connected | | onDisconnect | () => void | — | Called when disconnected | | className | string | — | Button class | | children | ReactNode | "Connect Wallet" | Button content |


Signer Components

SignerSelector

Select a signer from the wallet's signers.

import { SignerSelector } from 'cilantro-react';

const [signerId, setSignerId] = useState('');
const [signerType, setSignerType] = useState<'email' | 'phone'>('email');

<SignerSelector
  walletId={walletId}
  value={signerId}
  onChange={(id, type) => {
    setSignerId(id);
    setSignerType(type);
  }}
/>

// Headless
<SignerSelector walletId={walletId} onChange={handleChange}>
  {({ signers, selectedSigner, onSignerSelect, isLoading }) => (
    <div>
      {signers.map((s) => (
        <button key={s.id} onClick={() => onSignerSelect(s)}>
          {s.email || s.phone}
        </button>
      ))}
    </div>
  )}
</SignerSelector>

Props:

| Prop | Type | Default | Description | |------|------|---------|-------------| | walletId | string | — | Wallet ID to load signers for | | value | string | — | Selected signer ID | | onChange | (signerId: string, signerType: SignerType) => void | — | Called when signer changes | | useSkeleton | boolean | true | Show skeleton while loading | | className | string | — | Root element class | | classNames | SignerSelectorClassNames | — | Class names for inner elements | | children | (props) => ReactNode | — | Headless render function | | renderList | (props) => ReactNode | — | Custom list render |


SignerList

List signers with "Add signer" dialog (includes email, phone, passkey forms).

import { SignerList } from 'cilantro-react';

<SignerList
  walletId={walletId}
  onSignerAdded={() => console.log('Signer added')}
  onRevoke={(signerId) => console.log('Revoked:', signerId)}
/>

// Headless
<SignerList walletId={walletId}>
  {({ signers, isLoading, openAddSigner }) => (
    <div>
      {signers.map((s) => <div key={s.id}>{s.email}</div>)}
      <button onClick={openAddSigner}>Add Signer</button>
    </div>
  )}
</SignerList>

Props:

| Prop | Type | Default | Description | |------|------|---------|-------------| | walletId | string | — | Wallet ID | | onSignerAdded | () => void | — | Called after signer is added | | onRevoke | (signerId: string) => void | — | Called when signer is revoked | | useSkeleton | boolean | true | Show skeleton while loading | | className | string | — | Root element class | | classNames | SignerListClassNames | — | Class names for inner elements | | children | (props) => ReactNode | — | Headless render function |


CreateEmailSignerForm

Form to create an email signer.

import { CreateEmailSignerForm } from 'cilantro-react';

<CreateEmailSignerForm
  walletId={walletId}
  onCreated={(signer) => console.log('Created:', signer.signerId)}
  onError={(err) => console.error(err.message)}
/>

Props:

| Prop | Type | Default | Description | |------|------|---------|-------------| | walletId | string | — | Wallet ID | | onCreated | (signer: SignerInfo) => void | — | Called after signer is created | | onError | (error: Error) => void | — | Called on error | | className | string | — | Form class |


CreatePhoneSignerForm

Form to create a phone signer.

import { CreatePhoneSignerForm } from 'cilantro-react';

<CreatePhoneSignerForm
  walletId={walletId}
  onCreated={(signer) => console.log('Created:', signer.signerId)}
  onError={(err) => console.error(err.message)}
/>

Props:

| Prop | Type | Default | Description | |------|------|---------|-------------| | walletId | string | — | Wallet ID | | onCreated | (signer: SignerInfo) => void | — | Called after signer is created | | onError | (error: Error) => void | — | Called on error | | className | string | — | Form class |


AddPasskeyButton

Button to register a passkey signer. Only renders if WebAuthn is supported.

import { AddPasskeyButton } from 'cilantro-react';

<AddPasskeyButton
  walletId={walletId}
  onRegistered={(signer) => console.log('Registered:', signer.signerId)}
  onError={(err) => console.error(err.message)}
>
  Add Passkey
</AddPasskeyButton>

Props:

| Prop | Type | Default | Description | |------|------|---------|-------------| | walletId | string | — | Wallet ID | | onRegistered | (signer: SignerInfo) => void | — | Called after passkey is registered | | onError | (error: Error) => void | — | Called on error | | className | string | — | Container class | | children | ReactNode | "Add Passkey" | Button content |


Transaction Components

SendSOLForm

Form to send SOL.

import { SendSOLForm } from 'cilantro-react';

<SendSOLForm
  walletId={walletId}
  signerId={signerId}
  signerType="email"
  onSuccess={(result) => console.log('Sent! Signature:', result.data.signature)}
  onError={(err) => console.error(err.message)}
/>

Props:

| Prop | Type | Default | Description | |------|------|---------|-------------| | walletId | string | — | Wallet to send from | | signerId | string | — | Signer ID to authorize transaction | | signerType | 'email' \| 'phone' \| 'passkey' \| 'external' | — | Signer type | | onSuccess | (result: SubmitTransactionResult) => void | — | Called after successful send | | onError | (error: Error) => void | — | Called on error | | className | string | — | Form class |


SendSPLForm

Form to send SPL tokens.

import { SendSPLForm } from 'cilantro-react';

<SendSPLForm
  walletId={walletId}
  signerId={signerId}
  signerType="email"
  mintAddress="EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" // USDC
  onSuccess={(result) => console.log('Sent! Signature:', result.data.signature)}
  onError={(err) => console.error(err.message)}
/>

Props:

| Prop | Type | Default | Description | |------|------|---------|-------------| | walletId | string | — | Wallet to send from | | signerId | string | — | Signer ID | | signerType | SignerType | — | Signer type | | mintAddress | string | — | Pre-fill token mint address | | onSuccess | (result: SubmitTransactionResult) => void | — | Called after successful send | | onError | (error: Error) => void | — | Called on error | | className | string | — | Form class |


TransactionStatus

Display transaction status with explorer link.

import { TransactionStatus } from 'cilantro-react';

<TransactionStatus
  signature="5wHu1qwD7q9..."
  cluster="mainnet-beta"
/>

Props:

| Prop | Type | Default | Description | |------|------|---------|-------------| | signature | string | — | Transaction signature | | cluster | 'mainnet-beta' \| 'devnet' \| 'testnet' | 'mainnet-beta' | Solana cluster | | className | string | — | Element class |


TransactionHistory

Paginated list of recent transactions.

import { TransactionHistory } from 'cilantro-react';

<TransactionHistory walletId={walletId} pageSize={10} />

Props:

| Prop | Type | Default | Description | |------|------|---------|-------------| | walletId | string | — | Wallet ID | | pageSize | number | 10 | Transactions per page | | className | string | — | Element class |


Layout Components

CilantroConnect

All-in-one connect component combining wallet, signers, and auth.

import { CilantroConnect } from 'cilantro-react';

<CilantroConnect
  onConnect={(wallet, signers) => console.log('Connected:', wallet, signers)}
  onDisconnect={() => console.log('Disconnected')}
/>

Props:

| Prop | Type | Default | Description | |------|------|---------|-------------| | onConnect | (wallet: WalletInfo, signers: SignerInfo[]) => void | — | Called when connected | | onDisconnect | () => void | — | Called when disconnected | | className | string | — | Element class |


LoadingOverlay

Overlay with loading spinner for async operations.

import { LoadingOverlay } from 'cilantro-react';

<LoadingOverlay isLoading={isPending} message="Sending transaction...">
  <MyContent />
</LoadingOverlay>

Props:

| Prop | Type | Default | Description | |------|------|---------|-------------| | isLoading | boolean | — | Show overlay when true | | message | string | — | Loading message | | children | ReactNode | — | Content to wrap | | className | string | — | Overlay class |


ErrorBoundary

Error boundary with retry functionality.

import { ErrorBoundary } from 'cilantro-react';

<ErrorBoundary
  fallback={<div>Something went wrong</div>}
  onRetry={() => window.location.reload()}
>
  <App />
</ErrorBoundary>

Props:

| Prop | Type | Default | Description | |------|------|---------|-------------| | fallback | ReactNode | — | Content to show on error | | onRetry | () => void | — | Retry callback | | children | ReactNode | — | Content to wrap |


Theming

Components use Shadcn-style class names and CSS variables. Ensure your app has Tailwind CSS configured.

Built-in Theme (ThemeProvider)

import { ThemeProvider } from 'cilantro-react';

<ThemeProvider
  theme="system"
  defaultTheme="dark"
  injectStyles={true}
  className="min-h-screen"
>
  <CilantroProvider apiKey="...">
    <App />
  </CilantroProvider>
</ThemeProvider>

Props:

| Prop | Type | Default | Description | |------|------|---------|-------------| | theme | 'light' \| 'dark' \| 'system' | 'system' | Theme mode | | defaultTheme | 'light' \| 'dark' | 'dark' | Default when using system | | storageKey | string | — | localStorage key for persistence | | injectStyles | boolean | true | Inject CSS variables | | className | string | — | Wrapper class |

Import Theme CSS Directly

import 'cilantro-react/themes/default.css';

Advanced Usage

Low-Level Signer Signing

For advanced use cases without React hooks:

import {
  signMessageWithSigner,
  signTransactionWithSigner,
  signAndSendTransactionWithSigner,
  getWalletData,
  getSignerPublicKey,
  SIGNER_TYPES,
} from 'cilantro-react';

// Sign a message
const result = await signMessageWithSigner(walletId, signer, 'Hello, Solana!');

// Sign and send a transaction
const txResult = await signAndSendTransactionWithSigner(walletId, signer, transaction, connection);

Solana Connection Adapter

For @solana/web3.js connection compatibility:

import { Connection } from '@solana/web3.js';
import { adaptSolanaConnection } from 'cilantro-react';

const connection = new Connection('https://api.mainnet-beta.solana.com');
const adapted = adaptSolanaConnection(connection);

Utilities

| Function | Description | |----------|-------------| | extractErrorMessage(error) | Normalize unknown errors to string | | extractResponseData(response) | Unwrap SDK response shapes | | normalizeWallet(dto) | Normalize wallet data from SDK (ensures id and address) | | normalizeSigner(dto) | Normalize signer data from SDK | | getWalletId(wallet) | Get wallet ID from wallet-like object (id ?? walletId) | | getWalletAddress(wallet) | Get wallet address from wallet-like object (address ?? walletAddress) | | isJwtExpired(token, bufferSeconds?) | Check if JWT is expired. Uses exp claim; optional buffer (default 60s) treats tokens near expiry as expired. Exported for apps that sync JWT to cookies or need expiry checks. | | isAuthError(error) | Check if error is an auth error |

Wallet shape

Wallet data from the SDK may use walletId/walletAddress or id/address. The library normalizes at boundaries so id and address are preferred. Use getWalletId(wallet) and getWalletAddress(wallet) when you need to handle both shapes consistently.


Types

Key exported types:

import type {
  // Config
  CilantroConfig,
  CilantroContextValue,
  DeviceKeyStorage,

  // User/Auth
  User,
  UseAuthResult,

  // Wallet
  WalletData,
  UseWalletResult,
  WalletControllerCreateResult,

  // Signer
  SignerData,
  SignerInfo,
  SignerType,
  UseSignersResult,

  // Transaction
  SubmitTransactionResult,
  UseSendTransactionResult,

  // External Wallet
  SolanaWallet,
  UseExternalWalletResult,

  // Passkey
  UsePasskeyResult,

  // UI
  ActionState,
} from 'cilantro-react';

License

MIT