Appearance
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 desktop —
connect({ transport: 'injected' })(browser extension). - QR — WalletConnect pairing starts in a
useEffect(no extra button).pairingUriis the QR value. Some wallets use a browse URL instead (isDeepLinkConnection/getDeepLinkUrl) — that is also a QR, not WalletConnect.wallet.qrScanTargetsays whether to scan with the phone camera or the wallet app. - Connect mobile — only on a phone (
isMobileBrowser()). OpenspairingDeeplinkUrl(same WC pairing) oropenDeepLink(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.flow | Meaning |
|---|---|
direct | The session already receives this token on this network. The user just sends it. |
swap | Same chain, wrong token. The SDK swaps into what the session receives. |
bridge | Wrong chain (and usually wrong token). The SDK bridges into the session’s token/network. |
unavailable | No 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.