UNPKG

@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
# 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.