cilantro-react 0.1.0

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 ADDED
@@ -0,0 +1,647 @@
1
+ # cilantro-react
2
+
3
+ React SDK/UI for [Cilantro Smart Wallet](https://www.npmjs.com/package/cilantro-sdk). Provides providers, hooks, and UI components so you can add auth, wallets, signers, and signing flows without wiring the low-level SDK directly. Components use [Shadcn/UI](https://ui.shadcn.com)-style primitives and are customizable via `className` and `classNames`.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install cilantro-react cilantro-sdk react
9
+ ```
10
+
11
+ **Peer dependencies:** `react` (^18.0.0 || ^19.0.0), `cilantro-sdk` (^0.0.40). For transaction signing and Solana helpers, optionally install `@solana/web3.js` (^1.98.0).
12
+
13
+ **Styling:** Components use Tailwind CSS and Shadcn-compatible class names. Ensure your app has [Tailwind CSS](https://tailwindcss.com) configured; for theming, use your own Shadcn theme or CSS variables so styles apply correctly.
14
+
15
+ ## Polyfills
16
+
17
+ Import the SDK polyfills once before any SDK usage (e.g. in your app entry or root layout):
18
+
19
+ ```ts
20
+ import 'cilantro-sdk/polyfills'
21
+ ```
22
+
23
+ ## Setup
24
+
25
+ Wrap your app with `CilantroProvider` and pass your platform API key:
26
+
27
+ ```tsx
28
+ import { CilantroProvider } from 'cilantro-react'
29
+
30
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
31
+ return (
32
+ <CilantroProvider platformApiKey="your-platform-api-key">
33
+ {children}
34
+ </CilantroProvider>
35
+ )
36
+ }
37
+ ```
38
+
39
+ **CilantroProvider props:**
40
+
41
+ | Prop | Type | Description |
42
+ |------|------|-------------|
43
+ | `platformApiKey` | `string` | **Required.** Your Cilantro platform API key. |
44
+ | `apiUrl` | `string` | Optional API base URL. |
45
+ | `jwtStorageKey` | `string` | localStorage key for JWT (default: `cilantro_jwt`). |
46
+ | `walletStorageKey` | `string` | localStorage key for selected wallet id (default: `cilantro_selected_wallet_id`). |
47
+ | `onLoginSuccess` | `() => void` | Callback after successful login. |
48
+ | `onLogout` | `() => void` | Callback after logout. |
49
+ | `onRegisterSuccess` | `() => void` | Callback after successful registration. |
50
+
51
+ **Connection:** The library does not create a Solana RPC connection. For transaction sign-and-send (wallet-adapter or passkey), pass your own `connection` from `@solana/web3.js` (or your Solana config) into `useTransactionSigning` / `TransactionSigningForm`. See [Transaction signing](#transaction-signing) below.
52
+
53
+ ---
54
+
55
+ ## API reference
56
+
57
+ ### Providers
58
+
59
+ - **CilantroProvider** – Root provider: initializes storage, auth, and wallet context. Requires `platformApiKey`; optional `apiUrl`, storage keys, and callbacks. Use this unless you only need auth.
60
+ - **CilantroAuthProvider** – Auth only (token, user, login, register, logout). Use when you don’t need the full provider stack (e.g. auth-only pages).
61
+ - **WalletProvider** – Wallet list and selection. Depends on auth; usually used inside `CilantroProvider`.
62
+
63
+ **CilantroAuthProvider props:** `platformApiKey` (required), `apiUrl`, `jwtStorageKey`, `onLoginSuccess`, `onLogout`, `onRegisterSuccess`.
64
+
65
+ **WalletProvider props:** `storageKey` (default: `cilantro_selected_wallet_id`).
66
+
67
+ ---
68
+
69
+ ### Custom hooks (detailed)
70
+
71
+ All hooks must be used within the appropriate provider (see each hook below).
72
+
73
+ #### useCilantroAuth
74
+
75
+ Auth state and actions. Use inside `CilantroAuthProvider` or `CilantroProvider`.
76
+
77
+ **Returns:** `CilantroAuthContextType`
78
+
79
+ | Property | Type | Description |
80
+ |----------|------|-------------|
81
+ | `token` | `string \| null` | JWT token (null when not authenticated). |
82
+ | `user` | `User \| null` | User object: `{ username?, email?, userType? }`. |
83
+ | `isAuthenticated` | `boolean` | True when `token` is set. |
84
+ | `isLoading` | `boolean` | True while restoring session from storage. |
85
+ | `login` | `(usernameOrEmail: string, password: string) => Promise<void>` | Log in; throws on error. |
86
+ | `register` | `(username: string, email: string, password: string, isActive?: boolean) => Promise<void>` | Register and log in; throws on error. |
87
+ | `logout` | `() => void` | Clear token and user; calls `onLogout` if provided. |
88
+
89
+ ---
90
+
91
+ #### useWallets
92
+
93
+ Wallet list and selection. Use inside `CilantroProvider` (which includes `WalletProvider`).
94
+
95
+ **Returns:** `WalletContextType`
96
+
97
+ | Property | Type | Description |
98
+ |----------|------|-------------|
99
+ | `wallets` | `WalletData[]` | List of wallets for the authenticated user. |
100
+ | `selectedWallet` | `WalletData \| null` | Currently selected wallet (persisted in localStorage). |
101
+ | `selectWallet` | `(walletId: string) => void` | Set the selected wallet by id. |
102
+ | `refreshWallets` | `() => Promise<void>` | Reload wallets from the API. |
103
+ | `isLoading` | `boolean` | True while loading wallets. |
104
+
105
+ **WalletData:** `{ id, walletId, walletName?, address?, walletAddress?, chain?, active?, ... }`.
106
+
107
+ ---
108
+
109
+ #### useSigners
110
+
111
+ Load signers for a wallet. Use inside `CilantroProvider` (auth required).
112
+
113
+ **Options:** `UseSignersOptions`
114
+
115
+ | Option | Type | Description |
116
+ |--------|------|-------------|
117
+ | `walletId` | `string \| null \| undefined` | Wallet ID to load signers for. If omitted, you can pass it from `useWallets().selectedWallet?.id`. |
118
+
119
+ **Returns:** `UseSignersResult`
120
+
121
+ | Property | Type | Description |
122
+ |----------|------|-------------|
123
+ | `signers` | `SignerData[]` | List of signers for the wallet. |
124
+ | `isLoading` | `boolean` | True while loading. |
125
+ | `error` | `string \| null` | Error message if load failed. |
126
+ | `refresh` | `() => Promise<void>` | Reload signers. |
127
+
128
+ When `walletId` is null/undefined, `signers` is empty and `refresh` no-ops.
129
+
130
+ ---
131
+
132
+ #### useSignerSelection
133
+
134
+ Wallet + signer selection state for signing flows. Uses `useWallets` and `useSigners` internally. Use inside `CilantroProvider`.
135
+
136
+ **Options:** `UseSignerSelectionOptions`
137
+
138
+ | Option | Type | Description |
139
+ |--------|------|-------------|
140
+ | `walletId` | `string \| null \| undefined` | Override wallet ID; if not set, uses `useWallets().selectedWallet?.id`. |
141
+ | `signingMethod` | `'wallet-adapter' \| 'sdk-signer' \| null` | Current signing method (default: `'sdk-signer'`). When `'wallet-adapter'`, signer selection is ignored. |
142
+
143
+ **Returns:** `UseSignerSelectionResult`
144
+
145
+ | Property | Type | Description |
146
+ |----------|------|-------------|
147
+ | `selectedWalletId` | `string` | Effective wallet ID (override or from context). |
148
+ | `setSelectedWalletId` | `(id: string) => void` | Set wallet ID (when not using context selection). |
149
+ | `availableSigners` | `SignerData[]` | Signers for the selected wallet (from `useSigners`). |
150
+ | `selectedSigner` | `SignerData \| null` | Currently selected signer. |
151
+ | `setSelectedSigner` | `(signer: SignerData \| null) => void` | Set selected signer. |
152
+ | `isLoadingSigners` | `boolean` | True while signers are loading. |
153
+ | `reset` | `() => void` | Clear `selectedWalletId` and `selectedSigner`. |
154
+
155
+ **SigningMethod:** `'wallet-adapter'` = use wallet adapter for signing; `'sdk-signer'` = use a Cilantro signer (email, phone, passkey, external, etc.).
156
+
157
+ ---
158
+
159
+ #### useMessageSigning
160
+
161
+ Sign a message with the selected signer or wallet-adapter. Use inside `CilantroProvider`.
162
+
163
+ **Options:** `UseMessageSigningOptions`
164
+
165
+ | Option | Type | Description |
166
+ |--------|------|-------------|
167
+ | `token` | `string \| null` | JWT (usually from `useCilantroAuth().token`). |
168
+ | `signingMethod` | `SigningMethod \| null` | `'sdk-signer'` or `'wallet-adapter'`. |
169
+ | `selectedSigner` | `SignerData \| null` | Signer to use when `signingMethod === 'sdk-signer'`. |
170
+ | `selectedWalletId` | `string` | Wallet ID when using SDK signer. |
171
+ | `walletAdapterSignMessage` | `(message: Uint8Array) => Promise<Uint8Array>` | Optional; required when `signingMethod === 'wallet-adapter'`. |
172
+ | `walletAdapterPublicKey` | `string \| null` | Optional; included in success detail when using wallet-adapter. |
173
+
174
+ **Returns:** `UseMessageSigningResult`
175
+
176
+ | Property | Type | Description |
177
+ |----------|------|-------------|
178
+ | `messageText` | `string` | Current message (default: `"Hello, Solana!"`). |
179
+ | `setMessageText` | `(text: string) => void` | Update message. |
180
+ | `signResultState` | `ActionState` | `{ status, message?, detail? }` – idle, loading, success, or error. |
181
+ | `isSigning` | `boolean` | True while signing. |
182
+ | `handleSign` | `() => Promise<void>` | Sign the current message; updates `signResultState`. |
183
+ | `reset` | `() => void` | Reset message to default and clear result state. |
184
+
185
+ ---
186
+
187
+ #### useTransactionSigning
188
+
189
+ Sign and/or send a transaction. Use inside `CilantroProvider`.
190
+
191
+ **Options:** `UseTransactionSigningOptions`
192
+
193
+ | Option | Type | Description |
194
+ |--------|------|-------------|
195
+ | `token` | `string \| null` | JWT (usually from `useCilantroAuth().token`). |
196
+ | `signingMethod` | `SigningMethod \| null` | `'sdk-signer'` or `'wallet-adapter'`. |
197
+ | `selectedSigner` | `SignerData \| null` | Signer when `signingMethod === 'sdk-signer'`. |
198
+ | `selectedWalletId` | `string` | Wallet ID when using SDK signer. |
199
+ | `walletAdapterSignTransaction` | `(transaction: Transaction) => Promise<Transaction>` | Optional; required for wallet-adapter signing. |
200
+ | `walletAdapterPublicKey` | `string \| null` | Optional; required for wallet-adapter send. |
201
+ | `connection` | `SignAndSendConnection \| null` | **Required for sign-and-send** when using wallet-adapter or passkey. Provide your Solana `Connection` (e.g. from `@solana/web3.js`). |
202
+
203
+ **Returns:** `UseTransactionSigningResult`
204
+
205
+ | Property | Type | Description |
206
+ |----------|------|-------------|
207
+ | `transactionResultState` | `ActionState` | Result of last sign/send (idle, loading, success, error). |
208
+ | `isSigningTransaction` | `boolean` | True while `signTransaction` is running. |
209
+ | `isSendingTransaction` | `boolean` | True while `signAndSendTransaction` is running. |
210
+ | `signTransaction` | `(transaction: Transaction) => Promise<void>` | Sign only (no send). Passkey signers cannot sign-only; use sign-and-send. |
211
+ | `signAndSendTransaction` | `(transaction: Transaction) => Promise<void>` | Sign and send. Requires `connection` for wallet-adapter and passkey. |
212
+ | `reset` | `() => void` | Set `transactionResultState` back to idle. |
213
+
214
+ **Connection:** For passkey or wallet-adapter sign-and-send, you must pass a `connection` that has `getLatestBlockhash`, `sendRawTransaction`, and `confirmTransaction`. The library does not create a connection.
215
+
216
+ ---
217
+
218
+ ### Components (detailed)
219
+
220
+ All components accept `className`. Many accept a `classNames` object (see TypeScript types e.g. `WalletSelectorClassNames`, `MessageSigningFormClassNames`).
221
+
222
+ #### Auth components
223
+
224
+ | Component | Purpose | Key props |
225
+ |-----------|---------|-----------|
226
+ | **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). |
227
+ | **LoginForm** | Login (username/email + password). | `onSuccess?`, `onError?(error: string)`, `submitLabel`, `title`, `description`, `renderSwitchToRegister? () => ReactNode`, `className`, `classNames?`. |
228
+ | **RegisterForm** | Registration (username, email, password). | `onSuccess?`, `onError?(error: string)`, `submitLabel`, `title`, `description`, `isActive?` (default true), `renderSwitchToLogin? () => ReactNode`, `className`, `classNames?`. |
229
+ | **AuthGuard** | Renders children when authenticated; shows fallback otherwise. | `children`, `fallback?: ReactNode` (default: `<LoginForm />`), `showFallback?` (default true), `className`, `classNames?` (root, fallback). |
230
+
231
+ #### Wallet and signer components
232
+
233
+ | Component | Purpose | Key props |
234
+ |-----------|---------|-----------|
235
+ | **WalletSelector** | Wallet dropdown (Shadcn Select). | `value?`, `onWalletChange?(walletId, wallet)`, `placeholder`, `className`, `classNames?` (root, trigger, content, item). Headless: `children?`, `renderTrigger?`, `renderList?`. |
236
+ | **SignerSelector** | List/select signers (buttons). | `selectedWalletId?`, `availableSigners`, `selectedSigner`, `onSignerSelect(signer)`, `isLoadingSigners?`, `className`, `classNames?` (root, list, item, message). Headless: `children?`, `renderList?`. |
237
+ | **SignerList** | List signers + “Add signer” (opens AddSignerForm in dialog). | `walletId`, `onSignerAdded`, `className`, `classNames`. Headless: `children`. |
238
+ | **AddSignerForm** | Add signer (email, phone, passkey, external wallet). | `walletId`, `open?`, `onOpenChange?`, `onSuccess?`, `onCancel?`, `onError?(error)`, `asDialog?` (default true), `className`, `classNames?`. |
239
+ | **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). |
240
+
241
+ #### Signing form components
242
+
243
+ | Component | Purpose | Key props |
244
+ |-----------|---------|-----------|
245
+ | **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). |
246
+ | **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). |
247
+
248
+ When token/wallet/signer/signingMethod are omitted, MessageSigningForm and TransactionSigningForm use `useCilantroAuth().token` and `useSignerSelection()` internally.
249
+
250
+ ### Types (detailed)
251
+
252
+ #### ActionState
253
+
254
+ Used by signing hooks and forms for result state.
255
+
256
+ ```ts
257
+ type ActionState<T = unknown> = {
258
+ status: 'idle' | 'loading' | 'success' | 'error';
259
+ message?: string;
260
+ detail?: T;
261
+ };
262
+ ```
263
+
264
+ - **idle** – No operation yet.
265
+ - **loading** – Sign/send in progress.
266
+ - **success** – Done; `message` and optional `detail` (e.g. signature, explorer URL).
267
+ - **error** – Failed; `message` (and optional `detail`).
268
+
269
+ #### User
270
+
271
+ From `useCilantroAuth().user`:
272
+
273
+ ```ts
274
+ interface User {
275
+ username?: string;
276
+ email?: string;
277
+ userType?: string;
278
+ }
279
+ ```
280
+
281
+ #### WalletData
282
+
283
+ From `useWallets().wallets` / `selectedWallet`:
284
+
285
+ ```ts
286
+ interface WalletData {
287
+ id: string;
288
+ walletId: string;
289
+ walletName?: string;
290
+ address?: string;
291
+ walletAddress?: string;
292
+ chain?: string;
293
+ active?: boolean;
294
+ [key: string]: unknown;
295
+ }
296
+ ```
297
+
298
+ #### SignerData
299
+
300
+ 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.
301
+
302
+ #### SignAndSendConnection
303
+
304
+ Type for the `connection` prop when using sign-and-send (passkey or wallet-adapter). Your `Connection` from `@solana/web3.js` is compatible. Must have:
305
+
306
+ - `getLatestBlockhash(commitment?)` → `Promise<{ blockhash, lastValidBlockHeight }>`
307
+ - `confirmTransaction(opts: { signature, blockhash, lastValidBlockHeight })` → `Promise<void>`
308
+
309
+ For wallet-adapter send, the hook also uses `sendRawTransaction(buf)` (standard Solana Connection has this).
310
+
311
+ #### Other types
312
+
313
+ - **ErrorResponse** – `{ message?, error?, code?, status?, statusText?, data?, ... }` for API error shapes.
314
+ - **ApiResponse\<T\>** – SDK response shape; use `extractResponseData(response)` to unwrap.
315
+ - **DelegatedKeyData** – From DelegatedKeySelector: `id`, `walletId`, `name?`, `publicKey`, `permissions`, `isActive?`, `createdAt?`, `expiresAt?`, etc.
316
+ - **SigningMethod** – `'wallet-adapter' | 'sdk-signer'`.
317
+ - **AddSignerType** – `'email' | 'phone' | 'passkey' | 'external'`.
318
+ - **AuthFormMode** – `'login' | 'register'`.
319
+
320
+ #### Utilities
321
+
322
+ - **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.
323
+ - **extractResponseData\<T\>(response: unknown): T | null** – Unwraps SDK response shapes (`{ data }`, `{ success, data }`, signer arrays). Use after cilantro-sdk calls that return wrapped payloads.
324
+
325
+ ---
326
+
327
+ ## Examples
328
+
329
+ ### Auth – single form (AuthForm)
330
+
331
+ One form that switches between sign-in and create-account:
332
+
333
+ ```tsx
334
+ import { AuthForm } from 'cilantro-react'
335
+
336
+ function LoginPage() {
337
+ return (
338
+ <AuthForm
339
+ defaultMode="login"
340
+ onSuccess={() => window.location.href = '/dashboard'}
341
+ onError={(err) => console.error(err)}
342
+ loginTitle="Welcome back"
343
+ registerTitle="Create an account"
344
+ switchToRegisterText="Don't have an account? Create one"
345
+ switchToLoginText="Already have an account? Sign in"
346
+ />
347
+ )
348
+ }
349
+ ```
350
+
351
+ ### Auth – separate Login and Register
352
+
353
+ Use `LoginForm` and `RegisterForm` with links to switch pages:
354
+
355
+ ```tsx
356
+ import { LoginForm, RegisterForm } from 'cilantro-react'
357
+ import { Link } from 'react-router-dom'
358
+
359
+ function LoginPage() {
360
+ return (
361
+ <LoginForm
362
+ onSuccess={() => navigate('/dashboard')}
363
+ renderSwitchToRegister={() => (
364
+ <Link to="/register">Create account</Link>
365
+ )}
366
+ />
367
+ )
368
+ }
369
+
370
+ function RegisterPage() {
371
+ return (
372
+ <RegisterForm
373
+ onSuccess={() => navigate('/dashboard')}
374
+ renderSwitchToLogin={() => (
375
+ <Link to="/login">Already have an account? Sign in</Link>
376
+ )}
377
+ />
378
+ )
379
+ }
380
+ ```
381
+
382
+ ### Auth – protected route (AuthGuard)
383
+
384
+ Show login when not authenticated, otherwise render children:
385
+
386
+ ```tsx
387
+ import { AuthGuard, LoginForm } from 'cilantro-react'
388
+
389
+ function DashboardRoute() {
390
+ return (
391
+ <AuthGuard fallback={<LoginForm />}>
392
+ <Dashboard />
393
+ </AuthGuard>
394
+ )
395
+ }
396
+
397
+ // Or use default fallback (LoginForm)
398
+ <AuthGuard>
399
+ <Dashboard />
400
+ </AuthGuard>
401
+
402
+ // Hide content when unauthenticated (no fallback UI)
403
+ <AuthGuard showFallback={false}>
404
+ <Dashboard />
405
+ </AuthGuard>
406
+ ```
407
+
408
+ ### Wallets
409
+
410
+ Wallet dropdown with optional customization:
411
+
412
+ ```tsx
413
+ import { useWallets, WalletSelector } from 'cilantro-react'
414
+
415
+ function Wallets() {
416
+ const { wallets, selectedWallet } = useWallets()
417
+
418
+ return (
419
+ <div>
420
+ <WalletSelector
421
+ placeholder="Choose a wallet"
422
+ onWalletChange={(id, wallet) => console.log('Selected', id, wallet)}
423
+ classNames={{
424
+ root: 'w-full max-w-xs',
425
+ trigger: 'border-2',
426
+ }}
427
+ />
428
+ {selectedWallet && (
429
+ <p>Selected: {selectedWallet.walletName ?? selectedWallet.id}</p>
430
+ )}
431
+ </div>
432
+ )
433
+ }
434
+ ```
435
+
436
+ ### Signers – list and select
437
+
438
+ Load signers for the selected wallet and let the user pick one:
439
+
440
+ ```tsx
441
+ import { useSigners, useSignerSelection, SignerSelector } from 'cilantro-react'
442
+
443
+ function SignerPick() {
444
+ const { walletId } = useSignerSelection({ walletId: selectedWalletId })
445
+ const { signers, isLoading, refresh } = useSigners({ walletId: walletId ?? undefined })
446
+
447
+ return (
448
+ <SignerSelector
449
+ selectedWalletId={walletId ?? undefined}
450
+ availableSigners={signers}
451
+ selectedSigner={selectedSigner}
452
+ onSignerSelect={setSelectedSigner}
453
+ isLoadingSigners={isLoading}
454
+ className="w-full"
455
+ />
456
+ )
457
+ }
458
+ ```
459
+
460
+ Headless: use `children` or `renderList` to render the list yourself.
461
+
462
+ ### Signers – add signer (AddSignerForm + SignerList)
463
+
464
+ List signers and open a dialog to add a new one (email, phone, passkey, external wallet):
465
+
466
+ ```tsx
467
+ import { SignerList } from 'cilantro-react'
468
+
469
+ function SignersPage() {
470
+ const walletId = 'your-wallet-id'
471
+
472
+ return (
473
+ <SignerList
474
+ walletId={walletId}
475
+ onSignerAdded={() => console.log('Signer added')}
476
+ />
477
+ )
478
+ }
479
+ ```
480
+
481
+ AddSignerForm can also be used standalone (e.g. inline or in your own dialog):
482
+
483
+ ```tsx
484
+ import { AddSignerForm } from 'cilantro-react'
485
+
486
+ <AddSignerForm
487
+ walletId={walletId}
488
+ open={dialogOpen}
489
+ onOpenChange={setDialogOpen}
490
+ onSuccess={() => { refreshSigners(); setDialogOpen(false) }}
491
+ asDialog={true}
492
+ />
493
+ ```
494
+
495
+ ### Delegated keys
496
+
497
+ Select a delegated key for a wallet:
498
+
499
+ ```tsx
500
+ import { DelegatedKeySelector } from 'cilantro-react'
501
+
502
+ function DelegatedKeys() {
503
+ const [keyId, setKeyId] = useState<string>('')
504
+
505
+ return (
506
+ <DelegatedKeySelector
507
+ walletId={walletId}
508
+ value={keyId}
509
+ onChange={(id, key) => setKeyId(id)}
510
+ placeholder="Select a delegated key"
511
+ filterActive={true}
512
+ />
513
+ )
514
+ }
515
+ ```
516
+
517
+ Use `children` for full control over the list UI.
518
+
519
+ ### Message signing
520
+
521
+ Default UI with message textarea, sign button, and wallet/signer context:
522
+
523
+ ```tsx
524
+ import { MessageSigningForm } from 'cilantro-react'
525
+
526
+ function SignMessage() {
527
+ return (
528
+ <MessageSigningForm
529
+ showContext={true}
530
+ showCharCount={true}
531
+ />
532
+ )
533
+ }
534
+ ```
535
+
536
+ Headless: use `children` to get `messageText`, `setMessageText`, `handleSign`, `signResultState`, `isSigning`, `reset`:
537
+
538
+ ```tsx
539
+ <MessageSigningForm>
540
+ {({ messageText, setMessageText, handleSign, signResultState, isSigning, reset }) => (
541
+ <div>
542
+ <textarea value={messageText} onChange={e => setMessageText(e.target.value)} />
543
+ <button onClick={handleSign} disabled={isSigning}>Sign</button>
544
+ {signResultState.status !== 'idle' && <p>{signResultState.message}</p>}
545
+ <button onClick={reset}>Clear</button>
546
+ </div>
547
+ )}
548
+ </MessageSigningForm>
549
+ ```
550
+
551
+ ### Transaction signing
552
+
553
+ 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`):
554
+
555
+ ```tsx
556
+ import { Connection } from '@solana/web3.js'
557
+ import { TransactionSigningForm } from 'cilantro-react'
558
+
559
+ function SendTx() {
560
+ const connection = new Connection('https://api.devnet.solana.com')
561
+
562
+ return (
563
+ <TransactionSigningForm
564
+ connection={connection}
565
+ showContext={true}
566
+ >
567
+ {({ signTransaction, signAndSendTransaction, transactionResultState, isSigningTransaction, isSendingTransaction, reset }) => (
568
+ <div>
569
+ <button
570
+ onClick={() => {
571
+ const tx = buildMyTransaction() // your code
572
+ signAndSendTransaction(tx)
573
+ }}
574
+ disabled={isSendingTransaction}
575
+ >
576
+ {isSendingTransaction ? 'Sending...' : 'Sign and send'}
577
+ </button>
578
+ {transactionResultState.status !== 'idle' && (
579
+ <p>{transactionResultState.message}</p>
580
+ )}
581
+ <button onClick={reset}>Reset status</button>
582
+ </div>
583
+ )}
584
+ </TransactionSigningForm>
585
+ )
586
+ }
587
+ ```
588
+
589
+ 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.
590
+
591
+ ### Full flow example
592
+
593
+ Typical app flow:
594
+
595
+ 1. Wrap app with `CilantroProvider` (see [Setup](#setup)).
596
+ 2. **Auth:** Use `AuthForm` on a login page, or `AuthGuard` around protected routes with `LoginForm` as fallback.
597
+ 3. **Wallets:** Use `WalletSelector` so the user picks a wallet; get `selectedWallet` from `useWallets()`.
598
+ 4. **Signers:** Use `SignerSelector` with `useSigners({ walletId })` and `useSignerSelection`, or use `SignerList` to list + add signers.
599
+ 5. **Message signing:** Use `MessageSigningForm` (optionally with `showContext` and `showCharCount`).
600
+ 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.
601
+
602
+ ---
603
+
604
+ ## Customization
605
+
606
+ - **className** – All components accept `className` on the root element. Apply your own Tailwind or CSS.
607
+ - **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.
608
+ - **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`).
609
+
610
+ ---
611
+
612
+ ## Core types and utilities
613
+
614
+ - **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.
615
+ - **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.
616
+ - **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).
617
+ - **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`.
618
+
619
+ ---
620
+
621
+ ## Advanced / low-level
622
+
623
+ **Signer signing (without React hooks):** For advanced usage you can call the core signer APIs directly:
624
+
625
+ - `signMessageWithSigner(walletId, signer, message)` – Sign a message with a given signer.
626
+ - `signTransactionWithSigner(walletId, signer, transactionBuffer)` – Sign a transaction (no send).
627
+ - `signAndSendTransactionWithSigner(walletId, signer, transaction, connection?)` – Sign and send; **connection is required for passkey**.
628
+ - `getWalletData(walletId)`, `getSignerPublicKey(...)` – Helpers for wallet/signer data.
629
+ - `SIGNER_TYPES`, `SignerType` – Signer type constants.
630
+
631
+ See TypeScript types and [cilantro-sdk](https://www.npmjs.com/package/cilantro-sdk) for full behavior.
632
+
633
+ **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.
634
+
635
+ ---
636
+
637
+ ## Theming
638
+
639
+ Components use Shadcn-style class names and CSS variables (e.g. `border-input`, `bg-primary`, `text-muted-foreground`). Ensure your app has Tailwind and, if you use Shadcn, your theme or `globals.css` defines those variables so colors and spacing match your design. You can override any part via `className` or `classNames`.
640
+
641
+ For Shadcn theming, see [Theming - shadcn/ui](https://ui.shadcn.com/docs/theming).
642
+
643
+ ---
644
+
645
+ ## Low-level SDK
646
+
647
+ 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.