Voltar
+20 XP
10/12
Transaction UI Patterns
Sending transactions is the core interaction in any Solana dApp. Great UX requires clear transaction flows, status tracking, and helpful feedback.
Multi-Step Confirmation Flow
Break complex transactions into clear steps:
enum TxStep {
IDLE = 'idle',
CONFIRMING = 'confirming',
SIGNING = 'signing',
SENDING = 'sending',
CONFIRMING_TX = 'confirming_tx',
SUCCESS = 'success',
ERROR = 'error',
}
function SwapInterface() {
const [step, setStep] = useState<TxStep>(TxStep.IDLE);
const [signature, setSignature] = useState<string>('');
async function handleSwap() {
try {
setStep(TxStep.CONFIRMING);
// User reviews swap details
setStep(TxStep.SIGNING);
const { signature } = await sendTransaction(tx, connection);
setSignature(signature);
setStep(TxStep.SENDING);
// Transaction sent to network
setStep(TxStep.CONFIRMING_TX);
await connection.confirmTransaction(signature, 'confirmed');
setStep(TxStep.SUCCESS);
} catch (err) {
setStep(TxStep.ERROR);
}
}
return (
<div>
{step === TxStep.CONFIRMING && <ReviewModal />}
{step === TxStep.SIGNING && <WalletSigningPrompt />}
{step === TxStep.SENDING && <SendingAnimation />}
{step === TxStep.CONFIRMING_TX && <ConfirmingAnimation signature={signature} />}
{step === TxStep.SUCCESS && <SuccessMessage signature={signature} />}
{step === TxStep.ERROR && <ErrorMessage />}
</div>
);
}
Status Indicators
Show clear visual feedback for each stage:
function TransactionStatus({ step }: { step: TxStep }) {
const stages = [
{ label: 'Review', step: TxStep.CONFIRMING },
{ label: 'Sign', step: TxStep.SIGNING },
{ label: 'Send', step: TxStep.SENDING },
{ label: 'Confirm', step: TxStep.CONFIRMING_TX },
];
return (
<div className="flex gap-2">
{stages.map((stage, idx) => (
<div key={idx} className="flex items-center">
<div className={cn(
"w-8 h-8 rounded-full flex items-center justify-center",
step === stage.step && "bg-purple-600 text-white",
idx < stages.findIndex(s => s.step === step) && "bg-green-600",
)}>
{idx < stages.findIndex(s => s.step === step) ? '✓' : idx + 1}
</div>
<span className="ml-2">{stage.label}</span>
</div>
))}
</div>
);
}
Explorer Links
Always provide links to view transactions on-chain:
function ExplorerLink({
signature,
cluster = 'mainnet-beta',
}: {
signature: string;
cluster?: string;
}) {
const url = `https://explorer.solana.com/tx/${signature}${cluster !== 'mainnet-beta' ? `?cluster=${cluster}` : ''}`;
return (
<a
href={url}
target="_blank"
rel="noopener noreferrer"
className="text-purple-600 hover:underline flex items-center gap-1"
>
View on Solana Explorer
<ExternalLinkIcon className="w-4 h-4" />
</a>
);
}
Estimated Cost Display
Show users the transaction cost before signing:
async function estimateTransactionCost(
transaction: Transaction,
connection: Connection
): Promise<number> {
const { blockhash } = await connection.getLatestBlockhash();
transaction.recentBlockhash = blockhash;
// Estimate fee
const fee = await transaction.getEstimatedFee(connection);
return fee || 5000; // Default fallback
}
function TransactionPreview({ transaction }: { transaction: Transaction }) {
const { connection } = useConnection();
const [fee, setFee] = useState<number | null>(null);
useEffect(() => {
estimateTransactionCost(transaction, connection).then(setFee);
}, [transaction, connection]);
return (
<div className="bg-gray-100 p-4 rounded">
<p className="text-sm text-gray-600">Estimated network fee</p>
<p className="text-lg font-bold">
{fee ? `${(fee / LAMPORTS_PER_SOL).toFixed(6)} SOL` : 'Calculating...'}
</p>
</div>
);
}
Retry Logic
Handle failed transactions gracefully:
function useTransactionSender() {
const { connection } = useConnection();
const { sendTransaction } = useWallet();
const [attempts, setAttempts] = useState(0);
async function sendWithRetry(
transaction: Transaction,
maxRetries = 3
): Promise<string> {
for (let i = 0; i < maxRetries; i++) {
setAttempts(i + 1);
try {
const signature = await sendTransaction(transaction, connection);
await connection.confirmTransaction(signature, 'confirmed');
return signature;
} catch (err) {
if (i === maxRetries - 1) throw err;
// Wait before retry (exponential backoff)
await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, i)));
}
}
throw new Error('Transaction failed after retries');
}
return { sendWithRetry, attempts };
}
Transaction History
Show users their past transactions:
function TransactionHistory() {
const { publicKey } = useWallet();
const { connection } = useConnection();
const [txs, setTxs] = useState<ParsedTransactionWithMeta[]>([]);
useEffect(() => {
if (!publicKey) return;
connection.getSignaturesForAddress(publicKey, { limit: 10 })
.then(async (sigs) => {
const txPromises = sigs.map(sig =>
connection.getParsedTransaction(sig.signature, {
maxSupportedTransactionVersion: 0,
})
);
const txs = await Promise.all(txPromises);
setTxs(txs.filter(tx => tx !== null));
});
}, [publicKey, connection]);
return (
<div>
{txs.map((tx, idx) => (
<TransactionCard key={idx} tx={tx} />
))}
</div>
);
}
Real-World Examples
Jupiter Swap Flow
- Review swap (token amounts, slippage, price impact)
- Approve transaction in wallet
- Show "Swapping..." animation with explorer link
- Confirm on-chain (with retry logic)
- Show success with new token balance
Magic Eden NFT Purchase
- Show NFT details and price
- Check buyer has sufficient SOL
- Request wallet signature
- Stream transaction status updates
- Display purchase confirmation with NFT preview
Best Practices
- Always show transaction cost before signing
- Provide explorer links for every transaction
- Use optimistic updates for instant feedback
- Implement retry logic for network hiccups
- Clear error messages (insufficient funds, rejected, timeout)
- Show multi-step progress for complex flows