@hazbase/react
v0.1.0
Published
React toolkit for wallet connection, passkey-native account bootstrap, and sponsored smart-wallet UX built on ethers v6.
Maintainers
Readme
@hazbase/react
Overview
@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().
Requirements
- Node.js >= 18
- React >= 18
- Ethers v6
- A hazBase-compatible backend for the passkey flow
- WebAuthn support in the browser when you use passkeys
Installation
pnpm add @hazbase/react @hazbase/auth @hazbase/kit ethers react
# or
npm i @hazbase/react @hazbase/auth @hazbase/kit ethers reactUsage patterns
@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.
Quick start: WalletProvider (MetaMask / injected wallet)
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>
);
}Quick start: PasskeyAccountProvider
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: '[email protected]' });
await verifyOtp({ email: '[email protected]', 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>
);
}Quick start: usePasskeyOnboarding
import {
PasskeyAccountProvider,
createHazbasePasskeyClient,
usePasskeyOnboarding,
} from '@hazbase/react';
const client = createHazbasePasskeyClient();
function OnboardingPanel() {
const { sendOtp, completeOnboarding, isAccountReady, smartAccountAddress } = usePasskeyOnboarding();
async function onboard() {
await sendOtp({ email: '[email protected]' });
await completeOnboarding({
email: '[email protected]',
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>
);
}Common operations
Standard wallet hooks
useSigner()useAddress()useNetwork()
Passkey flow hooks
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
Main exports
WalletProviderPasskeyAccountProvidercreateHazbasePasskeyClientuseSigneruseAddressuseNetworkusePasskeyAccountusePasskeyOnboardinguseAccountSecurityencodeSmartAccountExecuteencodeSmartAccountExecuteBatchcreateExecuteUserOpcreateExecuteBatchUserOp
Notes
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.
Troubleshooting (FAQ)
MetaMask is not found
Make sure the browser has an injected wallet and that your app is running in a context where window.ethereum is available.
ensureSession() says that actionProfileKey is required
Pass an actionProfileKey directly, or set defaultActionProfileKey on PasskeyAccountProvider.
How do I build a device/session security screen?
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.
Why does passkey setup still need OTP?
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
Why does a new embedded session always ask for passkey step-up?
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.
When should I use sponsorAndSendExecute() instead of sponsorAndSend()?
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.
Low-level account security methods
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.
License
Apache-2.0
