@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 appsHazbaseX402Provider: token-agnostic x402 payment helpers for merchant pages
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()
x402 hooks:
HazbaseX402ProvideruseX402Client()useX402ExtensionBridge()useX402Requirement()useX402WalletHandoff()useX402Settlement()
x402 components:
X402PaywallX402RequirementScript
Execute helpers:
encodeSmartAccountExecute()encodeSmartAccountExecuteBatch()createExecuteUserOp()createExecuteBatchUserOp()
x402 pure helpers:
createHazbaseX402Client()createX402WalletUrl()readX402PaymentFromUrl()parseX402Payload()serializeX402ScriptTag()createX402BridgeRequest()postX402BridgeRequest()listenForX402BridgeMessages()
Design notes:
Advanced escape hatch:
raw: access to the low-level client for custom integrations
WalletProviderPasskeyAccountProvidercreateHazbasePasskeyClientuseSigneruseAddressuseNetworkusePasskeyAccountusePasskeyOnboardinguseAccountSecurityencodeSmartAccountExecuteencodeSmartAccountExecuteBatchcreateExecuteUserOpcreateExecuteBatchUserOpcreateHazbaseX402ClientcreateX402WalletUrlreadX402PaymentFromUrlparseX402PayloadserializeX402ScriptTagcreateX402BridgeRequestpostX402BridgeRequestlistenForX402BridgeMessagesX402PaywallX402RequirementScript
The x402 helpers are token-agnostic. Apps pass network, asset,
priceAtomic, payoutMethod, and wallet URLs as configuration instead of using
token-specific SDK methods.
import {
createHazbaseX402Client,
createX402WalletUrl,
readX402PaymentFromUrl,
serializeX402ScriptTag,
} from '@hazbase/react';
const x402Client = createHazbaseX402Client({
apiEndpoint: 'https://api.hazbase.com',
});
const requirement = await x402Client.createRequirement({
resourceId: 'member-page-v1',
resourceUrl: 'https://example.com/member-page',
description: 'Unlock member page',
network: 'sepolia',
asset: 'example-token',
priceAtomic: '100000000000000000',
payoutMethod: {
kind: 'external_eoa',
address: '0x1234567890AbcdEF1234567890aBcdef12345678',
},
});
const walletUrl = createX402WalletUrl({
walletUrl: 'https://wallet.example/pwa/',
x402: requirement.x402,
sourceUrl: 'https://example.com/member-page',
title: 'Member page',
completion: 'fragment',
});
const scriptTag = serializeX402ScriptTag(requirement.x402);
const returned = readX402PaymentFromUrl(window.location.href);
if (returned) {
await x402Client.settlePayment({
paymentRequestId: requirement.paymentRequestId,
xPayment: returned.xPayment,
});
}Use HazbaseX402Provider when a React page needs shared x402 client config or
a default wallet URL.
import {
HazbaseX402Provider,
useX402Settlement,
useX402WalletHandoff,
} from '@hazbase/react';
function Paywall({ requirement }) {
const handoff = useX402WalletHandoff({
x402: requirement.x402,
sourceUrl: window.location.href,
title: document.title,
completion: 'fragment',
});
const settlement = useX402Settlement({
paymentRequestId: requirement.paymentRequestId,
autoReadUrl: true,
onSettled() {
// App unlocks content or asks its backend to create an access grant.
},
});
if (settlement.status === 'settled') return <article>Unlocked</article>;
return (
<a href={handoff.walletUrl ?? '#'} onClick={handoff.openWallet}>
Pay to unlock
</a>
);
}
export function App({ requirement }) {
return (
<HazbaseX402Provider
apiEndpoint="https://api.hazbase.com"
walletUrl="https://wallet.example/pwa/"
completionParams={['xPayment', 'funafcXPayment']}
>
<Paywall requirement={requirement} />
</HazbaseX402Provider>
);
}useX402Requirement() is available for demos and static-price pages, but
production merchant pages should usually create requirements on their own
backend and pass the returned payload into the paywall UI.
X402Paywall wraps the handoff and settlement hooks while leaving the UI to the
application.
import { HazbaseX402Provider, X402Paywall } from '@hazbase/react';
function ArticleGate({ requirement }) {
return (
<X402Paywall
requirement={requirement}
sourceUrl={window.location.href}
title={document.title}
completionParam="xPayment"
>
{({ status, walletUrl, openWallet, error }) => {
if (status === 'settled') return <article>Unlocked</article>;
return (
<section>
<a href={walletUrl ?? '#'} onClick={openWallet}>
Pay to unlock
</a>
{error ? <p>{error.message}</p> : null}
</section>
);
}}
</X402Paywall>
);
}
export function App({ requirement }) {
return (
<HazbaseX402Provider walletUrl="https://wallet.example/pwa/">
<ArticleGate requirement={requirement} />
</HazbaseX402Provider>
);
}By default, X402Paywall also renders an
application/x-x402+json script tag for extension detection. Set
renderRequirementScript={false} if the page renders its own script tag.
Extension-enabled merchant pages can announce a payment request through a small
postMessage bridge. Wallet extensions decide their own domain allowlist,
side-panel behavior, and signing flow. Call announce() from a user action so
browser extension surfaces such as Chrome Side Panel can open reliably.
import { useX402ExtensionBridge } from '@hazbase/react';
function ExtensionAwarePaywall({ requirement }) {
const bridge = useX402ExtensionBridge({
x402: requirement.x402,
paymentRequestId: requirement.paymentRequestId,
sourceUrl: window.location.href,
title: document.title,
completion: { mode: 'fragment', param: 'xPayment' },
allowedOrigins: [window.location.origin],
onPayment(message) {
// Apps can settle message.xPayment, or keep URL fragment completion as
// the primary flow and use this as an extension-only enhancement.
},
});
return (
<button type="button" onClick={bridge.announce}>
Pay with wallet extension
</button>
);
}The bridge is token-agnostic. It carries an x402 payload and generic wallet capabilities, but does not include private keys, passkey assertions, or token-specific policy.
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.
Some transitive dependencies of this package carry known advisories with no upstream
fix yet. Because npm ignores overrides declared inside a dependency, the only way
to protect your own dependency tree is to add them to your application's
package.json and reinstall:
(yarn: use resolutions; pnpm: use pnpm.overrides.)
ws ^8.21.0— avoids awsadvisory in the versionetherscurrently pins.bfj ^9.1.3— removes thesnarkjs → bfj → jsonpathchain (prototype-pollution / code-injection advisories) pulled in via@hazbase/auth/@hazbase/kit.
These are workarounds until ethers / snarkjs ship fixed transitive ranges upstream.
Apache-2.0
{ "overrides": { "ws": "^8.21.0", "bfj": "^9.1.3" } }