diff --git a/app/positions/components/PositionsContent.tsx b/app/positions/components/PositionsContent.tsx
index 71d45b41..ac613443 100644
--- a/app/positions/components/PositionsContent.tsx
+++ b/app/positions/components/PositionsContent.tsx
@@ -6,7 +6,6 @@ import { useParams } from 'next/navigation';
import { FaHistory, FaPlus } from 'react-icons/fa';
import { IoRefreshOutline } from 'react-icons/io5';
import { TbReport } from 'react-icons/tb';
-import { toast } from 'react-toastify';
import { Address } from 'viem';
import { useAccount } from 'wagmi';
import { AddressDisplay } from '@/components/common/AddressDisplay';
@@ -16,6 +15,7 @@ import EmptyScreen from '@/components/Status/EmptyScreen';
import LoadingScreen from '@/components/Status/LoadingScreen';
import { SupplyModalV2 } from '@/components/SupplyModalV2';
import { useMarkets } from '@/hooks/useMarkets';
+import { useStyledToast } from '@/hooks/useStyledToast';
import useUserPositionsSummaryData from '@/hooks/useUserPositionsSummaryData';
import { MarketPosition } from '@/utils/types';
import { OnboardingModal } from './onboarding/OnboardingModal';
@@ -32,6 +32,8 @@ export default function Positions() {
const [mounted, setMounted] = useState(false);
+ const toast = useStyledToast();
+
const isOwner = useMemo(() => {
if (!account || !address || !mounted) return false;
return account === address;
diff --git a/package.json b/package.json
index dc3a8065..5f63496e 100644
--- a/package.json
+++ b/package.json
@@ -63,7 +63,6 @@
"react-remove-scroll": "^2.5.7",
"react-spinners": "^0.14.1",
"react-table": "^7.8.0",
- "react-toastify": "11.0.2",
"react-type-animation": "^3.2.0",
"recharts": "^2.13.0",
"rehype-pretty-code": "0.12.3",
@@ -73,6 +72,7 @@
"sharp": "^0.33.5",
"shikiji": "^0.9.17",
"shikiji-core": "^0.9.17",
+ "sonner": "^2.0.7",
"tailwind-merge": "^2.5.5",
"tailwind-variants": "^2.1.0",
"unified": "^11.0.4",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 0f62521f..8c815fea 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -140,9 +140,6 @@ importers:
react-table:
specifier: ^7.8.0
version: 7.8.0(react@18.3.1)
- react-toastify:
- specifier: 11.0.2
- version: 11.0.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-type-animation:
specifier: ^3.2.0
version: 3.2.0(prop-types@15.8.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -170,6 +167,9 @@ importers:
shikiji-core:
specifier: ^0.9.17
version: 0.9.19
+ sonner:
+ specifier: ^2.0.7
+ version: 2.0.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
tailwind-merge:
specifier: ^2.5.5
version: 2.6.0
@@ -6762,12 +6762,6 @@ packages:
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
- react-toastify@11.0.2:
- resolution: {integrity: sha512-GjHuGaiXMvbls3ywqv8XdWONwrcO4DXCJIY1zVLkHU73gEElKvTTXNI5Vom3s/k/M8hnkrfsqgBSX3OwmlonbA==}
- peerDependencies:
- react: ^18 || ^19
- react-dom: ^18 || ^19
-
react-transition-group@4.4.5:
resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==}
peerDependencies:
@@ -7068,6 +7062,12 @@ packages:
sonic-boom@3.8.1:
resolution: {integrity: sha512-y4Z8LCDBuum+PBP3lSV7RHrXscqksve/bi0as7mhwVnBW+/wUqKT/2Kb7um8yqcFy0duYbbPxzt89Zy2nOCaxg==}
+ sonner@2.0.7:
+ resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==}
+ peerDependencies:
+ react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
@@ -16947,12 +16947,6 @@ snapshots:
transitivePeerDependencies:
- '@types/react'
- react-toastify@11.0.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
- dependencies:
- clsx: 2.1.1
- react: 18.3.1
- react-dom: 18.3.1(react@18.3.1)
-
react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
'@babel/runtime': 7.28.3
@@ -17369,6 +17363,11 @@ snapshots:
dependencies:
atomic-sleep: 1.0.0
+ sonner@2.0.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
+ dependencies:
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+
source-map-js@1.2.1: {}
source-map-support@0.5.13:
diff --git a/src/components/common/StyledToast.tsx b/src/components/common/StyledToast.tsx
deleted file mode 100644
index 0e9c02e9..00000000
--- a/src/components/common/StyledToast.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import { TxHashDisplay } from '../TxHashDisplay';
-
-export function StyledToast({ title, message }: { title: string; message?: string }) {
- return (
-
-
{title}
- {message &&
{message}
}
-
- );
-}
-
-export function TransactionToast({
- title,
- description,
- hash,
-}: {
- title: string;
- hash?: string;
- description?: string;
-}) {
- return (
-
-
{title}
- {description &&
{description}
}
-
-
- );
-}
diff --git a/src/components/providers/ConnectRedirectProvider.tsx b/src/components/providers/ConnectRedirectProvider.tsx
index 63330804..106fd12c 100644
--- a/src/components/providers/ConnectRedirectProvider.tsx
+++ b/src/components/providers/ConnectRedirectProvider.tsx
@@ -21,7 +21,7 @@ export function ConnectRedirectProvider({ children }: { children: ReactNode }) {
if (redirectPath && !isReconnected) {
router.push(`/${redirectPath}/${address}`);
toast.success('Address connected', 'Redirecting to portfolio...', {
- toastId: 'address-connected',
+ id: 'address-connected',
});
// Reset the path after redirect
setRedirectPath(undefined);
diff --git a/src/components/providers/ThemeProvider.tsx b/src/components/providers/ThemeProvider.tsx
index db3496c1..48c285a5 100644
--- a/src/components/providers/ThemeProvider.tsx
+++ b/src/components/providers/ThemeProvider.tsx
@@ -1,8 +1,31 @@
'use client';
import { HeroUIProvider } from '@heroui/react';
-import { ThemeProvider as NextThemesProvider } from 'next-themes';
-import { ToastContainer } from 'react-toastify';
+import { ThemeProvider as NextThemesProvider, useTheme } from 'next-themes';
+import { Toaster } from 'sonner';
+
+function ToasterProvider() {
+ const { theme } = useTheme();
+
+ const isDark = theme === 'dark';
+
+ return (
+
+ );
+}
export function ThemeProviders({ children }: { children: React.ReactNode }) {
return (
@@ -14,11 +37,7 @@ export function ThemeProviders({ children }: { children: React.ReactNode }) {
themes={['light', 'dark']}
>
{children}
-
+
);
}
diff --git a/src/hooks/useStyledToast.tsx b/src/hooks/useStyledToast.tsx
index 383a8ce7..cfba2ba9 100644
--- a/src/hooks/useStyledToast.tsx
+++ b/src/hooks/useStyledToast.tsx
@@ -1,20 +1,66 @@
-import React, { useCallback } from 'react';
-import { toast, ToastOptions } from 'react-toastify';
-import 'react-toastify/dist/ReactToastify.css';
-import { StyledToast } from '../components/common/StyledToast';
+import { useCallback } from 'react';
+import { toast, ExternalToast } from 'sonner';
+
+type ToastMessage = string | undefined;
export function useStyledToast() {
- const success = useCallback((title: string, message?: string, options?: ToastOptions) => {
- toast.success(, options);
- }, []);
+ const success = useCallback(
+ (title: string, messageOrOptions?: ToastMessage | ExternalToast, options?: ExternalToast) => {
+ // If second param is an object, treat it as options
+ const isSecondParamOptions =
+ typeof messageOrOptions === 'object' && messageOrOptions !== null;
+ const message = isSecondParamOptions ? undefined : (messageOrOptions as string | undefined);
+ const finalOptions = isSecondParamOptions
+ ? (messageOrOptions as ExternalToast)
+ : options;
+
+ toast.success(title, {
+ description: message,
+ className: 'font-zen',
+ descriptionClassName: 'font-inter text-xs',
+ ...finalOptions,
+ });
+ },
+ [],
+ );
+
+ const error = useCallback(
+ (title: string, messageOrOptions?: ToastMessage | ExternalToast, options?: ExternalToast) => {
+ const isSecondParamOptions =
+ typeof messageOrOptions === 'object' && messageOrOptions !== null;
+ const message = isSecondParamOptions ? undefined : (messageOrOptions as string | undefined);
+ const finalOptions = isSecondParamOptions
+ ? (messageOrOptions as ExternalToast)
+ : options;
+
+ toast.error(title, {
+ description: message,
+ className: 'font-zen',
+ descriptionClassName: 'font-inter text-xs',
+ ...finalOptions,
+ });
+ },
+ [],
+ );
- const error = useCallback((title: string, message?: string, options?: ToastOptions) => {
- toast.error(, options);
- }, []);
+ const info = useCallback(
+ (title: string, messageOrOptions?: ToastMessage | ExternalToast, options?: ExternalToast) => {
+ const isSecondParamOptions =
+ typeof messageOrOptions === 'object' && messageOrOptions !== null;
+ const message = isSecondParamOptions ? undefined : (messageOrOptions as string | undefined);
+ const finalOptions = isSecondParamOptions
+ ? (messageOrOptions as ExternalToast)
+ : options;
- const info = useCallback((title: string, message?: string, options?: ToastOptions) => {
- toast.info(, options);
- }, []);
+ toast.info(title, {
+ description: message,
+ className: 'font-zen',
+ descriptionClassName: 'font-inter text-xs',
+ ...finalOptions,
+ });
+ },
+ [],
+ );
return { success, error, info };
}
diff --git a/src/hooks/useTransactionWithToast.tsx b/src/hooks/useTransactionWithToast.tsx
index 7ffd98ed..286eb2d6 100644
--- a/src/hooks/useTransactionWithToast.tsx
+++ b/src/hooks/useTransactionWithToast.tsx
@@ -1,8 +1,7 @@
import React, { useCallback, useEffect } from 'react';
-import { toast } from 'react-toastify';
-import 'react-toastify/dist/ReactToastify.css';
+import { toast } from 'sonner';
import { useSendTransaction, useWaitForTransactionReceipt } from 'wagmi';
-import { StyledToast, TransactionToast } from '@/components/common/StyledToast';
+import { TxHashDisplay } from '@/components/TxHashDisplay';
import { getExplorerTxURL } from '../utils/external';
import { SupportedNetworks } from '../utils/networks';
@@ -50,51 +49,68 @@ export function useTransactionWithToast({
}, [hash, chainId]);
useEffect(() => {
- if (isConfirming) {
- toast.loading(
- ,
- {
- toastId,
- onClick,
- closeButton: true,
- },
- );
+ if (isConfirming && hash) {
+ toast.loading(pendingText, {
+ id: toastId,
+ description: (
+
+ {pendingDescription &&
{pendingDescription}
}
+
+
+ ),
+ action: hash
+ ? {
+ label: 'View',
+ onClick,
+ }
+ : undefined,
+ className: 'font-zen',
+ });
}
}, [isConfirming, pendingText, pendingDescription, toastId, onClick, hash]);
useEffect(() => {
if (isConfirmed) {
- toast.update(toastId, {
- render: (
-
- ),
- type: 'success',
- isLoading: false,
- autoClose: 5000,
- onClick,
- closeButton: true,
- });
- if (onSuccess) {
- onSuccess();
+ if (hash) {
+ toast.success(`${successText} 🎉`, {
+ id: toastId,
+ description: (
+
+ {successDescription &&
{successDescription}
}
+
+
+ ),
+ action: {
+ label: 'View',
+ onClick,
+ },
+ duration: 5000,
+ className: 'font-zen',
+ });
+ if (onSuccess) {
+ onSuccess();
+ }
}
}
if (isError || txError) {
- toast.update(toastId, {
- render: (
-
+ toast.error(errorText, {
+ id: toastId,
+ description: (
+
+
+ {txError ? txError.message : 'Transaction Failed'}
+
+ {hash &&
}
+
),
- type: 'error',
- isLoading: false,
- autoClose: 5000,
- onClick,
- closeButton: true,
+ action: hash
+ ? {
+ label: 'View',
+ onClick,
+ }
+ : undefined,
+ duration: 5000,
+ className: 'font-zen',
});
}
}, [
diff --git a/src/hooks/useWrapLegacyMorpho.ts b/src/hooks/useWrapLegacyMorpho.ts
index 5e43fefa..d3c25f8d 100644
--- a/src/hooks/useWrapLegacyMorpho.ts
+++ b/src/hooks/useWrapLegacyMorpho.ts
@@ -1,9 +1,9 @@
import { useCallback, useState } from 'react';
-import { toast } from 'react-toastify';
import { Address, encodeFunctionData } from 'viem';
import { useAccount, useSwitchChain } from 'wagmi';
import wrapperABI from '@/abis/morpho-wrapper';
+import { useStyledToast } from '@/hooks/useStyledToast';
import { useTransactionWithToast } from '@/hooks/useTransactionWithToast';
import { SupportedNetworks } from '@/utils/networks';
import { MORPHO_LEGACY, MORPHO_TOKEN_WRAPPER } from '@/utils/tokens';
@@ -17,6 +17,7 @@ export function useWrapLegacyMorpho(amount: bigint, onSuccess?: () => void) {
const { address: account, chainId } = useAccount();
const { switchChainAsync } = useSwitchChain();
+ const toast = useStyledToast();
const { isApproved, approve } = useERC20Approval({
token: MORPHO_LEGACY as Address,
@@ -69,7 +70,7 @@ export function useWrapLegacyMorpho(amount: bigint, onSuccess?: () => void) {
toast.error('Failed to wrap MORPHO.');
setShowProcessModal(false);
}
- }, [account, amount, chainId, isApproved, approve, sendTransactionAsync, switchChainAsync]);
+ }, [account, amount, chainId, isApproved, approve, sendTransactionAsync, switchChainAsync, toast]);
return {
wrap,