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

# Flow: Building a Payment UI

> Build a multi-chain payment widget with Fireblocks Flow — wallet connection, token selection, quoting, signing, and status tracking.

<Note>
  This is an enterprise-only feature. Please [contact us](https://www.dynamic.xyz/book-a-call) to enable.
</Note>

Build a payment UI on top of [Fireblocks Flow](/docs/overview/fireblocks-flow) using the JavaScript SDK. Each section maps to a screen in the payment widget, with full component examples you can adapt.

<Info>
  The examples below use React for readability, but the Dynamic JavaScript SDK is framework-agnostic. The core functions (`attachFlowSource`, `getFlowQuote`, `submitFlowTransaction`, etc.) are plain JavaScript imported from `@dynamic-labs-sdk/client` and work with any framework — Vue, Svelte, Angular, or vanilla JS. The React hooks (e.g. `useGetTokenBalances`, `useGetWalletAccounts`) are convenience wrappers from `@dynamic-labs-sdk/react-hooks`; in other frameworks, call the equivalent client functions directly.
</Info>

<Warning>
  Do **not** mix the Dynamic React SDK (`@dynamic-labs/sdk-react-core`) with the JavaScript SDK (`@dynamic-labs-sdk/client`). They are separate SDKs with different architectures. Fireblocks Flow is only available in the JavaScript SDK. If your project currently uses the React SDK, you will need to migrate to the JavaScript SDK to use Flow.
</Warning>

Complete the [Getting Started](/docs/javascript/reference/flow-getting-started) setup before following this guide.

## Flow overview

<img src="https://mintcdn.com/dynamic-docs/j_VtW1XmnQpJlBpB/images/flow/flow-ui-overview.png?fit=max&auto=format&n=j_VtW1XmnQpJlBpB&q=85&s=ddabbf677aa31442df997b094f81d27a" alt="Flow UI overview — wallet and exchange payment paths" width="2200" height="752" data-path="images/flow/flow-ui-overview.png" />

## 1. Load the flow

Your backend [creates the flow](/docs/javascript/reference/flow-getting-started#5-create-a-flow-from-your-backend) and passes the `flowId` to the frontend. Always load the current state before choosing a screen. Do not replay `attachFlowSource` or another mutation just because the page reloaded:

```javascript theme={"system"}
import { getFlow } from "@dynamic-labs-sdk/client";

const STATUS_ONLY_STATES = new Set(["broadcasted", "source_confirmed"]);
const NEW_FLOW_REQUIRED_STATES = new Set(["cancelled", "expired", "failed"]);

const flow = await getFlow({ flowId });

if (STATUS_ONLY_STATES.has(flow.executionState)) {
  // Show the status screen and keep polling execution/settlement.
} else if (NEW_FLOW_REQUIRED_STATES.has(flow.executionState)) {
  // Show the final state and offer to create a new flow.
} else {
  // Resume the screen for initiated, source_attached, quoted, or signing.
}
```

<Tip>
  Read `flowId` from a URL parameter (e.g. `?flowId=...`) so users can resume an in-progress flow after navigating away. Route by `executionState`; never route a `broadcasted` or `source_confirmed` flow back to source selection.
</Tip>

## 2. Choose a payment source

Flow supports two source types: **wallet** (user signs from a Web3 wallet) and **exchange** (user pays via an exchange like Coinbase). Present both options and route to the matching path.

```jsx theme={"system"}
const SelectSourceView = ({ onSelectWallet, onSelectExchange }) => (
  <div>
    <h3>Choose payment method</h3>

    <button onClick={onSelectWallet}>
      <span>Pay with Wallet</span>
      <span>Sign the transaction with your connected Web3 wallet</span>
    </button>

    <button onClick={onSelectExchange}>
      <span>Pay with Exchange</span>
      <span>Complete the payment via your Coinbase account</span>
    </button>
  </div>
);
```

### Exchange path

For exchange sources, attach the source and open the exchange URL. After the user completes payment on the exchange, you call `broadcastFlow` to notify the backend (see [step 7](#7-confirm-exchange-transfer)).

```javascript theme={"system"}
import { attachFlowSource } from "@dynamic-labs-sdk/client";

const { flow } = await attachFlowSource({
  flowId,
  sourceType: "exchange",
  exchangeProvider: "coinbase",
});

// The exchange buy URL is in the flow's exchangeSource metadata
const buyUrl = flow.exchangeSource?.metadata?.url;
window.open(buyUrl, "_blank", "noopener,noreferrer");
```

## 3. Connect a wallet

With multiple [chain extensions](/docs/javascript/reference/flow-getting-started#3-add-chain-extensions-to-the-client) enabled, the SDK detects wallet providers on every chain. The same wallet (e.g. MetaMask) may appear multiple times — once per supported chain. Group providers by `groupKey` so each wallet shows as one entry in your UI.

### Wallet provider list

Use `useGetAvailableWalletProvidersData` to list detected wallets, and `connectWithWalletProvider` to connect:

```jsx theme={"system"}
import {
  useGetAvailableWalletProvidersData,
  useConnectWithWalletProvider,
} from "@dynamic-labs-sdk/react-hooks";

const WalletProviderList = ({ onConnected }) => {
  const { data: providers = [] } = useGetAvailableWalletProvidersData();
  const { mutate: connectWithWalletProvider } = useConnectWithWalletProvider();

  // Group providers by wallet (same wallet across chains = one entry)
  const groups = {};
  for (const p of providers) {
    if (!groups[p.groupKey]) groups[p.groupKey] = [];
    groups[p.groupKey].push(p);
  }

  const connect = (provider) => {
    connectWithWalletProvider(
      { walletProviderKey: provider.key },
      { onSuccess: () => onConnected() },
    );
  };

  const handleGroupClick = (groupKey) => {
    const group = groups[groupKey];
    if (group.length > 1) {
      // Wallet supports multiple chains — show a chain picker (see below)
      showChainPicker(group);
      return;
    }
    // Single chain — connect directly
    connect(group[0]);
  };

  return (
    <div>
      {Object.entries(groups)
        .sort(([a], [b]) => a.localeCompare(b))
        .map(([groupKey, ps]) => (
          <button key={groupKey} onClick={() => handleGroupClick(groupKey)}>
            {ps[0].metadata.icon && (
              <img src={ps[0].metadata.icon} alt="" width={28} height={28} />
            )}
            <span>{ps[0].metadata.displayName}</span>
          </button>
        ))}

      {providers.length === 0 && <p>No wallet extensions detected.</p>}
    </div>
  );
};
```

### Chain picker

When a wallet supports multiple chains (e.g. MetaMask on EVM, or Phantom on both SOL and EVM), show a chain picker. Each entry in the group has a `chain` property (`"EVM"`, `"SOL"`, `"BTC"`, `"SUI"`, `"TRON"`) and a unique `key`:

```jsx theme={"system"}
const ChainPicker = ({ providers, onConnect }) => (
  <div>
    <p>Select a chain</p>
    {providers.map((p) => (
      <button key={p.key} onClick={() => onConnect(p)}>
        {p.chain}
      </button>
    ))}
  </div>
);
```

### WalletConnect support

If you added WalletConnect extensions in your [setup](/docs/javascript/reference/flow-getting-started#3-add-chain-extensions-to-the-client), you can let users scan a QR code to connect mobile wallets. The SDK provides `connectWithWalletConnectEvm` and `connectWithWalletConnectSolana`:

```jsx theme={"system"}
import { connectWithWalletConnectEvm } from "@dynamic-labs-sdk/evm/wallet-connect";
import { connectWithWalletConnectSolana } from "@dynamic-labs-sdk/solana/wallet-connect";

const connectWalletConnect = async (chain) => {
  const connect =
    chain === "EVM" ? connectWithWalletConnectEvm : connectWithWalletConnectSolana;

  // Returns a pairing URI and an approval promise
  const { uri, approval } = await connect();

  // Render `uri` as a QR code (e.g. using the `qrcode` npm package)
  const QRCode = await import("qrcode");
  const dataUrl = await QRCode.toDataURL(uri, { width: 200, margin: 2 });
  // Display dataUrl in an <img> tag

  // Wait for the user to scan and approve
  await approval();
  // Wallet is now connected
};
```

## 4. Select a network and token

Once a wallet is connected, show the wallet's address, let the user pick a network (if applicable), and list their token balances. Use `useGetWalletAccounts` to get the connected wallet account.

### Network switching

EVM wallets can operate on multiple networks (Ethereum, Base, Arbitrum, etc.). Use `useNetworksData` to list available networks for the wallet's chain, and `switchActiveNetwork` to change:

```jsx theme={"system"}
import {
  useGetActiveNetworkData,
  useNetworksData,
  useSwitchActiveNetwork,
  useGetWalletAccounts,
} from "@dynamic-labs-sdk/react-hooks";

const NetworkAndTokenView = ({ flow, onQuoted }) => {
  const { data: walletAccounts = [] } = useGetWalletAccounts();
  const walletAccount = walletAccounts[0];

  const { data: activeNetworkResult } =
    useGetActiveNetworkData({ walletAccount });
  const activeNetwork = activeNetworkResult?.networkData;

  const { mutate: switchActiveNetwork } = useSwitchActiveNetwork();

  // Filter to networks matching the wallet's chain
  const { data: allNetworks = [] } = useNetworksData();
  const walletNetworks = allNetworks.filter(
    (n) => n.chain === walletAccount?.chain
  );

  const handleSwitch = (networkId) => {
    switchActiveNetwork({ networkId, walletAccount });
  };

  return (
    <div>
      {/* Wallet address */}
      <p>
        {walletAccount?.address?.slice(0, 6)}...
        {walletAccount?.address?.slice(-4)}
      </p>

      {/* Network switcher — only show if multiple networks available */}
      {walletNetworks.length > 1 && (
        <div>
          <span>Network</span>
          {walletNetworks.map((network) => (
            <button
              key={network.networkId}
              onClick={() => handleSwitch(String(network.networkId))}
              aria-pressed={network.networkId === activeNetwork?.networkId}
            >
              {network.iconUrl && <img src={network.iconUrl} alt="" width={20} />}
              {network.displayName}
            </button>
          ))}
        </div>
      )}

      {/* Token list — see below */}
      {activeNetwork && (
        <TokenList
          walletAccount={walletAccount}
          networkId={activeNetwork.networkId}
          flow={flow}
          onQuoted={onQuoted}
        />
      )}
    </div>
  );
};
```

### Token list with auto-attach and auto-quote

Use `useGetTokenBalances` to fetch the wallet's tokens on the active network. When the user picks a token, attach the wallet as the flow source and fetch a quote in one step:

```jsx theme={"system"}
import {
  useAttachFlowSource,
  useGetFlowQuote,
  useGetTokenBalances,
} from "@dynamic-labs-sdk/react-hooks";

const TokenList = ({ walletAccount, networkId, flow, onQuoted }) => {
  const {
    data: tokens = [],
    isLoading,
    refetch,
  } = useGetTokenBalances({
    walletAccount,
    networkId,
    includeNative: true,
    includePrices: true,
    filterSpamTokens: true,
  });

  const { mutate: attachFlowSource } = useAttachFlowSource();
  const { mutate: getFlowQuote } = useGetFlowQuote();

  const handleSelect = (token) => {
    // Attach the wallet as source, then fetch a quote
    attachFlowSource(
      {
        flowId: flow.id,
        fromAddress: walletAccount.address,
        fromChainId: String(networkId),
        fromChainName: walletAccount.chain,
        sourceType: "wallet",
      },
      {
        onSuccess: () => {
          getFlowQuote(
            {
              flowId: flow.id,
              fromTokenAddress: token.address,
              fromChainId: token.networkId?.toString(),
            },
            {
              onSuccess: (quotedFlow) =>
                onQuoted({ quotedFlow, walletAccount, fromTokenAddress: token.address }),
            },
          );
        },
      },
    );
  };

  if (isLoading) return <p>Loading balances…</p>;

  if (tokens.length === 0) {
    return (
      <div>
        <p>No tokens found</p>
        <button onClick={() => refetch()}>Refresh</button>
      </div>
    );
  }

  return (
    <div>
      {tokens.map((token) => (
        <button
          key={`${token.address}-${token.symbol}`}
          onClick={() => handleSelect(token)}
        >
          {token.logoURI && <img src={token.logoURI} alt="" width={32} />}
          <div>
            <span>{token.symbol}</span>
            <span>{token.name}</span>
          </div>
          <div>
            <span>{token.balance}</span>
            {token.marketValue !== undefined && (
              <span>${token.marketValue.toFixed(2)}</span>
            )}
          </div>
        </button>
      ))}
    </div>
  );
};
```

<Note>
  For Solana, pass `networkId: 101`. For EVM, use the chain ID (e.g. `8453` for Base). Set `includeNative: true` to include ETH/SOL in the list.
</Note>

## 5. Review the quote

Display the quote so the user can review amounts, fees, and estimated time before signing. Quotes expire after 60 seconds — offer a refresh button:

```jsx theme={"system"}
import { useGetFlowQuote } from "@dynamic-labs-sdk/react-hooks";

const ReviewQuoteView = ({ flow, fromTokenAddress, fromChainId, onConfirm }) => {
  const { mutate: requote, data: quotedFlow = flow, isPending } = useGetFlowQuote();
  const quote = quotedFlow.quote;

  const refreshQuote = () =>
    requote({ flowId: flow.id, fromTokenAddress, fromChainId });

  if (!quote) return <p>Loading quote…</p>;

  return (
    <div>
      <h3>Review quote</h3>

      <div>
        <div>
          <span>You send</span>
          <span>{quote.fromAmount}</span>
        </div>
        <div>
          <span>You receive</span>
          <span>{quote.toAmount}</span>
        </div>
        {quote.fees?.totalFeeUsd && (
          <div>
            <span>Estimated fees</span>
            <span>${quote.fees.totalFeeUsd}</span>
          </div>
        )}
        {quote.estimatedTimeSec && (
          <div>
            <span>Estimated time</span>
            <span>~{Math.ceil(quote.estimatedTimeSec / 60)} min</span>
          </div>
        )}
      </div>

      <p>Quote expires at {new Date(quote.expiresAt).toLocaleTimeString()}</p>

      <button onClick={() => onConfirm(quotedFlow)}>Confirm & Sign</button>
      <button onClick={refreshQuote} disabled={isPending}>
        {isPending ? "Refreshing…" : "Refresh Quote"}
      </button>
    </div>
  );
};
```

## 6. Sign and submit

[`submitFlowTransaction`](/docs/javascript/reference/client/submit-flow-transaction) handles the full signing pipeline: prepare → approve token spend (if needed) → sign transaction → broadcast. Use the `onStepChange` callback to show a two-step progress indicator:

```jsx theme={"system"}
import { useSubmitFlowTransaction } from "@dynamic-labs-sdk/react-hooks";

const STEP_LABELS = {
  approval: "Approve token spend",
  transaction: "Sign transaction",
};

const SubmitView = ({ flow, walletAccount, onSubmitted, onRequote }) => {
  const [currentStep, setCurrentStep] = useState(null);
  const hasStartedRef = useRef(false);

  const {
    mutate: submitFlow,
    isPending,
    error,
  } = useSubmitFlowTransaction({
    mutateParams: {
      onSuccess: (completedFlow) => onSubmitted(completedFlow),
      onError: (err) => {
        const msg = err instanceof Error ? err.message : "Transaction failed";
        const isRejected =
          msg.toLowerCase().includes("reject") ||
          msg.toLowerCase().includes("denied");

        if (isRejected) {
          // User rejected in wallet — show retry option
        }
      },
    },
  });

  useEffect(() => {
    if (hasStartedRef.current) return;
    hasStartedRef.current = true;

    submitFlow({
      flowId: flow.id,
      walletAccount,
      onStepChange: setCurrentStep,
    });
  }, []);

  const steps = ["approval", "transaction"];
  const currentIndex = currentStep ? steps.indexOf(currentStep) : -1;

  // Detect quote expiry errors
  const isQuoteExpired =
    error?.message?.toLowerCase().includes("expired") ||
    error?.message?.toLowerCase().includes("quote");

  return (
    <div>
      {isPending && (
        <div>
          <h3>Signing transaction</h3>
          <p>Please confirm the prompts in your wallet</p>
        </div>
      )}

      {/* Two-step progress */}
      {steps.map((step, i) => (
        <div key={step}>
          <span>
            {currentIndex > i ? "✓" : currentStep === step ? "…" : i + 1}
          </span>
          <span>{STEP_LABELS[step]}</span>
        </div>
      ))}

      {/* Error recovery */}
      {error && !isPending && (
        <div>
          <p>{error.message}</p>
          {isQuoteExpired ? (
            <button onClick={onRequote}>Get a new quote</button>
          ) : (
            <button
              onClick={() => {
                hasStartedRef.current = false;
                submitFlow({
                  flowId: flow.id,
                  walletAccount,
                  onStepChange: setCurrentStep,
                });
              }}
            >
              Try again
            </button>
          )}
        </div>
      )}
    </div>
  );
};
```

<Warning>
  Quotes expire after 60 seconds. If signing fails with a quote expiry error, navigate the user back to the quote view (step 5) to fetch a fresh quote.
</Warning>

## 7. Confirm exchange transfer

For exchange sources, show a confirmation screen after the user completes payment on the exchange. Call [`broadcastFlow`](/docs/javascript/reference/client/broadcast-flow) to notify the backend:

```jsx theme={"system"}
import { useBroadcastFlow } from "@dynamic-labs-sdk/react-hooks";

const ExchangeConfirmView = ({ flow, exchangeUrl, onCompleted }) => {
  const { mutate: broadcast, isPending } = useBroadcastFlow({
    mutateParams: {
      onSuccess: onCompleted,
    },
  });

  return (
    <div>
      <h3>Complete your transfer</h3>
      <p>Finish the payment on the exchange, then confirm below.</p>

      {exchangeUrl && (
        <a href={exchangeUrl} target="_blank" rel="noopener noreferrer">
          Reopen exchange
        </a>
      )}

      <button onClick={() => broadcast({ flowId: flow.id })} disabled={isPending}>
        {isPending ? "Recording payment…" : "I've completed the transfer"}
      </button>
    </div>
  );
};
```

## 8. Track status

After submission or exchange confirmation, poll [`getFlow`](/docs/javascript/reference/client/get-flow) to track the flow through three milestones:

1. **Payment submitted** — transaction broadcasted on-chain
2. **Source confirmed** — on-chain confirmation received
3. **Settlement complete** — funds delivered to the destination

```jsx theme={"system"}
import { useGetFlow } from "@dynamic-labs-sdk/react-hooks";

const shouldStopPolling = (flow) =>
  ["cancelled", "expired", "failed"].includes(flow.executionState) ||
  ["completed", "failed"].includes(flow.settlementState);

const StatusView = ({ flow, onReset }) => {
  const { data: polledFlow } = useGetFlow({
    flowId: flow.id,
    queryParams: {
      initialData: flow,
      refetchInterval: (query) => {
        const data = query.state.data;
        return data && shouldStopPolling(data) ? false : 3000;
      },
    },
  });

  const displayFlow = polledFlow ?? flow;
  const exec = displayFlow.executionState;
  const settle = displayFlow.settlementState;

  const steps = [
    {
      label: "Payment submitted",
      done: ["broadcasted", "source_confirmed"].includes(exec),
    },
    {
      label: "Source confirmed",
      done: exec === "source_confirmed",
    },
    {
      label: "Settlement",
      done: settle === "completed",
      active: exec === "source_confirmed" && settle !== "completed" && settle !== "failed",
      failed: settle === "failed",
    },
  ];

  const isDone = settle === "completed";
  const isFailed = exec === "failed" || settle === "failed";

  return (
    <div>
      <h3>
        {isDone
          ? "Payment complete"
          : isFailed
            ? "Payment failed"
            : "Processing payment…"}
      </h3>

      {steps.map((step, i) => (
        <div key={i}>
          <span>{step.done ? "✓" : step.failed ? "✗" : step.active ? "…" : "○"}</span>
          <span>{step.label}</span>
        </div>
      ))}

      {displayFlow.txHash && <p>Transaction: {displayFlow.txHash}</p>}

      {(isDone || isFailed) && (
        <button onClick={onReset}>Start a new flow</button>
      )}
    </div>
  );
};
```

## Cancellation

Users can cancel at any point before broadcast with [`cancelFlow`](/docs/javascript/reference/client/cancel-flow). After broadcast, the transaction is on-chain and cannot be reversed.

```javascript theme={"system"}
import { cancelFlow } from "@dynamic-labs-sdk/client";

await cancelFlow({ flowId });
```

Show a cancel button on the source selection, wallet connection, quote review, and exchange confirmation screens. Hide it once the transaction has been broadcasted.

## Error handling

See the [error reference](/docs/overview/fireblocks-flow-js-sdk#error-reference) for all errors by step and how to resolve them. Key patterns:

* **Quote failures** (`422`): The response includes a `quoteFailures` array with per-token failure details (`{ chainId, chainName, reason, symbol }`). Show the reason to the user (e.g. "Amount is below the minimum for this token"). Note that `unknown` often means the amount is too low for conversion. Offer a different token or amount.
* **Wallet rejection**: Detect `"reject"` / `"denied"` in the error message. Offer retry or cancel.
* **Quote expiry**: Navigate back to the quote view to refresh.
* **State conflicts** (`409`): Re-fetch with `getFlow` and resume from the current state. Poll `broadcasted` or `source_confirmed` flows instead of replaying mutations. Create a new flow after `cancelled`, `expired`, or `failed`.

## Related

* [Getting Started](/docs/javascript/reference/flow-getting-started) — Chain extensions, dashboard setup, API token
* [JS SDK guide](/docs/overview/fireblocks-flow-js-sdk) — Core SDK functions and full example
* [Error reference](/docs/overview/fireblocks-flow-js-sdk#error-reference) — Errors by step with fixes
* [API guide](/docs/overview/fireblocks-flow-api) — Raw HTTP implementation
