Voltar
+20 XP
11/12
Transaction Notifications
Real-time notifications keep users informed about transaction status without requiring them to poll or refresh. Let's build a robust notification system.
Toast Notifications
Toasts are perfect for transaction feedback. Use libraries like react-hot-toast or sonner:
import toast from 'react-hot-toast';
import { ExternalLink } from 'lucide-react';
function useTransactionToast() {
const cluster = 'mainnet-beta';
function toastSignature(signature: string) {
const explorerUrl = `https://explorer.solana.com/tx/${signature}?cluster=${cluster}`;
toast.success(
<div className="flex items-center gap-2">
<span>Transaction confirmed!</span>
<a
href={explorerUrl}
target="_blank"
rel="noopener noreferrer"
className="text-purple-600 hover:underline flex items-center gap-1"
>
View <ExternalLink className="w-3 h-3" />
</a>
</div>,
{ duration: 8000 }
);
}
function toastError(message: string) {
toast.error(message, { duration: 5000 });
}
return { toastSignature, toastError };
}
// Usage
async function handleTransfer() {
const { toastSignature, toastError } = useTransactionToast();
try {
toast.loading('Sending transaction...');
const signature = await sendTransaction(tx, connection);
toast.loading('Confirming...');
await connection.confirmTransaction(signature);
toast.dismiss();
toastSignature(signature);
} catch (err) {
toast.dismiss();
toastError('Transaction failed. Please try again.');
}
}
WebSocket Subscriptions for Real-Time Updates
Monitor accounts for changes without polling:
function useAccountChangeNotifications(publicKey: PublicKey | null) {
const { connection } = useConnection();
const previousBalance = useRef<number | null>(null);
useEffect(() => {
if (!publicKey) return;
const subscriptionId = connection.onAccountChange(
publicKey,
(accountInfo) => {
const newBalance = accountInfo.lamports / LAMPORTS_PER_SOL;
if (previousBalance.current !== null) {
const change = newBalance - previousBalance.current;
if (change > 0) {
toast.success(`Received ${change.toFixed(4)} SOL`);
} else if (change < 0) {
toast.info(`Sent ${Math.abs(change).toFixed(4)} SOL`);
}
}
previousBalance.current = newBalance;
},
'confirmed'
);
return () => {
connection.removeAccountChangeListener(subscriptionId);
};
}, [publicKey, connection]);
}
Transaction Status Polling
For critical operations, poll transaction status:
async function waitForConfirmation(
connection: Connection,
signature: string,
commitment: Commitment = 'confirmed',
timeout = 60000
): Promise<boolean> {
const start = Date.now();
while (Date.now() - start < timeout) {
const status = await connection.getSignatureStatus(signature);
if (status.value?.confirmationStatus === commitment) {
return true;
}
if (status.value?.err) {
throw new Error('Transaction failed');
}
// Wait 1s before next check
await new Promise(resolve => setTimeout(resolve, 1000));
}
throw new Error('Transaction confirmation timeout');
}
// Usage with notifications
async function sendWithNotifications(tx: Transaction) {
const toastId = toast.loading('Sending transaction...');
try {
const signature = await sendTransaction(tx, connection);
toast.loading('Confirming on-chain...', { id: toastId });
await waitForConfirmation(connection, signature);
toast.success(
<span>
Transaction confirmed!
<a href={`https://explorer.solana.com/tx/${signature}`}>View</a>
</span>,
{ id: toastId }
);
} catch (err) {
toast.error('Transaction failed', { id: toastId });
}
}
Notification Permission (Browser Notifications)
For background notifications when tab isn't focused:
function useNotificationPermission() {
const [permission, setPermission] = useState<NotificationPermission>(
typeof Notification !== 'undefined' ? Notification.permission : 'default'
);
async function requestPermission() {
if (typeof Notification === 'undefined') return;
const result = await Notification.requestPermission();
setPermission(result);
}
function notify(title: string, body: string) {
if (permission !== 'granted') return;
new Notification(title, {
body,
icon: '/solana-logo.png',
badge: '/notification-badge.png',
});
}
return { permission, requestPermission, notify };
}
// Usage
function TransactionMonitor() {
const { notify } = useNotificationPermission();
const { publicKey } = useWallet();
const { connection } = useConnection();
useEffect(() => {
if (!publicKey) return;
const sub = connection.onAccountChange(
publicKey,
() => {
if (!document.hasFocus()) {
notify('Solana Transaction', 'Your wallet balance changed');
}
},
'confirmed'
);
return () => connection.removeAccountChangeListener(sub);
}, [publicKey, connection, notify]);
return null;
}
Notification Sound
Add audio feedback for important events:
function useTransactionSound() {
const successSound = useRef<HTMLAudioElement | null>(null);
const errorSound = useRef<HTMLAudioElement | null>(null);
useEffect(() => {
successSound.current = new Audio('/sounds/success.mp3');
errorSound.current = new Audio('/sounds/error.mp3');
}, []);
function playSuccess() {
successSound.current?.play();
}
function playError() {
errorSound.current?.play();
}
return { playSuccess, playError };
}
Notification Queue
Manage multiple notifications without overwhelming the UI:
function NotificationQueue() {
const [queue, setQueue] = useState<Notification[]>([]);
const maxVisible = 3;
function addNotification(notification: Notification) {
setQueue(prev => [...prev, notification]);
setTimeout(() => {
setQueue(prev => prev.filter(n => n.id !== notification.id));
}, notification.duration || 5000);
}
return (
<div className="fixed bottom-4 right-4 space-y-2 z-50">
{queue.slice(-maxVisible).map(notification => (
<NotificationCard key={notification.id} {...notification} />
))}
</div>
);
}
Real-World Examples
Phantom Wallet
- Shows in-app toast when transaction is signed
- Browser notification when transaction confirms (if tab not focused)
- Sound effect on success/failure
Jupiter Aggregator
- Loading toast while routing
- Progress indicator during swap
- Success toast with explorer link
- Error toast with retry button
Best Practices
- Use toast notifications for transaction updates
- Include explorer links in success notifications
- Implement WebSocket subscriptions for real-time balance changes
- Request browser notification permission for background updates
- Add sound effects for important events (success, error)
- Limit visible notifications to avoid UI clutter
- Auto-dismiss notifications after 5-8 seconds