Skip to main content
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