cilantro-react 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,8 +1,68 @@
1
1
  # cilantro-react
2
2
 
3
- React SDK/UI for [Cilantro Smart Wallet](https://www.npmjs.com/package/cilantro-sdk). Provides a single context provider, hooks, and UI components for auth, wallets, signers, and transactions. Install as **cilantro-react**; documentation may show `@cilantro/react-sdk` for illustration—use `cilantro-react` in install and imports.
3
+ React SDK/UI for [Cilantro Smart Wallet](https://www.npmjs.com/package/cilantro-sdk). Provides a single context provider, hooks, and UI components for authentication, wallets, signers, and transactions.
4
+
5
+ ## Table of Contents
6
+
7
+ - [cilantro-react](#cilantro-react)
8
+ - [Table of Contents](#table-of-contents)
9
+ - [Installation](#installation)
10
+ - [Polyfills (Browser)](#polyfills-browser)
11
+ - [Quick Start](#quick-start)
12
+ - [CilantroProvider](#cilantroprovider)
13
+ - [Props](#props)
14
+ - [Storage Adapters](#storage-adapters)
15
+ - [Hooks](#hooks)
16
+ - [useAuth](#useauth)
17
+ - [useReturnUrl](#usereturnurl)
18
+ - [useWallet](#usewallet)
19
+ - [useSigners](#usesigners)
20
+ - [usePasskey](#usepasskey)
21
+ - [useExternalWallet](#useexternalwallet)
22
+ - [useSendTransaction](#usesendtransaction)
23
+ - [useCilantroConfig](#usecilantroconfig)
24
+ - [Components](#components)
25
+ - [Auth Components](#auth-components)
26
+ - [LoginForm](#loginform)
27
+ - [RegisterForm](#registerform)
28
+ - [LogoutButton](#logoutbutton)
29
+ - [AuthGuard](#authguard)
30
+ - [AuthShell](#authshell)
31
+ - [NextAuthGuard (Next.js)](#nextauthguard-nextjs)
32
+ - [Wallet Components](#wallet-components)
33
+ - [WalletSelector](#walletselector)
34
+ - [CreateWalletForm](#createwalletform)
35
+ - [WalletAddress](#walletaddress)
36
+ - [WalletBalance](#walletbalance)
37
+ - [ConnectWalletButton](#connectwalletbutton)
38
+ - [Signer Components](#signer-components)
39
+ - [SignerSelector](#signerselector)
40
+ - [SignerList](#signerlist)
41
+ - [CreateEmailSignerForm](#createemailsignerform)
42
+ - [CreatePhoneSignerForm](#createphonesignerform)
43
+ - [AddPasskeyButton](#addpasskeybutton)
44
+ - [Transaction Components](#transaction-components)
45
+ - [SendSOLForm](#sendsolform)
46
+ - [SendSPLForm](#sendsplform)
47
+ - [TransactionStatus](#transactionstatus)
48
+ - [TransactionHistory](#transactionhistory)
49
+ - [Layout Components](#layout-components)
50
+ - [CilantroConnect](#cilantroconnect)
51
+ - [LoadingOverlay](#loadingoverlay)
52
+ - [ErrorBoundary](#errorboundary)
53
+ - [Theming](#theming)
54
+ - [Built-in Theme (ThemeProvider)](#built-in-theme-themeprovider)
55
+ - [Import Theme CSS Directly](#import-theme-css-directly)
56
+ - [Advanced Usage](#advanced-usage)
57
+ - [Low-Level Signer Signing](#low-level-signer-signing)
58
+ - [Solana Connection Adapter](#solana-connection-adapter)
59
+ - [Utilities](#utilities)
60
+ - [Types](#types)
61
+ - [License](#license)
4
62
 
5
- ## Install
63
+ ---
64
+
65
+ ## Installation
6
66
 
7
67
  ```bash
8
68
  npm install cilantro-react cilantro-sdk
@@ -12,907 +72,1316 @@ yarn add cilantro-react cilantro-sdk
12
72
  pnpm add cilantro-react cilantro-sdk
13
73
  ```
14
74
 
15
- **Peer dependencies:** React 18+, `cilantro-sdk`. Optionally `@solana/web3.js` for transaction/signing. Components use Tailwind-compatible class names; ensure your app has [Tailwind CSS](https://tailwindcss.com) configured.
75
+ **Requirements:**
76
+
77
+ - React 18+
78
+ - `cilantro-sdk` (peer dependency)
79
+ - [Tailwind CSS](https://tailwindcss.com) for component styling (components use Tailwind-compatible class names)
16
80
 
17
- ## Polyfills (browser)
81
+ ### Polyfills (Browser)
18
82
 
19
- Import at the very top of your entry file (e.g. `main.tsx`, `App.tsx`):
83
+ Import polyfills at the very top of your entry file (before any other imports):
20
84
 
21
85
  ```ts
22
- import 'cilantro-sdk/polyfills'
86
+ import 'cilantro-sdk/polyfills';
23
87
  ```
24
88
 
25
- ## Wrap your app
89
+ ---
90
+
91
+ ## Quick Start
26
92
 
27
93
  ```tsx
28
- import { CilantroProvider } from 'cilantro-react'
94
+ import 'cilantro-sdk/polyfills';
95
+ import { CilantroProvider, LoginForm, AuthGuard, useAuth, useWallet } from 'cilantro-react';
29
96
 
30
- function Root() {
97
+ function App() {
31
98
  return (
32
99
  <CilantroProvider
33
100
  apiKey={import.meta.env.VITE_API_KEY}
34
101
  baseURL="https://api.cilantro.gg"
35
102
  >
36
- <App />
103
+ <AuthGuard>
104
+ <Dashboard />
105
+ </AuthGuard>
37
106
  </CilantroProvider>
38
- )
107
+ );
108
+ }
109
+
110
+ function Dashboard() {
111
+ const { user, logout } = useAuth();
112
+ const { wallet, wallets } = useWallet();
113
+
114
+ return (
115
+ <div>
116
+ <p>Welcome, {user?.username}</p>
117
+ <p>Wallet: {wallet?.walletName}</p>
118
+ <button onClick={logout}>Logout</button>
119
+ </div>
120
+ );
39
121
  }
40
122
  ```
41
123
 
42
- ### Storage adapter (device keys)
124
+ ---
125
+
126
+ ## CilantroProvider
43
127
 
44
- For email and phone signers, device keys must be stored. Configure a storage adapter:
128
+ The root provider that initializes configuration, storage, authentication, and wallet state.
45
129
 
46
130
  ```tsx
47
- import { CilantroProvider, createIndexedDBAdapter } from 'cilantro-react'
131
+ import { CilantroProvider, createIndexedDBAdapter } from 'cilantro-react';
48
132
 
49
- const storage = createIndexedDBAdapter()
133
+ const storage = createIndexedDBAdapter();
50
134
 
51
- <CilantroProvider apiKey={API_KEY} storageAdapter={storage}>
135
+ <CilantroProvider
136
+ apiKey="your-platform-api-key"
137
+ baseURL="https://api.cilantro.gg"
138
+ storageAdapter={storage}
139
+ >
52
140
  <App />
53
141
  </CilantroProvider>
54
142
  ```
55
143
 
56
- | Adapter | Use case |
57
- |---------|----------|
58
- | `createIndexedDBAdapter()` | Production – persistent, large capacity |
59
- | `createLocalStorageAdapter()` | Development – simple, limited capacity |
60
- | `createMemoryAdapter()` | Testing – not persisted |
144
+ ### Props
61
145
 
62
- **CilantroProvider props (guide API):**
146
+ | Prop | Type | Required | Default | Description |
147
+ |------|------|----------|---------|-------------|
148
+ | `apiKey` | `string` | No* | — | Platform API key for server-side operations |
149
+ | `baseURL` | `string` | No | `https://api.cilantro.gg` | API base URL |
150
+ | `storageAdapter` | `DeviceKeyStorage` | No | IndexedDB | Storage adapter for device keys |
151
+ | `syncJwtToCookie` | `boolean` | No | `false` | When true, sync JWT to a cookie for Next.js/middleware |
152
+ | `jwtCookieName` | `string` | No | `cilantro_jwt` | Cookie name when `syncJwtToCookie` is true |
153
+ | `children` | `ReactNode` | Yes | — | App content |
63
154
 
64
- | Prop | Type | Description |
65
- |------|------|-------------|
66
- | `apiKey` | `string` | Platform API key (optional if using JWT via login). |
67
- | `baseURL` | `string` | API base URL (default: `https://api.cilantro.gg`). |
68
- | `storageAdapter` | `DeviceKeyStorage` | Storage for device keys (default: IndexedDB in browser). |
69
- | `children` | `ReactNode` | App content. |
155
+ *Either `apiKey` or JWT (obtained via login) is required for authenticated requests.
70
156
 
71
- Backward compatibility: `platformApiKey` / `apiUrl` are still supported (deprecated). See [API reference](#api-reference) for full props.
157
+ ### SDK Auth (automatic)
72
158
 
73
- **Connection:** The library does not create a Solana RPC connection. For transaction sign-and-send, pass your own `connection` from `@solana/web3.js` into `useTransactionSigning` / `TransactionSigningForm`. See [Connection setup](#connection-setup-solana) below.
159
+ **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.
74
160
 
75
- ---
161
+ ### Storage Adapters
76
162
 
77
- ## Display logic and form visibility
163
+ For email and phone signers, device keys must be stored. Choose an adapter:
78
164
 
79
- Message and transaction signing forms only show the actual form when **both** a wallet and a signer are selected:
165
+ | Adapter | Use Case |
166
+ |---------|----------|
167
+ | `createIndexedDBAdapter()` | **Production** – persistent, large capacity |
168
+ | `createLocalStorageAdapter()` | Development – simple, limited capacity |
169
+ | `createMemoryAdapter()` | Testing – not persisted |
80
170
 
81
- - **Wallet selected:** Use `useWallets().selectedWallet` as the single source of truth. Derive `walletId` and `hasWallet` from it:
82
- `walletId = selectedWallet?.id ?? selectedWallet?.walletId ?? ""`,
83
- `hasWallet = !!walletId`.
84
- - **Signer selected:** Use `useSignerSelection().selectedSigner`; `hasSigner = !!selectedSigner`.
171
+ ```tsx
172
+ import { createIndexedDBAdapter, createLocalStorageAdapter, createMemoryAdapter } from 'cilantro-react';
85
173
 
86
- Show the form only when `hasWallet && hasSigner`. Do not rely only on `useSignerSelection().selectedWalletId` for “wallet selected” to avoid sync issues between the two hooks. When building custom pages, pass `selectedWalletId={walletId}` and `selectedSigner={selectedSigner}` explicitly into `MessageSigningForm` / `TransactionSigningForm` so the forms use the same wallet/signer state as your UI.
174
+ // Production
175
+ const storage = createIndexedDBAdapter();
87
176
 
88
- ---
177
+ // Development
178
+ const storage = createLocalStorageAdapter();
89
179
 
90
- ## Connection setup (Solana)
180
+ // Testing
181
+ const storage = createMemoryAdapter();
182
+ ```
91
183
 
92
- For transaction sign-and-send (wallet-adapter or passkey), the library expects a connection object with `sendRawTransaction`, `getLatestBlockhash`, and `confirmTransaction`. If you use `@solana/web3.js`, use the provided adapter so you don’t need type assertions:
184
+ ### Next.js / Middleware
93
185
 
94
- ```tsx
95
- import { Connection } from '@solana/web3.js'
96
- import { adaptSolanaConnection, TransactionSigningForm } from 'cilantro-react'
186
+ For Next.js apps, middleware runs on the server and cannot access `localStorage`. To protect routes or read the JWT in middleware:
97
187
 
98
- const connection = new Connection('https://api.mainnet-beta.solana.com')
188
+ 1. Enable JWT cookie sync: `<CilantroProvider syncJwtToCookie>`
189
+ 2. In middleware, read the cookie (default name: `cilantro_jwt`) to check auth.
190
+ 3. Optionally redirect unauthenticated users. Example:
99
191
 
100
- <TransactionSigningForm
101
- connection={adaptSolanaConnection(connection)}
102
- showContext
103
- />
192
+ ```ts
193
+ // middleware.ts
194
+ import { NextResponse } from 'next/server';
195
+ import type { NextRequest } from 'next/server';
196
+
197
+ export function middleware(request: NextRequest) {
198
+ const token = request.cookies.get('cilantro_jwt')?.value;
199
+ if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
200
+ return NextResponse.redirect(new URL('/login', request.url));
201
+ }
202
+ return NextResponse.next();
203
+ }
104
204
  ```
105
205
 
106
- `adaptSolanaConnection(connection)` returns an object that matches the library’s expected connection shape. You can pass it to `TransactionSigningForm` or `useTransactionSigning({ connection: adaptSolanaConnection(solanaConnection), ... })`.
107
-
108
206
  ---
109
207
 
110
- ## Headless usage and custom selectors
208
+ ## Hooks
111
209
 
112
- You can build your own wallet/signer UI with the hooks and render props instead of the built-in selectors:
210
+ ### useAuth
113
211
 
114
- - **Wallet:** Use `useWallets()` for `wallets`, `selectedWallet`, `selectWallet`, and `refreshWallets`. Use the `WalletSelector` `children` render prop to render your own list/dropdown; you get `{ wallets, selectedWallet, selectWallet, isLoading, refreshWallets }`.
115
- - **Signer:** Use `useSignerSelection()` for `availableSigners`, `selectedSigner`, `setSelectedSigner`, and `isLoadingSigners`. Use the `SignerSelector` `children` or `renderList` prop to render your own list (e.g. Shadcn Select or custom buttons).
116
-
117
- Example: custom wallet + signer selection and conditional form:
212
+ Authentication state and actions.
118
213
 
119
214
  ```tsx
120
- const { selectedWallet } = useWallets()
121
- const { availableSigners, selectedSigner, setSelectedSigner, isLoadingSigners } = useSignerSelection()
122
- const walletId = selectedWallet?.id ?? selectedWallet?.walletId ?? ''
123
- const hasWallet = !!walletId
124
- const hasSigner = !!selectedSigner
215
+ import { useAuth } from 'cilantro-react';
125
216
 
126
- return (
127
- <>
128
- <WalletSelector>{({ wallets, selectedWallet, selectWallet, isLoading }) => (
129
- /* your custom wallet dropdown */
130
- )}</WalletSelector>
131
- <SignerSelector
132
- selectedWalletId={walletId}
133
- availableSigners={availableSigners}
134
- selectedSigner={selectedSigner}
135
- onSignerSelect={setSelectedSigner}
136
- isLoadingSigners={isLoadingSigners}
137
- >
138
- {({ signers, selectedSigner, onSignerSelect, isLoading }) => (
139
- /* your custom signer list */
140
- )}
141
- </SignerSelector>
142
- {hasWallet && hasSigner && (
143
- <MessageSigningForm
144
- selectedWalletId={walletId}
145
- selectedSigner={selectedSigner}
146
- showContext={false}
147
- />
148
- )}
149
- </>
150
- )
151
- ```
217
+ function MyComponent() {
218
+ const { user, jwt, isLoading, isAuthenticated, login, register, logout, clearSessionDueToAuthError } = useAuth();
152
219
 
153
- ---
220
+ const handleLogin = async () => {
221
+ await login({ usernameOrEmail: 'user@example.com', password: 'password123' });
222
+ };
154
223
 
155
- ## Property names (SDK library)
224
+ const handleRegister = async () => {
225
+ await register('johndoe', 'john@example.com', 'password123');
226
+ };
156
227
 
157
- The library normalizes wallet and signer data at the boundary. You can rely on these fields in UI and hooks:
228
+ return (
229
+ <div>
230
+ {isLoading && <p>Loading...</p>}
231
+ {isAuthenticated ? (
232
+ <>
233
+ <p>Hello, {user?.username}</p>
234
+ <button onClick={logout}>Logout</button>
235
+ </>
236
+ ) : (
237
+ <button onClick={handleLogin}>Login</button>
238
+ )}
239
+ </div>
240
+ );
241
+ }
242
+ ```
158
243
 
159
- | SDK / API field | Normalized / library field | Notes |
160
- |-------------------|----------------------------|--------------------------------|
161
- | `walletId` | `id` | WalletData always has `id`. |
162
- | `walletAddress` | `address` | WalletData always has `address`. |
163
- | `isActive` | `active` | WalletData alias. |
164
- | `signerId` | `id` | SignerData always has `id` and `signerId`. |
165
- | `signerPubkey` / `publicKey` | `signerPubkey` and `publicKey` | SignerData has both. |
166
- | `signerType` / `type` | `signerType` and `type` | SignerData has both. |
244
+ **Returns: `UseAuthResult`**
167
245
 
168
- Use `normalizeWallet(dto)` and `normalizeSigner(dto, category)` at API boundaries if you ingest raw SDK responses; the provider and `loadSigners` already use normalized shapes.
246
+ | Property | Type | Description |
247
+ |----------|------|-------------|
248
+ | `user` | `User \| null` | Current user object (`{ username?, email?, userType? }`) |
249
+ | `jwt` | `string \| null` | JWT token (null when not authenticated). Prefer this name. |
250
+ | `token` | `string \| null` | Alias for `jwt`. Same value. |
251
+ | `isLoading` | `boolean` | True while restoring session from storage |
252
+ | `isAuthenticated` | `boolean` | True when user has valid JWT |
253
+ | `login` | `(params: { usernameOrEmail: string; password: string }) => Promise<void>` | Log in; throws on error |
254
+ | `register` | `(username: string, email: string, password: string, isActive?: boolean) => Promise<void>` | Register and auto-login |
255
+ | `logout` | `() => void` | Clear session and token |
256
+ | `clearSessionDueToAuthError` | `() => void` | Clear session when API returns 401/403; calls `onSessionExpired` |
169
257
 
170
258
  ---
171
259
 
172
- ## API reference
260
+ ### useReturnUrl
173
261
 
174
- ### Providers
262
+ Read `returnUrl` from URL search params for redirect-after-auth flows.
175
263
 
176
- - **CilantroProvider** – Root provider: initializes storage, auth, and wallet context. Requires `platformApiKey`; optional `apiUrl`, storage keys, and callbacks. Use this unless you only need auth.
177
- - **CilantroAuthProvider** Auth only (token, user, login, register, logout). Use when you don’t need the full provider stack (e.g. auth-only pages).
178
- - **WalletProvider** – Wallet list and selection. Depends on auth; usually used inside `CilantroProvider`.
179
- - **ThemeProvider** – Optional: sets `data-theme="light"` or `"dark"` on a wrapper and injects default CSS variables for light/dark. Props: `theme` (`"light"` | `"dark"` | `"system"`), `defaultTheme`, `storageKey`, `className`, `injectStyles`. See [Themes](#built-in-theme-themeprovider).
264
+ ```tsx
265
+ import { useReturnUrl } from 'cilantro-react';
180
266
 
181
- **CilantroAuthProvider props:** `platformApiKey` (required), `apiUrl`, `jwtStorageKey`, `onLoginSuccess`, `onLogout`, `onRegisterSuccess`.
267
+ function LoginPage() {
268
+ const { returnPath, redirect, rawReturnUrl } = useReturnUrl({ param: 'returnUrl', defaultPath: '/' });
182
269
 
183
- **WalletProvider props:** `storageKey` (default: `cilantro_selected_wallet_id`).
270
+ return (
271
+ <LoginForm
272
+ redirectAfterSuccess={returnPath}
273
+ onRedirect={(path) => router.replace(path)}
274
+ />
275
+ );
276
+ }
277
+ ```
184
278
 
185
279
  ---
186
280
 
187
- ### Custom hooks (detailed)
281
+ ### useWallet
282
+
283
+ Wallet state and actions.
284
+
285
+ ```tsx
286
+ import { useWallet } from 'cilantro-react';
188
287
 
189
- All hooks must be used within the appropriate provider (see each hook below).
288
+ function WalletManager() {
289
+ const { wallet, wallets, createWallet, selectWallet, refreshWallets, isLoading, error } = useWallet();
190
290
 
191
- #### useCilantroAuth
291
+ const handleCreate = async () => {
292
+ const result = await createWallet({ name: 'My New Wallet' });
293
+ console.log('Created:', result.data.id);
294
+ };
192
295
 
193
- Auth state and actions. Use inside `CilantroAuthProvider` or `CilantroProvider`.
296
+ return (
297
+ <div>
298
+ {isLoading && <p>Loading wallets...</p>}
299
+ {error && <p className="text-red-500">{error}</p>}
300
+ <p>Current wallet: {wallet?.walletName ?? 'None selected'}</p>
301
+ <ul>
302
+ {wallets.map((w) => (
303
+ <li key={w.id} onClick={() => selectWallet(w.id)}>
304
+ {w.walletName}
305
+ </li>
306
+ ))}
307
+ </ul>
308
+ <button onClick={handleCreate}>Create Wallet</button>
309
+ <button onClick={refreshWallets}>Refresh</button>
310
+ </div>
311
+ );
312
+ }
313
+ ```
194
314
 
195
- **Returns:** `CilantroAuthContextType`
315
+ **Returns: `UseWalletResult`**
196
316
 
197
317
  | Property | Type | Description |
198
318
  |----------|------|-------------|
199
- | `token` | `string \| null` | JWT token (null when not authenticated). |
200
- | `user` | `User \| null` | User object: `{ username?, email?, userType? }`. |
201
- | `isAuthenticated` | `boolean` | True when `token` is set. |
202
- | `isLoading` | `boolean` | True while restoring session from storage. |
203
- | `login` | `(usernameOrEmail: string, password: string) => Promise<void>` | Log in; throws on error. |
204
- | `register` | `(username: string, email: string, password: string, isActive?: boolean) => Promise<void>` | Register and log in; throws on error. |
205
- | `logout` | `() => void` | Clear token and user; calls `onLogout` if provided. |
319
+ | `wallet` | `WalletData \| null` | Currently selected wallet |
320
+ | `wallets` | `WalletData[]` | All wallets for the authenticated user |
321
+ | `createWallet` | `(params: { name: string; userId?: string }) => Promise<WalletControllerCreateResult>` | Create a new wallet |
322
+ | `selectWallet` | `(walletId: string) => void` | Select a wallet by ID |
323
+ | `refreshWallets` | `() => Promise<void>` | Refresh wallet list from API |
324
+ | `isLoading` | `boolean` | True while loading wallets |
325
+ | `error` | `string \| null` | Error from wallet operations (e.g. fetch failed) |
206
326
 
207
327
  ---
208
328
 
209
- #### useWallets
210
-
211
- Wallet list and selection. Use inside `CilantroProvider` (which includes `WalletProvider`).
212
-
213
- **Returns:** `WalletContextType`
329
+ ### useSigners
214
330
 
215
- | Property | Type | Description |
216
- |----------|------|-------------|
217
- | `wallets` | `WalletData[]` | List of wallets for the authenticated user. |
218
- | `selectedWallet` | `WalletData \| null` | Currently selected wallet (persisted in localStorage). |
219
- | `selectWallet` | `(walletId: string) => void` | Set the selected wallet by id. |
220
- | `refreshWallets` | `() => Promise<void>` | Reload wallets from the API. |
221
- | `isLoading` | `boolean` | True while loading wallets. |
331
+ Signers for a specific wallet.
222
332
 
223
- **WalletData:** `{ id, walletId, walletName?, address?, walletAddress?, chain?, active?, ... }`.
333
+ ```tsx
334
+ import { useSigners } from 'cilantro-react';
335
+
336
+ function SignerManager({ walletId }: { walletId: string }) {
337
+ const {
338
+ signers,
339
+ isLoading,
340
+ error,
341
+ refresh,
342
+ createEmailSigner,
343
+ createPhoneSigner,
344
+ revokeSigner,
345
+ } = useSigners(walletId);
346
+
347
+ const handleCreateEmail = async () => {
348
+ const signer = await createEmailSigner({ email: 'user@example.com' });
349
+ console.log('Created signer:', signer.signerId);
350
+ };
351
+
352
+ const handleCreatePhone = async () => {
353
+ const signer = await createPhoneSigner({ phone: '+1234567890' });
354
+ console.log('Created signer:', signer.signerId);
355
+ };
224
356
 
225
- ---
357
+ return (
358
+ <div>
359
+ {isLoading && <p>Loading signers...</p>}
360
+ {error && <p className="text-red-500">{error}</p>}
361
+ <ul>
362
+ {signers.map((s) => (
363
+ <li key={s.id}>
364
+ {s.email || s.phone || s.id} ({s.signerType})
365
+ <button onClick={() => revokeSigner(s.id)}>Revoke</button>
366
+ </li>
367
+ ))}
368
+ </ul>
369
+ <button onClick={handleCreateEmail}>Add Email Signer</button>
370
+ <button onClick={handleCreatePhone}>Add Phone Signer</button>
371
+ <button onClick={refresh}>Refresh</button>
372
+ </div>
373
+ );
374
+ }
375
+ ```
226
376
 
227
- #### useSelectedWallet
377
+ **Parameters:**
228
378
 
229
- Convenience hook when you only need the currently selected wallet (and loading/refresh), not the full list. Use inside `CilantroProvider`.
379
+ - `walletId: string | null | { walletId?: string | null }` Wallet ID to load signers for
230
380
 
231
- **Returns:** `UseSelectedWalletResult`
381
+ **Returns: `UseSignersResult`**
232
382
 
233
383
  | Property | Type | Description |
234
384
  |----------|------|-------------|
235
- | `selectedWallet` | `WalletData \| null` | Currently selected wallet. |
236
- | `isLoading` | `boolean` | True while loading wallets. |
237
- | `refreshWallets` | `() => Promise<void>` | Reload wallets from the API. |
238
-
239
- **Use when:** You need the current wallet without the full wallet list or `selectWallet`.
385
+ | `signers` | `SignerData[]` | List of signers for the wallet |
386
+ | `isLoading` | `boolean` | True while loading |
387
+ | `error` | `string \| null` | Error message if load failed |
388
+ | `refresh` | `() => Promise<void>` | Reload signers |
389
+ | `createEmailSigner` | `(params: { email: string }) => Promise<SignerInfo>` | Create email signer |
390
+ | `createPhoneSigner` | `(params: { phone: string }) => Promise<SignerInfo>` | Create phone signer |
391
+ | `revokeSigner` | `(signerId: string) => Promise<void>` | Revoke a signer |
240
392
 
241
393
  ---
242
394
 
243
- #### useWalletAddress
395
+ ### usePasskey
244
396
 
245
- Returns the address of the currently selected wallet (handles both `address` and `walletAddress` on WalletData). Use inside `CilantroProvider`.
397
+ Passkey (WebAuthn) registration and authentication.
246
398
 
247
- **Returns:** `string | null`
399
+ ```tsx
400
+ import { usePasskey } from 'cilantro-react';
248
401
 
249
- **Use when:** You need the selected wallet's address for display or building transactions.
402
+ function PasskeyManager({ walletId }: { walletId: string }) {
403
+ const { isSupported, register, authenticate, isLoading } = usePasskey(walletId);
250
404
 
251
- ---
405
+ if (!isSupported) {
406
+ return <p>Passkeys not supported in this browser.</p>;
407
+ }
252
408
 
253
- #### useSigners
409
+ const handleRegister = async () => {
410
+ const result = await register();
411
+ console.log('Passkey registered:', result.signerId);
412
+ };
254
413
 
255
- Load signers for a wallet. Use inside `CilantroProvider` (auth required).
414
+ const handleAuthenticate = async () => {
415
+ const result = await authenticate();
416
+ console.log('Authenticated with passkey');
417
+ };
256
418
 
257
- **Options:** `UseSignersOptions`
419
+ return (
420
+ <div>
421
+ <button onClick={handleRegister} disabled={isLoading}>
422
+ {isLoading ? 'Registering...' : 'Register Passkey'}
423
+ </button>
424
+ <button onClick={handleAuthenticate} disabled={isLoading}>
425
+ {isLoading ? 'Authenticating...' : 'Authenticate'}
426
+ </button>
427
+ </div>
428
+ );
429
+ }
430
+ ```
258
431
 
259
- | Option | Type | Description |
260
- |--------|------|-------------|
261
- | `walletId` | `string \| null \| undefined` | Wallet ID to load signers for. If omitted, you can pass it from `useWallets().selectedWallet?.id`. |
432
+ **Parameters:**
262
433
 
263
- **Returns:** `UseSignersResult`
434
+ - `walletId: string | undefined` — Wallet ID to register passkey for
435
+
436
+ **Returns: `UsePasskeyResult`**
264
437
 
265
438
  | Property | Type | Description |
266
439
  |----------|------|-------------|
267
- | `signers` | `SignerData[]` | List of signers for the wallet. |
268
- | `isLoading` | `boolean` | True while loading. |
269
- | `error` | `string \| null` | Error message if load failed. |
270
- | `refresh` | `() => Promise<void>` | Reload signers. |
271
-
272
- When `walletId` is null/undefined, `signers` is empty and `refresh` no-ops.
440
+ | `isSupported` | `boolean` | True if WebAuthn is available |
441
+ | `register` | `() => Promise<{ signerId: string; isNew: boolean }>` | Register a new passkey |
442
+ | `authenticate` | `(signerId?: string) => Promise<{ credential: unknown }>` | Authenticate with passkey |
443
+ | `isLoading` | `boolean` | True during operation |
273
444
 
274
445
  ---
275
446
 
276
- #### useSignersForSelectedWallet
447
+ ### useExternalWallet
277
448
 
278
- Signers for the currently selected wallet (or a `walletId` override). Composes `useWallets` + `useSigners` so you don't pass `walletId` yourself. Use inside `CilantroProvider`.
449
+ Connect to external Solana wallets (Phantom, Solflare, etc.).
279
450
 
280
- **Options:** `UseSignersForSelectedWalletOptions`
451
+ ```tsx
452
+ import { useExternalWallet } from 'cilantro-react';
281
453
 
282
- | Option | Type | Description |
283
- |--------|------|-------------|
284
- | `walletId` | `string \| null \| undefined` | Override; when omitted, uses the selected wallet from context. |
454
+ function ExternalWalletConnect() {
455
+ const { wallets, connectedWallet, connect, disconnect } = useExternalWallet();
285
456
 
286
- **Returns:** Same as `useSigners` – `UseSignersResult` (`signers`, `isLoading`, `error`, `refresh`).
457
+ return (
458
+ <div>
459
+ {connectedWallet ? (
460
+ <div>
461
+ <p>Connected: {connectedWallet.name}</p>
462
+ <p>Public Key: {connectedWallet.publicKey?.toBase58()}</p>
463
+ <button onClick={disconnect}>Disconnect</button>
464
+ </div>
465
+ ) : (
466
+ <div>
467
+ {wallets.map((wallet) => (
468
+ <button key={wallet.name} onClick={() => connect(wallet)}>
469
+ Connect {wallet.name}
470
+ </button>
471
+ ))}
472
+ </div>
473
+ )}
474
+ </div>
475
+ );
476
+ }
477
+ ```
478
+
479
+ **Returns: `UseExternalWalletResult`**
287
480
 
288
- **Use when:** You need signers for the current wallet without calling `useWallets()` and `useSigners({ walletId })` in every component (e.g. SignerList-style UIs).
481
+ | Property | Type | Description |
482
+ |----------|------|-------------|
483
+ | `wallets` | `SolanaWallet[]` | Detected Solana wallets (Phantom, Solflare, etc.) |
484
+ | `connectedWallet` | `SolanaWallet \| null` | Currently connected wallet |
485
+ | `connect` | `(wallet: SolanaWallet) => Promise<void>` | Connect to a wallet |
486
+ | `disconnect` | `() => void` | Disconnect current wallet |
289
487
 
290
488
  ---
291
489
 
292
- #### useSignerSelection
490
+ ### useSendTransaction
491
+
492
+ Send SOL and SPL tokens.
493
+
494
+ ```tsx
495
+ import { useSendTransaction } from 'cilantro-react';
496
+
497
+ function SendTransaction({ walletId, signerId }: { walletId: string; signerId: string }) {
498
+ const { sendSOL, sendSPL, isPending, error } = useSendTransaction(walletId, signerId, 'email');
499
+
500
+ const handleSendSOL = async () => {
501
+ const result = await sendSOL({
502
+ recipientAddress: '7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU',
503
+ amountLamports: 100000000, // 0.1 SOL
504
+ });
505
+ console.log('Transaction signature:', result.data.signature);
506
+ };
507
+
508
+ const handleSendSPL = async () => {
509
+ const result = await sendSPL({
510
+ mintAddress: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', // USDC
511
+ recipientAddress: '7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU',
512
+ amount: 1000000, // 1 USDC (6 decimals)
513
+ });
514
+ console.log('Transaction signature:', result.data.signature);
515
+ };
293
516
 
294
- Wallet + signer selection state for signing flows. Uses `useWallets` and `useSigners` internally. Use inside `CilantroProvider`.
517
+ return (
518
+ <div>
519
+ {error && <p className="text-red-500">{error}</p>}
520
+ <button onClick={handleSendSOL} disabled={isPending}>
521
+ {isPending ? 'Sending...' : 'Send SOL'}
522
+ </button>
523
+ <button onClick={handleSendSPL} disabled={isPending}>
524
+ {isPending ? 'Sending...' : 'Send USDC'}
525
+ </button>
526
+ </div>
527
+ );
528
+ }
529
+ ```
295
530
 
296
- **Options:** `UseSignerSelectionOptions`
531
+ **Parameters:**
297
532
 
298
- | Option | Type | Description |
299
- |--------|------|-------------|
300
- | `walletId` | `string \| null \| undefined` | Override wallet ID; if not set, uses `useWallets().selectedWallet?.id`. |
301
- | `signingMethod` | `'wallet-adapter' \| 'sdk-signer' \| null` | Current signing method (default: `'sdk-signer'`). When `'wallet-adapter'`, signer selection is ignored. |
533
+ - `walletId: string | undefined` Wallet to send from
534
+ - `signerId?: string` — Signer ID (optional)
535
+ - `signerType?: 'email' | 'phone' | 'passkey' | 'external'` Signer type (optional)
302
536
 
303
- **Returns:** `UseSignerSelectionResult`
537
+ **Returns: `UseSendTransactionResult`**
304
538
 
305
539
  | Property | Type | Description |
306
540
  |----------|------|-------------|
307
- | `selectedWalletId` | `string` | Effective wallet ID (override or from context). |
308
- | `setSelectedWalletId` | `(id: string) => void` | Set wallet ID (when not using context selection). |
309
- | `availableSigners` | `SignerData[]` | Signers for the selected wallet (from `useSigners`). |
310
- | `selectedSigner` | `SignerData \| null` | Currently selected signer. |
311
- | `setSelectedSigner` | `(signer: SignerData \| null) => void` | Set selected signer. |
312
- | `isLoadingSigners` | `boolean` | True while signers are loading. |
313
- | `reset` | `() => void` | Clear `selectedWalletId` and `selectedSigner`. |
314
-
315
- **SigningMethod:** `'wallet-adapter'` = use wallet adapter for signing; `'sdk-signer'` = use a Cilantro signer (email, phone, passkey, external, etc.).
541
+ | `sendSOL` | `(params: { recipientAddress: string; amountLamports: number }) => Promise<SubmitTransactionResult>` | Send SOL |
542
+ | `sendSPL` | `(params: { mintAddress: string; recipientAddress: string; amount: number }) => Promise<SubmitTransactionResult>` | Send SPL tokens |
543
+ | `isPending` | `boolean` | True while sending |
544
+ | `error` | `string \| null` | Error message if send failed |
316
545
 
317
546
  ---
318
547
 
319
- #### useDelegatedKeys
548
+ ### useCilantroConfig
549
+
550
+ Access SDK configuration and storage adapter.
320
551
 
321
- Load delegated keys for a wallet (same data as DelegatedKeySelector). Use for custom delegated-key UI or logic. Use inside `CilantroProvider` (auth required).
552
+ ```tsx
553
+ import { useCilantroConfig } from 'cilantro-react';
322
554
 
323
- **Options:** `UseDelegatedKeysOptions`
555
+ function ConfigDisplay() {
556
+ const { config, storage } = useCilantroConfig();
324
557
 
325
- | Option | Type | Description |
326
- |--------|------|-------------|
327
- | `walletId` | `string \| null \| undefined` | Wallet ID to load delegated keys for. When null/undefined, keys are empty. |
328
- | `filterActive` | `boolean` | When true (default), filter to active and non-expired keys only. |
558
+ return (
559
+ <div>
560
+ <p>API Key: {config.apiKey ? '***' : 'Not set'}</p>
561
+ <p>Base URL: {config.baseURL}</p>
562
+ <p>Storage: {storage ? 'Configured' : 'Not configured'}</p>
563
+ </div>
564
+ );
565
+ }
566
+ ```
329
567
 
330
- **Returns:** `UseDelegatedKeysResult`
568
+ **Returns: `UseCilantroConfigResult`**
331
569
 
332
570
  | Property | Type | Description |
333
571
  |----------|------|-------------|
334
- | `keys` | `DelegatedKeyData[]` | List of delegated keys. |
335
- | `isLoading` | `boolean` | True while loading. |
336
- | `error` | `string \| null` | Error message if load failed. |
337
- | `refresh` | `() => Promise<void>` | Reload delegated keys. |
338
-
339
- **Use when:** You need the list of delegated keys in custom UI or logic without using the DelegatedKeySelector component.
572
+ | `config` | `{ apiKey?: string; baseURL?: string; jwt?: string }` | Current configuration |
573
+ | `storage` | `DeviceKeyStorage \| null` | Storage adapter instance |
340
574
 
341
575
  ---
342
576
 
343
- #### useCanSign
344
-
345
- Derived "is the user ready to sign?" for disabling sign buttons or showing prompts. Use inside `CilantroProvider`.
577
+ ## Components
346
578
 
347
- **Options:** `UseCanSignOptions`
579
+ ### Auth Components
348
580
 
349
- | Option | Type | Description |
350
- |--------|------|-------------|
351
- | `signingMethod` | `SigningMethod \| null` | Override; default `'sdk-signer'`. |
352
- | `requireSigner` | `boolean` | When true (default), require a selected signer for sdk-signer. |
353
- | `walletAdapterConnected` | `boolean` | When using wallet-adapter, pass true when the adapter is connected so `canSign*` reflects it. |
581
+ #### LoginForm
354
582
 
355
- **Returns:** `UseCanSignResult`
583
+ Username/email + password login form with loading and error states.
356
584
 
357
- | Property | Type | Description |
358
- |----------|------|-------------|
359
- | `hasToken` | `boolean` | True when the user has a valid token. |
360
- | `hasWallet` | `boolean` | True when a wallet is selected. |
361
- | `hasSigner` | `boolean` | True when a signer is selected (sdk-signer) or wallet-adapter is connected (when `walletAdapterConnected` is passed). |
362
- | `canSignMessage` | `boolean` | True when ready to sign a message. |
363
- | `canSignTransaction` | `boolean` | True when ready to sign/send a transaction (connection still required for send). |
585
+ ```tsx
586
+ import { LoginForm } from 'cilantro-react';
587
+
588
+ <LoginForm
589
+ onSuccess={() => console.log('Logged in!')}
590
+ onError={(err) => console.error(err.message)}
591
+ redirectAfterSuccess="/dashboard"
592
+ onRedirect={(path) => router.replace(path)}
593
+ title="Welcome back"
594
+ description="Sign in to your account"
595
+ submitLabel="Sign in"
596
+ renderSwitchToRegister={() => <a href="/register">Create account</a>}
597
+ className="max-w-sm"
598
+ />
599
+ ```
364
600
 
365
- **Use when:** You need to disable sign buttons or show "Select wallet" / "Select signer" prompts without reimplementing token/wallet/signer checks. For wallet-adapter, pass `walletAdapterConnected` from your adapter state.
601
+ **Props:**
602
+
603
+ | Prop | Type | Default | Description |
604
+ |------|------|---------|-------------|
605
+ | `onSuccess` | `() => void` | — | Called after successful login |
606
+ | `onError` | `(error: Error) => void` | — | Called on login error |
607
+ | `redirectAfterSuccess` | `string` | — | Redirect to this path after login. If unset, reads from URL `?returnUrl=` |
608
+ | `onRedirect` | `(path: string) => void` | — | Custom redirect for SPA routers |
609
+ | `returnUrlParam` | `string` | `"returnUrl"` | Query param to read return URL from |
610
+ | `title` | `string` | `"Sign in"` | Form title |
611
+ | `description` | `string` | — | Form description |
612
+ | `submitLabel` | `string` | `"Sign in"` | Submit button label |
613
+ | `rememberMe` | `boolean` | — | Show remember me option |
614
+ | `renderSwitchToRegister` | `() => ReactNode` | — | Render link to register page |
615
+ | `className` | `string` | — | Root element class |
616
+ | `classNames` | `LoginFormClassNames` | — | Class names for inner elements |
366
617
 
367
618
  ---
368
619
 
369
- #### useMessageSigning
620
+ #### LogoutButton
370
621
 
371
- Sign a message with the selected signer or wallet-adapter. Use inside `CilantroProvider`.
622
+ Logout button with optional confirmation dialog.
372
623
 
373
- **Options:** `UseMessageSigningOptions`
374
-
375
- | Option | Type | Description |
376
- |--------|------|-------------|
377
- | `token` | `string \| null` | JWT (usually from `useCilantroAuth().token`). |
378
- | `signingMethod` | `SigningMethod \| null` | `'sdk-signer'` or `'wallet-adapter'`. |
379
- | `selectedSigner` | `SignerData \| null` | Signer to use when `signingMethod === 'sdk-signer'`. |
380
- | `selectedWalletId` | `string` | Wallet ID when using SDK signer. |
381
- | `walletAdapterSignMessage` | `(message: Uint8Array) => Promise<Uint8Array>` | Optional; required when `signingMethod === 'wallet-adapter'`. |
382
- | `walletAdapterPublicKey` | `string \| null` | Optional; included in success detail when using wallet-adapter. |
624
+ ```tsx
625
+ import { LogoutButton } from 'cilantro-react';
626
+
627
+ <LogoutButton
628
+ onLogout={() => console.log('Logged out')}
629
+ redirectAfterLogout="/login"
630
+ onRedirect={(path) => router.replace(path)}
631
+ confirmBeforeLogout={true}
632
+ >
633
+ Sign out
634
+ </LogoutButton>
635
+ ```
383
636
 
384
- **Returns:** `UseMessageSigningResult`
637
+ **Props:**
385
638
 
386
- | Property | Type | Description |
387
- |----------|------|-------------|
388
- | `messageText` | `string` | Current message (default: `"Hello, Solana!"`). |
389
- | `setMessageText` | `(text: string) => void` | Update message. |
390
- | `signResultState` | `ActionState` | `{ status, message?, detail? }` idle, loading, success, or error. |
391
- | `isSigning` | `boolean` | True while signing. |
392
- | `handleSign` | `() => Promise<void>` | Sign the current message; updates `signResultState`. |
393
- | `reset` | `() => void` | Reset message to default and clear result state. |
639
+ | Prop | Type | Default | Description |
640
+ |------|------|---------|-------------|
641
+ | `onLogout` | `() => void` | | Called after logout |
642
+ | `redirectAfterLogout` | `string` | | Redirect to this path after logout |
643
+ | `onRedirect` | `(path: string) => void` | | Custom redirect for SPA routers |
644
+ | `confirmBeforeLogout` | `boolean` | `false` | Show confirmation dialog |
645
+ | `className` | `string` | | Button class |
646
+ | `children` | `ReactNode` | `"Log out"` | Button content |
394
647
 
395
648
  ---
396
649
 
397
- #### useTransactionSigning
650
+ #### AuthGuard
398
651
 
399
- Sign and/or send a transaction. Use inside `CilantroProvider`.
652
+ Protect content behind authentication. Shows fallback (default: LoginForm) when not authenticated.
400
653
 
401
- **Options:** `UseTransactionSigningOptions`
654
+ ```tsx
655
+ import { AuthGuard, LoginForm } from 'cilantro-react';
402
656
 
403
- | Option | Type | Description |
404
- |--------|------|-------------|
405
- | `token` | `string \| null` | JWT (usually from `useCilantroAuth().token`). |
406
- | `signingMethod` | `SigningMethod \| null` | `'sdk-signer'` or `'wallet-adapter'`. |
407
- | `selectedSigner` | `SignerData \| null` | Signer when `signingMethod === 'sdk-signer'`. |
408
- | `selectedWalletId` | `string` | Wallet ID when using SDK signer. |
409
- | `walletAdapterSignTransaction` | `(transaction: Transaction) => Promise<Transaction>` | Optional; required for wallet-adapter signing. |
410
- | `walletAdapterPublicKey` | `string \| null` | Optional; required for wallet-adapter send. |
411
- | `connection` | `SignAndSendConnection \| null` | **Required for sign-and-send** when using wallet-adapter or passkey. Use `adaptSolanaConnection(connection)` when you have a `@solana/web3.js` Connection; see [Connection setup](#connection-setup-solana). |
657
+ // Basic usage - shows LoginForm when not authenticated
658
+ <AuthGuard>
659
+ <ProtectedContent />
660
+ </AuthGuard>
412
661
 
413
- **Returns:** `UseTransactionSigningResult`
662
+ // Custom fallback
663
+ <AuthGuard fallback={<CustomLoginPage />}>
664
+ <ProtectedContent />
665
+ </AuthGuard>
414
666
 
415
- | Property | Type | Description |
416
- |----------|------|-------------|
417
- | `transactionResultState` | `ActionState` | Result of last sign/send (idle, loading, success, error). |
418
- | `isSigningTransaction` | `boolean` | True while `signTransaction` is running. |
419
- | `isSendingTransaction` | `boolean` | True while `signAndSendTransaction` is running. |
420
- | `signTransaction` | `(transaction: Transaction) => Promise<void>` | Sign only (no send). Passkey signers cannot sign-only; use sign-and-send. |
421
- | `signAndSendTransaction` | `(transaction: Transaction) => Promise<void>` | Sign and send. Requires `connection` for wallet-adapter and passkey. |
422
- | `reset` | `() => void` | Set `transactionResultState` back to idle. |
667
+ // Redirect to login page (appends ?returnUrl=currentPath for redirect-after-login)
668
+ <AuthGuard redirectTo="/login">
669
+ <ProtectedContent />
670
+ </AuthGuard>
423
671
 
424
- **Connection:** For passkey or wallet-adapter sign-and-send, pass a `connection` that has `getLatestBlockhash`, `sendRawTransaction`, and `confirmTransaction`. Use `adaptSolanaConnection(connection)` when you have a `@solana/web3.js` Connection; see [Connection setup](#connection-setup-solana).
672
+ // Next.js / React Router: use onRedirect for client-side navigation (no full page reload)
673
+ import { useRouter } from 'next/navigation';
674
+ const router = useRouter();
675
+ <AuthGuard redirectTo="/login" onRedirect={(path) => router.replace(path)}>
676
+ <ProtectedContent />
677
+ </AuthGuard>
425
678
 
426
- ---
679
+ // Polished UX: centered or fullscreen layout for auth fallback
680
+ <AuthGuard layout="centered"> {/* or "fullscreen" */}
681
+ <ProtectedContent />
682
+ </AuthGuard>
427
683
 
428
- ### Components (detailed)
684
+ // Hide content when not authenticated (no fallback)
685
+ <AuthGuard showFallback={false}>
686
+ <ProtectedContent />
687
+ </AuthGuard>
429
688
 
430
- All components accept `className`. Many accept a `classNames` object (see TypeScript types e.g. `WalletSelectorClassNames`, `MessageSigningFormClassNames`).
689
+ // Show skeleton while loading
690
+ <AuthGuard useSkeleton={true}>
691
+ <ProtectedContent />
692
+ </AuthGuard>
693
+ ```
431
694
 
432
- #### Auth components
695
+ **Props:**
696
+
697
+ | Prop | Type | Default | Description |
698
+ |------|------|---------|-------------|
699
+ | `children` | `ReactNode` | — | Content to show when authenticated |
700
+ | `fallback` | `ReactNode` | `<LoginForm />` | Content when not authenticated |
701
+ | `redirectTo` | `string` | — | Redirect unauthenticated users to this path (appends `?returnUrl=`) |
702
+ | `onRedirect` | `(path: string) => void` | — | Custom redirect for SPA routers (e.g. `router.replace`) |
703
+ | `returnUrlParam` | `string` | `"returnUrl"` | Query param name for return URL |
704
+ | `layout` | `"inline" \| "centered" \| "fullscreen"` | `"inline"` | Layout for fallback content |
705
+ | `showFallback` | `boolean` | `true` | Whether to show fallback or null |
706
+ | `useSkeleton` | `boolean` | `false` | Show skeleton while loading auth state |
707
+ | `className` | `string` | — | Root element class |
708
+ | `classNames` | `AuthGuardClassNames` | — | Class names for inner elements |
433
709
 
434
- | Component | Purpose | Key props |
435
- |-----------|---------|-----------|
436
- | **AuthForm** | Single form that toggles login/register. | `defaultMode?: 'login' \| 'register'`, `onSuccess?`, `onError?(error: string)`, `loginTitle`, `registerTitle`, `loginDescription`, `registerDescription`, `loginSubmitLabel`, `registerSubmitLabel`, `switchToRegisterText`, `switchToLoginText`, `isActive?`, `className`, `classNames?` (root, header, title, description, form, label, input, submitButton, toggle, toggleLink, error). |
437
- | **LoginForm** | Login (username/email + password). | `onSuccess?`, `onError?(error: string)`, `submitLabel`, `title`, `description`, `renderSwitchToRegister? () => ReactNode`, `className`, `classNames?`. |
438
- | **RegisterForm** | Registration (username, email, password). | `onSuccess?`, `onError?(error: string)`, `submitLabel`, `title`, `description`, `isActive?` (default true), `renderSwitchToLogin? () => ReactNode`, `className`, `classNames?`. |
439
- | **AuthGuard** | Renders children when authenticated; shows fallback otherwise. | `children`, `fallback?: ReactNode` (default: `<LoginForm />`), `showFallback?` (default true), `className`, `classNames?` (root, fallback). |
710
+ ---
440
711
 
441
- #### Wallet and signer components
712
+ #### RegisterForm
442
713
 
443
- | Component | Purpose | Key props |
444
- |-----------|---------|-----------|
445
- | **WalletSelector** | Wallet dropdown (Shadcn Select). | `value?`, `onWalletChange?(walletId, wallet)`, `placeholder`, `className`, `classNames?` (root, trigger, content, item). Headless: `children?`, `renderTrigger?`, `renderList?`. |
446
- | **SignerSelector** | List/select signers (buttons). | `selectedWalletId?`, `availableSigners`, `selectedSigner`, `onSignerSelect(signer)`, `isLoadingSigners?`, `className`, `classNames?` (root, list, item, message). Headless: `children?`, `renderList?`. |
447
- | **SignerList** | List signers + “Add signer” (opens AddSignerForm in dialog). | `walletId`, `onSignerAdded`, `className`, `classNames`. Headless: `children`. |
448
- | **AddSignerForm** | Add signer (email, phone, passkey, external wallet). | `walletId`, `open?`, `onOpenChange?`, `onSuccess?`, `onCancel?`, `onError?(error)`, `asDialog?` (default true), `className`, `classNames?`. |
449
- | **DelegatedKeySelector** | Select delegated key for a wallet. | `walletId`, `value?`, `onChange?(keyId, key)`, `filterActive?` (default true), `placeholder`, `className`, `classNames?`. Headless: `children?` (keys, selectedKeyId, onSelect, isLoading, error, refresh). |
714
+ Registration form with username, email, and password. Auto-login after success.
450
715
 
451
- #### UI primitives
716
+ ```tsx
717
+ import { RegisterForm } from 'cilantro-react';
452
718
 
453
- - **Skeleton** – Shadcn-style skeleton placeholder (`rounded-md bg-muted animate-pulse`). Use for loading states or custom UI. Exported for headless use.
454
- - **ThemeProvider** – See [Themes](#built-in-theme-themeprovider).
719
+ <RegisterForm
720
+ onSuccess={() => console.log('Registered!')}
721
+ redirectAfterSuccess="/dashboard"
722
+ onRedirect={(path) => router.replace(path)} // For Next.js/React Router
723
+ renderSwitchToLogin={() => <a href="/login">Already have an account? Sign in</a>}
724
+ />
725
+ ```
455
726
 
456
- #### Signing form components
727
+ ---
457
728
 
458
- | Component | Purpose | Key props |
459
- |-----------|---------|-----------|
460
- | **MessageSigningForm** | Message input + sign button; shows wallet/signer context. | `token?`, `selectedWalletId?`, `selectedSigner?`, `signingMethod?`, `walletAdapterSignMessage?`, `walletAdapterPublicKey?`, `showContext?`, `showCharCount?`, `className`, `classNames?`. Headless: `children?` (messageText, setMessageText, signResultState, isSigning, handleSign, reset). |
461
- | **TransactionSigningForm** | Sign/send transaction; pass `connection` for sign-and-send. | `token?`, `selectedWalletId?`, `selectedSigner?`, `signingMethod?`, `walletAdapterSignTransaction?`, `walletAdapterPublicKey?`, **`connection?`** (required for wallet-adapter or passkey sign-and-send), `showContext?`, `className`, `classNames?`. Headless: `children?` (transactionResultState, isSigningTransaction, isSendingTransaction, signTransaction, signAndSendTransaction, reset). |
729
+ #### AuthShell
462
730
 
463
- When token/wallet/signer/signingMethod are omitted, MessageSigningForm and TransactionSigningForm use `useCilantroAuth().token` and `useSignerSelection()` internally.
731
+ Full-page centered layout for auth screens. Use for standalone login/register pages.
464
732
 
465
- ### Types (detailed)
733
+ ```tsx
734
+ import { AuthShell, LoginForm } from 'cilantro-react';
466
735
 
467
- #### ActionState
736
+ <AuthShell logo={<img src="/logo.svg" alt="App" />} maxWidth="sm">
737
+ <LoginForm redirectAfterSuccess="/dashboard" />
738
+ </AuthShell>
739
+ ```
468
740
 
469
- Used by signing hooks and forms for result state.
741
+ ---
470
742
 
471
- ```ts
472
- type ActionState<T = unknown> = {
473
- status: 'idle' | 'loading' | 'success' | 'error';
474
- message?: string;
475
- detail?: T;
476
- };
743
+ #### NextAuthGuard (Next.js)
744
+
745
+ Drop-in AuthGuard for Next.js App Router. Uses `router.replace()` for client-side redirects.
746
+ Requires `next` to be installed.
747
+
748
+ ```tsx
749
+ import { NextAuthGuard } from 'cilantro-react/next';
750
+
751
+ <NextAuthGuard redirectTo="/login" layout="fullscreen">
752
+ <ProtectedContent />
753
+ </NextAuthGuard>
477
754
  ```
478
755
 
479
- - **idle** – No operation yet.
480
- - **loading** – Sign/send in progress.
481
- - **success** – Done; `message` and optional `detail` (e.g. signature, explorer URL).
482
- - **error** – Failed; `message` (and optional `detail`).
756
+ ---
483
757
 
484
- #### User
758
+ ### Wallet Components
485
759
 
486
- From `useCilantroAuth().user`:
760
+ #### WalletSelector
487
761
 
488
- ```ts
489
- interface User {
490
- username?: string;
491
- email?: string;
492
- userType?: string;
493
- }
494
- ```
762
+ Dropdown to select from available wallets.
495
763
 
496
- #### WalletData
764
+ ```tsx
765
+ import { WalletSelector } from 'cilantro-react';
497
766
 
498
- From `useWallets().wallets` / `selectedWallet`:
767
+ // Controlled
768
+ const [walletId, setWalletId] = useState('');
769
+ <WalletSelector value={walletId} onChange={setWalletId} />
499
770
 
500
- ```ts
501
- interface WalletData {
502
- id: string;
503
- walletId: string;
504
- walletName?: string;
505
- address?: string;
506
- walletAddress?: string;
507
- chain?: string;
508
- active?: boolean;
509
- [key: string]: unknown;
510
- }
771
+ // With callback
772
+ <WalletSelector
773
+ onWalletChange={(id, wallet) => console.log('Selected:', wallet?.walletName)}
774
+ placeholder="Choose a wallet"
775
+ />
776
+
777
+ // Headless (custom rendering)
778
+ <WalletSelector>
779
+ {({ wallets, selectedWallet, selectWallet, isLoading }) => (
780
+ <div>
781
+ {wallets.map((w) => (
782
+ <button key={w.id} onClick={() => selectWallet(w.id)}>
783
+ {w.walletName}
784
+ </button>
785
+ ))}
786
+ </div>
787
+ )}
788
+ </WalletSelector>
511
789
  ```
512
790
 
513
- #### SignerData
791
+ **Props:**
792
+
793
+ | Prop | Type | Default | Description |
794
+ |------|------|---------|-------------|
795
+ | `value` | `string` | — | Controlled wallet ID |
796
+ | `onChange` | `(walletId: string) => void` | — | Called when wallet changes |
797
+ | `onWalletChange` | `(walletId: string, wallet: WalletData \| null) => void` | — | Called with wallet object |
798
+ | `placeholder` | `string` | `"Select a wallet"` | Placeholder text |
799
+ | `useSkeleton` | `boolean` | `true` | Show skeleton while loading |
800
+ | `className` | `string` | — | Root element class |
801
+ | `classNames` | `WalletSelectorClassNames` | — | Class names for inner elements |
802
+ | `children` | `(props) => ReactNode` | — | Headless render function |
803
+ | `renderTrigger` | `(props) => ReactNode` | — | Custom trigger render |
804
+ | `renderList` | `(props) => ReactNode` | — | Custom list render |
805
+
806
+ ---
514
807
 
515
- From `useSigners().signers` or `useSignerSelection().availableSigners` / `selectedSigner`. Used by SignerSelector, MessageSigningForm, TransactionSigningForm. Contains at least: `id`, `signerId`, `type` or `signerType`, `walletId`, `publicKey` / `signerPubkey`, and optional `email`, `phone`, `isActive`, `signerConfig`, etc. See TypeScript type `SignerData` for full shape.
808
+ #### CreateWalletForm
516
809
 
517
- #### SignAndSendConnection / adaptSolanaConnection
810
+ Form to create a new wallet.
811
+
812
+ ```tsx
813
+ import { CreateWalletForm } from 'cilantro-react';
518
814
 
519
- Type for the `connection` prop when using sign-and-send (passkey or wallet-adapter). Must have:
815
+ <CreateWalletForm
816
+ onCreated={(result) => console.log('Created wallet:', result.data.id)}
817
+ onError={(err) => console.error(err.message)}
818
+ userId="optional-user-id"
819
+ />
820
+ ```
520
821
 
521
- - `sendRawTransaction(buf: Buffer)` → `Promise<string>`
522
- - `getLatestBlockhash()` → `Promise<{ blockhash, lastValidBlockHeight }>`
523
- - `confirmTransaction(opts: { signature, blockhash, lastValidBlockHeight })` → `Promise<void>`
822
+ **Props:**
524
823
 
525
- If you use `@solana/web3.js`, call `adaptSolanaConnection(connection)` and pass the result; see [Connection setup](#connection-setup-solana).
824
+ | Prop | Type | Default | Description |
825
+ |------|------|---------|-------------|
826
+ | `userId` | `string` | — | Optional user ID to associate wallet with |
827
+ | `onCreated` | `(wallet: WalletControllerCreateResult) => void` | — | Called after wallet is created |
828
+ | `onError` | `(error: Error) => void` | — | Called on error |
829
+ | `className` | `string` | — | Form class |
526
830
 
527
- #### Other types
831
+ ---
528
832
 
529
- - **ErrorResponse** – `{ message?, error?, code?, status?, statusText?, data?, ... }` for API error shapes.
530
- - **ApiResponse\<T\>** – SDK response shape; use `extractResponseData(response)` to unwrap.
531
- - **DelegatedKeyData** – From DelegatedKeySelector: `id`, `walletId`, `name?`, `publicKey`, `permissions`, `isActive?`, `createdAt?`, `expiresAt?`, etc.
532
- - **SigningMethod** – `'wallet-adapter' | 'sdk-signer'`.
533
- - **AddSignerType** – `'email' | 'phone' | 'passkey' | 'external'`.
534
- - **AuthFormMode** – `'login' | 'register'`.
833
+ #### WalletAddress
535
834
 
536
- #### Utilities
835
+ Display a wallet address with copy functionality.
537
836
 
538
- - **extractErrorMessage(error: unknown): string** – Returns a string from any thrown value (Error.message, object.message, response.data.message, etc.). Use in catch blocks when calling auth, signer, or SDK methods.
539
- - **extractResponseData\<T\>(response: unknown): T | null** – Unwraps SDK response shapes (`{ data }`, `{ success, data }`, signer arrays). Use after cilantro-sdk calls that return wrapped payloads.
837
+ ```tsx
838
+ import { WalletAddress } from 'cilantro-react';
540
839
 
541
- ---
840
+ <WalletAddress
841
+ address="7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU"
842
+ short={true}
843
+ copyable={true}
844
+ />
845
+ ```
846
+
847
+ **Props:**
542
848
 
543
- ## Examples
849
+ | Prop | Type | Default | Description |
850
+ |------|------|---------|-------------|
851
+ | `address` | `string` | — | Wallet address to display |
852
+ | `short` | `boolean` | `false` | Truncate address for display |
853
+ | `copyable` | `boolean` | `false` | Show copy button |
854
+ | `className` | `string` | — | Element class |
855
+
856
+ ---
544
857
 
545
- ### Auth – single form (AuthForm)
858
+ #### WalletBalance
546
859
 
547
- One form that switches between sign-in and create-account:
860
+ Display wallet SOL and token balances.
548
861
 
549
862
  ```tsx
550
- import { AuthForm } from 'cilantro-react'
863
+ import { WalletBalance } from 'cilantro-react';
551
864
 
552
- function LoginPage() {
553
- return (
554
- <AuthForm
555
- defaultMode="login"
556
- onSuccess={() => window.location.href = '/dashboard'}
557
- onError={(err) => console.error(err)}
558
- loginTitle="Welcome back"
559
- registerTitle="Create an account"
560
- switchToRegisterText="Don't have an account? Create one"
561
- switchToLoginText="Already have an account? Sign in"
562
- />
563
- )
564
- }
865
+ <WalletBalance walletId={walletId} showTokens={true} />
565
866
  ```
566
867
 
567
- ### Auth – separate Login and Register
868
+ **Props:**
568
869
 
569
- Use `LoginForm` and `RegisterForm` with links to switch pages:
870
+ | Prop | Type | Default | Description |
871
+ |------|------|---------|-------------|
872
+ | `walletId` | `string` | — | Wallet ID to show balance for |
873
+ | `showTokens` | `boolean` | `false` | Show SPL token balances |
874
+ | `className` | `string` | — | Element class |
570
875
 
571
- ```tsx
572
- import { LoginForm, RegisterForm } from 'cilantro-react'
573
- import { Link } from 'react-router-dom'
876
+ ---
574
877
 
575
- function LoginPage() {
576
- return (
577
- <LoginForm
578
- onSuccess={() => navigate('/dashboard')}
579
- renderSwitchToRegister={() => (
580
- <Link to="/register">Create account</Link>
581
- )}
582
- />
583
- )
584
- }
878
+ #### ConnectWalletButton
585
879
 
586
- function RegisterPage() {
587
- return (
588
- <RegisterForm
589
- onSuccess={() => navigate('/dashboard')}
590
- renderSwitchToLogin={() => (
591
- <Link to="/login">Already have an account? Sign in</Link>
592
- )}
593
- />
594
- )
595
- }
880
+ Connect to external Solana wallets (Phantom, Solflare).
881
+
882
+ ```tsx
883
+ import { ConnectWalletButton } from 'cilantro-react';
884
+
885
+ <ConnectWalletButton
886
+ onConnect={(publicKey, wallet) => console.log('Connected:', publicKey)}
887
+ onDisconnect={() => console.log('Disconnected')}
888
+ >
889
+ Connect External Wallet
890
+ </ConnectWalletButton>
596
891
  ```
597
892
 
598
- ### Auth – protected route (AuthGuard)
893
+ **Props:**
599
894
 
600
- Show login when not authenticated, otherwise render children:
895
+ | Prop | Type | Default | Description |
896
+ |------|------|---------|-------------|
897
+ | `onConnect` | `(publicKey: string, wallet: SolanaWallet) => void` | — | Called when connected |
898
+ | `onDisconnect` | `() => void` | — | Called when disconnected |
899
+ | `className` | `string` | — | Button class |
900
+ | `children` | `ReactNode` | `"Connect Wallet"` | Button content |
901
+
902
+ ---
903
+
904
+ ### Signer Components
905
+
906
+ #### SignerSelector
907
+
908
+ Select a signer from the wallet's signers.
601
909
 
602
910
  ```tsx
603
- import { AuthGuard, LoginForm } from 'cilantro-react'
911
+ import { SignerSelector } from 'cilantro-react';
604
912
 
605
- function DashboardRoute() {
606
- return (
607
- <AuthGuard fallback={<LoginForm />}>
608
- <Dashboard />
609
- </AuthGuard>
610
- )
611
- }
913
+ const [signerId, setSignerId] = useState('');
914
+ const [signerType, setSignerType] = useState<'email' | 'phone'>('email');
612
915
 
613
- // Or use default fallback (LoginForm)
614
- <AuthGuard>
615
- <Dashboard />
616
- </AuthGuard>
916
+ <SignerSelector
917
+ walletId={walletId}
918
+ value={signerId}
919
+ onChange={(id, type) => {
920
+ setSignerId(id);
921
+ setSignerType(type);
922
+ }}
923
+ />
617
924
 
618
- // Hide content when unauthenticated (no fallback UI)
619
- <AuthGuard showFallback={false}>
620
- <Dashboard />
621
- </AuthGuard>
925
+ // Headless
926
+ <SignerSelector walletId={walletId} onChange={handleChange}>
927
+ {({ signers, selectedSigner, onSignerSelect, isLoading }) => (
928
+ <div>
929
+ {signers.map((s) => (
930
+ <button key={s.id} onClick={() => onSignerSelect(s)}>
931
+ {s.email || s.phone}
932
+ </button>
933
+ ))}
934
+ </div>
935
+ )}
936
+ </SignerSelector>
622
937
  ```
623
938
 
624
- ### Wallets
939
+ **Props:**
625
940
 
626
- Wallet dropdown with optional customization:
941
+ | Prop | Type | Default | Description |
942
+ |------|------|---------|-------------|
943
+ | `walletId` | `string` | — | Wallet ID to load signers for |
944
+ | `value` | `string` | — | Selected signer ID |
945
+ | `onChange` | `(signerId: string, signerType: SignerType) => void` | — | Called when signer changes |
946
+ | `useSkeleton` | `boolean` | `true` | Show skeleton while loading |
947
+ | `className` | `string` | — | Root element class |
948
+ | `classNames` | `SignerSelectorClassNames` | — | Class names for inner elements |
949
+ | `children` | `(props) => ReactNode` | — | Headless render function |
950
+ | `renderList` | `(props) => ReactNode` | — | Custom list render |
951
+
952
+ ---
953
+
954
+ #### SignerList
955
+
956
+ List signers with "Add signer" dialog (includes email, phone, passkey forms).
627
957
 
628
958
  ```tsx
629
- import { useWallets, WalletSelector } from 'cilantro-react'
959
+ import { SignerList } from 'cilantro-react';
630
960
 
631
- function Wallets() {
632
- const { wallets, selectedWallet } = useWallets()
961
+ <SignerList
962
+ walletId={walletId}
963
+ onSignerAdded={() => console.log('Signer added')}
964
+ onRevoke={(signerId) => console.log('Revoked:', signerId)}
965
+ />
633
966
 
634
- return (
967
+ // Headless
968
+ <SignerList walletId={walletId}>
969
+ {({ signers, isLoading, openAddSigner }) => (
635
970
  <div>
636
- <WalletSelector
637
- placeholder="Choose a wallet"
638
- onWalletChange={(id, wallet) => console.log('Selected', id, wallet)}
639
- classNames={{
640
- root: 'w-full max-w-xs',
641
- trigger: 'border-2',
642
- }}
643
- />
644
- {selectedWallet && (
645
- <p>Selected: {selectedWallet.walletName ?? selectedWallet.id}</p>
646
- )}
971
+ {signers.map((s) => <div key={s.id}>{s.email}</div>)}
972
+ <button onClick={openAddSigner}>Add Signer</button>
647
973
  </div>
648
- )
649
- }
974
+ )}
975
+ </SignerList>
650
976
  ```
651
977
 
652
- ### Signers – list and select
978
+ **Props:**
653
979
 
654
- Load signers for the selected wallet and let the user pick one:
980
+ | Prop | Type | Default | Description |
981
+ |------|------|---------|-------------|
982
+ | `walletId` | `string` | — | Wallet ID |
983
+ | `onSignerAdded` | `() => void` | — | Called after signer is added |
984
+ | `onRevoke` | `(signerId: string) => void` | — | Called when signer is revoked |
985
+ | `useSkeleton` | `boolean` | `true` | Show skeleton while loading |
986
+ | `className` | `string` | — | Root element class |
987
+ | `classNames` | `SignerListClassNames` | — | Class names for inner elements |
988
+ | `children` | `(props) => ReactNode` | — | Headless render function |
655
989
 
656
- ```tsx
657
- import { useSigners, useSignerSelection, SignerSelector } from 'cilantro-react'
990
+ ---
658
991
 
659
- function SignerPick() {
660
- const { walletId } = useSignerSelection({ walletId: selectedWalletId })
661
- const { signers, isLoading, refresh } = useSigners({ walletId: walletId ?? undefined })
992
+ #### CreateEmailSignerForm
662
993
 
663
- return (
664
- <SignerSelector
665
- selectedWalletId={walletId ?? undefined}
666
- availableSigners={signers}
667
- selectedSigner={selectedSigner}
668
- onSignerSelect={setSelectedSigner}
669
- isLoadingSigners={isLoading}
670
- className="w-full"
671
- />
672
- )
673
- }
994
+ Form to create an email signer.
995
+
996
+ ```tsx
997
+ import { CreateEmailSignerForm } from 'cilantro-react';
998
+
999
+ <CreateEmailSignerForm
1000
+ walletId={walletId}
1001
+ onCreated={(signer) => console.log('Created:', signer.signerId)}
1002
+ onError={(err) => console.error(err.message)}
1003
+ />
674
1004
  ```
675
1005
 
676
- Headless: use `children` or `renderList` to render the list yourself.
1006
+ **Props:**
677
1007
 
678
- ### Signers add signer (AddSignerForm + SignerList)
1008
+ | Prop | Type | Default | Description |
1009
+ |------|------|---------|-------------|
1010
+ | `walletId` | `string` | — | Wallet ID |
1011
+ | `onCreated` | `(signer: SignerInfo) => void` | — | Called after signer is created |
1012
+ | `onError` | `(error: Error) => void` | — | Called on error |
1013
+ | `className` | `string` | — | Form class |
679
1014
 
680
- List signers and open a dialog to add a new one (email, phone, passkey, external wallet):
1015
+ ---
1016
+
1017
+ #### CreatePhoneSignerForm
1018
+
1019
+ Form to create a phone signer.
681
1020
 
682
1021
  ```tsx
683
- import { SignerList } from 'cilantro-react'
1022
+ import { CreatePhoneSignerForm } from 'cilantro-react';
684
1023
 
685
- function SignersPage() {
686
- const walletId = 'your-wallet-id'
1024
+ <CreatePhoneSignerForm
1025
+ walletId={walletId}
1026
+ onCreated={(signer) => console.log('Created:', signer.signerId)}
1027
+ onError={(err) => console.error(err.message)}
1028
+ />
1029
+ ```
687
1030
 
688
- return (
689
- <SignerList
690
- walletId={walletId}
691
- onSignerAdded={() => console.log('Signer added')}
692
- />
693
- )
694
- }
1031
+ **Props:**
1032
+
1033
+ | Prop | Type | Default | Description |
1034
+ |------|------|---------|-------------|
1035
+ | `walletId` | `string` | — | Wallet ID |
1036
+ | `onCreated` | `(signer: SignerInfo) => void` | — | Called after signer is created |
1037
+ | `onError` | `(error: Error) => void` | — | Called on error |
1038
+ | `className` | `string` | — | Form class |
1039
+
1040
+ ---
1041
+
1042
+ #### AddPasskeyButton
1043
+
1044
+ Button to register a passkey signer. Only renders if WebAuthn is supported.
1045
+
1046
+ ```tsx
1047
+ import { AddPasskeyButton } from 'cilantro-react';
1048
+
1049
+ <AddPasskeyButton
1050
+ walletId={walletId}
1051
+ onRegistered={(signer) => console.log('Registered:', signer.signerId)}
1052
+ onError={(err) => console.error(err.message)}
1053
+ >
1054
+ Add Passkey
1055
+ </AddPasskeyButton>
695
1056
  ```
696
1057
 
697
- AddSignerForm can also be used standalone (e.g. inline or in your own dialog):
1058
+ **Props:**
1059
+
1060
+ | Prop | Type | Default | Description |
1061
+ |------|------|---------|-------------|
1062
+ | `walletId` | `string` | — | Wallet ID |
1063
+ | `onRegistered` | `(signer: SignerInfo) => void` | — | Called after passkey is registered |
1064
+ | `onError` | `(error: Error) => void` | — | Called on error |
1065
+ | `className` | `string` | — | Container class |
1066
+ | `children` | `ReactNode` | `"Add Passkey"` | Button content |
1067
+
1068
+ ---
1069
+
1070
+ ### Transaction Components
1071
+
1072
+ #### SendSOLForm
1073
+
1074
+ Form to send SOL.
698
1075
 
699
1076
  ```tsx
700
- import { AddSignerForm } from 'cilantro-react'
1077
+ import { SendSOLForm } from 'cilantro-react';
701
1078
 
702
- <AddSignerForm
1079
+ <SendSOLForm
703
1080
  walletId={walletId}
704
- open={dialogOpen}
705
- onOpenChange={setDialogOpen}
706
- onSuccess={() => { refreshSigners(); setDialogOpen(false) }}
707
- asDialog={true}
1081
+ signerId={signerId}
1082
+ signerType="email"
1083
+ onSuccess={(result) => console.log('Sent! Signature:', result.data.signature)}
1084
+ onError={(err) => console.error(err.message)}
708
1085
  />
709
1086
  ```
710
1087
 
711
- ### Delegated keys
1088
+ **Props:**
712
1089
 
713
- Select a delegated key for a wallet:
1090
+ | Prop | Type | Default | Description |
1091
+ |------|------|---------|-------------|
1092
+ | `walletId` | `string` | — | Wallet to send from |
1093
+ | `signerId` | `string` | — | Signer ID to authorize transaction |
1094
+ | `signerType` | `'email' \| 'phone' \| 'passkey' \| 'external'` | — | Signer type |
1095
+ | `onSuccess` | `(result: SubmitTransactionResult) => void` | — | Called after successful send |
1096
+ | `onError` | `(error: Error) => void` | — | Called on error |
1097
+ | `className` | `string` | — | Form class |
714
1098
 
715
- ```tsx
716
- import { DelegatedKeySelector } from 'cilantro-react'
1099
+ ---
717
1100
 
718
- function DelegatedKeys() {
719
- const [keyId, setKeyId] = useState<string>('')
1101
+ #### SendSPLForm
720
1102
 
721
- return (
722
- <DelegatedKeySelector
723
- walletId={walletId}
724
- value={keyId}
725
- onChange={(id, key) => setKeyId(id)}
726
- placeholder="Select a delegated key"
727
- filterActive={true}
728
- />
729
- )
730
- }
1103
+ Form to send SPL tokens.
1104
+
1105
+ ```tsx
1106
+ import { SendSPLForm } from 'cilantro-react';
1107
+
1108
+ <SendSPLForm
1109
+ walletId={walletId}
1110
+ signerId={signerId}
1111
+ signerType="email"
1112
+ mintAddress="EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" // USDC
1113
+ onSuccess={(result) => console.log('Sent! Signature:', result.data.signature)}
1114
+ onError={(err) => console.error(err.message)}
1115
+ />
731
1116
  ```
732
1117
 
733
- Use `children` for full control over the list UI.
1118
+ **Props:**
734
1119
 
735
- ### Message signing
1120
+ | Prop | Type | Default | Description |
1121
+ |------|------|---------|-------------|
1122
+ | `walletId` | `string` | — | Wallet to send from |
1123
+ | `signerId` | `string` | — | Signer ID |
1124
+ | `signerType` | `SignerType` | — | Signer type |
1125
+ | `mintAddress` | `string` | — | Pre-fill token mint address |
1126
+ | `onSuccess` | `(result: SubmitTransactionResult) => void` | — | Called after successful send |
1127
+ | `onError` | `(error: Error) => void` | — | Called on error |
1128
+ | `className` | `string` | — | Form class |
736
1129
 
737
- Default UI with message textarea, sign button, and wallet/signer context:
1130
+ ---
1131
+
1132
+ #### TransactionStatus
1133
+
1134
+ Display transaction status with explorer link.
738
1135
 
739
1136
  ```tsx
740
- import { MessageSigningForm } from 'cilantro-react'
1137
+ import { TransactionStatus } from 'cilantro-react';
741
1138
 
742
- function SignMessage() {
743
- return (
744
- <MessageSigningForm
745
- showContext={true}
746
- showCharCount={true}
747
- />
748
- )
749
- }
1139
+ <TransactionStatus
1140
+ signature="5wHu1qwD7q9..."
1141
+ cluster="mainnet-beta"
1142
+ />
750
1143
  ```
751
1144
 
752
- Headless: use `children` to get `messageText`, `setMessageText`, `handleSign`, `signResultState`, `isSigning`, `reset`:
1145
+ **Props:**
1146
+
1147
+ | Prop | Type | Default | Description |
1148
+ |------|------|---------|-------------|
1149
+ | `signature` | `string` | — | Transaction signature |
1150
+ | `cluster` | `'mainnet-beta' \| 'devnet' \| 'testnet'` | `'mainnet-beta'` | Solana cluster |
1151
+ | `className` | `string` | — | Element class |
1152
+
1153
+ ---
1154
+
1155
+ #### TransactionHistory
1156
+
1157
+ Paginated list of recent transactions.
753
1158
 
754
1159
  ```tsx
755
- <MessageSigningForm>
756
- {({ messageText, setMessageText, handleSign, signResultState, isSigning, reset }) => (
757
- <div>
758
- <textarea value={messageText} onChange={e => setMessageText(e.target.value)} />
759
- <button onClick={handleSign} disabled={isSigning}>Sign</button>
760
- {signResultState.status !== 'idle' && <p>{signResultState.message}</p>}
761
- <button onClick={reset}>Clear</button>
762
- </div>
763
- )}
764
- </MessageSigningForm>
1160
+ import { TransactionHistory } from 'cilantro-react';
1161
+
1162
+ <TransactionHistory walletId={walletId} pageSize={10} />
765
1163
  ```
766
1164
 
767
- ### Transaction signing
1165
+ **Props:**
768
1166
 
769
- Build a transaction in your app, then call `signTransaction(tx)` to sign only, or `signAndSendTransaction(tx)` to sign and send. **For wallet-adapter or passkey sign-and-send, pass `connection`** from your Solana config (e.g. `@solana/web3.js`):
1167
+ | Prop | Type | Default | Description |
1168
+ |------|------|---------|-------------|
1169
+ | `walletId` | `string` | — | Wallet ID |
1170
+ | `pageSize` | `number` | `10` | Transactions per page |
1171
+ | `className` | `string` | — | Element class |
770
1172
 
771
- ```tsx
772
- import { Connection } from '@solana/web3.js'
773
- import { TransactionSigningForm } from 'cilantro-react'
1173
+ ---
774
1174
 
775
- function SendTx() {
776
- const connection = new Connection('https://api.devnet.solana.com')
1175
+ ### Layout Components
777
1176
 
778
- return (
779
- <TransactionSigningForm
780
- connection={connection}
781
- showContext={true}
782
- >
783
- {({ signTransaction, signAndSendTransaction, transactionResultState, isSigningTransaction, isSendingTransaction, reset }) => (
784
- <div>
785
- <button
786
- onClick={() => {
787
- const tx = buildMyTransaction() // your code
788
- signAndSendTransaction(tx)
789
- }}
790
- disabled={isSendingTransaction}
791
- >
792
- {isSendingTransaction ? 'Sending...' : 'Sign and send'}
793
- </button>
794
- {transactionResultState.status !== 'idle' && (
795
- <p>{transactionResultState.message}</p>
796
- )}
797
- <button onClick={reset}>Reset status</button>
798
- </div>
799
- )}
800
- </TransactionSigningForm>
801
- )
802
- }
803
- ```
1177
+ #### CilantroConnect
1178
+
1179
+ All-in-one connect component combining wallet, signers, and auth.
804
1180
 
805
- If you use passkey or wallet-adapter for sign-and-send, **connection is required**; otherwise the library will throw. The library does not create a Solana connection—you pass it in.
1181
+ ```tsx
1182
+ import { CilantroConnect } from 'cilantro-react';
806
1183
 
807
- ### Full flow example
1184
+ <CilantroConnect
1185
+ onConnect={(wallet, signers) => console.log('Connected:', wallet, signers)}
1186
+ onDisconnect={() => console.log('Disconnected')}
1187
+ />
1188
+ ```
808
1189
 
809
- Typical app flow:
1190
+ **Props:**
810
1191
 
811
- 1. Wrap app with `CilantroProvider` (see [Setup](#setup)).
812
- 2. **Auth:** Use `AuthForm` on a login page, or `AuthGuard` around protected routes with `LoginForm` as fallback.
813
- 3. **Wallets:** Use `WalletSelector` so the user picks a wallet; get `selectedWallet` from `useWallets()`.
814
- 4. **Signers:** Use `SignerSelector` with `useSigners({ walletId })` and `useSignerSelection`, or use `SignerList` to list + add signers.
815
- 5. **Message signing:** Use `MessageSigningForm` (optionally with `showContext` and `showCharCount`).
816
- 6. **Transaction signing:** Use `TransactionSigningForm` with your `connection` and build the transaction in your app; call `signTransaction(tx)` or `signAndSendTransaction(tx)` from the render props.
1192
+ | Prop | Type | Default | Description |
1193
+ |------|------|---------|-------------|
1194
+ | `onConnect` | `(wallet: WalletInfo, signers: SignerInfo[]) => void` | | Called when connected |
1195
+ | `onDisconnect` | `() => void` | | Called when disconnected |
1196
+ | `className` | `string` | | Element class |
817
1197
 
818
1198
  ---
819
1199
 
820
- ## Customization
1200
+ #### LoadingOverlay
821
1201
 
822
- - **className** All components accept `className` on the root element. Apply your own Tailwind or CSS.
823
- - **classNames** – Many components accept a `classNames` object to style inner parts (e.g. `root`, `trigger`, `input`, `button`, `label`). See TypeScript types: `WalletSelectorClassNames`, `SignerSelectorClassNames`, `MessageSigningFormClassNames`, `TransactionSigningFormClassNames`, `LoginFormClassNames`, `AuthFormClassNames`, etc.
824
- - **Headless** – Where supported, use `children` render props or `renderList` / `renderTrigger` to render the UI yourself and still use the component’s logic (e.g. `WalletSelector`, `SignerSelector`, `MessageSigningForm`, `TransactionSigningForm`, `SignerList`, `DelegatedKeySelector`).
1202
+ Overlay with loading spinner for async operations.
825
1203
 
826
- ---
1204
+ ```tsx
1205
+ import { LoadingOverlay } from 'cilantro-react';
827
1206
 
828
- ## Core types and utilities
1207
+ <LoadingOverlay isLoading={isPending} message="Sending transaction...">
1208
+ <MyContent />
1209
+ </LoadingOverlay>
1210
+ ```
829
1211
 
830
- - **ActionState** – `{ status: 'idle' | 'loading' | 'success' | 'error', message?: string, detail?: unknown }`. Returned by signing hooks and used in signing form result UI. See [Types (detailed)](#types-detailed) for full shape.
831
- - **extractErrorMessage(error: unknown): string** – Normalizes unknown errors to a string. Handles `Error`, `{ message }`, `{ response: { data } }`, etc. Use when catching SDK or API errors to show a consistent message.
832
- - **extractResponseData\<T\>(response: unknown): T | null** Extracts the data payload from SDK API responses. Handles `{ data }`, `{ success, data }`, and signer list shapes. Use when you need to unwrap the response shape (e.g. after calling cilantro-sdk).
833
- - **SignAndSendConnection** – Type for the `connection` prop when using sign-and-send. Your `Connection` from `@solana/web3.js` is compatible. Must provide `getLatestBlockhash`, `confirmTransaction`, and (for wallet-adapter send) `sendRawTransaction`.
1212
+ **Props:**
1213
+
1214
+ | Prop | Type | Default | Description |
1215
+ |------|------|---------|-------------|
1216
+ | `isLoading` | `boolean` | — | Show overlay when true |
1217
+ | `message` | `string` | — | Loading message |
1218
+ | `children` | `ReactNode` | — | Content to wrap |
1219
+ | `className` | `string` | — | Overlay class |
834
1220
 
835
1221
  ---
836
1222
 
837
- ## Advanced / low-level
1223
+ #### ErrorBoundary
838
1224
 
839
- **Signer signing (without React hooks):** For advanced usage you can call the core signer APIs directly:
1225
+ Error boundary with retry functionality.
840
1226
 
841
- - `signMessageWithSigner(walletId, signer, message)` – Sign a message with a given signer.
842
- - `signTransactionWithSigner(walletId, signer, transactionBuffer)` Sign a transaction (no send).
843
- - `signAndSendTransactionWithSigner(walletId, signer, transaction, connection?)` – Sign and send; **connection is required for passkey**.
844
- - `getWalletData(walletId)`, `getSignerPublicKey(...)` – Helpers for wallet/signer data.
845
- - `SIGNER_TYPES`, `SignerType` – Signer type constants.
1227
+ ```tsx
1228
+ import { ErrorBoundary } from 'cilantro-react';
846
1229
 
847
- See TypeScript types and [cilantro-sdk](https://www.npmjs.com/package/cilantro-sdk) for full behavior.
1230
+ <ErrorBoundary
1231
+ fallback={<div>Something went wrong</div>}
1232
+ onRetry={() => window.location.reload()}
1233
+ >
1234
+ <App />
1235
+ </ErrorBoundary>
1236
+ ```
1237
+
1238
+ **Props:**
848
1239
 
849
- **Connection:** The library never creates a Solana RPC connection. Your app creates it (e.g. `new Connection(rpcUrl)`) and passes it into `useTransactionSigning` or `TransactionSigningForm` when using wallet-adapter or passkey sign-and-send.
1240
+ | Prop | Type | Default | Description |
1241
+ |------|------|---------|-------------|
1242
+ | `fallback` | `ReactNode` | — | Content to show on error |
1243
+ | `onRetry` | `() => void` | — | Retry callback |
1244
+ | `children` | `ReactNode` | — | Content to wrap |
850
1245
 
851
1246
  ---
852
1247
 
853
1248
  ## Theming
854
1249
 
855
- Components use Shadcn-style class names and CSS variables (e.g. `border-input`, `bg-primary`, `text-muted-foreground`). Ensure your app has **Tailwind CSS** so utility classes work; the library does not bundle Tailwind.
1250
+ Components use Shadcn-style class names and CSS variables. Ensure your app has **Tailwind CSS** configured.
856
1251
 
857
- ### Built-in theme (ThemeProvider)
858
-
859
- You can opt into a built-in light/dark theme without bringing your own Shadcn setup. Wrap your app (or just the Cilantro UI) with `ThemeProvider`:
1252
+ ### Built-in Theme (ThemeProvider)
860
1253
 
861
1254
  ```tsx
862
- import { ThemeProvider } from 'cilantro-react'
863
-
864
- <ThemeProvider theme="system" defaultTheme="dark" className="min-h-screen">
865
- <CilantroProvider platformApiKey="...">
866
- {children}
1255
+ import { ThemeProvider } from 'cilantro-react';
1256
+
1257
+ <ThemeProvider
1258
+ theme="system"
1259
+ defaultTheme="dark"
1260
+ injectStyles={true}
1261
+ className="min-h-screen"
1262
+ >
1263
+ <CilantroProvider apiKey="...">
1264
+ <App />
867
1265
  </CilantroProvider>
868
1266
  </ThemeProvider>
869
1267
  ```
870
1268
 
871
- - **theme** – `"light"` | `"dark"` | `"system"`. `"system"` follows `prefers-color-scheme`.
872
- - **defaultTheme** – Initial theme when using `"system"` (default: `"dark"`).
873
- - **storageKey** – Optional localStorage key to persist the resolved theme when using `"system"`.
874
- - **injectStyles** – When `true` (default), the provider injects a minimal set of CSS variables (`--background`, `--foreground`, `--primary`, `--muted`, `--border`, `--input`, `--ring`, etc.) for light and `[data-theme="dark"]`. You can set `injectStyles={false}` and import the theme file yourself: `import 'cilantro-react/themes/default.css'`.
1269
+ **Props:**
875
1270
 
876
- Tailwind is still required so utility classes apply; ThemeProvider only sets the variables.
1271
+ | Prop | Type | Default | Description |
1272
+ |------|------|---------|-------------|
1273
+ | `theme` | `'light' \| 'dark' \| 'system'` | `'system'` | Theme mode |
1274
+ | `defaultTheme` | `'light' \| 'dark'` | `'dark'` | Default when using system |
1275
+ | `storageKey` | `string` | — | localStorage key for persistence |
1276
+ | `injectStyles` | `boolean` | `true` | Inject CSS variables |
1277
+ | `className` | `string` | — | Wrapper class |
877
1278
 
878
- For full Shadcn theming, see [Theming - shadcn/ui](https://ui.shadcn.com/docs/theming).
1279
+ ### Import Theme CSS Directly
1280
+
1281
+ ```ts
1282
+ import 'cilantro-react/themes/default.css';
1283
+ ```
879
1284
 
880
1285
  ---
881
1286
 
882
- ## Accessibility
1287
+ ## Advanced Usage
883
1288
 
884
- Components are built with screen readers and keyboard use in mind:
1289
+ ### Low-Level Signer Signing
885
1290
 
886
- - **Live regions** – Signing result messages (MessageSigningForm, TransactionSigningForm) use `role="status"` and `aria-live="polite"` (or `assertive` for errors). Loading states use `aria-busy`.
887
- - **Form labels** – Auth, login, register, message signing, and add-signer forms use visible labels with correct `id` / `htmlFor`. Error blocks use `role="alert"` so errors are announced immediately.
888
- - **Selects and dialogs** – WalletSelector and DelegatedKeySelector triggers have `aria-label`. Radix Dialog handles focus trap and restore in AddSignerForm.
889
- - **Loading** – WalletSelector, SignerSelector, SignerList, DelegatedKeySelector, and AuthGuard use `aria-busy` and `aria-live="polite"` when loading so screen readers announce loading state.
1291
+ For advanced use cases without React hooks:
890
1292
 
891
- Buttons use `focus-visible:ring-2` for focus indication; icon-only or custom buttons can receive `aria-label` via standard HTML attributes.
1293
+ ```tsx
1294
+ import {
1295
+ signMessageWithSigner,
1296
+ signTransactionWithSigner,
1297
+ signAndSendTransactionWithSigner,
1298
+ getWalletData,
1299
+ getSignerPublicKey,
1300
+ SIGNER_TYPES,
1301
+ } from 'cilantro-react';
1302
+
1303
+ // Sign a message
1304
+ const result = await signMessageWithSigner(walletId, signer, 'Hello, Solana!');
1305
+
1306
+ // Sign and send a transaction
1307
+ const txResult = await signAndSendTransactionWithSigner(walletId, signer, transaction, connection);
1308
+ ```
892
1309
 
893
- ---
1310
+ ### Solana Connection Adapter
1311
+
1312
+ For `@solana/web3.js` connection compatibility:
894
1313
 
895
- ## Mobile
1314
+ ```tsx
1315
+ import { Connection } from '@solana/web3.js';
1316
+ import { adaptSolanaConnection } from 'cilantro-react';
1317
+
1318
+ const connection = new Connection('https://api.mainnet-beta.solana.com');
1319
+ const adapted = adaptSolanaConnection(connection);
1320
+ ```
896
1321
 
897
- Key actions use touch-friendly targets and dialogs are responsive:
1322
+ ### Utilities
898
1323
 
899
- - **Touch targets** The Button component has a `touch` size (`min-h-[44px] min-w-[44px]`) used on primary actions: AuthForm/LoginForm/RegisterForm submit, MessageSigningForm sign button, and SignerList “Add signer”. Use `size="touch"` on your own primary buttons when needed.
900
- - **Responsive layout** – Auth forms use `w-full max-w-sm`. MessageSigningForm and TransactionSigningForm use responsive button groups (`flex-col` on small screens). Result blocks use `overflow-auto` on `<pre>` so content doesn’t overflow.
901
- - **Dialog** – DialogContent has `max-h-[100dvh] sm:max-h-[90vh]` and `overflow-y-auto` so dialogs (e.g. AddSignerForm) are scrollable on small screens. You can add `className="rounded-none sm:rounded-lg"` or full-height classes for a more mobile-friendly dialog.
1324
+ | Function | Description |
1325
+ |----------|-------------|
1326
+ | `extractErrorMessage(error)` | Normalize unknown errors to string |
1327
+ | `extractResponseData(response)` | Unwrap SDK response shapes |
1328
+ | `normalizeWallet(dto)` | Normalize wallet data from SDK (ensures `id` and `address`) |
1329
+ | `normalizeSigner(dto)` | Normalize signer data from SDK |
1330
+ | `getWalletId(wallet)` | Get wallet ID from wallet-like object (`id ?? walletId`) |
1331
+ | `getWalletAddress(wallet)` | Get wallet address from wallet-like object (`address ?? walletAddress`) |
1332
+ | `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. |
1333
+ | `isAuthError(error)` | Check if error is an auth error |
1334
+
1335
+ ### Wallet shape
1336
+
1337
+ 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.
902
1338
 
903
1339
  ---
904
1340
 
905
- ## Loading states
1341
+ ## Types
906
1342
 
907
- Loading is shown with skeleton placeholders where it improves perceived performance:
1343
+ Key exported types:
908
1344
 
909
- - **Skeleton** – A `Skeleton` UI primitive is available (`import { Skeleton } from 'cilantro-react'`). Components use it when loading: WalletSelector and DelegatedKeySelector show a skeleton trigger; SignerSelector and SignerList show 2–3 skeleton rows.
910
- - **useSkeleton** – WalletSelector, SignerSelector, SignerList, and DelegatedKeySelector accept an optional **useSkeleton** prop (default: `true`). Set `useSkeleton={false}` to show plain “Loading…” text instead.
911
- - **classNames** – These components support `classNames.skeleton` and `classNames.loading` so you can style the skeleton or the loading container.
912
- - **AuthGuard** – Optional skeleton while checking auth: set **useSkeleton={true}** to show a small skeleton card instead of “Loading…” text.
1345
+ ```ts
1346
+ import type {
1347
+ // Config
1348
+ CilantroConfig,
1349
+ CilantroContextValue,
1350
+ DeviceKeyStorage,
1351
+
1352
+ // User/Auth
1353
+ User,
1354
+ UseAuthResult,
1355
+
1356
+ // Wallet
1357
+ WalletData,
1358
+ UseWalletResult,
1359
+ WalletControllerCreateResult,
1360
+
1361
+ // Signer
1362
+ SignerData,
1363
+ SignerInfo,
1364
+ SignerType,
1365
+ UseSignersResult,
1366
+
1367
+ // Transaction
1368
+ SubmitTransactionResult,
1369
+ UseSendTransactionResult,
1370
+
1371
+ // External Wallet
1372
+ SolanaWallet,
1373
+ UseExternalWalletResult,
1374
+
1375
+ // Passkey
1376
+ UsePasskeyResult,
1377
+
1378
+ // UI
1379
+ ActionState,
1380
+ } from 'cilantro-react';
1381
+ ```
913
1382
 
914
1383
  ---
915
1384
 
916
- ## Low-level SDK
1385
+ ## License
917
1386
 
918
- For direct cilantro-sdk usage (auth, storage, wallets, signers, delegated keys, message/transaction signing), see the [cilantro-sdk](https://www.npmjs.com/package/cilantro-sdk) package.
1387
+ MIT