React Integration

Use Clossir in React with TanStack Query hooks and wallet management
View as Markdown

@signum-tech/sdk-react is a companion package that wraps the Clossir SDK with TanStack Query hooks and a wallet management hook for React applications.

Installation

Install
$npm install @signum-tech/sdk @signum-tech/sdk-react @tanstack/react-query

Peer dependencies: React 18+, @tanstack/react-query 5+, @signum-tech/sdk 0.3.1+.

SignumProvider

Wrap your app with SignumProvider to make the SDK client available to all hooks:

app.tsx
1import { SignumProvider } from "@signum-tech/sdk-react";
2
3function App() {
4 return (
5 <SignumProvider
6 options={{
7 serverURL: process.env.NEXT_PUBLIC_SIGNUM_API_URL,
8 bearerAuth: getAccessToken,
9 }}
10 >
11 <YourApp />
12 </SignumProvider>
13 );
14}

SignumProvider creates a client instance (memoized on options) and wraps your app in a TanStack QueryClientProvider. If you already have a QueryClient, pass it via the queryClient prop to share it.

The default QueryClient is configured with staleTime: 30s and retry: 1.

useSignumQuery

Type-safe queries with the SDK client auto-injected from context:

use-query.tsx
1import { useSignumQuery } from "@signum-tech/sdk-react";
2
3function PartyList() {
4 const { data, isLoading, error } = useSignumQuery({
5 queryKey: ["parties"],
6 queryFn: (client) => client.accounts.getAccounts({}),
7 });
8
9 if (isLoading) return <div>Loading...</div>;
10 if (error) return <div>Error: {error.title}</div>;
11
12 return (
13 <ul>
14 {data.data.map((party) => (
15 <li key={party.id}>{party.displayName}</li>
16 ))}
17 </ul>
18 );
19}

The queryFn receives the SDK client as its first argument — no need to import or instantiate the client yourself.

The error type defaults to SignumProblemDetailsError, so error.title, error.status, and other RFC 9457 fields are available without type casting.

useSignumMutation

Type-safe mutations for write operations:

use-mutation.tsx
1import { useSignumMutation } from "@signum-tech/sdk-react";
2
3function CreateAttestationButton({ partyId }: { partyId: string }) {
4 const mutation = useSignumMutation({
5 mutationFn: (client, variables: { partyId: string; type: string }) =>
6 client.attestations.postAttestations({
7 partyId: variables.partyId,
8 type: variables.type,
9 chainEid: 30101,
10 }),
11 onSuccess: (data) => {
12 console.log("Attestation queued:", data.attestationId);
13 },
14 onError: (error) => {
15 // error is SignumProblemDetailsError by default
16 console.error(error.title, error.detail);
17 },
18 });
19
20 return (
21 <button
22 onClick={() => mutation.mutate({ partyId, type: "accredited_investor" })}
23 disabled={mutation.isPending}
24 >
25 {mutation.isPending ? "Submitting..." : "Create Attestation"}
26 </button>
27 );
28}

The mutationFn receives (client, variables) — the SDK client from context and the variables you pass to mutate().

Polling after a command

Combine useSignumMutation with useSignumQuery and refetchInterval to poll for command completion:

poll-after-command.tsx
1import { useState } from "react";
2import { useSignumMutation, useSignumQuery } from "@signum-tech/sdk-react";
3
4function TransferFlow({ payload }: { payload: TransferPayload }) {
5 const [transferId, setTransferId] = useState<string | null>(null);
6
7 const mutation = useSignumMutation({
8 mutationFn: (client, vars: TransferPayload) =>
9 client.transfers.postTransfers(vars),
10 onSuccess: (data) => setTransferId(data.transferId),
11 });
12
13 const status = useSignumQuery({
14 queryKey: ["transfer", transferId],
15 queryFn: (client) =>
16 client.transfers.getTransfersByTransferId({
17 transferId: transferId!,
18 }),
19 enabled: !!transferId,
20 refetchInterval: (query) =>
21 query.state.data?.status === "pending" ? 2000 : false,
22 });
23
24 if (status.data?.status === "completed") {
25 return <div>Transfer complete!</div>;
26 }
27
28 return (
29 <button
30 onClick={() => mutation.mutate(payload)}
31 disabled={mutation.isPending}
32 >
33 Transfer
34 </button>
35 );
36}

useSignumWallet

The useSignumWallet hook manages wallet state across two modes:

  • Managed — Privy-provisioned wallets with Biconomy gasless UX (no user key management)
  • External — Bring-your-own wallet via EIP-712 signature binding (e.g., MetaMask, WalletConnect)
wallet-management.tsx
1import { useSignumWallet } from "@signum-tech/sdk-react";
2
3function WalletManager() {
4 const {
5 walletMode,
6 setWalletMode,
7 wallets,
8 isLoading,
9 linkExternalWallet,
10 createManagedWallet,
11 isLinking,
12 } = useSignumWallet();
13
14 return (
15 <div>
16 <div>
17 <button onClick={() => setWalletMode("managed")}>Managed</button>
18 <button onClick={() => setWalletMode("external")}>External</button>
19 </div>
20
21 <p>Mode: {walletMode}</p>
22
23 {walletMode === "managed" ? (
24 <button onClick={() => createManagedWallet()} disabled={isLoading}>
25 Create Managed Wallet
26 </button>
27 ) : (
28 <button
29 onClick={() =>
30 linkExternalWallet(userAddress, async (message) => {
31 // signFn — delegate to your wallet provider (wagmi, ethers, etc.)
32 return await signer.signMessage(message);
33 })
34 }
35 disabled={isLinking}
36 >
37 Link External Wallet
38 </button>
39 )}
40
41 <h3>Wallets</h3>
42 <ul>
43 {wallets.map((w) => (
44 <li key={w.address}>
45 {w.address} {w.isPrimary && "(primary)"}
46 </li>
47 ))}
48 </ul>
49 </div>
50 );
51}

External wallet linking flow

When using the external wallet mode, linkExternalWallet performs a nonce-sign-link flow:

  1. Requests a nonce from the Clossir API
  2. Calls your signFn with the nonce message
  3. Submits the signature to bind the wallet to the user’s account

The signFn is injected by you — use whatever wallet library your app already has (wagmi useSignMessage, ethers.js Signer.signMessage, etc.).

Re-exports

@signum-tech/sdk-react re-exports commonly used types from @signum-tech/sdk so you don’t need to import from both packages:

re-exports.ts
1import type {
2 SDKOptions,
3 SignumProblemDetailsError,
4} from "@signum-tech/sdk-react";
5import { operations, components } from "@signum-tech/sdk-react";

Available re-exports:

  • SDKOptions — client configuration type
  • SignumProblemDetailsError — the typed error class
  • operations — all request/response types per endpoint
  • components — shared schema types

Next steps