@hazbase/react is a React toolkit for two frontend patterns:
- standard injected wallet apps with
WalletProvider - passkey-native hazBase smart-wallet apps with
PasskeyAccountProvider
The passkey flow is intentionally flow-oriented. Instead of wiring every backend step yourself, you create one client and then use helpers like ensurePasskey(), ensureAccount(), ensureSession(), ensureLiveSession(), sponsorAndSend(), and sponsorAndSendExecute().
- Node.js >= 18
- React >= 18
- Ethers v6
- A hazBase-compatible backend for the passkey flow
- WebAuthn support in the browser when you use passkeys
pnpm add @hazbase/react @hazbase/auth @hazbase/kit ethers react
# or
npm i @hazbase/react @hazbase/auth @hazbase/kit ethers react@hazbase/react supports two main frontend patterns:
WalletProvider: MetaMask / injected wallet appsPasskeyAccountProvider: email OTP + passkey + account bootstrap + sponsored action apps
Use WalletProvider when your app should behave like a normal wallet-connected dApp. Use PasskeyAccountProvider when your app should guide users through a hazBase-managed smart-wallet flow.
import {
WalletProvider,
useAddress,
useNetwork,
useSigner,
} from '@hazbase/react';
function WalletPanel() {
const { signer, connectMetaMask, disconnect } = useSigner();
const { address, isConnected } = useAddress();
const network = useNetwork();
return (
<div>
<button onClick={() => connectMetaMask()}>Connect MetaMask</button>
<button onClick={() => disconnect()}>Disconnect</button>
<pre>
{JSON.stringify(
{
isConnected,
address,
chainId: network.chainId,
hasSigner: Boolean(signer),
},
null,
2,
)}
</pre>
</div>
);
}
export function App() {
return (
<WalletProvider autoConnect>
<WalletPanel />
</WalletProvider>
);
}import {
PasskeyAccountProvider,
createHazbasePasskeyClient,
usePasskeyAccount,
} from '@hazbase/react';
const client = createHazbasePasskeyClient();
function PasskeyPanel() {
const {
sendOtp,
verifyOtp,
ensurePasskey,
ensureSession,
ensureAccount,
sponsorAndSendExecute,
} = usePasskeyAccount();
async function runFlow() {
await sendOtp({ email: 'demo@example.com' });
await verifyOtp({ email: 'demo@example.com', code: '123456' });
await ensurePasskey({ deviceLabel: 'Chrome on MacBook' });
const account = await ensureAccount({ chainId: 11155111, accountSalt: 'user-owned-account' });
await ensureSession({ actionProfileKey: 'first_party_l2' });
const sent = await sponsorAndSendExecute({
mode: 'session',
nonce: '0',
target: '0x1111111111111111111111111111111111111111',
data: '0x12345678',
value: '0',
callGasLimit: '150000',
verificationGasLimit: '120000',
preVerificationGas: '50000',
maxFeePerGas: '1000000000',
maxPriorityFeePerGas: '100000000',
});
console.log(account.smartAccountAddress, sent.userOpHash, sent.transactionHash);
}
return <button onClick={runFlow}>Run passkey account flow</button>;
}
export function App() {
return (
<PasskeyAccountProvider
client={client}
defaultChainId={11155111}
defaultAccountSalt="user-owned-account"
defaultActionProfileKey="first_party_l2"
>
<PasskeyPanel />
</PasskeyAccountProvider>
);
}import {
PasskeyAccountProvider,
createHazbasePasskeyClient,
usePasskeyOnboarding,
} from '@hazbase/react';
const client = createHazbasePasskeyClient();
function OnboardingPanel() {
const { sendOtp, completeOnboarding, isAccountReady, smartAccountAddress } = usePasskeyOnboarding();
async function onboard() {
await sendOtp({ email: 'demo@example.com' });
await completeOnboarding({
email: 'demo@example.com',
code: '123456',
deviceLabel: 'Chrome on MacBook',
chainId: 11155111,
accountSalt: 'user-owned-account',
});
}
return (
<div>
<button onClick={onboard}>Bootstrap account</button>
{isAccountReady ? <pre>{smartAccountAddress}</pre> : null}
</div>
);
}
export function App() {
return (
<PasskeyAccountProvider client={client} defaultChainId={11155111}>
<OnboardingPanel />
</PasskeyAccountProvider>
);
}useSigner()useAddress()useNetwork()
The main passkey hook is usePasskeyAccount().
High-level helpers:
sendOtp()verifyOtp()ensurePasskey()ensureHighTrust()ensureAccount()ensureSession()grantSession()ensureLiveSession()sponsorUserOp()executeSessionDirect()executeSessionDirectExecute()executeSessionDirectExecuteBatch()sponsorAndSend()sponsorAndSendExecute()sponsorAndSendExecuteBatch()authorizeOwnerUserOp()refreshAccount()endSession()signOut()
Onboarding-focused helper:
usePasskeyOnboarding()
Account security helper:
useAccountSecurity()
Execute helpers:
encodeSmartAccountExecute()encodeSmartAccountExecuteBatch()createExecuteUserOp()createExecuteBatchUserOp()
Advanced escape hatch:
raw: access to the low-level client for custom integrations
WalletProviderPasskeyAccountProvidercreateHazbasePasskeyClientuseSigneruseAddressuseNetworkusePasskeyAccountusePasskeyOnboardinguseAccountSecurityencodeSmartAccountExecuteencodeSmartAccountExecuteBatchcreateExecuteUserOpcreateExecuteBatchUserOp
PasskeyAccountProviderassumes a first-party or allowlisted partner backend.@hazbase/reactstays backend-contract based: apps integrate against a backend URL, and the React surface does not depend on any specific infrastructure provider.- The same React integration should work with any hazBase-compatible backend as long as the backend API contract is preserved.
sendOtp()andverifyOtp()manage the application session, not wallet ownership by themselves.ensureAccount()will reuse an existing bound smart account when possible and bootstrap only when needed.- New embedded sessions always require a fresh
purpose=sessionpasskey step-up. Existing active sessions are reused until revoked or expired. - Backends that expose the V2 bundler path return additive fields such as
accountVariant,relayMode, andsubmittedUserOpHash; existing callers can ignore them safely. - Session mode is sponsor-required. The backend returns the final
accountSignaturefor the sponsored payload, and the React layer forwards it to the bundler. - Embedded sessions use a snapshot of the action profile taken at issuance time. Later profile broadening does not widen already-issued sessions.
- Profile deactivation still acts as a kill switch for active sessions.
rawis useful when you want to override one step without giving up the higher-level flow state.useAccountSecurity()wraps device/session inventory plus reauth-gated revoke flows for first-party security settings screens.
Make sure the browser has an injected wallet and that your app is running in a context where window.ethereum is available.
Pass an actionProfileKey directly, or set defaultActionProfileKey on PasskeyAccountProvider.
Use useAccountSecurity() to list active devices and embedded sessions, then call revokeDevice() or revokeSession() when the user confirms a revoke. The hook triggers a fresh passkey reauth before destructive operations.
The intended model is:
- OTP starts the app session
- passkey binds the device and handles step-up authentication
- account bootstrap and sponsorship happen only after those steps succeed
Session issuance is treated as a privileged action. A fresh purpose=session high-trust token is required whenever the provider needs to mint a new embedded session.
Use sponsorAndSendExecute() when you want the React layer to build SmartAccount.execute(...) callData for you. Use sponsorAndSend() when you already have a full userOp draft.
PasskeyAccountProvider does not add first-class settings UI in this phase, but the underlying client exposed as raw includes backend-first account security methods:
raw.listPasskeyDevices()raw.revokePasskeyDevice()raw.listEmbeddedSessions()raw.revokeEmbeddedSession()
Listing uses app-session auth. Revoke calls require a fresh purpose=reauth passkey step-up token. Device revoke cascades to active embedded sessions on that device.
Apache-2.0