@keyban/sdk-react
Version:
Keyban SDK React simplifies the integration of Keyban's MPC wallet in React apps with TypeScript support, flexible storage, and Ethereum blockchain integration.
856 lines (675 loc) • 19.2 kB
Markdown
# 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
```bash
npm install @keyban/sdk-react @keyban/sdk-base @keyban/types
```
## Quick Start
```tsx
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
```tsx
import { KeybanProvider, Network } from "@keyban/sdk-react";
<KeybanProvider appId="your-app-id" network={Network.PolygonAmoy}>
{/* Your app */}
</KeybanProvider>
```
### With Custom Client Share Provider
```tsx
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:
```tsx
<Suspense fallback={<LoadingSpinner />}>
<ComponentsUsingKeybanHooks />
</Suspense>
```
## Authentication
### useKeybanAuth Hook
Access authentication state and methods.
```tsx
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
```tsx
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
```tsx
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
```tsx
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
```tsx
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
```tsx
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
```tsx
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
```tsx
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
```tsx
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
```tsx
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)
```tsx
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
```tsx
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
```tsx
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
```tsx
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
```tsx
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
```tsx
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
```tsx
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
```tsx
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
```tsx
import { useFormattedBalance } from "@keyban/sdk-react";
function TokenBalance({ balance, token }) {
const formatted = useFormattedBalance(balance, token);
return <span>{formatted}</span>;
}
```
### FormattedBalance Component
```tsx
import { FormattedBalance } from "@keyban/sdk-react";
function Balance({ balance, token }) {
return <FormattedBalance balance={balance} token={token} />;
}
```
## Application Info
```tsx
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:
```tsx
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:
```tsx
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:
```typescript
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:
```typescript
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
```bash
# 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
```tsx
// Next.js example
import dynamic from 'next/dynamic';
const WalletComponent = dynamic(
() => import('./WalletComponent'),
{ ssr: false }
);
```
## Related Packages
- **[@keyban/types](https://docs.keyban.io/api/types/)** - Shared TypeScript types and Zod schemas
- **[@keyban/sdk-base](https://docs.keyban.io/api/sdk-base/)** - Core JavaScript SDK
- **[API Documentation](https://docs.keyban.io/api/sdk-react/)** - Full TypeDoc reference
## License
See the [main repository](https://github.com/keyban-io/dap) for license information.