@keyban/sdk-react
v0.14.0
Published
Keyban SDK React simplifies the integration of Keyban's MPC wallet in React apps with TypeScript support, flexible storage, and Ethereum blockchain integration.
Readme
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/typesQuick 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: "[email protected]",
password: "secure-password",
});
};
// Passwordless OTP flow
const handleOtpLogin = async () => {
// Step 1: Send OTP
await sendOtp({ email: "[email protected]" });
// Step 2: User enters OTP, then sign in
await signIn({
username: "[email protected]",
strategy: "email-otp",
code: "123456",
});
};
// Traditional login
const handleLogin = async () => {
await signIn({
username: "[email protected]",
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 lintCompatibility
- 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.
