@waaskey/react 0.3.1 → 0.4.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 CHANGED
@@ -2,51 +2,153 @@
2
2
 
3
3
  React hooks + provider for the [Waaskey](https://waaskey.com) SDK — embedded,
4
4
  non-custodial **MPC wallets**. A thin layer over [`@waaskey/sdk`](../sdk); the same
5
- hooks work in React Native.
5
+ hooks work in React Native (via [`@waaskey/react-native`](../react-native)).
6
6
 
7
7
  ## Install
8
8
 
9
+ `@waaskey/sdk` and `react` are **peer dependencies** — the SDK holds your end-user session and
10
+ the MPC/share state, so a second copy in the tree would mean a second client and a lost session.
11
+
9
12
  ```bash
10
13
  pnpm add @waaskey/react @waaskey/sdk @waaskey/client-wasm react
11
14
  ```
12
15
 
16
+ `@waaskey/client-wasm` carries the browser MPC engine; `qrcode` is optional (the Receive tab shows
17
+ the address either way), as is `@simplewebauthn/browser` (only for passkey sign-in).
18
+
13
19
  ## Use
14
20
 
15
21
  ```tsx
16
- import { WaaskeyProvider, useCreateWallet, useBalance } from '@waaskey/react';
17
- import { WasmMpcCore, EncryptedShareStore } from '@waaskey/sdk';
22
+ import { useMemo, useState } from 'react';
23
+ import { ConnectModal, WaasProvider, useAuth, useBalance, useCreateWallet } from '@waaskey/react';
24
+ import { EncryptedShareStore, WasmMpcCore, loadClientWasm } from '@waaskey/sdk';
25
+ import type { EmbeddedSession } from '@waaskey/sdk';
26
+
27
+ export default function App() {
28
+ // The device share is sealed with a secret derived from the signed-in user, so the MPC pieces
29
+ // can only be wired once you have a session. `useMemo` keeps the options (and therefore the
30
+ // client) stable — see "Client identity" below.
31
+ const [session, setSession] = useState<EmbeddedSession | null>(null);
32
+ const options = useMemo(() => {
33
+ const base = { apiKey: import.meta.env.VITE_WAASKEY_KEY as string };
34
+ if (!session) return base;
35
+ return {
36
+ ...base,
37
+ mpc: new WasmMpcCore(loadClientWasm),
38
+ shareStore: EncryptedShareStore.browser(`${session.token.slice(0, 32)}:my-app`),
39
+ };
40
+ }, [session]);
18
41
 
19
- const options = {
20
- apiKey: import.meta.env.VITE_WAASKEY_KEY,
21
- mpc: new WasmMpcCore(loadClientWasm),
22
- shareStore: EncryptedShareStore.browser(sessionSecret),
23
- };
24
-
25
- function App() {
26
42
  return (
27
- <WaaskeyProvider options={options}>
28
- <Wallet />
29
- </WaaskeyProvider>
43
+ <WaasProvider options={options} persistSession="session">
44
+ <Wallet onSession={setSession} />
45
+ </WaasProvider>
30
46
  );
31
47
  }
32
48
 
33
- function Wallet() {
34
- const { create, wallet, isPending } = useCreateWallet();
49
+ function Wallet({ onSession }: { onSession: (s: EmbeddedSession) => void }) {
50
+ const { isAuthenticated, ready, user, logout } = useAuth();
51
+ const { create, wallet, isPending, error } = useCreateWallet();
35
52
  const { data: balance } = useBalance('ethereum', wallet?.address);
36
- return isPending ? <Spinner /> : <button onClick={() => create({ chain: 'ethereum' })}>Create</button>;
53
+
54
+ if (!ready) return null; // a persisted session may still be restoring
55
+ if (!isAuthenticated) return <ConnectModal open onClose={() => undefined} onConnect={onSession} />;
56
+
57
+ return (
58
+ <>
59
+ <p>
60
+ {user?.email} · <button onClick={logout}>Sign out</button>
61
+ </p>
62
+ {error && <p role="alert">{error.message}</p>}
63
+ {wallet ? (
64
+ <p>
65
+ {wallet.address} — {balance?.formatted ?? '…'} ETH
66
+ </p>
67
+ ) : (
68
+ <button disabled={isPending} onClick={() => create({ chain: 'ethereum' })}>
69
+ {isPending ? 'Creating…' : 'Create wallet'}
70
+ </button>
71
+ )}
72
+ </>
73
+ );
37
74
  }
38
75
  ```
39
76
 
40
- | Export | Description |
41
- | ---------------------------- | ------------------------------------------------------- |
42
- | `WaaskeyProvider` | Provides a configured client (`options` or `client`). |
43
- | `useWaaskey()` | The `Waaskey` client from context. |
44
- | `useCreateWallet()` | `{ create, wallet, error, isPending }`. |
45
- | `useWallet(id)` | `{ data, loading, error, refresh }`. |
46
- | `useBalance(chain, address)` | `{ data, loading, error, refresh }` — client-side read. |
77
+ Keygen runs in the browser and takes a while (Paillier safe primes) — render the pending state.
78
+ Persist `wallet.id` in your own database and reload it later with `useWallet(id)`; the device share
79
+ stays in the `shareStore`.
80
+
81
+ ### Hooks
82
+
83
+ | Export | Description |
84
+ | ---------------------------- | ------------------------------------------------------------------- |
85
+ | `useWaaskey()` | The `Waaskey` client from context. |
86
+ | `useWaas()` | `{ client, theme, auth }` — the unified surface. |
87
+ | `useAuth()` | `{ user, session, isAuthenticated, ready, logout, setSession, … }`. |
88
+ | `useUser()` | The signed-in end-user, or `undefined`. |
89
+ | `useLogin()` | Headless login: email/phone OTP, Google, Firebase, passkey. |
90
+ | `useCreateWallet()` | `{ create, wallet, error, isPending }`. |
91
+ | `useWallet(id)` | `{ data, loading, error, refresh }`. |
92
+ | `useWallets(query?)` | `{ data, total, loading, error, refresh }` — the tenant's wallets. |
93
+ | `useBalance(chain, address)` | `{ data, loading, error, refresh }` — client-side read. |
94
+ | `useBalances(chains, addr)` | The same across several chains. |
95
+ | `useSend(wallet)` | `{ send, status, result, error, reset }` — `idle → pending → sent`. |
96
+ | `useSignatures(wallet)` | A wallet's signing activity. |
97
+ | `useQrCode(text)` | A QR data URL (needs the optional `qrcode` package). |
98
+
99
+ `useSend`/`useSignatures` accept a wallet **id or a loaded `Wallet`** — pass the object you already
100
+ have to skip a `wallets.get`. Every read cancels the request it supersedes, so a slow earlier
101
+ response can't overwrite a newer one.
47
102
 
48
103
  All of `@waaskey/sdk` is re-exported, so a single import covers types and values.
49
104
 
105
+ ### Signing in
106
+
107
+ `<ConnectModal>` drives `useLogin`, and the session it establishes is published to the provider's
108
+ shared auth state — so `useAuth()` sees it everywhere, not only inside the modal.
109
+
110
+ ```tsx
111
+ <ConnectModal open={open} onClose={() => setOpen(false)} methods={['email', 'phone', 'passkey']} onConnect={(s) => console.log(s.endUser)} />
112
+ ```
113
+
114
+ Google is available through `useLogin().loginWithGoogle(idToken)` — and in the modal by passing a
115
+ `googleIdToken` resolver. Your app owns the Google button (Google Identity Services /
116
+ `@react-oauth/google`); the kit ships no Google SDK, so without that prop the option is hidden.
117
+
118
+ ### Staying signed in
119
+
120
+ The SDK keeps the session **in memory**, so by default a reload signs the user out. Opt in with
121
+ `persistSession`:
122
+
123
+ | Value | Where the session goes |
124
+ | -------------------- | ------------------------------------------------------------------------------- |
125
+ | `'none'` _(default)_ | Memory only — a reload signs out. |
126
+ | `'session'` | `sessionStorage` — survives a reload, cleared when the tab closes. |
127
+ | `'local'` | `localStorage` — survives a restart. |
128
+ | a custom store | Your `{ load, save, clear }` — e.g. an httpOnly cookie set by your own backend. |
129
+
130
+ A session token in web storage is readable by any XSS or malicious extension, which is why the
131
+ default is `'none'` and `'session'` is the safer built-in. On restore the token is validated with
132
+ `auth.me()`: a token the API no longer accepts signs the user out instead of failing on the first
133
+ call. Gate your signed-out UI on `ready`, or the app flashes the login screen on every reload.
134
+ On React Native use `createSecureSessionStore(storage)` from `@waaskey/react-native` (Keychain /
135
+ Keystore) — the web values do nothing there.
136
+
137
+ ### Client identity
138
+
139
+ The client is built **once** and kept while the option values keep their identity, so an inline
140
+ `options={{ … }}` literal is safe: primitives are compared by value, objects (MPC core, share store)
141
+ by reference. Changing a value really does build a new client — abandoning any running ceremony and
142
+ cached state — and the kit warns in development when that happens. The end-user session is
143
+ re-adopted on the new client automatically, so enriching the options once after login (the pattern
144
+ above) does not sign the user out.
145
+
146
+ ## Next.js (App Router)
147
+
148
+ The package ships a `'use client'` directive, so importing it from a server component works; the
149
+ components themselves still have to be rendered inside a client boundary. Build the options in a
150
+ client module (they hold browser-only objects) and mount `<WaasProvider>` there.
151
+
50
152
  ## Widget kit (Privy-style, drop-in)
51
153
 
52
154
  Wrap the app once and drop in prebuilt, themeable components instead of building wallet
@@ -62,22 +164,20 @@ function App() {
62
164
  </WaasProvider>
63
165
  );
64
166
  }
65
-
66
- function Connect() {
67
- const [open, setOpen] = useState(true);
68
- return <ConnectModal open={open} onClose={() => setOpen(false)} onConnect={(s) => console.log(s.endUser)} />;
69
- }
70
167
  ```
71
168
 
72
169
  | Component / hook | What it does |
73
170
  | ------------------------------ | ---------------------------------------------------------------------------------------- |
74
- | `<WaasProvider options/theme>` | Client + theme context (alias of `WaaskeyProvider`; also accepts `client`). |
75
- | `useWaas()` | `{ client, theme, auth }` — the unified surface. |
76
- | `<ConnectModal>` | Email-OTP login modal (`auth`), `onConnect(session)`. |
171
+ | `<WaasProvider options/theme>` | Client + theme + auth context (alias of `WaaskeyProvider`; also accepts `client`). |
172
+ | `<ConnectModal>` | Login modal — email/phone OTP, passkey, Google; `onConnect(session)`. |
77
173
  | `<WalletWidget>` | Assets / Receive (QR) / Send / Activity panel. |
78
174
  | `<FundWidget>` | Fiat on-ramp (provider widget URL) to fund the wallet. |
79
175
  | `useSignPrompt()` | `requestSignature(tx)` → resolves on approve, rejects on cancel; renders `<SignPrompt>`. |
80
176
 
177
+ **WaaS signs, it does not broadcast.** A send returns `result.signedTx` (plus an offline
178
+ `result.txHash`); submitting it to a node is the integrator's step — `waaskey.broadcast(signedTx,
179
+ { rpcUrl })` or your own infrastructure. The widget says so on the success screen.
180
+
81
181
  ### Theming / white-label
82
182
 
83
183
  Pass a partial `theme` to the provider (merged onto `lightTheme`); `darkTheme` and