Handle complex multi-step transaction flows with proper state management, visual feedback, and error handling.
Implementation
import { useSendTransaction } from '@moon-key/react-auth/ethereum';
import { useState } from 'react';
type FlowStep = 'idle' | 'approving' | 'approved' | 'swapping' | 'complete' | 'error';
export default function MultiStepTransaction() {
const { sendTransaction } = useSendTransaction();
const [step, setStep] = useState<FlowStep>('idle');
const [error, setError] = useState('');
const handleTokenSwap = async () => {
try {
// Step 1: Approve token spending
setStep('approving');
const approveTx = await sendTransaction({
to: '0xTOKEN_ADDRESS',
data: '0x...' // approve function call data
});
console.log('Approval tx:', approveTx);
setStep('approved');
// Wait a bit for confirmation
await new Promise(resolve => setTimeout(resolve, 2000));
// Step 2: Execute swap
setStep('swapping');
const swapTx = await sendTransaction({
to: '0xSWAP_CONTRACT',
data: '0x...' // swap function call data
});
console.log('Swap tx:', swapTx);
setStep('complete');
} catch (err) {
setStep('error');
setError(err instanceof Error ? err.message : 'Transaction failed');
console.error(err);
}
};
return (
<div className="multi-step-container">
<h2>Token Swap</h2>
<div className="steps">
<div className={`step ${step === 'approving' || step === 'approved' ? 'active' : ''}`}>
<span className="step-number">1</span>
<span className="step-label">Approve Token</span>
{step === 'approving' && <span className="spinner" />}
{step === 'approved' && <span className="checkmark">✓</span>}
</div>
<div className={`step ${step === 'swapping' || step === 'complete' ? 'active' : ''}`}>
<span className="step-number">2</span>
<span className="step-label">Execute Swap</span>
{step === 'swapping' && <span className="spinner" />}
{step === 'complete' && <span className="checkmark">✓</span>}
</div>
</div>
{error && <div className="error-message">{error}</div>}
{step === 'complete' && <div className="success-message">Swap completed!</div>}
<button
onClick={handleTokenSwap}
disabled={step !== 'idle' && step !== 'error' && step !== 'complete'}
>
{step === 'idle' || step === 'error' ? 'Start Swap' : 'Processing...'}
</button>
</div>
);
}
Key concepts
- State management - Tracks progress through multiple steps
- Visual feedback - Shows loading indicators and checkmarks for each step
- Error handling - Catches errors at any step and allows retry
- User experience - Disables button during processing
- Confirmation delays - Waits for transaction confirmation before proceeding
Common patterns
With transaction confirmation
import { waitForTransaction } from 'wagmi';
const handleMultiStep = async () => {
setStep('approving');
const approveTx = await sendTransaction({...});
// Wait for confirmation
await waitForTransaction({ hash: approveTx });
setStep('approved');
// Continue with next step
setStep('swapping');
const swapTx = await sendTransaction({...});
await waitForTransaction({ hash: swapTx });
setStep('complete');
};
With progress percentage
const [progress, setProgress] = useState(0);
const handleFlow = async () => {
setProgress(0);
setProgress(25);
await step1();
setProgress(50);
await step2();
setProgress(75);
await step3();
setProgress(100);
};
With cancellation support
const [isCancelled, setIsCancelled] = useState(false);
const handleFlow = async () => {
setIsCancelled(false);
await step1();
if (isCancelled) return;
await step2();
if (isCancelled) return;
await step3();
};
const handleCancel = () => setIsCancelled(true);
Important notes
Always wait for transaction confirmation before proceeding to the next step to avoid errors from transactions not being mined yet.
Consider adding retry logic for failed steps, especially for network-related errors.
Styling suggestion
.steps {
display: flex;
gap: 2rem;
margin: 2rem 0;
}
.step {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
opacity: 0.5;
transition: opacity 0.2s;
}
.step.active {
opacity: 1;
}
.step-number {
width: 40px;
height: 40px;
border-radius: 50%;
background: #e5e7eb;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
}
.step.active .step-number {
background: #6366f1;
color: white;
}
.spinner {
width: 16px;
height: 16px;
border: 2px solid #e5e7eb;
border-top-color: #6366f1;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.checkmark {
color: #10b981;
font-size: 20px;
}
Next steps