npm package: @keyban/sdk-react
Description: Keyban SDK React simplifies the integration of Keyban's MPC wallet in React apps with TypeScript support, flexible storage, and Ethereum blockchain integration.
Author: Keyban
Latest version: 0.15.0

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