Skip to content

Example

End-to-end wallet deposit with React hooks: connect, pick a token, enter crypto/fiat, submit, handle requires_retry, show the summary.

Requires SwappedConnectProvider, WalletsProvider, and a loaded active session. After a completed payment, call restartSession() before starting again.

1. Providers + session

WalletsProvider owns connect / disconnect. usePrepareWalletTransfer is optional. Pass enabled: Boolean(session) so it waits until a session is loaded.

tsx
import { createSwappedConnectClient } from '@swapped/connect-sdk'
import {
  SwappedConnectProvider,
  WalletsProvider,
  usePrepareWalletTransfer,
  useSession,
} from '@swapped/connect-sdk/react'

const client = createSwappedConnectClient({
  sessionId: 'your-session-id',
})

// Provider does not load the session — do it once at bootstrap
void client.loadSession()

function App() {
  return (
    <SwappedConnectProvider client={client}>
      {/* Required for availability / connect / connections / balances hooks */}
      <WalletsProvider>
        <WalletsFlow />
      </WalletsProvider>
    </SwappedConnectProvider>
  )
}

function WalletsFlow() {
  const { session } = useSession()
  usePrepareWalletTransfer({ enabled: Boolean(session) })
  return <ConnectAndDeposit />
}

2. Availability + connect

useAvailableWallets lists wallets this device can connect — not the connected list (isInstalled, supportsWalletConnect, isDeepLinkConnection, qrScanTarget). After the user picks a wallet:

  • Connect desktopconnect({ transport: 'injected' }) (browser extension).
  • QR — WalletConnect pairing starts in a useEffect (no extra button). pairingUri is the QR value. Some wallets use a browse URL instead (isDeepLinkConnection / getDeepLinkUrl) — that is also a QR, not WalletConnect. wallet.qrScanTarget says whether to scan with the phone camera or the wallet app.
  • Connect mobile — only on a phone (isMobileBrowser()). Opens pairingDeeplinkUrl (same WC pairing) or openDeepLink (browse-in-wallet).

The SDK does not ship a QR component. The demo uses qrcode.react (QRCodeSVG).

tsx
import { useEffect, useState } from 'react'
import { QRCodeSVG } from 'qrcode.react'
import {
  ConnectSdkError,
  ConnectSdkErrorCode,
  isMobileBrowser,
  type AvailableWallet,
} from '@swapped/connect-sdk'
import { useAvailableWallets, useConnectWallet } from '@swapped/connect-sdk/react'

function ConnectAndDeposit() {
  const { wallets, isLoading } = useAvailableWallets()
  const {
    connect,
    isConnecting,
    pairingUri,
    pairingDeeplinkUrl,
    requiresDeepLink,
    supportsWalletConnect,
    getDeepLinkUrl,
    openDeepLink,
    cancelPairing,
  } = useConnectWallet()
  const [selected, setSelected] = useState<AvailableWallet | null>(null)

  // Start WalletConnect as soon as a WC wallet is selected.
  // Cleanup cancels the pairing (WALLET_PAIRING_CANCELLED) — ignore that reject.
  useEffect(() => {
    if (!selected || !supportsWalletConnect(selected.provider)) {
      return
    }

    let cancelled = false
    void connect({
      provider: selected.provider,
      transport: 'walletconnect',
      force: true, // only one WC pairing at a time
    }).catch(error => {
      if (cancelled) return
      if (
        error instanceof ConnectSdkError &&
        error.code === ConnectSdkErrorCode.WALLET_PAIRING_CANCELLED
      ) {
        return
      }
    })

    return () => {
      cancelled = true
      void cancelPairing()
    }
  }, [selected, connect, cancelPairing, supportsWalletConnect])

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

  if (!selected) {
    return (
      <ul>
        {wallets.map(wallet => (
          <li key={wallet.provider}>
            <button type="button" onClick={() => setSelected(wallet)}>
              {wallet.name}
              {wallet.isInstalled ? ' (extension installed)' : ''}
            </button>
          </li>
        ))}
      </ul>
    )
  }

  const connecting = isConnecting(selected.provider)
  const browseUrl = requiresDeepLink(selected.provider)
    ? getDeepLinkUrl(selected.provider)
    : null
  // WC URI, or browse-in-wallet URL when this wallet does not use WC
  const qrValue = pairingUri ?? browseUrl
  const onMobile = isMobileBrowser()
  const scanHint =
    selected.qrScanTarget === 'camera'
      ? 'Scan with your phone camera'
      : `Open ${selected.name} and scan this QR`

  return (
    <div>
      <p>{selected.name}</p>

      <button
        type="button"
        disabled={connecting}
        onClick={() =>
          void connect({
            provider: selected.provider,
            transport: 'injected',
            // If the host cannot see the extension, open a popup instead
            // of failing the injected connect.
            popupIfUnavailable: true,
          })
        }
      >
        {connecting ? 'Connecting…' : 'Connect desktop'}
      </button>

      {qrValue ? <QRCodeSVG value={qrValue} size={180} /> : <p>Starting…</p>}
      {qrValue ? <p>{scanHint}</p> : null}

      {onMobile ? (
        <button
          type="button"
          onClick={() => {
            if (requiresDeepLink(selected.provider)) {
              openDeepLink(selected.provider)
              return
            }
            if (pairingDeeplinkUrl) {
              window.open(pairingDeeplinkUrl, '_blank', 'noopener,noreferrer')
            }
          }}
        >
          Connect mobile
        </button>
      ) : null}
    </div>
  )
}

Leaving the selected wallet (or unmounting) should call cancelPairing() so the in-flight WalletConnect connect() does not stay open. Full options: useConnectWallet.

3. Connections + balances

A connection is one approved wallet instance (walletId) — not a row from useAvailableWallets. The same brand can appear more than once. See useWalletConnections.

isRestoring is true while previous connections are rehydrated — addresses may already exist, but sign / send wait for status === 'connected'. activeWalletId is the instance the provider currently treats as selected (persisted; falls back to the newest connection). Balances are fetched per walletId. isLoading stays true until exchange rates are on the tokens (formatted.fiatValue). Eligibility is supported → has balance → meets min → eligible. See useWalletBalances.

tsx
import {
  useWalletBalances,
  useWalletConnections,
} from '@swapped/connect-sdk/react'

function TokenPicker({
  onPick,
}: {
  onPick: (walletId: string, token: WalletBalance) => void
}) {
  const { isRestoring, activeWalletId } = useWalletConnections({
    sort: 'connectedAt', // newest first
  })
  const { balances, isLoading } = useWalletBalances(activeWalletId, {
    sort: 'balanceDesc', // eligible first, then higher fiat
  })

  if (isRestoring) return <p>Restoring…</p>
  if (!activeWalletId) return <p>No wallet connected</p>
  // Wait for rates so fiat labels are not empty on first paint
  if (isLoading) return <p>Loading balances with rates…</p>

  return (
    <ul>
      {balances.map(token => (
        <li key={`${token.symbol}-${token.network}-${token.tokenAddress}`}>
          <button type="button" onClick={() => onPick(activeWalletId, token)}>
            {token.formatted.balance} {token.symbol}
            {token.formatted.fiatValue ? ` · $${token.formatted.fiatValue}` : ''}
            {/* below_min / unsupported / zero_balance — still listed here */}
            {token.eligible ? '' : ` (${token.ineligibleReason ?? 'unavailable'})`}
          </button>
        </li>
      ))}
    </ul>
  )
}

4. Plan + amount + quote

A plan is the SDK’s answer to: this wallet is sending this token — how does it become the deposit the session expects?

You do not pick the route. You pass walletId + token (network, symbol, tokenAddress). useWalletTransferPlan returns a WalletTransferPlan:

plan.flowMeaning
directThe session already receives this token on this network. The user just sends it.
swapSame chain, wrong token. The SDK swaps into what the session receives.
bridgeWrong chain (and usually wrong token). The SDK bridges into the session’s token/network.
unavailableNo route. This token cannot complete the payment.

plan.source is what the user spends. plan.destination is what lands in the session (symbol, network, address). Swap/bridge also set plan.restrictions and, for bridge, plan.bridgeDestinations. Details: Transfer plan.

useWalletTransferMinAmount is the source crypto minimum (min.crypto) plus optional fiat (min.fiat). Pass minAmount into useDualAmountInput so both fields share that floor. Submit amount (full-precision crypto), not amountDisplay. useWalletTransferQuote stays idle for empty / invalid / unavailable plans (300ms debounce) so invalid keystrokes do not hit the quote API.

tsx
import {
  useDualAmountInput,
  useWalletTransferMinAmount,
  useWalletTransferPlan,
  useWalletTransferQuote,
} from '@swapped/connect-sdk/react'

function DepositForm({
  walletId,
  token,
}: {
  walletId: string
  token: WalletBalance
}) {
  const { plan, isLoading: planLoading } = useWalletTransferPlan({
    walletId,
    network: token.network,
    symbol: token.symbol,
    tokenAddress: token.tokenAddress, // null for native
  })
  const { minAmount, min } = useWalletTransferMinAmount(plan)
  const {
    amount, // canonical crypto — pass this to quote / submit
    amountDisplay, // bind the crypto <input>
    setAmount,
    amountFiat,
    setAmountFiat,
    setAmountExact, // Max: keeps full precision (skips the typing sanitizer)
    onBlur,
    isValid,
    isFiatAvailable, // false when token.exchangeRate is null
    error: amountError,
    touched,
    setTouched,
  } = useDualAmountInput({
    exchangeRate: token.exchangeRate,
    token: token.symbol,
    tokenDecimals: token.decimals,
    minAmount, // number from getMinAmount; drives below_min on both fields
    maxAmount: Number(token.displayBalance),
    balanceAmount: Number(token.displayBalance),
    maxAmountMode: 'balance',
    clampToMax: true, // typing above balance snaps to max instead of a field error
  })
  const { quote, isFetching } = useWalletTransferQuote({
    plan,
    amount,
  })

  return (
    <form
      onSubmit={event => {
        event.preventDefault()
        setTouched(true) // show min / invalid errors after submit attempt
      }}
    >
      <p>{planLoading ? 'Resolving…' : plan?.flow}</p>
      <input
        value={amountDisplay}
        onChange={event => setAmount(event.target.value)}
        onBlur={onBlur}
      />
      {isFiatAvailable ? (
        <input
          value={amountFiat}
          onChange={event => setAmountFiat(event.target.value)}
          onBlur={onBlur}
        />
      ) : null}
      <button
        type="button"
        onClick={() => setAmountExact(token.displayBalance, { touch: true })}
      >
        Max
      </button>
      {min?.crypto ? (
        <p>
          Minimum: {min.crypto} {token.symbol}
          {min.fiat ? ` ($${min.fiat})` : ''}
        </p>
      ) : null}
      {touched && amountError ? <p>{amountError.message}</p> : null}
      {isFetching ? <p>Updating quote…</p> : null}
      {/* amountOut is set for swap/bridge (what the session receives) */}
      {quote?.amountOut ? (
        <p>
          You receive ≈ {quote.amountOut.crypto} {quote.amountOut.symbol}
        </p>
      ) : null}
      <ConfirmButton plan={plan} amount={amount} quote={quote} isValid={isValid} />
    </form>
  )
}

5. Submit + requires_retry

Call submit with the plan, amount, and quote (required for swap / bridge; optional for direct). Pass onStatus for progress events.

If result.status === 'requires_retry', the wallet already sent — show Retry, do not submit again.

tsx
import {
  ConnectSdkError,
  ConnectSdkErrorCode,
} from '@swapped/connect-sdk'
import { useWalletTransfer } from '@swapped/connect-sdk/react'

function ConfirmButton({
  plan,
  amount,
  quote,
  isValid,
}: {
  plan: WalletTransferPlan | null
  amount: string
  quote: WalletTransferQuote | null
  isValid: boolean
}) {
  const { submit, retry, result, isPending, error } = useWalletTransfer()

  return (
    <div>
      <button
        type="button"
        disabled={
          isPending ||
          !plan ||
          plan.flow === 'unavailable' ||
          result?.status === 'requires_retry' ||
          !isValid
        }
        onClick={() =>
          void submit({
            plan: plan!,
            amount,
            quote: quote ?? undefined,
            onStatus: event => {
              event.status
            },
          })
        }
      >
        {isPending ? 'Confirming…' : 'Confirm in wallet'}
      </button>
      {result?.status === 'requires_retry' ? (
        <button
          type="button"
          disabled={isPending}
          onClick={() => void retry()}
        >
          Retry
        </button>
      ) : null}
      {error instanceof ConnectSdkError &&
      error.code !== ConnectSdkErrorCode.WALLET_TRANSACTION_REJECTED ? (
        <p>
          {error.code === ConnectSdkErrorCode.WALLET_TRANSFER_INSUFFICIENT_FEE
            ? 'Not enough to cover fees'
            : 'Transfer failed'}
        </p>
      ) : null}
    </div>
  )
}

6. Summary

useWalletCompletedTransactionSummary is the success-screen payload (null if this session has no completed wallet deposit). Call restartSession() before starting another payment — connections stay. Fields: Summary.

tsx
import { useWalletCompletedTransactionSummary } from '@swapped/connect-sdk/react'
import { useSwappedConnectClient } from '@swapped/connect-sdk/react'

function Success() {
  const client = useSwappedConnectClient()
  const { summary, isLoadingFees, isLoadingRates } =
    useWalletCompletedTransactionSummary()
  if (!summary) return null

  return (
    <div>
      <p>
        {summary.flow} · {summary.provider}
        {summary.isSponsored ? ' · sponsored' : ''}
      </p>
      <p>
        Sent {summary.send.formatted.amount} {summary.send.currency}
        {summary.send.formatted.fiatValue
          ? ` ($${summary.send.formatted.fiatValue})`
          : isLoadingRates
            ? ' (estimating…)'
            : ''}{' '}
        on {summary.send.network}
      </p>
      <p>
        Received {summary.receive.formatted.amount} {summary.receive.currency}
        {summary.receive.formatted.fiatValue
          ? ` ($${summary.receive.formatted.fiatValue})`
          : isLoadingRates
            ? ' (estimating…)'
            : ''}{' '}
        on {summary.receive.network}
      </p>
      {isLoadingFees ? (
        <p>Estimating fees…</p>
      ) : (
        summary.fees.map(fee => (
          <p key={`${fee.type}-${fee.currency}`}>
            {fee.type} {fee.formatted.amount} {fee.currency}
            {fee.formatted.fiatValue ? ` ($${fee.formatted.fiatValue})` : ''}
          </p>
        ))
      )}
      <p>
        From {summary.from.formatted}
        {summary.from.explorerUrl ? (
          <a href={summary.from.explorerUrl}>Explorer</a>
        ) : null}
      </p>
      <p>
        To {summary.to.formatted}
        {summary.to.explorerUrl ? (
          <a href={summary.to.explorerUrl}>Explorer</a>
        ) : null}
      </p>
      <p>Destination {summary.destination.formatted}</p>
      <p>
        {summary.transaction.formatted}
        {summary.transaction.explorerUrl ? (
          <a href={summary.transaction.explorerUrl}>Explorer</a>
        ) : null}
      </p>
      <button type="button" onClick={() => void client.restartSession()}>
        New payment
      </button>
    </div>
  )
}

Deep dives: WalletsProvider, useAvailableWallets, useConnectWallet, useWallet, useWalletConnections, useActiveWallet, useWalletBalances, Transfer plan, Amount & quote, Submit, Summary, Event hooks, Errors.