@yieldfabric/wallet 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,680 @@
1
+ # @yieldfabric/wallet
2
+
3
+ Reusable YieldFabric wallet for service developers — login + onboarding,
4
+ signing (manual MQ + generic API), profile, and key management. Drop
5
+ `<WalletProvider>` into your React app and the rest of the surface
6
+ (hooks, UIs, services) becomes available.
7
+
8
+ ## Install
9
+
10
+ Published to the **public npm registry** — no scope `.npmrc`, no token:
11
+
12
+ ```bash
13
+ npm install @yieldfabric/wallet
14
+ ```
15
+
16
+ Peer deps: `react ^18`, `react-dom ^18` (and optionally `@stripe/stripe-js` for
17
+ the Stripe-backed KYC provider). All other runtime deps (`@avatune/*`,
18
+ `@averer/averer-websdk`, `@heroicons/react`, `js-sha3`, `zod`) install
19
+ automatically from the public npm registry. The SDK bundles a Tailwind-styled
20
+ UI; consumers must add it to their Tailwind content glob (see below).
21
+
22
+ ## Quick start
23
+
24
+ ```tsx
25
+ // app/layout.tsx (Next.js App Router)
26
+ 'use client';
27
+ import { WalletProvider, SignatureWorkflow } from '@yieldfabric/wallet';
28
+ import { useRouter } from 'next/navigation';
29
+
30
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
31
+ const router = useRouter();
32
+ return (
33
+ <html>
34
+ <body>
35
+ <WalletProvider
36
+ config={{
37
+ // Imperative navigation hook so the SDK can route between
38
+ // login / onboarding screens. Wire to your router.
39
+ onNavigate: (href) => router.push(href),
40
+ // (Optional) Called after a session is established.
41
+ // onSessionBootstrapped: (userId) => { /* invalidate caches */ },
42
+ }}
43
+ >
44
+ {children}
45
+ {/* Zero-config global mount — wires the wallet's signature
46
+ queue, drawer, toast notifier, and (optionally) the
47
+ Signature Center launcher. This is the component every
48
+ host wants at the root; <SignatureDrawer /> by itself is
49
+ a *controlled inner* that needs `message` / `onClose` /
50
+ `userId` props and is rendered by <SignatureWorkflow>
51
+ for each pending message. */}
52
+ <SignatureWorkflow />
53
+ </WalletProvider>
54
+ </body>
55
+ </html>
56
+ );
57
+ }
58
+ ```
59
+
60
+ ```tsx
61
+ // app/login/page.tsx
62
+ 'use client';
63
+ import { LoginForm, useAuth } from '@yieldfabric/wallet';
64
+
65
+ export default function LoginPage() {
66
+ const { isAuthenticated } = useAuth();
67
+ if (isAuthenticated) /* redirect */;
68
+ return <LoginForm />;
69
+ }
70
+ ```
71
+
72
+ ### Configuring service URLs
73
+
74
+ The SDK reads service base URLs from `process.env.REACT_APP_*` at
75
+ build time (see [`src/core/config.ts`](./src/core/config.ts)):
76
+
77
+ | Var | Default | Use |
78
+ |--|--|--|
79
+ | `REACT_APP_AUTH_SERVICE_URL` | `http://localhost:3000` | auth REST |
80
+ | `REACT_APP_PAYMENTS_SERVICE_URL` | `http://localhost:3002` | payments + WebSocket |
81
+ | `REACT_APP_API_URL` | `http://localhost:3002` | shared API base (WS derivation) |
82
+ | `REACT_APP_CORE_SERVICE_URL` | `http://localhost:3002` | core service URL |
83
+ | `REACT_APP_CHAIN_ID` | `31337` | default chain id |
84
+
85
+ How to expose them to the browser depends on your bundler:
86
+
87
+ - **CRA / craco**: `REACT_APP_*` works natively. Put them in `.env`.
88
+ - **Vite**: define them with `define: { 'process.env.REACT_APP_*': ... }` in `vite.config.ts`, or fork `core/config.ts` to read `import.meta.env.VITE_*`.
89
+ - **Next.js**: prefix with `NEXT_PUBLIC_` in `.env.local` AND map them in `next.config.js`'s `env` block so `process.env.REACT_APP_*` resolves in client bundles.
90
+
91
+ Future work: a follow-up will move URL resolution onto the
92
+ `WalletRuntimeConfig` so consumers can pass URLs at mount time and
93
+ not rely on env-var plumbing. See the comment at the top of
94
+ `core/config.ts`.
95
+
96
+ The full public surface — providers, hooks, UI components, core REST/crypto
97
+ services, types — is enumerated in [`src/index.ts`](./src/index.ts).
98
+
99
+ ## Login surface API
100
+
101
+ The login screen is built from three independently-mountable pieces:
102
+
103
+ | Component | Use when |
104
+ |---|---|
105
+ | `<LoginAltMethods />` | You want the wallet / passkey chip row (Averer, MetaMask, Dynamic, Passkey). No props. Reads `auth.providers.enabled` from `/auth/providers/config`. |
106
+ | `<LoginForm />` | You want the bundled email/password form with sensible defaults (`services: ['vault', 'payments']`, `autoRedirect: false`). |
107
+ | `<LoginComponent>` | You want full control. Pass `options` for hook config; pass `render={...}` or `children={...}` to take over the JSX while keeping the form state machine. |
108
+
109
+ ### Custom-JSX login via `render` prop
110
+
111
+ `<LoginComponent render={...}>` receives the *full* form state — both
112
+ the submission half (`login`, `clearError`, `isLoading`, `isSubmitting`,
113
+ `error`) and the `useForm` machine (`values`, `errors`, `touched`,
114
+ `handleChange`, `handleBlur`, `handleSubmit`). This means you can render
115
+ any layout you want without re-implementing form state:
116
+
117
+ ```tsx
118
+ <LoginComponent
119
+ options={{ services: ['vault', 'payments'], autoRedirect: false }}
120
+ render={({ values, errors, touched, handleChange, handleSubmit, isSubmitting, error }) => (
121
+ <form onSubmit={handleSubmit} className="space-y-5">
122
+ <input
123
+ type="email"
124
+ value={values.email}
125
+ onChange={(e) => handleChange('email', e.target.value)}
126
+ className="my-custom-field"
127
+ />
128
+ {/* …password, error, submit… */}
129
+ </form>
130
+ )}
131
+ />
132
+ ```
133
+
134
+ ### Incoming payments + obligations inbox
135
+
136
+ Embeddable surface for letting an authenticated user view and accept
137
+ pending payments (instant sends, swap legs) and obligations (contracts
138
+ whose status is `ACTIVE` and whose counterpart is the active entity).
139
+
140
+ Three drop-in components, ordered most opinionated → least:
141
+
142
+ | Component | Use when |
143
+ |---|---|
144
+ | `<IncomingPaymentsLauncher />` | You want a nav-chip with a badge count. Click opens an internal drawer with the full inbox. Mirrors `<SignatureCenterLauncher>` in shape. |
145
+ | `<IncomingPaymentsCenter />` | You want a dedicated `/payments/inbox`-style page. Adds a header, refresh button, and summary tiles on top of the inbox. |
146
+ | `<IncomingPaymentsInbox />` | You want just the card list — embed in your own chrome (drawer, dashboard card, sidebar). |
147
+ | `useIncomingPayments()` | You want full UI control. Returns `{ items, groups, accept, acceptState, loading, error, refetch, activeEntityId, … }`. |
148
+
149
+ ```tsx
150
+ import {
151
+ IncomingPaymentsLauncher,
152
+ IncomingPaymentsCenter,
153
+ IncomingPaymentsInbox,
154
+ useIncomingPayments,
155
+ } from '@yieldfabric/wallet';
156
+
157
+ // Launcher in your nav.
158
+ <IncomingPaymentsLauncher
159
+ variant="icon-label"
160
+ onReview={(item) => router.push(`/contracts/${item.contract?.id ?? ''}`)}
161
+ getAccountLabel={(id) => entityNameCache.get(id) ?? id}
162
+ />
163
+
164
+ // Or a full page.
165
+ <Route path="/payments/inbox" element={<IncomingPaymentsCenter />} />
166
+
167
+ // Or build your own layout, SDK does the data.
168
+ function MyInbox() {
169
+ const { items, accept, acceptState } = useIncomingPayments();
170
+ return items.map((item) => (
171
+ <MyRow
172
+ key={item.key}
173
+ item={item}
174
+ onAccept={() => accept(item)}
175
+ busy={acceptState.itemKey === item.key}
176
+ />
177
+ ));
178
+ }
179
+ ```
180
+
181
+ #### Acting-as / delegation is automatic
182
+
183
+ When a host mints a delegation token via
184
+ `useAuthContext().createDelegationToken(group)`, the inbox automatically
185
+ scopes every query and mutation to the delegated group. No prop
186
+ threading required. The resolution order for the active entity is:
187
+
188
+ 1. Explicit `entityId` prop on the hook / component (if passed).
189
+ 2. `localStorage.delegation_group_id` — set by `<AppAuthProvider>` when
190
+ delegation is active.
191
+ 3. `useAuth().user.id` — the authenticated user's own entity.
192
+
193
+ Hosts that maintain their own actingAs model can pass `entityId`
194
+ explicitly to override the auto-detection.
195
+
196
+ #### Signature requirement
197
+
198
+ Accepting a payment or obligation enqueues an on-chain transaction
199
+ through the MQ. When the user is in **Manual** signature mode the
200
+ SDK's signature drawer prompts them to sign before the executor
201
+ fires. Consumers using this inbox **must mount
202
+ `<SignatureWorkflow />`** once at the app root — that's the zero-prop
203
+ component that wires the queue + drawer + toast + Signature Center
204
+ launcher together. `<SignatureDrawer />` by itself is a controlled
205
+ inner that takes `message` / `onClose` / `userId` props; you don't
206
+ mount it directly.
207
+
208
+ ```tsx
209
+ // Once at the app root.
210
+ import { WalletProvider, SignatureWorkflow } from '@yieldfabric/wallet';
211
+
212
+ <WalletProvider config={…}>
213
+ {children}
214
+ <SignatureWorkflow />
215
+ </WalletProvider>
216
+ ```
217
+
218
+ Without this mount, manual-mode users will see the inbox's `Accept`
219
+ mutation succeed at the resolver but the on-chain settlement will
220
+ never run.
221
+
222
+ #### Live refresh
223
+
224
+ The hook subscribes to the same entity-events SSE stream the
225
+ signature center uses (`Notification`, `MessageStatusChange`,
226
+ `WorkflowUpdate`, `PositionChange`). When any of those fire for the
227
+ active entity, the inbox refetches automatically. The cost of a
228
+ refetch is small enough that a coarse "refresh on any relevant
229
+ event" strategy is correct — no manual cache invalidation is
230
+ required.
231
+
232
+ Pass `disableLiveRefresh: true` to opt out (e.g. for tests).
233
+
234
+ ### Send payment (confidential YF "instant send")
235
+
236
+ Embeddable surface for the outbound side of the wallet. Submits a YF
237
+ confidential payment via the `instant` GraphQL mutation — amounts are
238
+ encrypted on chain, queued through the MQ, gated by
239
+ `<SignatureWorkflow />` when the user is in Manual mode.
240
+
241
+ Three drop-in surfaces:
242
+
243
+ | Component | Use when |
244
+ |---|---|
245
+ | `<SendPaymentDrawer />` | You want a "Send" button somewhere that opens a drawer. Zero-config. |
246
+ | `<SendPaymentForm />` | You want the form embedded in your own chrome (page, modal, sidebar). |
247
+ | `useSendPayment()` | You want full UI control. Returns `{ send, sending, error, lastResponse, reset }`. |
248
+
249
+ ```tsx
250
+ import {
251
+ SendPaymentDrawer,
252
+ SendPaymentForm,
253
+ useSendPayment,
254
+ convertAmountToRaw,
255
+ } from '@yieldfabric/wallet';
256
+
257
+ // 1. Drawer — open from a button in your nav / wallet panel.
258
+ const [open, setOpen] = useState(false);
259
+
260
+ <button onClick={() => setOpen(true)}>Send</button>
261
+
262
+ <SendPaymentDrawer
263
+ isOpen={open}
264
+ onClose={() => setOpen(false)}
265
+ assets={[
266
+ { id: 'aud-token-asset', currency: 'AUD', decimals: '18' },
267
+ { id: 'usdc-token-asset', currency: 'USDC', decimals: '6' },
268
+ ]}
269
+ onSent={(response) => {
270
+ // Resolver accepted — the recipient's inbox will now show the
271
+ // payment as PENDING. `response.messageId` is the MQ id for
272
+ // optional `messagesService.pollMessageCompletion` polling.
273
+ }}
274
+ />
275
+
276
+ // 2. Embed the form directly (no drawer).
277
+ <SendPaymentForm
278
+ assets={assets}
279
+ defaultAssetId="aud-token-asset"
280
+ onSuccess={(response) => navigate(`/payments/${response.paymentId}`)}
281
+ />
282
+
283
+ // 3. Build your own form, SDK handles the submit + state.
284
+ function MySendForm() {
285
+ const { send, sending, error } = useSendPayment();
286
+ return (
287
+ <form onSubmit={async (e) => {
288
+ e.preventDefault();
289
+ await send({
290
+ assetId: 'aud-token-asset',
291
+ amount: convertAmountToRaw('100.50', 18),
292
+ destinationId: 'someone@example.com',
293
+ });
294
+ }}>
295
+ … your fields …
296
+ <button disabled={sending}>{sending ? 'Sending…' : 'Send'}</button>
297
+ {error && <p>{error}</p>}
298
+ </form>
299
+ );
300
+ }
301
+ ```
302
+
303
+ #### Asset list is host-supplied
304
+
305
+ The SDK doesn't bundle an asset-lookup query — that's host-specific
306
+ (GraphQL `assets.all` for some hosts, a hardcoded enum for others).
307
+ Pass the asset list as a prop to `<SendPaymentForm>` /
308
+ `<SendPaymentDrawer>`. The shape is minimal:
309
+
310
+ ```ts
311
+ interface SendPaymentAsset {
312
+ id: string; // token asset id (e.g. "aud-token-asset")
313
+ currency: string; // display label ("AUD", "USDC")
314
+ decimals?: string | number; // defaults to 18 when omitted
315
+ name?: string; // optional long name for the picker
316
+ }
317
+ ```
318
+
319
+ #### Amount conversion
320
+
321
+ The resolver wants a raw on-chain integer string (`"100500000000000000000"`
322
+ for 100.50 AUD with 18 decimals). The form does this conversion
323
+ internally via `convertAmountToRaw(humanAmount, decimals)`. If you're
324
+ calling the service / hook directly, do the same:
325
+
326
+ ```ts
327
+ import { convertAmountToRaw } from '@yieldfabric/wallet';
328
+ const raw = convertAmountToRaw('100.50', 18); // → "100500000000000000000"
329
+ ```
330
+
331
+ `convertAmountToRaw` rejects negatives, NaN, scientific notation,
332
+ and over-precision (more fractional digits than the token's
333
+ decimals), so you can use it as your validator too.
334
+
335
+ #### Recipient resolution
336
+
337
+ The form's "Recipient" field accepts an **entity name or id**
338
+ (`someone@example.com`, a UUID, …). Behind the scenes the resolver
339
+ looks up the entity's default wallet. For multi-wallet entities
340
+ where the sender wants to target a specific wallet, set
341
+ `enableWalletIdMode` to expose a small toggle that switches the field
342
+ to wallet-UUID input.
343
+
344
+ Direct calls have all three options:
345
+
346
+ ```ts
347
+ await send({ assetId, amount, destinationId: 'someone@example.com' });
348
+ await send({ assetId, amount, destinationWalletId: 'd1b3…wallet-uuid' });
349
+ await send({ assetId, amount, contractId: 'CONTRACT-OBLIGATION-…' });
350
+ ```
351
+
352
+ Pass exactly one. The resolver throws when zero or multiple are
353
+ provided.
354
+
355
+ #### Acting-as is automatic
356
+
357
+ Same story as the inbox: when a delegation token is installed
358
+ (`useAuthContext().createDelegationToken(group)`), the send
359
+ originates from the delegated group instead of the user. No prop
360
+ threading needed — `tokenManager` picks the right token, the
361
+ resolver reads the `acting_as` claim and signs against the group's
362
+ wallet.
363
+
364
+ #### Signature requirement
365
+
366
+ For Manual-mode users (external wallets — passkey, MetaMask,
367
+ Dynamic), the on-chain send is gated by `<SignatureWorkflow />`.
368
+ Mount it once at the app root. Without it, `send()` resolves
369
+ successfully but the executor never runs.
370
+
371
+ ### Block-explorer links
372
+
373
+ Transaction hashes rendered by the SDK's signature drawer + signature
374
+ center row + EOA-send status panel are automatically wrapped in
375
+ explorer links when the SDK has a registered explorer for the
376
+ message's chain id. Click the hash → opens the chain's explorer
377
+ (Etherscan, Polygonscan, BscScan, SnowTrace, Redbelly Explorer, …)
378
+ in a new tab.
379
+
380
+ Built-in coverage:
381
+
382
+ | Chain | Explorer |
383
+ |---|---|
384
+ | Ethereum Mainnet (`1`) | etherscan.io |
385
+ | Goerli (`5`) / Sepolia (`11155111`) | (goerli\|sepolia).etherscan.io |
386
+ | Polygon (`137`) / Mumbai (`80001`) | polygonscan.com / mumbai.polygonscan.com |
387
+ | BSC (`56`) / Testnet (`97`) | bscscan.com / testnet.bscscan.com |
388
+ | Avalanche (`43114`) / Fuji (`43113`) | snowtrace.io / testnet.snowtrace.io |
389
+ | Redbelly Mainnet (`151`) | redbelly.routescan.io |
390
+ | Redbelly Testnet (`153`) | redbelly.testnet.routescan.io |
391
+
392
+ Local-dev chains (`31337`, `33117`, `1337`) intentionally have no
393
+ explorer registered — the hash renders as plain text.
394
+
395
+ #### Adding or overriding an explorer
396
+
397
+ ```ts
398
+ import { registerBlockExplorer } from '@yieldfabric/wallet';
399
+
400
+ // Add a chain not in the defaults:
401
+ registerBlockExplorer(8453, {
402
+ name: 'Basescan',
403
+ txUrl: (hash) => `https://basescan.org/tx/${hash}`,
404
+ addressUrl: (addr) => `https://basescan.org/address/${addr}`,
405
+ });
406
+
407
+ // Override one path while inheriting the rest (e.g. prefer
408
+ // Blockscout for addresses but keep Etherscan for txs on mainnet):
409
+ registerBlockExplorer(1, {
410
+ addressUrl: (addr) => `https://eth.blockscout.com/address/${addr}`,
411
+ });
412
+ ```
413
+
414
+ Direct helpers if you need to render your own link:
415
+
416
+ ```ts
417
+ import {
418
+ getBlockExplorerTxUrl,
419
+ getBlockExplorerAddressUrl,
420
+ } from '@yieldfabric/wallet';
421
+
422
+ const txUrl = getBlockExplorerTxUrl(chainId, txHash); // string | null
423
+ const addrUrl = getBlockExplorerAddressUrl(chainId, addr); // string | null
424
+ ```
425
+
426
+ Both helpers return `null` when the chain has no registered explorer
427
+ or the input fails validation (empty / not `0x…`-hex / wrong length
428
+ for addresses), so the calling component can fall back to plain text
429
+ without an `if`-cascade.
430
+
431
+ ### Passkey sign-in: no chain id needed by default
432
+
433
+ `signInWithPasskey()` and `signUpWithPasskey()` both accept an *optional*
434
+ `chainId`. When omitted, the SDK falls back to
435
+ `Number(getDefaultChainId())` (which reads `REACT_APP_CHAIN_ID`, defaulting
436
+ to `31337`):
437
+
438
+ ```ts
439
+ // All three call the same default chain
440
+ const r1 = await signInWithPasskey();
441
+ const r2 = await signInWithPasskey({});
442
+ const r3 = await signInWithPasskey({ chainId: Number(getDefaultChainId()) });
443
+
444
+ // Multi-chain consumers can still pin the chain explicitly
445
+ const r4 = await signInWithPasskey({ chainId: 1 });
446
+ ```
447
+
448
+ ## Tailwind setup
449
+
450
+ The SDK ships UI components styled with Tailwind utility classes. Tailwind
451
+ needs to scan the SDK source so it picks up which utilities to keep.
452
+
453
+ ```js
454
+ // tailwind.config.js
455
+ module.exports = {
456
+ content: [
457
+ './src/**/*.{ts,tsx}',
458
+ // Scan the published bundles — className strings are intact (tsup
459
+ // does not minify), so the utilities survive purge.
460
+ './node_modules/@yieldfabric/wallet/dist/**/*.{js,cjs}',
461
+ ],
462
+ // …
463
+ };
464
+ ```
465
+
466
+ The package ships only `dist/` (compiled, source closed), so scan the bundles —
467
+ the class strings are preserved there verbatim.
468
+
469
+ ## Theming for consumer apps
470
+
471
+ The SDK is theme-portable: every surface (login, onboarding, signature
472
+ drawer / center, keys manager, profile editor, primitives, identity card)
473
+ renders against a fixed set of **semantic** Tailwind token names. Map
474
+ them in your `tailwind.config.js` to re-skin the SDK to any palette —
475
+ dark, light, brand-coloured, anything. No `dark:` prefix is required; no
476
+ runtime theme switch; no SDK code change.
477
+
478
+ ### The token contract
479
+
480
+ The SDK uses these names. Define them under `theme.colors` (or
481
+ `theme.extend.colors`) in your tailwind config.
482
+
483
+ ```js
484
+ // tailwind.config.js
485
+ module.exports = {
486
+ // …content paths…
487
+ theme: {
488
+ extend: {
489
+ colors: {
490
+ // ── Surfaces ────────────────────────────────────────────
491
+ page: '#ffffff', // viewport background
492
+ surface: '#ffffff', // card / sheet background
493
+ 'surface-alt': '#f7f8fa', // muted card / striped row
494
+ raised: '#ffffff', // floating elements (toasts, drawers)
495
+ 'raised-hover': '#f7f8fa',
496
+ overlay: 'rgba(255,255,255,0.92)',
497
+ background: '#ffffff', // legacy alias, still used by some SDK chrome
498
+
499
+ // ── Borders ─────────────────────────────────────────────
500
+ 'border-default': '#e3e8f0',
501
+ 'border-light': '#eef1f6',
502
+ 'border-strong': '#c7d0dc',
503
+
504
+ // ── Text ────────────────────────────────────────────────
505
+ 'text-primary': '#0a1b30', // body text
506
+ 'text-secondary': '#32373c', // secondary copy
507
+ 'text-muted': '#5f6b7a', // labels, captions
508
+ 'text-inverse': '#ffffff', // text on dark surfaces
509
+
510
+ // ── Brand primary (accent / glow / focus ring) ─────────
511
+ primary: '#054e92',
512
+ 'primary-muted': 'rgba(5,78,146,0.10)',
513
+ accent: '#0b6acf',
514
+
515
+ // ── CTA (high-emphasis button) ─────────────────────────
516
+ // In a dark theme this is intentionally near-text-color so the
517
+ // button reads as inverted; in a light theme it's typically the
518
+ // brand color. The two tokens are kept distinct so each consumer
519
+ // can choose what "CTA" should look like vs. the brand accent.
520
+ cta: '#054e92',
521
+ 'cta-hover':'#054075',
522
+ 'on-cta': '#ffffff',
523
+
524
+ // ── Chip (elevated small surface: sign-in chips,
525
+ // segmented controls, selected pills) ──────────
526
+ chip: '#f7f8fa',
527
+ 'chip-hover': '#eef2f8',
528
+ 'chip-active': '#e3e8f0',
529
+
530
+ // ── Input (subtle filled input / inline card bg) ───────
531
+ 'input-bg': '#ffffff',
532
+
533
+ // ── Status (used by signature drawer / center / toasts) ─
534
+ 'status-success-bg': '#dcf0e6',
535
+ 'status-success-text': '#1a6b42',
536
+ 'status-warning-bg': '#fbf3d4',
537
+ 'status-warning-text': '#7a6518',
538
+ 'status-error-bg': '#fde2e7',
539
+ 'status-error-text': '#9b2c3c',
540
+ 'status-info-bg': '#dceaf8',
541
+ 'status-info-text': '#1a5090',
542
+
543
+ // ── Pastels (soft badge fills) ──────────────────────────
544
+ 'pastel-peach': '#fde6d5',
545
+ 'pastel-lavender': '#e9e2f5',
546
+ 'pastel-mint': '#d4f0e4',
547
+ 'pastel-sky': '#dceaf8',
548
+ 'pastel-rose': '#fbe2e6',
549
+ 'pastel-lemon': '#fbf3d4',
550
+ 'pastel-lilac': '#ecdef6',
551
+ 'pastel-coral': '#fde4dd',
552
+ },
553
+ },
554
+ },
555
+ };
556
+ ```
557
+
558
+ The example values above are a light theme. Drop the same token names
559
+ into your own palette and the SDK picks up your colours.
560
+
561
+ ### Backing tokens with CSS variables (optional)
562
+
563
+ If you run a single app with both light and dark modes, back each token
564
+ with a CSS variable in `:root` and `.dark`, then point Tailwind at the
565
+ variable. See the YieldFabric host's `index.css` for the canonical
566
+ pattern; the values to define are the same as the table above, prefixed
567
+ with `--color-`.
568
+
569
+ ### Autofill (Chrome / Safari)
570
+
571
+ The login + signup inputs use a `.autofill-fix` class to keep autofilled
572
+ text legible. Add this CSS once in your global stylesheet (light theme
573
+ shown; flip the box-shadow + fill-color for dark):
574
+
575
+ ```css
576
+ .autofill-fix:-webkit-autofill,
577
+ .autofill-fix:-webkit-autofill:hover,
578
+ .autofill-fix:-webkit-autofill:focus {
579
+ -webkit-box-shadow: 0 0 0 1000px #ffffff inset !important;
580
+ -webkit-text-fill-color: #0a1b30 !important;
581
+ caret-color: #0a1b30;
582
+ transition: background-color 5000s ease-in-out 0s;
583
+ }
584
+ ```
585
+
586
+ ### What you don't have to do
587
+
588
+ - **No `theme="light" | "dark"` prop** — the SDK has no theme switch;
589
+ the same JSX renders in either palette.
590
+ - **No CSS-in-JS** — Tailwind classes only, so your build pipeline is
591
+ unchanged.
592
+ - **No code forks per consumer** — every SDK surface is portable today;
593
+ a future consumer needs only the token map above.
594
+
595
+ ## Package layout
596
+
597
+ ```
598
+ src/
599
+ ├── core/ no React, no DOM — REST clients, WebAuthn, crypto, token store
600
+ ├── providers/ auth provider impls (email, passkey, MetaMask, Averer) + config
601
+ ├── react/ hooks + headless controllers
602
+ └── ui/ styled components (LoginWidget, SignatureDrawer, KeysManager, …)
603
+ ```
604
+
605
+ Import graph: `ui → react → providers → core`. Forbid backward edges.
606
+
607
+ ## Build
608
+
609
+ ```bash
610
+ npm install
611
+ npm run build # one-shot — emits dist/index.{js,cjs,d.ts}
612
+ npm run dev # watch mode — recompiles on save
613
+ npm run typecheck # tsc --noEmit
614
+ npm run clean # rm -rf dist
615
+ ```
616
+
617
+ `tsup` (esbuild + dts) produces:
618
+
619
+ - `dist/index.js` — ESM bundle (consumed by modern bundlers / Next.js)
620
+ - `dist/index.cjs` — CommonJS bundle (Node fallback)
621
+ - `dist/index.d.ts` / `.d.cts` — type definitions
622
+
623
+ Both bundles are prepended with `"use client";` so Next.js App Router
624
+ treats the SDK as client-side by default (the surface is hook + DOM-heavy).
625
+
626
+ Externals: `react`, `react-dom`, and the runtime deps (`@averer/averer-websdk`,
627
+ `@heroicons/react`, `js-sha3`, `zod`) are NOT bundled — consumers install
628
+ them via the `dependencies` / `peerDependencies` declarations.
629
+
630
+ ## Consuming from another monorepo package
631
+
632
+ If your app lives in the same repo and uses `file:` reference, the consumer
633
+ reads `dist/` via the `main` / `module` / `exports` fields:
634
+
635
+ ```json
636
+ {
637
+ "dependencies": {
638
+ "@yieldfabric/wallet": "file:../yieldfabric-wallet-sdk"
639
+ }
640
+ }
641
+ ```
642
+
643
+ Workflow:
644
+
645
+ ```bash
646
+ # In the SDK
647
+ cd yieldfabric-wallet-sdk
648
+ npm install
649
+ npm run build # one-shot, or `npm run dev` for watch mode
650
+
651
+ # In the consumer
652
+ cd ../my-app
653
+ npm install # picks up the freshly-built dist
654
+ npm run dev
655
+ ```
656
+
657
+ If you're iterating on the SDK while running the consumer, leave
658
+ `npm run dev` running in the SDK directory; the consumer's bundler will
659
+ pick up changes on the next reload.
660
+
661
+ ## Publishing
662
+
663
+ Ships to the **public npm registry** as a public scoped package
664
+ (`publishConfig.access: public`). Publishing `@yieldfabric/*` requires the
665
+ `yieldfabric` scope on npm — i.e. an npm org (or user) named `yieldfabric`.
666
+ Authenticate once with `npm login`, then:
667
+
668
+ ```bash
669
+ # bump "version" first — re-publishing an existing version is rejected
670
+ npm publish # prepublishOnly runs clean && build automatically
671
+ ```
672
+
673
+ `prepublishOnly` runs `clean && build`, so a stale `dist/` can't leak into the
674
+ tarball. The published tarball ships **`dist/` + `README.md` only** — no `src/`
675
+ and no source maps, so the TypeScript source stays closed (only compiled
676
+ output ships). Verify with `npm pack --dry-run`.
677
+
678
+ ## License
679
+
680
+ Proprietary. See repo root for terms.