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
- cilantro-react
Installation
npm install cilantro-react cilantro-sdk
# or
yarn add cilantro-react cilantro-sdk
# or
pnpm add cilantro-react cilantro-sdkRequirements:
- 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:
- Enable JWT cookie sync:
<CilantroProvider syncJwtToCookie> - In middleware, read the cookie (default name:
cilantro_jwt) to check auth. - 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 fromsignerId?: 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
