Keyban React SDK
The official React SDK for Keyban's Wallet as a Service (WaaS). Build secure, non-custodial wallet experiences with React hooks, real-time blockchain data, and MPC-powered transaction signing.
Installation
npm install @keyban/sdk-react @keyban/sdk-base @keyban/types
Quick Start
import { Suspense } from "react";
import { KeybanProvider, useKeybanAccount, Network } from "@keyban/sdk-react";
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<KeybanProvider appId="your-app-id" network={Network.PolygonAmoy}>
<WalletInfo />
</KeybanProvider>
</Suspense>
);
}
function WalletInfo() {
const [account, error] = useKeybanAccount();
if (error) throw error;
return <div>Address: {account.address}</div>;
}
Key Features
- React Hooks - Complete set of hooks for accounts, balances, NFTs, transfers
- Real-Time Updates - GraphQL subscriptions for live blockchain data
- React Suspense - Seamless async data loading with Suspense boundaries
- Type Safety - Full TypeScript support with
@keyban/types - Secure Input - Iframe-isolated input component for sensitive data
- Authentication - Built-in auth flows (password, OTP, OAuth)
- Multi-Chain - EVM (Ethereum, Polygon), Starknet, Stellar support
- Pagination - Cursor-based pagination with
fetchMore() - Digital Product Passports - Claim and manage tokenized products
- Loyalty Programs - Points, rewards, and wallet passes
Provider Setup
Basic Setup
import { KeybanProvider, Network } from "@keyban/sdk-react";
<KeybanProvider appId="your-app-id" network={Network.PolygonAmoy}>
{/* Your app */}
</KeybanProvider>
With Custom Client Share Provider
import { ClientShareProvider } from "@keyban/sdk-base";
class MyStorage implements ClientShareProvider {
async get(key: string) {
return localStorage.getItem(key);
}
async set(key: string, value: string) {
localStorage.setItem(key, value);
}
}
<KeybanProvider
appId="your-app-id"
network={Network.PolygonAmoy}
clientShareProvider={new MyStorage()}
>
{/* Your app */}
</KeybanProvider>
Suspense Requirement
All Keyban hooks use React Suspense for data loading. Always wrap your components in a <Suspense> boundary:
<Suspense fallback={<LoadingSpinner />}>
<ComponentsUsingKeybanHooks />
</Suspense>
Authentication
useKeybanAuth Hook
Access authentication state and methods.
import { useKeybanAuth } from "@keyban/sdk-react";
function AuthComponent() {
const {
user, // Current user or null
isAuthenticated, // boolean | undefined
isLoading, // boolean
signUp,
signIn,
signOut,
sendOtp,
updateUser,
} = useKeybanAuth();
// Sign up with username/password
const handleSignUp = async () => {
await signUp({
username: "user@example.com",
password: "secure-password",
});
};
// Passwordless OTP flow
const handleOtpLogin = async () => {
// Step 1: Send OTP
await sendOtp({ email: "user@example.com" });
// Step 2: User enters OTP, then sign in
await signIn({
username: "user@example.com",
strategy: "email-otp",
code: "123456",
});
};
// Traditional login
const handleLogin = async () => {
await signIn({
username: "user@example.com",
password: "secure-password",
});
};
// Sign out
const handleLogout = async () => {
await signOut();
};
return (
<div>
{isAuthenticated ? (
<div>
<p>Welcome, {user?.email}</p>
<button onClick={handleLogout}>Sign Out</button>
</div>
) : (
<div>
<button onClick={handleLogin}>Sign In</button>
<button onClick={handleOtpLogin}>Sign In with OTP</button>
</div>
)}
</div>
);
}
Account Management
Get Account
import { useKeybanAccount } from "@keyban/sdk-react";
function AccountInfo() {
const [account, error] = useKeybanAccount();
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<p>Address: {account.address}</p>
<p>Public Key: {account.publicKey}</p>
<p>Account ID: {account.accountId}</p>
</div>
);
}
Native Balance
import { useKeybanAccount, useKeybanAccountBalance, FormattedBalance } from "@keyban/sdk-react";
function Balance() {
const [account] = useKeybanAccount();
const [balance, error] = useKeybanAccountBalance(account);
if (error) return <div>Error loading balance</div>;
return (
<div>
<p>Balance: {balance}</p>
{/* Or use FormattedBalance component */}
<FormattedBalance balance={{ raw: BigInt(balance), isNative: true }} />
</div>
);
}
Token Balances
import { useKeybanAccount, useKeybanAccountTokenBalances } from "@keyban/sdk-react";
function TokenList() {
const [account] = useKeybanAccount();
const [data, error, { loading, fetchMore }] = useKeybanAccountTokenBalances(
account,
{ first: 20 }
);
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h3>Tokens ({data.totalCount})</h3>
<ul>
{data.nodes.map((token) => (
<li key={token.id}>
{token.token.symbol}: {token.balance}
</li>
))}
</ul>
{data.hasNextPage && (
<button onClick={fetchMore} disabled={loading}>
Load More
</button>
)}
</div>
);
}
Specific Token Balance
import { useKeybanAccount, useKeybanAccountTokenBalance } from "@keyban/sdk-react";
function UsdcBalance() {
const [account] = useKeybanAccount();
const [balance, error] = useKeybanAccountTokenBalance(
account,
"0xTokenContractAddress"
);
return <div>USDC Balance: {balance?.balance || "0"}</div>;
}
NFTs
List All NFTs
import { useKeybanAccount, useKeybanAccountNfts } from "@keyban/sdk-react";
function NftGallery() {
const [account] = useKeybanAccount();
const [data, error, { loading, fetchMore }] = useKeybanAccountNfts(
account,
{ first: 20 }
);
if (error) return <div>Error loading NFTs</div>;
return (
<div>
<h3>My NFTs ({data.totalCount})</h3>
<div className="grid">
{data.nodes.map((nft) => (
<div key={nft.id}>
<img src={nft.nft.image} alt={nft.nft.name} />
<p>{nft.nft.name}</p>
<p>Token ID: {nft.nft.tokenId}</p>
<p>Balance: {nft.balance}</p>
</div>
))}
</div>
{data.hasNextPage && (
<button onClick={fetchMore} disabled={loading}>
Load More
</button>
)}
</div>
);
}
Get Specific NFT
import { useKeybanAccount, useKeybanAccountNft } from "@keyban/sdk-react";
function NftDetails({ contractAddress, tokenId }: { contractAddress: string; tokenId: string }) {
const [account] = useKeybanAccount();
const [nft, error] = useKeybanAccountNft(account, contractAddress, tokenId);
if (error) return <div>NFT not found</div>;
return (
<div>
<img src={nft.nft.image} alt={nft.nft.name} />
<h2>{nft.nft.name}</h2>
<p>{nft.nft.description}</p>
<p>Collection: {nft.nft.collection?.name}</p>
<p>Owned: {nft.balance}</p>
</div>
);
}
Transaction History
import { useKeybanAccount, useKeybanAccountTransferHistory } from "@keyban/sdk-react";
function TransactionHistory() {
const [account] = useKeybanAccount();
const [data, error, { loading, fetchMore }] = useKeybanAccountTransferHistory(
account,
{ first: 50 }
);
if (error) return <div>Error loading history</div>;
return (
<div>
<h3>Transaction History</h3>
<table>
<thead>
<tr>
<th>Type</th>
<th>Amount</th>
<th>From/To</th>
<th>Date</th>
</tr>
</thead>
<tbody>
{data.nodes.map((transfer) => (
<tr key={transfer.id}>
<td>{transfer.type}</td>
<td>{transfer.value}</td>
<td>{transfer.from === account.address ? transfer.to : transfer.from}</td>
<td>{new Date(transfer.timestamp).toLocaleString()}</td>
</tr>
))}
</tbody>
</table>
{data.hasNextPage && (
<button onClick={fetchMore} disabled={loading}>
Load More
</button>
)}
</div>
);
}
Orders History
import { useKeybanAccount, useKeybanAccountOrders } from "@keyban/sdk-react";
function OrdersHistory() {
const [account] = useKeybanAccount();
const [data, error, { fetchMore }] = useKeybanAccountOrders(
account,
{ first: 20 }
);
if (error) return <div>Error loading orders</div>;
return (
<div>
<h3>Orders ({data.totalCount})</h3>
<ul>
{data.nodes.map((order) => (
<li key={order.id}>
Order #{order.id} - {order.status}
<ul>
{order.items.map((item, idx) => (
<li key={idx}>
{item.productName} x{item.quantity}
</li>
))}
</ul>
</li>
))}
</ul>
{data.hasNextPage && <button onClick={fetchMore}>Load More</button>}
</div>
);
}
Digital Product Passports (DPP)
Get Product Sheet
import { useKeybanProduct } from "@keyban/sdk-react";
function Product({ productId }: { productId: string }) {
const [product, error] = useKeybanProduct(productId);
if (error) return <div>Product not found</div>;
return (
<div>
<h2>{product.name}</h2>
<p>Status: {product.status}</p>
<p>Collection: {product.collection?.name}</p>
</div>
);
}
Get DPP (Digital Product Passport)
import { useKeybanPassport } from "@keyban/sdk-react";
function DppDetails({ productId, dppId }: { productId: string; dppId: string }) {
const [dpp, error] = useKeybanPassport(productId, dppId);
if (error) return <div>DPP not found</div>;
return (
<div>
<h2>{dpp.productName}</h2>
<p>Token ID: {dpp.tokenId}</p>
<p>Owner: {dpp.owner}</p>
<p>Claimed: {dpp.claimedAt ? new Date(dpp.claimedAt).toLocaleDateString() : "Not claimed"}</p>
</div>
);
}
Loyalty Programs
import { useLoyaltyOptimisticBalance } from "@keyban/sdk-react";
function LoyaltyBalance() {
const [balance, error] = useLoyaltyOptimisticBalance();
if (error) return <div>Error loading loyalty balance</div>;
return (
<div>
<h3>Loyalty Points</h3>
<p>{balance.points} points</p>
<p>Tier: {balance.tier}</p>
</div>
);
}
Note: This hook auto-refreshes every 5 seconds to provide optimistic updates.
Secure Input Component
KeybanInput is an iframe-isolated input component for handling sensitive data securely.
Basic Usage
import { useRef } from "react";
import { KeybanInput, KeybanInputRef } from "@keyban/sdk-react";
function LoginForm() {
const emailRef = useRef<KeybanInputRef>(null);
const passwordRef = useRef<KeybanInputRef>(null);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// Values are securely handled inside iframe
// Use KeybanAuth hooks for actual authentication
};
return (
<form onSubmit={handleSubmit}>
<KeybanInput
ref={emailRef}
name="email"
type="email"
inputMode="email"
inputStyles={{
padding: "12px",
fontSize: "16px",
border: "1px solid #ccc",
borderRadius: "4px",
}}
/>
<KeybanInput
ref={passwordRef}
name="password"
type="password"
inputStyles={{
padding: "12px",
fontSize: "16px",
border: "1px solid #ccc",
borderRadius: "4px",
}}
/>
<button type="submit">Sign In</button>
</form>
);
}
Material-UI Integration
import { KeybanInput } from "@keyban/sdk-react";
import { TextField } from "@mui/material";
function MuiLoginForm() {
return (
<div>
<TextField
label="Email"
fullWidth
InputProps={{
inputComponent: KeybanInput,
inputProps: {
name: "email",
type: "email",
inputMode: "email",
},
}}
/>
</div>
);
}
Phone Input with MUI Tel Input
import { KeybanInput } from "@keyban/sdk-react";
import { MuiTelInput } from "mui-tel-input";
function PhoneInput() {
return (
<MuiTelInput
defaultCountry="FR"
InputProps={{
inputComponent: KeybanInput,
inputProps: {
name: "phone",
type: "tel",
inputMode: "tel",
},
}}
/>
);
}
Transactions
Use the account object returned by useKeybanAccount() to perform transactions:
Transfer Native Currency
import { useKeybanAccount } from "@keyban/sdk-react";
function SendNative() {
const [account] = useKeybanAccount();
const handleSend = async () => {
try {
// Estimate fees first
const fees = await account.estimateTransfer("0xRecipient");
// Execute transfer
const txHash = await account.transfer(
"0xRecipient",
1_000_000_000_000_000_000n, // 1 ETH
fees
);
console.log("Transaction hash:", txHash);
} catch (error) {
console.error("Transfer failed:", error);
}
};
return <button onClick={handleSend}>Send 1 ETH</button>;
}
Transfer ERC-20 Token
import { useKeybanAccount } from "@keyban/sdk-react";
function SendToken() {
const [account] = useKeybanAccount();
const handleSend = async () => {
try {
const txHash = await account.transferERC20({
contractAddress: "0xTokenContract",
to: "0xRecipient",
value: 1_000_000n, // 1 USDC (6 decimals)
});
console.log("Transaction hash:", txHash);
} catch (error) {
console.error("Transfer failed:", error);
}
};
return <button onClick={handleSend}>Send 1 USDC</button>;
}
Transfer NFT
import { useKeybanAccount } from "@keyban/sdk-react";
function SendNft() {
const [account] = useKeybanAccount();
const handleSend = async () => {
try {
// ERC-721
const txHash = await account.transferNft({
contractAddress: "0xNftContract",
to: "0xRecipient",
tokenId: "123",
standard: "ERC721",
});
// ERC-1155 (with quantity)
const txHash1155 = await account.transferNft({
contractAddress: "0xNftContract",
to: "0xRecipient",
tokenId: "456",
standard: "ERC1155",
value: 5n, // Transfer 5 copies
});
console.log("Transaction hash:", txHash);
} catch (error) {
console.error("Transfer failed:", error);
}
};
return <button onClick={handleSend}>Send NFT</button>;
}
Formatting Balances
useFormattedBalance Hook
import { useFormattedBalance } from "@keyban/sdk-react";
function TokenBalance({ balance, token }) {
const formatted = useFormattedBalance(balance, token);
return <span>{formatted}</span>;
}
FormattedBalance Component
import { FormattedBalance } from "@keyban/sdk-react";
function Balance({ balance, token }) {
return <FormattedBalance balance={balance} token={token} />;
}
Application Info
import { useKeybanApplication } from "@keyban/sdk-react";
function AppInfo() {
const [app, error] = useKeybanApplication();
if (error) return <div>Error loading app</div>;
return (
<div>
<h2>{app.name}</h2>
<p>Features: {app.features.join(", ")}</p>
<p>Theme: {app.theme.mode}</p>
</div>
);
}
Error Handling
All hooks return errors as the second element in the tuple:
import { useKeybanAccount, SdkError, SdkErrorTypes } from "@keyban/sdk-react";
function MyComponent() {
const [account, error] = useKeybanAccount();
if (error) {
if (error instanceof SdkError) {
switch (error.type) {
case SdkErrorTypes.InsufficientFunds:
return <div>Not enough balance</div>;
case SdkErrorTypes.AddressInvalid:
return <div>Invalid address</div>;
default:
return <div>Error: {error.message}</div>;
}
}
return <div>Unexpected error: {error.message}</div>;
}
return <div>Account: {account.address}</div>;
}
Direct Client Access
Access the underlying SDK client for advanced operations:
import { useKeybanClient } from "@keyban/sdk-react";
function AdvancedComponent() {
const client = useKeybanClient();
const handleAdvancedOperation = async () => {
// Access Apollo Client for custom queries
const { data } = await client.apolloClient.query({
query: myCustomQuery,
variables: { ... },
});
// Access API services
await client.api.dpp.claim({ productId, password });
await client.api.loyalty.getBalance();
// Get application info
const app = await client.api.application.getApplication();
};
return <button onClick={handleAdvancedOperation}>Advanced</button>;
}
Hook Return Types
ApiResult Pattern
All hooks return an ApiResult tuple:
type ApiResult<T, Extra = undefined> =
| readonly [T, null, Extra] // Success
| readonly [null, Error, Extra]; // Error
// Usage
const [data, error, extra] = useKeybanHook();
PaginatedData
Hooks with pagination return:
type PaginatedData<T> = {
nodes: T[];
hasPrevPage: boolean;
hasNextPage: boolean;
totalCount: number;
};
type PaginationExtra = {
loading: boolean;
fetchMore?: () => void;
};
// Usage
const [data, error, { loading, fetchMore }] = useKeybanAccountTokenBalances(account);
Development
# Build the package
npm run build
# Type check
npm run typecheck
# Run tests
npm test
# Lint
npm run lint
Compatibility
- React: 19+ (React 18 supported)
- TypeScript: 5.0+ recommended
- Bundlers: Vite, webpack, Next.js, Create React App
- SSR: Compatible with Next.js App Router and Pages Router
SSR Considerations
When using with Next.js or other SSR frameworks:
- Ensure
KeybanProvideris rendered client-side only - Use dynamic imports with
ssr: falsefor components using Keyban hooks - Wrap data-fetching components in
<Suspense>boundaries
// Next.js example
import dynamic from 'next/dynamic';
const WalletComponent = dynamic(
() => import('./WalletComponent'),
{ ssr: false }
);
Related Packages
- @keyban/types - Shared TypeScript types and Zod schemas
- @keyban/sdk-base - Core JavaScript SDK
- API Documentation - Full TypeDoc reference
License
See the main repository for license information.