Appearance
Example
End-to-end Coinbase OAuth withdraw with React hooks: connect, build the withdraw form from CoinbaseProvider, handle 2FA, branch screens, show a success UI.
Route here when the payment method has provider: coinbase and type: exchange_oauth. Requires SwappedConnectProvider, CoinbaseProvider, and a loaded active session. After a completed payment, call restartSession() before starting again.
Use state hooks for UI (including which step to show). Use event hooks only for side effects (navigate, toast).
1. Providers + session
tsx
import { createSwappedConnectClient } from '@swapped/connect-sdk'
import {
CoinbaseProvider,
SwappedConnectProvider,
} from '@swapped/connect-sdk/react'
const client = createSwappedConnectClient({
sessionId: 'your-session-id', // from your backend
})
void client.loadSession()
function App() {
return (
<SwappedConnectProvider client={client}>
<CoinbaseProvider>
<CoinbaseFlow />
</CoinbaseProvider>
</SwappedConnectProvider>
)
}2. Gate on Coinbase availability
Use CoinbaseGuard (or useCoinbaseAvailability for custom branching).
tsx
import { CoinbaseGuard } from '@swapped/connect-sdk/react'
function App() {
return (
<CoinbaseGuard
loading={<p>Loading…</p>}
fallback={<p>Coinbase is not available for this session</p>}
onNotAvailable={() => {
// e.g. navigate('/payment-methods')
}}
>
<CoinbaseFlow />
</CoinbaseGuard>
)
}3. Connect
tsx
import { useCoinbaseConnection } from '@swapped/connect-sdk/react'
function ConnectStep() {
const {
isConnected,
isConnecting,
isInitializing,
isPopupOpen,
connect,
error,
} = useCoinbaseConnection()
if (isInitializing) {
return <p>Restoring Coinbase session…</p>
}
if (isConnected) {
return <p>Connected</p>
}
return (
<div>
{error && <p>{error.message}</p>}
<button
type="button"
disabled={isConnecting || isPopupOpen}
onClick={() => void connect()}
>
{isPopupOpen ? 'Complete login in the popup…' : 'Connect Coinbase'}
</button>
</div>
)
}4. Withdraw form (selection from context)
CoinbaseProvider owns token / network / funding / amount. Hooks read and update that state; startWithdrawal() builds the request from context. When the amount is above the fee-reserve max, the SDK auto-adjusts it, syncs the amount input, and runs onAmountAdjusted (toast, confirm dialog, etc.).
tsx
import {
useCoinbaseAmount,
useCoinbaseBalances,
useCoinbaseFundingTokens,
useCoinbaseNetwork,
useCoinbaseSelection,
useCoinbaseToken,
useCoinbaseWithdrawal,
useCoinbaseWithdrawalCooldown,
} from '@swapped/connect-sdk/react'
function WithdrawForm() {
const { allBalances, isLoading, error: balancesError } = useCoinbaseBalances()
const { selectedCurrency, selectToken, isTokenSelected } = useCoinbaseToken()
const {
activeNetworks,
selectedNetwork,
select: selectNetwork,
isLoading: networksLoading,
} = useCoinbaseNetwork()
const funding = useCoinbaseFundingTokens()
const {
amountDisplay,
setAmount,
amountFiat,
setAmountFiat,
setMax,
formatted,
error: amountError,
setTouched,
isFiatAvailable,
} = useCoinbaseAmount()
const { canSubmit } = useCoinbaseSelection()
const { startWithdrawal, isStartingWithdrawal, error: withdrawError } =
useCoinbaseWithdrawal()
const { isInCooldown, remainingMs } = useCoinbaseWithdrawalCooldown()
async function onWithdraw() {
setTouched(true)
const result = await startWithdrawal(undefined, {
onAmountAdjusted: ({ formatted }) => {
// Toast / banner — withdrawal continues with the adjusted amount.
// Return false (or await a confirm dialog) to cancel instead.
showToast(
`Amount reduced to ${formatted.adjustedAmount} for the $${formatted.feeReserveFiat} network fee reserve.`,
)
},
})
if (result.status === 'idle') {
// Cancelled via onAmountAdjusted — stay on the form
}
}
if (isLoading) return <p>Loading balances…</p>
if (balancesError) return <p>{balancesError.message}</p>
return (
<div>
<ul>
{allBalances.map(balance => (
<li key={balance.id}>
<button
type="button"
disabled={!balance.eligible}
aria-pressed={isTokenSelected(balance.currency)}
onClick={() => selectToken(balance.currency)}
>
{balance.currency} — {balance.formatted.aggregatedBalance}
</button>
</li>
))}
</ul>
<ul>
{activeNetworks.map(network => (
<li key={network.id}>
<button
type="button"
disabled={!network.eligible || networksLoading}
aria-pressed={selectedNetwork === network.id}
onClick={() => selectNetwork(network.id)}
>
{network.name}
</button>
</li>
))}
</ul>
<ul>
{funding.fundingTokens.map(token => (
<li key={token.currency}>
<label>
<input
type="checkbox"
checked={funding.isTokenSelected(token.currency)}
onChange={() => funding.toggle(token.currency)}
disabled={token.isCurrentToken}
/>
{token.currency} — {token.formatted.balance}
</label>
</li>
))}
</ul>
<p>
Aggregated: {funding.aggregatedBalance.formatted.balance}{' '}
{selectedCurrency}
</p>
<input
value={amountDisplay}
onChange={event => setAmount(event.target.value)}
onBlur={() => setTouched(true)}
inputMode="decimal"
/>
{isFiatAvailable && (
<input
value={amountFiat}
onChange={event => setAmountFiat(event.target.value)}
onBlur={() => setTouched(true)}
inputMode="decimal"
/>
)}
<button type="button" onClick={setMax}>
Max
</button>
<p>
Between {formatted.minAmount} and {formatted.maxAmount}
{formatted.minAmountFiat
? ` ($${formatted.minAmountFiat} – $${formatted.maxAmountFiat})`
: ''}
</p>
{amountError && <p>{amountError.crypto.message}</p>}
{amountError?.fiat && <p>{amountError.fiat.message}</p>}
{withdrawError && <p>{withdrawError.message}</p>}
<button
type="button"
disabled={!canSubmit || isStartingWithdrawal || isInCooldown}
onClick={() => void onWithdraw()}
>
{isInCooldown
? `Wait ${Math.ceil(remainingMs / 1000)}s`
: 'Continue to confirmation'}
</button>
</div>
)
}5. Confirm 2FA
The 2FA code can come from SMS, email, or an authenticator app and is typically 6–7 characters. Do not hardcode a 6-digit-only input.
tsx
import { useCoinbaseWithdrawal } from '@swapped/connect-sdk/react'
import { useState } from 'react'
function TwoFactorStep() {
const {
requires2fa,
confirmWithdrawal,
cancelWithdrawal,
isConfirming,
is2faInvalid,
clear2faInvalid,
error,
} = useCoinbaseWithdrawal()
const [code, setCode] = useState('')
if (!requires2fa) return null
return (
<div>
{is2faInvalid && <p>Invalid 2FA code. Please try again.</p>}
{error && <p>{error.message}</p>}
<input
value={code}
onChange={event => {
setCode(event.target.value)
clear2faInvalid()
}}
autoComplete="one-time-code"
placeholder="2FA code"
/>
<button
type="button"
disabled={isConfirming || !code}
onClick={() => void confirmWithdrawal(code)}
>
Confirm
</button>
<button type="button" onClick={cancelWithdrawal}>
Cancel
</button>
</div>
)
}6. Drive steps from withdrawal state
Derive the current screen from connection + withdrawal flags (same pattern as a typical page-level flow):
tsx
import {
useCoinbaseConnection,
useCoinbaseWithdrawal,
} from '@swapped/connect-sdk/react'
type CoinbaseStep = 'login' | 'form' | 'twofa' | 'done'
function CoinbaseFlow() {
const { isConnected } = useCoinbaseConnection()
const withdrawal = useCoinbaseWithdrawal()
const step: CoinbaseStep = withdrawal.result
? 'done'
: withdrawal.requires2fa ||
withdrawal.isConfirming ||
withdrawal.status === 'confirming'
? 'twofa'
: isConnected
? 'form'
: 'login'
if (step === 'login') return <ConnectStep />
if (step === 'form') return <WithdrawForm />
if (step === 'twofa') return <TwoFactorStep />
return <SuccessStep result={withdrawal.result!} />
}| Step | Condition |
|---|---|
done | withdrawal.result is set (status completed) |
twofa | requires2fa, isConfirming, or status === 'confirming' |
form | Connected, withdrawal not pending confirmation |
login | Not connected |
Prefer state hooks for this branching. Use event hooks only if you need a side effect (e.g. useOnCoinbaseWithdrawalCompleted to navigate to a separate route).
7. Success screen
Two options:
useCoinbaseWithdrawal().result— fullCoinbaseWithdrawal(id, amounts, network fee, destination). Best when the success UI lives next to the withdraw form.useCoinbaseCompletedTransactionSummary()— flatter display-ready summary. Best for a dedicated success route or when you only need amount / fee / destination.
After a completed payment the session is no longer active — call restartSession() before starting another one.
From withdrawal.result
tsx
import type { CoinbaseWithdrawal } from '@swapped/connect-sdk'
import { useSwappedConnectClient } from '@swapped/connect-sdk/react'
function SuccessStep({ result }: { result: CoinbaseWithdrawal }) {
const client = useSwappedConnectClient()
return (
<div>
<p>Withdrawal submitted</p>
<p>
{result.amount.amount} {result.amount.currency}
</p>
<p>
Native: {result.nativeAmount.amount} {result.nativeAmount.currency}
</p>
<p>
Network fee: {result.network.transactionFee.amount}{' '}
{result.network.transactionFee.currency}
</p>
<p>Status: {result.status}</p>
<p>Network status: {result.network.status}</p>
{result.destinationAddress && (
<p>Destination: {result.destinationAddress}</p>
)}
<p>Id: {result.id}</p>
<p>Created: {new Date(result.createdAt).toLocaleString()}</p>
<button type="button" onClick={() => void client.restartSession()}>
Start another payment
</button>
</div>
)
}From the summary hook
tsx
import {
useCoinbaseCompletedTransactionSummary,
useSwappedConnectClient,
} from '@swapped/connect-sdk/react'
function SuccessStep() {
const summary = useCoinbaseCompletedTransactionSummary()
const client = useSwappedConnectClient()
if (!summary) return null
return (
<div>
<p>
Sent {summary.amount.amount} {summary.amount.currency}
</p>
{summary.network && <p>Network: {summary.network}</p>}
{summary.fee && (
<p>
Fee {summary.fee.amount} {summary.fee.currency}
</p>
)}
{summary.destinationAddress && (
<p>Destination: {summary.destinationAddress}</p>
)}
{summary.status && <p>Status: {summary.status}</p>}
{summary.id && <p>Withdrawal id: {summary.id}</p>}
<button type="button" onClick={() => void client.restartSession()}>
Start another payment
</button>
</div>
)
}See Summary for field details and nullability after reload / restartSession().
Putting it together
- Gate with CoinbaseGuard
- Connect → withdraw form → 2FA (if required) → success
- Branch screens from
isConnected+useCoinbaseWithdrawal()(section 6) - Show success from
resultor useCoinbaseCompletedTransactionSummary restartSession()before another payment
Next
- CoinbaseProvider props (
autoSelectToken,resetAmountOnTokenChange, …) - Errors for code-by-code handling
- Event hooks for navigation / analytics