txToaster

Showing transaction notifications with real-time progress updates

The txToaster utility provides a generic, adapter-based system for displaying transaction notifications with real-time progress updates. It supports any toast notification library through adapters, keeping users informed about transaction status, block inclusion, and errors.

Quick look

Quick Start

1. Install a Toast Library

Choose and install one of the supported toast libraries:

# Sonner (recommended)
npm install sonner

# React-Toastify
npm install react-toastify

# React-Hot-Toast
npm install react-hot-toast

2. Setup the Adapter

Configure the global adapter once in your app's entry point:

import { setupTxToaster, SonnerAdapter } from 'typink';
import { toast } from 'sonner';

// Setup once at app initialization
setupTxToaster({
  adapter: new SonnerAdapter(toast),
});

3. Use in Components

Create toaster instances for each transaction:

import { txToaster } from 'typink';

const toaster = txToaster('Signing transaction...');

try {
  await tx.signAndSend({
    args: [/* ... */],
    callback: (result) => {
      toaster.onTxProgress(result);
    },
  });
} catch (error) {
  toaster.onTxError(error);
}

Built-in Adapters

Typink provides three built-in adapters for popular toast libraries:

SonnerAdapter

For Sonner toast library (recommended for its simplicity and beautiful default styling).

import { setupTxToaster, SonnerAdapter } from 'typink';
import { toast } from 'sonner';
import { Toaster } from 'sonner';

setupTxToaster({
  adapter: new SonnerAdapter(toast),
});

// Add Toaster component to your app
function App() {
  return (
    <>
      <Toaster position="top-right" />
      {/* Your app content */}
    </>
  );
}

ReactToastifyAdapter

For React-Toastify library.

import { setupTxToaster, ReactToastifyAdapter } from 'typink';
import { toast, ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';

setupTxToaster({
  adapter: new ReactToastifyAdapter(toast),
});

// Add ToastContainer component to your app
function App() {
  return (
    <>
      <ToastContainer position="top-right" />
      {/* Your app content */}
    </>
  );
}

ReactHotToastAdapter

For React-Hot-Toast library.

import { setupTxToaster, ReactHotToastAdapter } from 'typink';
import { toast, Toaster } from 'react-hot-toast';

setupTxToaster({
  adapter: new ReactHotToastAdapter(toast),
});

// Add Toaster component to your app
function App() {
  return (
    <>
      <Toaster position="top-right" />
      {/* Your app content */}
    </>
  );
}

Complete Usage Examples

Contract Transaction

import { useContract, useContractTx, txToaster } from 'typink';
import { GreeterContractApi } from './types/greeter';

function GreeterComponent() {
  const [message, setMessage] = useState('');
  const { contract } = useContract<GreeterContractApi>('greeter');
  const setMessageTx = useContractTx(contract, 'setMessage');

  const handleSetMessage = async () => {
    if (!message) return;

    const toaster = txToaster('Setting message...');

    try {
      await setMessageTx.signAndSend({
        args: [message],
        callback: (progress) => {
          // Update toast with transaction progress
          toaster.onTxProgress(progress);

          // Reset input on success
          if (progress.status.type === 'Finalized' && !progress.dispatchError) {
            setMessage('');
          }
        },
      });
    } catch (error) {
      console.error('Transaction failed:', error);
      toaster.onTxError(error);
    }
  };

  return (
    <div>
      <input
        value={message}
        onChange={(e) => setMessage(e.target.value)}
        placeholder="Enter message"
      />
      <button
        onClick={handleSetMessage}
        disabled={setMessageTx.inProgress}>
        {setMessageTx.inProgress ? 'Setting...' : 'Set Message'}
      </button>
    </div>
  );
}

General Substrate Transaction

import { useTx, txToaster } from 'typink';

function TransferComponent() {
  const [recipient, setRecipient] = useState('');
  const [amount, setAmount] = useState('');
  const transferTx = useTx((tx) => tx.balances.transferKeepAlive);

  const handleTransfer = async () => {
    const toaster = txToaster('Transferring tokens...');

    try {
      await transferTx.signAndSend({
        args: [recipient, BigInt(amount)],
        callback: (result) => {
          toaster.onTxProgress(result);

          const { status, dispatchError } = result;

          if (status.type === 'BestChainBlockIncluded') {
            if (!dispatchError) {
              console.log('Transfer included in block!');
            } else {
              console.error('Transfer failed in block:', dispatchError);
            }
          }

          if (status.type === 'Finalized') {
            if (!dispatchError) {
              setRecipient('');
              setAmount('');
            }
          }
        },
      });
    } catch (error) {
      toaster.onTxError(error);
    }
  };

  return (
    <div>
      <input
        value={recipient}
        onChange={(e) => setRecipient(e.target.value)}
        placeholder="Recipient address"
      />
      <input
        value={amount}
        onChange={(e) => setAmount(e.target.value)}
        placeholder="Amount"
      />
      <button onClick={handleTransfer}>Transfer</button>
    </div>
  );
}

With Custom Messages

const handleMint = async () => {
  const toaster = txToaster({
    initialMessage: 'Preparing to mint tokens...',
    messages: {
      inProgress: '⏳ Minting tokens...',
      successful: '✅ Tokens minted successfully!',
      failed: '❌ Minting failed',
    },
    autoCloseDelay: 10000,
  });

  try {
    await mintTx.signAndSend({
      args: [amount],
      callback: (progress) => {
        toaster.onTxProgress(progress);

        // Custom logic based on status
        if (progress.status.type === 'BestChainBlockIncluded') {
          console.log('Tokens minted in block:', progress.status.value.blockNumber);
        }
      },
    });
  } catch (error) {
    toaster.onTxError(error);
  }
};

Last updated

Was this helpful?