> ## Documentation Index
> Fetch the complete documentation index at: https://docs.socket.fi/llms.txt
> Use this file to discover all available pages before exploring further.

# React Transaction Flow and Error Handling with SocketFi

> A complete React transaction flow with form validation, full status lifecycle UI, error handling, and a reusable service layer for SocketFi.

Every state-changing blockchain operation in SocketFi goes through `requestTransaction()`, which launches a hosted approval screen where your user reviews and signs the action with their passkey. This example builds a complete token transfer flow — from a validated form through the full `idle → loading → awaiting approval → confirmed / failed` lifecycle — and shows you how to structure a service layer so your components stay clean.

## Project structure

```text theme={null}
src/
├── socketfi/
│   └── client.ts                  # SDK singleton
├── services/
│   └── transactionService.ts      # Wraps requestTransaction() calls
├── components/
│   ├── TransferForm.tsx            # Transfer form with validation
│   └── TransactionButton.tsx       # Button with full status lifecycle
└── pages/
    └── WalletPage.tsx              # Wallet page: balance + send form
```

## Transaction status lifecycle

Every transaction in your UI should move through these states explicitly. Keeping them in a single discriminated union makes rendering straightforward and prevents impossible states.

```typescript theme={null}
type TxStatus =
  | { type: "idle" }
  | { type: "loading" }
  | { type: "awaiting_approval" }
  | { type: "confirmed"; hash: string }
  | { type: "failed"; message: string };
```

```text theme={null}
idle
  ↓  user submits form
loading  (validating inputs, calling requestTransaction)
  ↓  SDK opens hosted approval screen
awaiting_approval  (user is reviewing on the SocketFi screen)
  ↓  user approves with passkey
confirmed  ✓  (transactionHash returned)
  — or —
failed  ✗  (user rejected or contract error)
```

## Step-by-step files

<Steps>
  ### transactionService.ts — Service layer

  Abstract all `requestTransaction()` calls into a service module. Components import domain-level functions (`transfer`, `stake`, `vote`) rather than calling the SDK directly.

  ```typescript src/services/transactionService.ts theme={null}
  import { socketfi } from "../socketfi/client";

  export interface TransferParams {
    contractId: string;
    from: string;
    to: string;
    amount: string;
  }

  export interface TxResult {
    success: boolean;
    transactionHash: string;
  }

  export async function transfer(params: TransferParams): Promise<TxResult> {
    const result = await socketfi.requestTransaction({
      contractId: params.contractId,
      method: "transfer",
      args: [params.from, params.to, params.amount],
    });
    return result as TxResult;
  }

  // Example: staking contract
  export async function stake(contractId: string, amount: string): Promise<TxResult> {
    const result = await socketfi.requestTransaction({
      contractId,
      method: "stake",
      args: [amount],
    });
    return result as TxResult;
  }

  // Example: governance contract
  export async function vote(
    contractId: string,
    proposalId: string,
    support: boolean
  ): Promise<TxResult> {
    const result = await socketfi.requestTransaction({
      contractId,
      method: "vote",
      args: [proposalId, support],
    });
    return result as TxResult;
  }
  ```

  <Tip>
    A service layer is the recommended pattern. It lets you update SDK call signatures in one place and keeps your components free of blockchain-specific logic.
  </Tip>

  ### TransferForm.tsx — Form with validation

  Validate all inputs before calling the service — never rely solely on the contract to catch bad data.

  ```typescript src/components/TransferForm.tsx theme={null}
  import { useState } from "react";
  import { transfer } from "../services/transactionService";

  type TxStatus =
    | { type: "idle" }
    | { type: "loading" }
    | { type: "awaiting_approval" }
    | { type: "confirmed"; hash: string }
    | { type: "failed"; message: string };

  interface TransferFormProps {
    walletAddress: string;
    tokenContractId: string;
  }

  export function TransferForm({ walletAddress, tokenContractId }: TransferFormProps) {
    const [recipient, setRecipient] = useState("");
    const [amount, setAmount] = useState("");
    const [status, setStatus] = useState<TxStatus>({ type: "idle" });

    const validate = (): string | null => {
      if (!recipient.trim()) return "Recipient address is required.";
      if (!amount.trim() || isNaN(Number(amount)) || Number(amount) <= 0) {
        return "Enter a valid positive amount.";
      }
      return null;
    };

    const handleSubmit = async (e: React.FormEvent) => {
      e.preventDefault();

      const validationError = validate();
      if (validationError) {
        setStatus({ type: "failed", message: validationError });
        return;
      }

      try {
        setStatus({ type: "loading" });

        // Give feedback that we're waiting for the user on the approval screen
        setTimeout(() => {
          setStatus((prev) =>
            prev.type === "loading" ? { type: "awaiting_approval" } : prev
          );
        }, 800);

        const result = await transfer({
          contractId: tokenContractId,
          from: walletAddress,
          to: recipient,
          amount,
        });

        setStatus({ type: "confirmed", hash: result.transactionHash });
        setRecipient("");
        setAmount("");
      } catch (err: unknown) {
        const code = (err as { code?: string })?.code;
        const message = (err as { message?: string })?.message ?? "Transaction failed.";

        if (code === "TRANSACTION_REJECTED" || code === "USER_CANCELLED") {
          setStatus({ type: "failed", message: "Transaction cancelled." });
        } else if (code === "POLICY_VIOLATION") {
          setStatus({ type: "failed", message: "This action was blocked by your wallet policy." });
        } else {
          setStatus({ type: "failed", message });
        }
      }
    };

    const isSubmitting =
      status.type === "loading" || status.type === "awaiting_approval";

    return (
      <form onSubmit={handleSubmit}>
        <label>
          Recipient address
          <input
            type="text"
            placeholder="CDXXX…"
            value={recipient}
            onChange={(e) => setRecipient(e.target.value)}
            disabled={isSubmitting}
          />
        </label>

        <label>
          Amount
          <input
            type="text"
            placeholder="0.00"
            value={amount}
            onChange={(e) => setAmount(e.target.value)}
            disabled={isSubmitting}
          />
        </label>

        <TransactionButton status={status} />

        {status.type === "confirmed" && (
          <p>
            ✓ Sent! Transaction hash:{" "}
            <code>{status.hash}</code>
          </p>
        )}

        {status.type === "failed" && (
          <p role="alert">✗ {status.message}</p>
        )}
      </form>
    );
  }
  ```

  ### TransactionButton.tsx — Status-aware button

  Extract the button into its own component so the loading and approval states are always displayed consistently across your app.

  ```typescript src/components/TransactionButton.tsx theme={null}
  type TxStatus =
    | { type: "idle" }
    | { type: "loading" }
    | { type: "awaiting_approval" }
    | { type: "confirmed"; hash: string }
    | { type: "failed"; message: string };

  interface TransactionButtonProps {
    status: TxStatus;
    label?: string;
  }

  const STATUS_LABELS: Record<TxStatus["type"], string> = {
    idle: "Send",
    loading: "Preparing…",
    awaiting_approval: "Approve in SocketFi…",
    confirmed: "Sent ✓",
    failed: "Try again",
  };

  export function TransactionButton({
    status,
    label,
  }: TransactionButtonProps) {
    const isDisabled =
      status.type === "loading" || status.type === "awaiting_approval";

    return (
      <button type="submit" disabled={isDisabled}>
        {label ?? STATUS_LABELS[status.type]}
      </button>
    );
  }
  ```

  <Note>
    Disable the button during `loading` and `awaiting_approval` to prevent duplicate transaction submissions. Always re-enable it after `confirmed` or `failed` so users can retry.
  </Note>

  ### WalletPage.tsx — Full wallet page

  Combine a balance read with the transfer form. Use `readContract()` to fetch the balance, and invalidate the query after a successful transaction so the display refreshes.

  ```typescript src/pages/WalletPage.tsx theme={null}
  import { useQuery, useQueryClient } from "@tanstack/react-query";
  import { socketfi } from "../socketfi/client";
  import { TransferForm } from "../components/TransferForm";
  import { useAuth } from "../auth/useAuth";

  const TOKEN_CONTRACT_ID = import.meta.env.VITE_TOKEN_CONTRACT_ID;

  export default function WalletPage() {
    const { session } = useAuth();
    const queryClient = useQueryClient();
    const walletAddress = session?.userProfile?.wallet ?? "";

    const {
      data: balance,
      isLoading,
      isError,
    } = useQuery({
      queryKey: ["balance", walletAddress],
      queryFn: async () => {
        const result = await socketfi.readContract({
          contractId: TOKEN_CONTRACT_ID,
          method: "balance",
          args: [walletAddress],
        });
        return result as string;
      },
      enabled: !!walletAddress,
    });

    // Refresh balance after any transaction on this page
    const handleTransactionSuccess = () => {
      queryClient.invalidateQueries({ queryKey: ["balance", walletAddress] });
    };

    return (
      <main>
        <h1>Wallet</h1>

        <section>
          <h2>Address</h2>
          <code>{walletAddress}</code>
        </section>

        <section>
          <h2>Balance</h2>
          {isLoading && <p>Loading balance…</p>}
          {isError && <p>Could not load balance.</p>}
          {balance !== undefined && <p>{balance} tokens</p>}
        </section>

        <section>
          <h2>Send tokens</h2>
          <TransferForm
            walletAddress={walletAddress}
            tokenContractId={TOKEN_CONTRACT_ID}
          />
        </section>
      </main>
    );
  }
  ```
</Steps>

## Full transaction lifecycle

When you call `requestTransaction()`, SocketFi automatically opens the hosted approval experience. Your user sees the contract address, method name, arguments, and fees before signing with their passkey.

```text theme={null}
User submits the transfer form
       ↓
Client-side validation passes
       ↓
socketfi.requestTransaction() called  →  status: "loading"
       ↓
Hosted approval screen opens           →  status: "awaiting_approval"
       ↓
User approves with passkey
       ↓
Transaction executed on Soroban        →  status: "confirmed"
       ↓
{ success: true, transactionHash: "abc123…" }
```

If the user declines or an error occurs:

```text theme={null}
User taps "Reject" on approval screen
       ↓
SDK throws { code: "TRANSACTION_REJECTED" }  →  status: "failed"
       ↓
Display retry option, preserve form values
```

## Error handling reference

<Accordion title="TRANSACTION_REJECTED / USER_CANCELLED">
  The user closed the approval screen or tapped reject. This is normal user behaviour — don't log it as an application error. Show a friendly message and allow retry.
</Accordion>

<Accordion title="POLICY_VIOLATION">
  The transaction was blocked by a wallet spending policy — for example, a per-transaction limit was exceeded. Explain the policy to the user and suggest an alternative amount or contact support.
</Accordion>

<Accordion title="CONTRACT_EXECUTION_FAILED">
  The Soroban contract returned an error. Check that your `contractId`, `method`, and `args` are correct and that the contract's state allows the operation (e.g. sufficient balance).
</Accordion>

<Accordion title="Network / RPC errors">
  Transient connectivity issues. Implement retry logic with exponential back-off and surface a "Try again" prompt to the user.
</Accordion>

## Additional contract invocation examples

<CodeGroup>
  ```typescript Deposit theme={null}
  await socketfi.requestTransaction({
    contractId: VAULT_CONTRACT_ID,
    method: "deposit",
    args: [amount],
  });
  ```

  ```typescript Governance vote theme={null}
  await socketfi.requestTransaction({
    contractId: GOVERNANCE_CONTRACT_ID,
    method: "vote",
    args: [proposalId, true],
  });
  ```

  ```typescript Staking theme={null}
  await socketfi.requestTransaction({
    contractId: STAKING_CONTRACT_ID,
    method: "stake",
    args: [amount],
  });
  ```
</CodeGroup>

## Production recommendations

<CardGroup cols={2}>
  <Card title="Validate before submitting" icon="circle-check">
    Always validate recipient address format and amount before calling `requestTransaction()`. Contract validation is a safety net, not a substitute.
  </Card>

  <Card title="Disable during flight" icon="ban">
    Keep the submit button disabled while `status` is `loading` or `awaiting_approval` to prevent duplicate transactions.
  </Card>

  <Card title="Invalidate cached data" icon="arrows-rotate">
    After a confirmed transaction, call `queryClient.invalidateQueries()` on balance and activity queries so your UI reflects the new chain state.
  </Card>

  <Card title="Store transaction hashes" icon="database">
    Persist the `transactionHash` in your backend with a timestamp and status. It's essential for transaction history, support, and analytics.
  </Card>
</CardGroup>

<Warning>
  Never call `requestTransaction()` in response to a programmatic trigger without explicit user intent (e.g. a button click). Every transaction requires an intentional user action.
</Warning>
