React
Drop-in React bindings for @cavos/kit — provider, hooks, and the customizable login modal.
@cavos/kit ships React bindings under the @cavos/kit/react subpath (same
package). Wrap your app once in <CavosProvider> and every descendant can call
useCavos() to read wallet state and trigger actions — no prop drilling, no
imperative connect calls.
npm install @cavos/kitimport { CavosProvider } from "@cavos/kit/react";
export function Providers({ children }) {
return (
<CavosProvider
config={{
appId: process.env.NEXT_PUBLIC_CAVOS_APP_ID,
chains: ["solana", "stellar"],
defaultChain: "solana",
network: "testnet",
appSalt: "my-app",
socialRecovery: true, // optional — see Recovery
}}
modal={{
appName: "My App",
theme: "light",
}}
>
{children}
</CavosProvider>
);
}The provider manages one or more chains. Behind the scenes it resolves the identity, derives addresses for all configured chains, and keeps the wallet handles ready for sponsored transactions. Deployment happens lazily on first execute per chain — see Concepts.
Props
| Prop | Description |
|---|---|
config | Chains, network, appSalt and credentials. One or more chains via chains + defaultChain. |
modal | Login modal appearance and providers. Omit to render no modal. |
identity | Bring your own auth. Pass your signed-in user and Cavos login turns off. null while your auth loads or after sign-out. See Authentication. |
In development the provider checks the config when it mounts and reports
problems in the console — a missing appSalt, a paymaster key on a chain that
ignores it, and in particular an appSalt that changed since this browser last
connected, which silently moves every existing user to a new, empty wallet. Run
the same checks yourself with validateCavosConfig(config).
useCavos()
The primary hook. Returns the full wallet surface:
import { useCavos } from "@cavos/kit/react";
function Wallet() {
const { address, isAuthenticated, walletStatus, openModal, logout } = useCavos();
if (!isAuthenticated) return <button onClick={openModal}>Sign in</button>;
return <div>{address} · ready={walletStatus.isReady}</div>;
}State
| Field | Description |
|---|---|
isAuthenticated: boolean | Whether a wallet is connected. |
user: UserInfo | null | { userId, email?, provider? } from the login. |
wallet: CavosWallet | null | The connected wallet for the selected chain, discriminated by wallet.chain. Narrow on it before chain-native calls. |
session: CavosWallet & CavosSession | null | The full multi-chain session. Use session.wallet("stellar") to access other chains. |
address: string | null | The account address for the selected chain. |
chain: Chain | The active chain ("starknet" | "solana" | "stellar"). |
configuredChains: Chain[] | All chains configured for this session. |
walletStatus: WalletStatus | Deployment / device-approval / passkey flags (see below). |
isLoading: boolean | True during an in-flight login or connect. |
authError: string | null | Last unrecoverable auth/connect error. Null while healthy. |
passkeySupported: boolean | Whether this device/browser can use a platform passkey. |
WalletStatus: { isDeploying, isReady, isUndeployed, needsDeviceApproval, awaitingApproval, pendingRequestId, hasPasskey, isNewAccount, isSocialRecovering, socialRecoveryReadyAt, recovery }.
isUndeployed: True when the account has not been deployed on-chain yet. First execute will deploy + run atomically.isReady: True when deployed and this device is authorized.needsDeviceApproval: True when deployed but this device is not authorized.
Both isUndeployed and isReady allow execution. Only needsDeviceApproval
blocks transactions. An undeployed wallet can sign messages and execute — the
first execute deploys the account.
walletStatus.recovery is the one flag most apps actually read:
{ protected: boolean; methods: ("passkey" | "social")[] } — can this user get
back in from a device they do not have yet? The other flags describe work in
flight; this describes the standing guarantee.
const { walletStatus } = useCavos();
if (walletStatus.isReady && !walletStatus.recovery.protected) {
// Nudge: "Add a passkey so you can sign in on a new device."
}recovery.protected starts false and flips once the lookup that fills it
returns. Read it as "not known to be protected" during the first moments of a
connect, not as "known to be unprotected" — don't show a scary warning on frame one.
Chain switching
Switch the selected chain without re-authenticating or remounting:
const { chain, setChain, configuredChains, session } = useCavos();
// Switch to Stellar
await setChain("stellar");
// Now `chain` is "stellar", `wallet` is the Stellar wallet
// The walletStatus updates to reflect the Stellar account's statesetChain does not re-deploy or re-authenticate — it just changes which wallet
is active. The session keeps all chains connected.
Login
| Method | Description |
|---|---|
login(provider) | OAuth social login ("google" | "apple") — opens the hosted flow. |
sendOtp(email) | Send an email OTP code. |
verifyOtp(email, code) | Verify an OTP / complete a magic link and deploy the wallet. |
sendMagicLink(email) | Send a passwordless magic-link email. |
handleCallback(authData, redirectUri?) | Resolve identity from an OAuth callback (?cavos_auth_code=…) and deploy. Pass the callback URL as redirectUri if it differs from the current one. |
Transactions
| Method | Description |
|---|---|
execute(calls, opts?) | Starknet only. Multicall signed by the device key, gasless by default. Returns { transactionHash }. Works on undeployed (deploys first) and ready wallets. |
signMessage(message) | All chains. Sign an arbitrary message off-chain; returns a uniform MessageSignature. Works on undeployed and ready wallets. See Wallet actions. |
On Solana and Stellar, call the wallet directly for chain-native actions
instead of the context's execute: narrow on wallet.chain, then
wallet.execute(amount, destination). For signTransaction (sign without
submit), also call wallet.signTransaction(...) directly after narrowing —
the args differ per chain. See Wallet actions.
Device & recovery
| Method | Description |
|---|---|
addSigner(pubkey) | Authorize another device signer (gasless add_signer). |
removeSigner(pubkey) | Revoke a device signer (gasless remove_signer) — the "this wasn't me" escape hatch. Must run from a device that is already authorized, and cannot revoke itself. On Stellar use wallet.removeDevice(...) instead: devices there are envelope slots, not signer pubkeys. |
listDevices() | Device signer pubkeys currently authorized on this wallet, for a management UI. Starknet/Solana only — on Stellar call wallet.listDevices() for the envelope slot ids. |
enrollPasskey(passkey, params) | Low-level passkey enrollment when you drive WebAuthn yourself. Most apps want enrollPasskeyDefault(). |
enrollPasskeyDefault() | Enroll a synced passkey as an approver (2FA for new devices). Works on undeployed (pending until first execute) and ready wallets. |
approveDeviceWithPasskey() | From a needs-device-approval browser, prompt the synced passkey to authorize this device. |
setupRecovery() | Register a backup signer derived from a generated recovery code. Resolves with the code — show it to the user once. Works on undeployed (pending until first execute) and ready wallets. |
submitSocialRecoveryToken(idToken) | Drive social recovery with a provider id_token your own login obtained. Enrols when the device is ready, recovers when it is not. See Hardware-isolated recovery. |
recover(code) | Recover access after losing every device. Brings the provider to a ready state. |
resendDeviceApproval() | Re-request the device-approval email for the current pending request. |
When hardware-isolated social recovery is enabled in the selected environment,
the dashboard's single provider overrides modal.providers. Email is forced to
magic-link mode; OTP is not used for this recovery flow. The provider
automatically enrolls a ready wallet after fresh authentication and restores an
unregistered device through the attested enclave.
These wrappers present a shared product flow across the current adapters. Their cryptographic implementation and some availability constraints remain chain-specific. See Passkeys and Recovery for the capability details.
Modal & session
| Method | Description |
|---|---|
openModal() / closeModal() | Open/close the built-in auth modal (the provider mounts it automatically when modal is set). |
logout() | Sign out and clear local wallet state. |
clearAuthError() | Clear authError (e.g. when the user starts a new login attempt). |
useCavosAuth()
A deliberately thin subset for components that only need to open the modal and read basic auth state — no wallet, no transactions:
{
openModal, closeModal,
isAuthenticated, address, user, walletStatus, logout,
}<CavosAuthModal>
A fully customizable, themeable login modal. The provider mounts it
automatically when you pass modal={...}; you can also render it directly (for
a live preview, or to control its lifecycle yourself).
import { CavosAuthModal } from "@cavos/kit/react";
<CavosAuthModal
open={open}
onClose={() => setOpen(false)}
appName="My App"
appLogo="https://myapp.com/logo.png"
appLogoSize={56}
providers={["email", "google", "apple"]}
emailMode="otp"
primaryColor="#402AFF"
theme="light"
radius={16}
secureStep="optional"
/>Props
| Prop | Type | Description |
|---|---|---|
open | boolean | Whether the modal is shown (ignored when inline). |
onClose | () => void | Called when the user dismisses the modal. |
appName | string? | Shown in the heading ("Sign in to {appName}"). |
appLogo | string? | Image URL for the logo. If omitted, the Cavos star is shown. The provider also loads it from your app's dashboard config automatically; a local appLogo overrides that. |
appLogoSize | number? | Logo height in px. Defaults to 40 (image) / 34 (Cavos star). |
providers | ("google" | "apple" | "email")[]? | Which login buttons to show. Defaults to all three. |
emailMode | "magic-link" | "otp"? | How the email provider authenticates. Defaults to magic-link. |
primaryColor | string? | Accent color for buttons / focus rings (hex). |
theme | "light" | "dark"? | Card theme. Defaults to light. |
backgroundColor | string? | Override the card background (defaults to white / #111 per theme). |
radius | number? | Card & button corner radius in px (card defaults to 16). |
inline | boolean? | Render the card in-flow (no overlay/backdrop) — for live previews. When true, open is ignored. |
secureStep | "optional" | "required" | "off"? | The one-time "secure your account" step after a new account. optional (default) shows Skip; required forces it; off skips it. |
onSuccess | (address: string) => void? | Fired when authentication + deployment succeed. |
CavosModalConfig
The object you pass as modal={...} to <CavosProvider>. Same fields as
CavosAuthModalProps except no open / onClose / inline (the provider
manages those), plus onSuccess.
Theming
All visual props (primaryColor, theme, backgroundColor, radius,
appLogo, appLogoSize) apply identically whether you set them on
<CavosProvider modal={...}> or on a direct <CavosAuthModal ... />.
<CavosProvider
config={{ chains: ["solana"], defaultChain: "solana", network: "testnet", appSalt: "my-app", appId }}
modal={{
appName: "My App",
appLogo: "/logo.png",
appLogoSize: 48,
primaryColor: "#7C3AED",
theme: "dark",
backgroundColor: "#0A0A0F",
radius: 20,
}}
>
<App />
</CavosProvider>Mobile: automatic bottom sheet
On viewports max-width: 640px the modal becomes a bottom sheet — it
slides up from the bottom with rounded top corners and a grab handle, overlaying
the page with a blurred backdrop. This is fully automatic; there is no prop to
toggle. On desktop the modal renders centered. The inline mode disables the
overlay/sheet entirely (the card is embedded in your layout).
Device approval & revocation pages
Every integrating app needs two routes: one where a user approves a new device, and one where they revoke a device they don't recognise (the links in the device-added email). Both ship ready to mount:
import { ApproveDevicePage } from "@cavos/kit/react";
const configs = [
{ chains: ["starknet"], defaultChain: "starknet", network: "testnet", appSalt: "my-app", appId, paymasterApiKey },
{ chains: ["solana"], defaultChain: "solana", network: "testnet", appSalt: "my-app", appId },
];
export default function Page() {
return <ApproveDevicePage configs={configs} />;
}Pass every chain your app supports — the request carries its own network and
the page selects the matching config (configForNetwork / chainForNetwork do
this, and are exported if you mount the provider yourself). Mounting the wrong
chain's provider fails in a way that reads like the device is gone.
RevokeDevicePage is the same flow with the opposite verb. Pass a function as
children to keep your own markup — see Multi-device.
Multi-chain in React
The provider handles multiple chains natively. Configure them all up front:
<CavosProvider
config={{
chains: ["solana", "stellar", "starknet"],
defaultChain: "solana",
network: "testnet",
appSalt: "my-app",
appId,
paymasterApiKey, // for Starknet
}}
modal={{ appName: "My App" }}
>
<App />
</CavosProvider>Then switch chains without remounting:
function ChainSwitcher() {
const { chain, setChain, configuredChains, walletStatus } = useCavos();
return (
<select value={chain} onChange={(e) => setChain(e.target.value as Chain)}>
{configuredChains.map((c) => (
<option key={c} value={c}>{c}</option>
))}
</select>
);
}setChain updates the active chain, and walletStatus reflects the new
chain's status. No re-authentication, no remount, no lost state.
Each chain has its own address, deployment status, and device authorization.
Switching chains may show a different walletStatus — one chain might be
ready while another is undeployed or needs-device-approval.
Chain and NetworkEnv types are exported from the core entry
(import type { Chain } from "@cavos/kit"), not from @cavos/kit/react.