@waaskey/react
v0.5.0
Published
React hooks + provider for the WAASKey SDK — non-custodial MPC wallets.
Downloads
1,085
Maintainers
Readme
@waaskey/react
React hooks + provider for the Waaskey SDK — embedded,
non-custodial MPC wallets. A thin layer over @waaskey/sdk; the same
hooks work in React Native (via @waaskey/react-native).
Install
@waaskey/sdk and react are peer dependencies — the SDK holds your end-user session and
the MPC/share state, so a second copy in the tree would mean a second client and a lost session.
pnpm add @waaskey/react @waaskey/sdk @waaskey/client-wasm react@waaskey/client-wasm carries the browser MPC engine; qrcode is optional (the Receive tab shows
the address either way), as is @simplewebauthn/browser (only for passkey sign-in).
Use
import { useMemo, useState } from 'react';
import { ConnectModal, WaasProvider, useAuth, useBalance, useCreateWallet } from '@waaskey/react';
import { EncryptedShareStore, WasmMpcCore, loadClientWasm } from '@waaskey/sdk';
import type { EmbeddedSession } from '@waaskey/sdk';
export default function App() {
// The device share is sealed with a secret derived from the signed-in user, so the MPC pieces
// can only be wired once you have a session. `useMemo` keeps the options (and therefore the
// client) stable — see "Client identity" below.
const [session, setSession] = useState<EmbeddedSession | null>(null);
const options = useMemo(() => {
const base = { apiKey: import.meta.env.VITE_WAASKEY_KEY as string };
if (!session) return base;
return {
...base,
mpc: new WasmMpcCore(loadClientWasm),
shareStore: EncryptedShareStore.browser(`${session.token.slice(0, 32)}:my-app`),
};
}, [session]);
return (
<WaasProvider options={options} persistSession="session">
<Wallet onSession={setSession} />
</WaasProvider>
);
}
function Wallet({ onSession }: { onSession: (s: EmbeddedSession) => void }) {
const { isAuthenticated, ready, user, logout } = useAuth();
const { create, wallet, isPending, error } = useCreateWallet();
const { data: balance } = useBalance('ethereum', wallet?.address);
if (!ready) return null; // a persisted session may still be restoring
if (!isAuthenticated) return <ConnectModal open onClose={() => undefined} onConnect={onSession} />;
return (
<>
<p>
{user?.email} · <button onClick={logout}>Sign out</button>
</p>
{error && <p role="alert">{error.message}</p>}
{wallet ? (
<p>
{wallet.address} — {balance?.formatted ?? '…'} ETH
</p>
) : (
<button disabled={isPending} onClick={() => create({ chain: 'ethereum' })}>
{isPending ? 'Creating…' : 'Create wallet'}
</button>
)}
</>
);
}Keygen runs in the browser and takes a while (Paillier safe primes) — render the pending state.
Persist wallet.id in your own database and reload it later with useWallet(id); the device share
stays in the shareStore.
Hooks
| Export | Description |
| ---------------------------- | ------------------------------------------------------------------- |
| useWaaskey() | The Waaskey client from context. |
| useWaas() | { client, theme, auth } — the unified surface. |
| useAuth() | { user, session, isAuthenticated, ready, logout, setSession, … }. |
| useUser() | The signed-in end-user, or undefined. |
| useLogin() | Headless login: email/phone OTP, Google, Firebase, passkey. |
| useCreateWallet() | { create, wallet, error, isPending }. |
| useWallet(id) | { data, loading, error, refresh }. |
| useWallets(query?) | { data, total, loading, error, refresh } — the tenant's wallets. |
| useBalance(chain, address) | { data, loading, error, refresh } — client-side read. |
| useBalances(chains, addr) | The same across several chains. |
| useSend(wallet) | { send, status, result, error, reset } — idle → pending → sent. |
| useSignatures(wallet) | A wallet's signing activity. |
| useQrCode(text) | A QR data URL (needs the optional qrcode package). |
useSend/useSignatures accept a wallet id or a loaded Wallet — pass the object you already
have to skip a wallets.get. Every read cancels the request it supersedes, so a slow earlier
response can't overwrite a newer one.
All of @waaskey/sdk is re-exported, so a single import covers types and values.
Signing in
<ConnectModal> drives useLogin, and the session it establishes is published to the provider's
shared auth state — so useAuth() sees it everywhere, not only inside the modal.
<ConnectModal open={open} onClose={() => setOpen(false)} methods={['email', 'phone', 'passkey']} onConnect={(s) => console.log(s.endUser)} />Google is available through useLogin().loginWithGoogle(idToken) — and in the modal by passing a
googleIdToken resolver. Your app owns the Google button (Google Identity Services /
@react-oauth/google); the kit ships no Google SDK, so without that prop the option is hidden.
Staying signed in
The SDK keeps the session in memory, so by default a reload signs the user out. Opt in with
persistSession:
| Value | Where the session goes |
| -------------------- | ------------------------------------------------------------------------------- |
| 'none' (default) | Memory only — a reload signs out. |
| 'session' | sessionStorage — survives a reload, cleared when the tab closes. |
| 'local' | localStorage — survives a restart. |
| a custom store | Your { load, save, clear } — e.g. an httpOnly cookie set by your own backend. |
A session token in web storage is readable by any XSS or malicious extension, which is why the
default is 'none' and 'session' is the safer built-in. On restore the token is validated with
auth.me(): a token the API no longer accepts signs the user out instead of failing on the first
call. Gate your signed-out UI on ready, or the app flashes the login screen on every reload.
On React Native use createSecureSessionStore(storage) from @waaskey/react-native (Keychain /
Keystore) — the web values do nothing there.
Client identity
The client is built once and kept while the option values keep their identity, so an inline
options={{ … }} literal is safe: primitives are compared by value, objects (MPC core, share store)
by reference. Changing a value really does build a new client — abandoning any running ceremony and
cached state — and the kit warns in development when that happens. The end-user session is
re-adopted on the new client automatically, so enriching the options once after login (the pattern
above) does not sign the user out.
Next.js (App Router)
The package ships a 'use client' directive, so importing it from a server component works; the
components themselves still have to be rendered inside a client boundary. Build the options in a
client module (they hold browser-only objects) and mount <WaasProvider> there.
Widget kit (Privy-style, drop-in)
Wrap the app once and drop in prebuilt, themeable components instead of building wallet UI from scratch. Everything reads the client + theme from the provider:
import { WaasProvider, ConnectModal, WalletWidget, FundWidget, useSignPrompt, darkTheme } from '@waaskey/react';
function App() {
return (
<WaasProvider options={options} theme={darkTheme}>
<Connect />
</WaasProvider>
);
}| Component / hook | What it does |
| ------------------------------ | ---------------------------------------------------------------------------------------- |
| <WaasProvider options/theme> | Client + theme + auth context (alias of WaaskeyProvider; also accepts client). |
| <ConnectModal> | Login modal — email/phone OTP, passkey, Google; onConnect(session). |
| <WalletWidget> | Assets / Receive (QR) / Send / Activity panel. |
| <FundWidget> | Fiat on-ramp (provider widget URL) to fund the wallet. |
| useSignPrompt() | requestSignature(tx) → resolves on approve, rejects on cancel; renders <SignPrompt>. |
WaaS signs, it does not broadcast. A send returns result.signedTx (plus an offline
result.txHash); submitting it to a node is the integrator's step — waaskey.broadcast(signedTx,
{ rpcUrl }) or your own infrastructure. The widget says so on the success screen.
Theming / white-label
Pass a partial theme to the provider (merged onto lightTheme); darkTheme and
lightTheme are exported, and every component is themed from that single source — no
hardcoded brand. The same kit ships for React Native from @waaskey/react-native (the
visual components are RN-native; hooks/theme are shared).
<WaasProvider options={options} theme={{ accent: '#10b981', radius: '20px' }} />License
MIT © WAASKey
