Skip to main content

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:

  1. Ensure KeybanProvider is rendered client-side only
  2. Use dynamic imports with ssr: false for components using Keyban hooks
  3. Wrap data-fetching components in <Suspense> boundaries
// Next.js example
import dynamic from 'next/dynamic';

const WalletComponent = dynamic(
() => import('./WalletComponent'),
{ ssr: false }
);

License

See the main repository for license information.