Selaa lähdekoodia

refactor: replace usages of custom toaster with react-hot-toast

Nicolas Meienberger 2 vuotta sitten
vanhempi
commit
9fa8452e24

+ 13 - 50
src/client/modules/Apps/containers/AppDetailsContainer/AppDetailsContainer.test.tsx

@@ -1,9 +1,8 @@
 import React from 'react';
 import React from 'react';
-import { fireEvent, render, renderHook, screen, waitFor } from '../../../../../../tests/test-utils';
+import { fireEvent, render, screen, waitFor } from '../../../../../../tests/test-utils';
 import { createAppEntity } from '../../../../mocks/fixtures/app.fixtures';
 import { createAppEntity } from '../../../../mocks/fixtures/app.fixtures';
 import { getTRPCMock, getTRPCMockError } from '../../../../mocks/getTrpcMock';
 import { getTRPCMock, getTRPCMockError } from '../../../../mocks/getTrpcMock';
 import { server } from '../../../../mocks/server';
 import { server } from '../../../../mocks/server';
-import { useToastStore } from '../../../../state/toastStore';
 import { AppDetailsContainer } from './AppDetailsContainer';
 import { AppDetailsContainer } from './AppDetailsContainer';
 
 
 describe('Test: AppDetailsContainer', () => {
 describe('Test: AppDetailsContainer', () => {
@@ -112,7 +111,6 @@ describe('Test: AppDetailsContainer', () => {
       // Arrange
       // Arrange
       const app = createAppEntity({ overrides: { status: 'missing' } });
       const app = createAppEntity({ overrides: { status: 'missing' } });
       server.use(getTRPCMock({ path: ['app', 'installApp'], type: 'mutation', response: app }));
       server.use(getTRPCMock({ path: ['app', 'installApp'], type: 'mutation', response: app }));
-      const { result } = renderHook(() => useToastStore());
       render(<AppDetailsContainer app={app} />);
       render(<AppDetailsContainer app={app} />);
       const openModalButton = screen.getByRole('button', { name: 'Install' });
       const openModalButton = screen.getByRole('button', { name: 'Install' });
       fireEvent.click(openModalButton);
       fireEvent.click(openModalButton);
@@ -122,15 +120,12 @@ describe('Test: AppDetailsContainer', () => {
       fireEvent.click(installButton);
       fireEvent.click(installButton);
 
 
       await waitFor(() => {
       await waitFor(() => {
-        expect(result.current.toasts).toHaveLength(1);
-        expect(result.current.toasts[0].status).toEqual('success');
-        expect(result.current.toasts[0].title).toEqual('App installed successfully');
+        expect(screen.getByText('App installed successfully')).toBeInTheDocument();
       });
       });
     });
     });
 
 
     it('should display a toast error when install mutation fails', async () => {
     it('should display a toast error when install mutation fails', async () => {
       // Arrange
       // Arrange
-      const { result } = renderHook(() => useToastStore());
       server.use(
       server.use(
         getTRPCMockError({
         getTRPCMockError({
           path: ['app', 'installApp'],
           path: ['app', 'installApp'],
@@ -149,9 +144,7 @@ describe('Test: AppDetailsContainer', () => {
       fireEvent.click(installButton);
       fireEvent.click(installButton);
 
 
       await waitFor(() => {
       await waitFor(() => {
-        expect(result.current.toasts).toHaveLength(1);
-        expect(result.current.toasts[0].description).toEqual('my big error');
-        expect(result.current.toasts[0].status).toEqual('error');
+        expect(screen.getByText('Failed to install app: my big error')).toBeInTheDocument();
       });
       });
     });
     });
   });
   });
@@ -161,7 +154,6 @@ describe('Test: AppDetailsContainer', () => {
       // Arrange
       // Arrange
       const app = createAppEntity({ overrides: { version: 2, latestVersion: 3 } });
       const app = createAppEntity({ overrides: { version: 2, latestVersion: 3 } });
       server.use(getTRPCMock({ path: ['app', 'updateApp'], type: 'mutation', response: app }));
       server.use(getTRPCMock({ path: ['app', 'updateApp'], type: 'mutation', response: app }));
-      const { result } = renderHook(() => useToastStore());
       render(<AppDetailsContainer app={app} />);
       render(<AppDetailsContainer app={app} />);
       const openModalButton = screen.getByRole('button', { name: 'Update' });
       const openModalButton = screen.getByRole('button', { name: 'Update' });
       fireEvent.click(openModalButton);
       fireEvent.click(openModalButton);
@@ -171,15 +163,12 @@ describe('Test: AppDetailsContainer', () => {
       modalUpdateButton.click();
       modalUpdateButton.click();
 
 
       await waitFor(() => {
       await waitFor(() => {
-        expect(result.current.toasts).toHaveLength(1);
-        expect(result.current.toasts[0].status).toEqual('success');
-        expect(result.current.toasts[0].title).toEqual('App updated successfully');
+        expect(screen.getByText('App updated successfully')).toBeInTheDocument();
       });
       });
     });
     });
 
 
     it('should display a toast error when update mutation fails', async () => {
     it('should display a toast error when update mutation fails', async () => {
       // Arrange
       // Arrange
-      const { result } = renderHook(() => useToastStore());
       server.use(getTRPCMockError({ path: ['app', 'updateApp'], type: 'mutation', message: 'my big error' }));
       server.use(getTRPCMockError({ path: ['app', 'updateApp'], type: 'mutation', message: 'my big error' }));
       const app = createAppEntity({ overrides: { version: 2, latestVersion: 3 }, overridesInfo: { tipi_version: 3 } });
       const app = createAppEntity({ overrides: { version: 2, latestVersion: 3 }, overridesInfo: { tipi_version: 3 } });
       render(<AppDetailsContainer app={app} />);
       render(<AppDetailsContainer app={app} />);
@@ -192,9 +181,7 @@ describe('Test: AppDetailsContainer', () => {
 
 
       // Assert
       // Assert
       await waitFor(() => {
       await waitFor(() => {
-        expect(result.current.toasts).toHaveLength(1);
-        expect(result.current.toasts[0].description).toEqual('my big error');
-        expect(result.current.toasts[0].status).toEqual('error');
+        expect(screen.getByText('Failed to update app: my big error')).toBeInTheDocument();
       });
       });
     });
     });
   });
   });
@@ -204,7 +191,6 @@ describe('Test: AppDetailsContainer', () => {
       // Arrange
       // Arrange
       const app = createAppEntity({ status: 'stopped' });
       const app = createAppEntity({ status: 'stopped' });
       server.use(getTRPCMock({ path: ['app', 'uninstallApp'], type: 'mutation', response: { id: app.id, config: {}, status: 'missing' } }));
       server.use(getTRPCMock({ path: ['app', 'uninstallApp'], type: 'mutation', response: { id: app.id, config: {}, status: 'missing' } }));
-      const { result } = renderHook(() => useToastStore());
       render(<AppDetailsContainer app={app} />);
       render(<AppDetailsContainer app={app} />);
       const openModalButton = screen.getByRole('button', { name: 'Remove' });
       const openModalButton = screen.getByRole('button', { name: 'Remove' });
       fireEvent.click(openModalButton);
       fireEvent.click(openModalButton);
@@ -215,15 +201,12 @@ describe('Test: AppDetailsContainer', () => {
 
 
       // Assert
       // Assert
       await waitFor(() => {
       await waitFor(() => {
-        expect(result.current.toasts).toHaveLength(1);
-        expect(result.current.toasts[0].status).toEqual('success');
-        expect(result.current.toasts[0].title).toEqual('App uninstalled successfully');
+        expect(screen.getByText('App uninstalled successfully')).toBeInTheDocument();
       });
       });
     });
     });
 
 
     it('should display a toast error when uninstall mutation fails', async () => {
     it('should display a toast error when uninstall mutation fails', async () => {
       // Arrange
       // Arrange
-      const { result } = renderHook(() => useToastStore());
       server.use(getTRPCMockError({ path: ['app', 'uninstallApp'], type: 'mutation', message: 'my big error' }));
       server.use(getTRPCMockError({ path: ['app', 'uninstallApp'], type: 'mutation', message: 'my big error' }));
       const app = createAppEntity({ status: 'stopped' });
       const app = createAppEntity({ status: 'stopped' });
       render(<AppDetailsContainer app={app} />);
       render(<AppDetailsContainer app={app} />);
@@ -236,9 +219,7 @@ describe('Test: AppDetailsContainer', () => {
 
 
       // Assert
       // Assert
       await waitFor(() => {
       await waitFor(() => {
-        expect(result.current.toasts).toHaveLength(1);
-        expect(result.current.toasts[0].description).toEqual('my big error');
-        expect(result.current.toasts[0].status).toEqual('error');
+        expect(screen.getByText('Failed to uninstall app: my big error')).toBeInTheDocument();
       });
       });
     });
     });
   });
   });
@@ -248,7 +229,6 @@ describe('Test: AppDetailsContainer', () => {
       // Arrange
       // Arrange
       const app = createAppEntity({ status: 'stopped' });
       const app = createAppEntity({ status: 'stopped' });
       server.use(getTRPCMock({ path: ['app', 'startApp'], type: 'mutation', response: app }));
       server.use(getTRPCMock({ path: ['app', 'startApp'], type: 'mutation', response: app }));
-      const { result } = renderHook(() => useToastStore());
       render(<AppDetailsContainer app={app} />);
       render(<AppDetailsContainer app={app} />);
 
 
       // Act
       // Act
@@ -257,15 +237,12 @@ describe('Test: AppDetailsContainer', () => {
 
 
       // Assert
       // Assert
       await waitFor(() => {
       await waitFor(() => {
-        expect(result.current.toasts).toHaveLength(1);
-        expect(result.current.toasts[0].status).toEqual('success');
-        expect(result.current.toasts[0].title).toEqual('App started successfully');
+        expect(screen.getByText('App started successfully')).toBeInTheDocument();
       });
       });
     });
     });
 
 
     it('should display a toast error when start mutation fails', async () => {
     it('should display a toast error when start mutation fails', async () => {
       // Arrange
       // Arrange
-      const { result } = renderHook(() => useToastStore());
       server.use(getTRPCMockError({ path: ['app', 'startApp'], type: 'mutation', message: 'my big error' }));
       server.use(getTRPCMockError({ path: ['app', 'startApp'], type: 'mutation', message: 'my big error' }));
       const app = createAppEntity({ status: 'stopped' });
       const app = createAppEntity({ status: 'stopped' });
       render(<AppDetailsContainer app={app} />);
       render(<AppDetailsContainer app={app} />);
@@ -276,9 +253,7 @@ describe('Test: AppDetailsContainer', () => {
 
 
       // Assert
       // Assert
       await waitFor(() => {
       await waitFor(() => {
-        expect(result.current.toasts).toHaveLength(1);
-        expect(result.current.toasts[0].description).toEqual('my big error');
-        expect(result.current.toasts[0].status).toEqual('error');
+        expect(screen.getByText('Failed to start app: my big error')).toBeInTheDocument();
       });
       });
     });
     });
   });
   });
@@ -288,7 +263,6 @@ describe('Test: AppDetailsContainer', () => {
       // Arrange
       // Arrange
       const app = createAppEntity({ status: 'running' });
       const app = createAppEntity({ status: 'running' });
       server.use(getTRPCMock({ path: ['app', 'stopApp'], type: 'mutation', response: app }));
       server.use(getTRPCMock({ path: ['app', 'stopApp'], type: 'mutation', response: app }));
-      const { result } = renderHook(() => useToastStore());
       render(<AppDetailsContainer app={app} />);
       render(<AppDetailsContainer app={app} />);
       const openModalButton = screen.getByRole('button', { name: 'Stop' });
       const openModalButton = screen.getByRole('button', { name: 'Stop' });
       fireEvent.click(openModalButton);
       fireEvent.click(openModalButton);
@@ -299,15 +273,12 @@ describe('Test: AppDetailsContainer', () => {
 
 
       // Assert
       // Assert
       await waitFor(() => {
       await waitFor(() => {
-        expect(result.current.toasts).toHaveLength(1);
-        expect(result.current.toasts[0].status).toEqual('success');
-        expect(result.current.toasts[0].title).toEqual('App stopped successfully');
+        expect(screen.getByText('App stopped successfully')).toBeInTheDocument();
       });
       });
     });
     });
 
 
     it('should display a toast error when stop mutation fails', async () => {
     it('should display a toast error when stop mutation fails', async () => {
       // Arrange
       // Arrange
-      const { result } = renderHook(() => useToastStore());
       server.use(getTRPCMockError({ path: ['app', 'stopApp'], type: 'mutation', message: 'my big error' }));
       server.use(getTRPCMockError({ path: ['app', 'stopApp'], type: 'mutation', message: 'my big error' }));
       const app = createAppEntity({ status: 'running' });
       const app = createAppEntity({ status: 'running' });
       render(<AppDetailsContainer app={app} />);
       render(<AppDetailsContainer app={app} />);
@@ -320,9 +291,7 @@ describe('Test: AppDetailsContainer', () => {
 
 
       // Assert
       // Assert
       await waitFor(() => {
       await waitFor(() => {
-        expect(result.current.toasts).toHaveLength(1);
-        expect(result.current.toasts[0].description).toEqual('my big error');
-        expect(result.current.toasts[0].status).toEqual('error');
+        expect(screen.getByText('Failed to stop app: my big error')).toBeInTheDocument();
       });
       });
     });
     });
   });
   });
@@ -332,7 +301,6 @@ describe('Test: AppDetailsContainer', () => {
       // Arrange
       // Arrange
       const app = createAppEntity({ status: 'running', overridesInfo: { exposable: true } });
       const app = createAppEntity({ status: 'running', overridesInfo: { exposable: true } });
       server.use(getTRPCMock({ path: ['app', 'updateAppConfig'], type: 'mutation', response: app }));
       server.use(getTRPCMock({ path: ['app', 'updateAppConfig'], type: 'mutation', response: app }));
-      const { result } = renderHook(() => useToastStore());
       render(<AppDetailsContainer app={app} />);
       render(<AppDetailsContainer app={app} />);
       const openModalButton = screen.getByRole('button', { name: 'Settings' });
       const openModalButton = screen.getByRole('button', { name: 'Settings' });
       fireEvent.click(openModalButton);
       fireEvent.click(openModalButton);
@@ -343,15 +311,12 @@ describe('Test: AppDetailsContainer', () => {
 
 
       // Assert
       // Assert
       await waitFor(() => {
       await waitFor(() => {
-        expect(result.current.toasts).toHaveLength(1);
-        expect(result.current.toasts[0].status).toEqual('success');
-        expect(result.current.toasts[0].title).toEqual('App config updated successfully. Restart the app to apply the changes');
+        expect(screen.getByText('App config updated successfully. Restart the app to apply the changes')).toBeInTheDocument();
       });
       });
     });
     });
 
 
     it('should display a toast error when update config mutation fails', async () => {
     it('should display a toast error when update config mutation fails', async () => {
       // Arrange
       // Arrange
-      const { result } = renderHook(() => useToastStore());
       server.use(getTRPCMockError({ path: ['app', 'updateAppConfig'], type: 'mutation', message: 'my big error' }));
       server.use(getTRPCMockError({ path: ['app', 'updateAppConfig'], type: 'mutation', message: 'my big error' }));
       const app = createAppEntity({ status: 'running', overridesInfo: { exposable: true } });
       const app = createAppEntity({ status: 'running', overridesInfo: { exposable: true } });
       render(<AppDetailsContainer app={app} />);
       render(<AppDetailsContainer app={app} />);
@@ -364,9 +329,7 @@ describe('Test: AppDetailsContainer', () => {
 
 
       // Assert
       // Assert
       await waitFor(() => {
       await waitFor(() => {
-        expect(result.current.toasts).toHaveLength(1);
-        expect(result.current.toasts[0].description).toEqual('my big error');
-        expect(result.current.toasts[0].status).toEqual('error');
+        expect(screen.getByText('Failed to update app config: my big error')).toBeInTheDocument();
       });
       });
     });
     });
   });
   });

+ 13 - 14
src/client/modules/Apps/containers/AppDetailsContainer/AppDetailsContainer.tsx

@@ -1,6 +1,6 @@
 import React from 'react';
 import React from 'react';
+import { toast } from 'react-hot-toast';
 import { useDisclosure } from '../../../../hooks/useDisclosure';
 import { useDisclosure } from '../../../../hooks/useDisclosure';
-import { useToastStore } from '../../../../state/toastStore';
 import { AppLogo } from '../../../../components/AppLogo/AppLogo';
 import { AppLogo } from '../../../../components/AppLogo/AppLogo';
 import { AppStatus } from '../../../../components/AppStatus';
 import { AppStatus } from '../../../../components/AppStatus';
 import { AppActions } from '../../components/AppActions';
 import { AppActions } from '../../components/AppActions';
@@ -20,7 +20,6 @@ interface IProps {
 }
 }
 
 
 export const AppDetailsContainer: React.FC<IProps> = ({ app }) => {
 export const AppDetailsContainer: React.FC<IProps> = ({ app }) => {
-  const { addToast } = useToastStore();
   const installDisclosure = useDisclosure();
   const installDisclosure = useDisclosure();
   const uninstallDisclosure = useDisclosure();
   const uninstallDisclosure = useDisclosure();
   const stopDisclosure = useDisclosure();
   const stopDisclosure = useDisclosure();
@@ -41,11 +40,11 @@ export const AppDetailsContainer: React.FC<IProps> = ({ app }) => {
     },
     },
     onSuccess: () => {
     onSuccess: () => {
       invalidate();
       invalidate();
-      addToast({ title: 'App installed successfully', status: 'success' });
+      toast.success('App installed successfully');
     },
     },
     onError: (e) => {
     onError: (e) => {
       invalidate();
       invalidate();
-      addToast({ title: 'Install error', description: e.message, status: 'error' });
+      toast.error(`Failed to install app: ${e.message}`);
     },
     },
   });
   });
 
 
@@ -56,9 +55,9 @@ export const AppDetailsContainer: React.FC<IProps> = ({ app }) => {
     },
     },
     onSuccess: () => {
     onSuccess: () => {
       invalidate();
       invalidate();
-      addToast({ title: 'App uninstalled successfully', status: 'success' });
+      toast.success('App uninstalled successfully');
     },
     },
-    onError: (e) => addToast({ title: 'Uninstall error', description: e.message, status: 'error' }),
+    onError: (e) => toast.error(`Failed to uninstall app: ${e.message}`),
   });
   });
 
 
   const stop = trpc.app.stopApp.useMutation({
   const stop = trpc.app.stopApp.useMutation({
@@ -68,9 +67,9 @@ export const AppDetailsContainer: React.FC<IProps> = ({ app }) => {
     },
     },
     onSuccess: () => {
     onSuccess: () => {
       invalidate();
       invalidate();
-      addToast({ title: 'App stopped successfully', status: 'success' });
+      toast.success('App stopped successfully');
     },
     },
-    onError: (e) => addToast({ title: 'Stop error', description: e.message, status: 'error' }),
+    onError: (e) => toast.error(`Failed to stop app: ${e.message}`),
   });
   });
 
 
   const update = trpc.app.updateApp.useMutation({
   const update = trpc.app.updateApp.useMutation({
@@ -80,9 +79,9 @@ export const AppDetailsContainer: React.FC<IProps> = ({ app }) => {
     },
     },
     onSuccess: () => {
     onSuccess: () => {
       invalidate();
       invalidate();
-      addToast({ title: 'App updated successfully', status: 'success' });
+      toast.success('App updated successfully');
     },
     },
-    onError: (e) => addToast({ title: 'Update error', description: e.message, status: 'error' }),
+    onError: (e) => toast.error(`Failed to update app: ${e.message}`),
   });
   });
 
 
   const start = trpc.app.startApp.useMutation({
   const start = trpc.app.startApp.useMutation({
@@ -91,18 +90,18 @@ export const AppDetailsContainer: React.FC<IProps> = ({ app }) => {
     },
     },
     onSuccess: () => {
     onSuccess: () => {
       invalidate();
       invalidate();
-      addToast({ title: 'App started successfully', status: 'success' });
+      toast.success('App started successfully');
     },
     },
-    onError: (e) => addToast({ title: 'Start error', description: e.message, status: 'error' }),
+    onError: (e) => toast.error(`Failed to start app: ${e.message}`),
   });
   });
 
 
   const updateConfig = trpc.app.updateAppConfig.useMutation({
   const updateConfig = trpc.app.updateAppConfig.useMutation({
     onMutate: () => updateSettingsDisclosure.close(),
     onMutate: () => updateSettingsDisclosure.close(),
     onSuccess: () => {
     onSuccess: () => {
       invalidate();
       invalidate();
-      addToast({ title: 'App config updated successfully. Restart the app to apply the changes', status: 'success' });
+      toast.success('App config updated successfully. Restart the app to apply the changes');
     },
     },
-    onError: (e) => addToast({ title: 'Update error', description: e.message, status: 'error' }),
+    onError: (e) => toast.error(`Failed to update app config: ${e.message}`),
   });
   });
 
 
   const updateAvailable = Number(app.version || 0) < Number(app?.latestVersion || 0);
   const updateAvailable = Number(app.version || 0) < Number(app?.latestVersion || 0);

+ 3 - 10
src/client/modules/Auth/containers/LoginContainer/LoginContainer.test.tsx

@@ -1,9 +1,8 @@
 import { faker } from '@faker-js/faker';
 import { faker } from '@faker-js/faker';
 import React from 'react';
 import React from 'react';
-import { fireEvent, render, renderHook, screen, waitFor } from '../../../../../../tests/test-utils';
+import { fireEvent, render, screen, waitFor } from '../../../../../../tests/test-utils';
 import { getTRPCMock, getTRPCMockError } from '../../../../mocks/getTrpcMock';
 import { getTRPCMock, getTRPCMockError } from '../../../../mocks/getTrpcMock';
 import { server } from '../../../../mocks/server';
 import { server } from '../../../../mocks/server';
-import { useToastStore } from '../../../../state/toastStore';
 import { LoginContainer } from './LoginContainer';
 import { LoginContainer } from './LoginContainer';
 
 
 describe('Test: LoginContainer', () => {
 describe('Test: LoginContainer', () => {
@@ -64,7 +63,6 @@ describe('Test: LoginContainer', () => {
 
 
   it('should show error message if login fails', async () => {
   it('should show error message if login fails', async () => {
     // Arrange
     // Arrange
-    const { result } = renderHook(() => useToastStore());
     server.use(getTRPCMockError({ path: ['auth', 'login'], type: 'mutation', status: 500, message: 'my big error' }));
     server.use(getTRPCMockError({ path: ['auth', 'login'], type: 'mutation', status: 500, message: 'my big error' }));
     render(<LoginContainer />);
     render(<LoginContainer />);
 
 
@@ -79,9 +77,7 @@ describe('Test: LoginContainer', () => {
 
 
     // Assert
     // Assert
     await waitFor(() => {
     await waitFor(() => {
-      expect(result.current.toasts).toHaveLength(1);
-      expect(result.current.toasts[0].description).toEqual('my big error');
-      expect(result.current.toasts[0].status).toEqual('error');
+      expect(screen.getByText(/my big error/)).toBeInTheDocument();
     });
     });
     const token = localStorage.getItem('token');
     const token = localStorage.getItem('token');
     expect(token).toBeNull();
     expect(token).toBeNull();
@@ -118,7 +114,6 @@ describe('Test: LoginContainer', () => {
 
 
   it('should show error message if totp code is invalid', async () => {
   it('should show error message if totp code is invalid', async () => {
     // arrange
     // arrange
-    const { result } = renderHook(() => useToastStore());
     const email = faker.internet.email();
     const email = faker.internet.email();
     const password = faker.internet.password();
     const password = faker.internet.password();
     const totpSessionId = faker.datatype.uuid();
     const totpSessionId = faker.datatype.uuid();
@@ -150,9 +145,7 @@ describe('Test: LoginContainer', () => {
 
 
     // assert
     // assert
     await waitFor(() => {
     await waitFor(() => {
-      expect(result.current.toasts).toHaveLength(1);
-      expect(result.current.toasts[0].description).toEqual('Invalid totp code');
-      expect(result.current.toasts[0].status).toEqual('error');
+      expect(screen.getByText(/Invalid totp code/)).toBeInTheDocument();
     });
     });
   });
   });
 
 

+ 3 - 4
src/client/modules/Auth/containers/LoginContainer/LoginContainer.tsx

@@ -1,6 +1,6 @@
 import { useRouter } from 'next/router';
 import { useRouter } from 'next/router';
 import React, { useState } from 'react';
 import React, { useState } from 'react';
-import { useToastStore } from '../../../../state/toastStore';
+import { toast } from 'react-hot-toast';
 import { trpc } from '../../../../utils/trpc';
 import { trpc } from '../../../../utils/trpc';
 import { AuthFormLayout } from '../../components/AuthFormLayout';
 import { AuthFormLayout } from '../../components/AuthFormLayout';
 import { LoginForm } from '../../components/LoginForm';
 import { LoginForm } from '../../components/LoginForm';
@@ -11,12 +11,11 @@ type FormValues = { email: string; password: string };
 export const LoginContainer: React.FC = () => {
 export const LoginContainer: React.FC = () => {
   const [totpSessionId, setTotpSessionId] = useState<string | null>(null);
   const [totpSessionId, setTotpSessionId] = useState<string | null>(null);
   const router = useRouter();
   const router = useRouter();
-  const { addToast } = useToastStore();
   const utils = trpc.useContext();
   const utils = trpc.useContext();
   const login = trpc.auth.login.useMutation({
   const login = trpc.auth.login.useMutation({
     onError: (e) => {
     onError: (e) => {
       localStorage.removeItem('token');
       localStorage.removeItem('token');
-      addToast({ title: 'Login error', description: e.message, status: 'error' });
+      toast.error(`Login failed: ${e.message}`);
     },
     },
     onSuccess: (data) => {
     onSuccess: (data) => {
       if (data.totpSessionId) {
       if (data.totpSessionId) {
@@ -31,7 +30,7 @@ export const LoginContainer: React.FC = () => {
 
 
   const verifyTotp = trpc.auth.verifyTotp.useMutation({
   const verifyTotp = trpc.auth.verifyTotp.useMutation({
     onError: (e) => {
     onError: (e) => {
-      addToast({ title: 'Error', description: e.message, status: 'error' });
+      toast.error(`Verification failed: ${e.message}`);
     },
     },
     onSuccess: (data) => {
     onSuccess: (data) => {
       localStorage.setItem('token', data.token);
       localStorage.setItem('token', data.token);

+ 2 - 6
src/client/modules/Auth/containers/RegisterContainer/RegisterContainer.test.tsx

@@ -1,9 +1,8 @@
 import { faker } from '@faker-js/faker';
 import { faker } from '@faker-js/faker';
 import React from 'react';
 import React from 'react';
-import { fireEvent, render, renderHook, screen, waitFor } from '../../../../../../tests/test-utils';
+import { fireEvent, render, screen, waitFor } from '../../../../../../tests/test-utils';
 import { getTRPCMock, getTRPCMockError } from '../../../../mocks/getTrpcMock';
 import { getTRPCMock, getTRPCMockError } from '../../../../mocks/getTrpcMock';
 import { server } from '../../../../mocks/server';
 import { server } from '../../../../mocks/server';
-import { useToastStore } from '../../../../state/toastStore';
 import { RegisterContainer } from './RegisterContainer';
 import { RegisterContainer } from './RegisterContainer';
 
 
 describe('Test: RegisterContainer', () => {
 describe('Test: RegisterContainer', () => {
@@ -42,7 +41,6 @@ describe('Test: RegisterContainer', () => {
     const email = faker.internet.email();
     const email = faker.internet.email();
     const password = faker.internet.password();
     const password = faker.internet.password();
 
 
-    const { result } = renderHook(() => useToastStore());
     server.use(getTRPCMockError({ path: ['auth', 'register'], type: 'mutation', status: 500, message: 'my big error' }));
     server.use(getTRPCMockError({ path: ['auth', 'register'], type: 'mutation', status: 500, message: 'my big error' }));
     render(<RegisterContainer />);
     render(<RegisterContainer />);
 
 
@@ -59,9 +57,7 @@ describe('Test: RegisterContainer', () => {
 
 
     // Assert
     // Assert
     await waitFor(() => {
     await waitFor(() => {
-      expect(result.current.toasts).toHaveLength(1);
-      expect(result.current.toasts[0].description).toEqual('my big error');
-      expect(result.current.toasts[0].status).toEqual('error');
+      expect(screen.getByText('Registration failed: my big error')).toBeInTheDocument();
     });
     });
   });
   });
 });
 });

+ 2 - 3
src/client/modules/Auth/containers/RegisterContainer/RegisterContainer.tsx

@@ -1,6 +1,6 @@
 import { useRouter } from 'next/router';
 import { useRouter } from 'next/router';
 import React from 'react';
 import React from 'react';
-import { useToastStore } from '../../../../state/toastStore';
+import { toast } from 'react-hot-toast';
 import { trpc } from '../../../../utils/trpc';
 import { trpc } from '../../../../utils/trpc';
 import { AuthFormLayout } from '../../components/AuthFormLayout';
 import { AuthFormLayout } from '../../components/AuthFormLayout';
 import { RegisterForm } from '../../components/RegisterForm';
 import { RegisterForm } from '../../components/RegisterForm';
@@ -8,13 +8,12 @@ import { RegisterForm } from '../../components/RegisterForm';
 type FormValues = { email: string; password: string };
 type FormValues = { email: string; password: string };
 
 
 export const RegisterContainer: React.FC = () => {
 export const RegisterContainer: React.FC = () => {
-  const { addToast } = useToastStore();
   const router = useRouter();
   const router = useRouter();
   const utils = trpc.useContext();
   const utils = trpc.useContext();
   const register = trpc.auth.register.useMutation({
   const register = trpc.auth.register.useMutation({
     onError: (e) => {
     onError: (e) => {
       localStorage.removeItem('token');
       localStorage.removeItem('token');
-      addToast({ title: 'Register error', description: e.message, status: 'error' });
+      toast.error(`Registration failed: ${e.message}`);
     },
     },
     onSuccess: (data) => {
     onSuccess: (data) => {
       localStorage.setItem('token', data.token);
       localStorage.setItem('token', data.token);

+ 3 - 10
src/client/modules/Auth/containers/ResetPasswordContainer/ResetPasswordContainer.test.tsx

@@ -1,8 +1,7 @@
 import React from 'react';
 import React from 'react';
-import { fireEvent, render, screen, waitFor, renderHook } from '../../../../../../tests/test-utils';
+import { fireEvent, render, screen, waitFor } from '../../../../../../tests/test-utils';
 import { getTRPCMock, getTRPCMockError } from '../../../../mocks/getTrpcMock';
 import { getTRPCMock, getTRPCMockError } from '../../../../mocks/getTrpcMock';
 import { server } from '../../../../mocks/server';
 import { server } from '../../../../mocks/server';
-import { useToastStore } from '../../../../state/toastStore';
 import { ResetPasswordContainer } from './ResetPasswordContainer';
 import { ResetPasswordContainer } from './ResetPasswordContainer';
 
 
 const pushFn = jest.fn();
 const pushFn = jest.fn();
@@ -55,7 +54,6 @@ describe('ResetPasswordContainer', () => {
 
 
   it('should show error toast if reset password mutation fails', async () => {
   it('should show error toast if reset password mutation fails', async () => {
     // Arrange
     // Arrange
-    const { result, unmount } = renderHook(() => useToastStore());
     render(<ResetPasswordContainer isRequested />);
     render(<ResetPasswordContainer isRequested />);
     const resetPasswordForm = screen.getByRole('button', { name: 'Reset password' });
     const resetPasswordForm = screen.getByRole('button', { name: 'Reset password' });
     fireEvent.click(resetPasswordForm);
     fireEvent.click(resetPasswordForm);
@@ -74,15 +72,12 @@ describe('ResetPasswordContainer', () => {
 
 
     // Assert
     // Assert
     await waitFor(() => {
     await waitFor(() => {
-      expect(result.current.toasts[0].description).toBe(error.message);
+      expect(screen.getByText(/Something went wrong/)).toBeInTheDocument();
     });
     });
-
-    unmount();
   });
   });
 
 
   it('should call the cancel request mutation when cancel button is clicked', async () => {
   it('should call the cancel request mutation when cancel button is clicked', async () => {
     // Arrange
     // Arrange
-    const { result, unmount } = renderHook(() => useToastStore());
     render(<ResetPasswordContainer isRequested />);
     render(<ResetPasswordContainer isRequested />);
     server.use(getTRPCMock({ path: ['auth', 'cancelPasswordChangeRequest'], type: 'mutation', response: true }));
     server.use(getTRPCMock({ path: ['auth', 'cancelPasswordChangeRequest'], type: 'mutation', response: true }));
 
 
@@ -93,10 +88,8 @@ describe('ResetPasswordContainer', () => {
 
 
     // Assert
     // Assert
     await waitFor(() => {
     await waitFor(() => {
-      expect(result.current.toasts[0].title).toBe('Password change request cancelled');
+      expect(screen.getByText('Password change request cancelled')).toBeInTheDocument();
     });
     });
-
-    unmount();
   });
   });
 
 
   it('should redirect to login page when Back to login button is clicked', async () => {
   it('should redirect to login page when Back to login button is clicked', async () => {

+ 3 - 4
src/client/modules/Auth/containers/ResetPasswordContainer/ResetPasswordContainer.tsx

@@ -1,7 +1,7 @@
 import { useRouter } from 'next/router';
 import { useRouter } from 'next/router';
 import React from 'react';
 import React from 'react';
+import { toast } from 'react-hot-toast';
 import { Button } from '../../../../components/ui/Button';
 import { Button } from '../../../../components/ui/Button';
-import { useToastStore } from '../../../../state/toastStore';
 import { trpc } from '../../../../utils/trpc';
 import { trpc } from '../../../../utils/trpc';
 import { AuthFormLayout } from '../../components/AuthFormLayout';
 import { AuthFormLayout } from '../../components/AuthFormLayout';
 import { ResetPasswordForm } from '../../components/ResetPasswordForm';
 import { ResetPasswordForm } from '../../components/ResetPasswordForm';
@@ -13,7 +13,6 @@ type Props = {
 type FormValues = { password: string };
 type FormValues = { password: string };
 
 
 export const ResetPasswordContainer: React.FC<Props> = ({ isRequested }) => {
 export const ResetPasswordContainer: React.FC<Props> = ({ isRequested }) => {
-  const { addToast } = useToastStore();
   const router = useRouter();
   const router = useRouter();
   const utils = trpc.useContext();
   const utils = trpc.useContext();
   const resetPassword = trpc.auth.changeOperatorPassword.useMutation({
   const resetPassword = trpc.auth.changeOperatorPassword.useMutation({
@@ -21,13 +20,13 @@ export const ResetPasswordContainer: React.FC<Props> = ({ isRequested }) => {
       utils.auth.checkPasswordChangeRequest.invalidate();
       utils.auth.checkPasswordChangeRequest.invalidate();
     },
     },
     onError: (error) => {
     onError: (error) => {
-      addToast({ title: 'Reset password error', description: error.message, status: 'error' });
+      toast.error(`Failed to reset password ${error.message}`);
     },
     },
   });
   });
   const cancelRequest = trpc.auth.cancelPasswordChangeRequest.useMutation({
   const cancelRequest = trpc.auth.cancelPasswordChangeRequest.useMutation({
     onSuccess: () => {
     onSuccess: () => {
       utils.auth.checkPasswordChangeRequest.invalidate();
       utils.auth.checkPasswordChangeRequest.invalidate();
-      addToast({ title: 'Password change request cancelled', status: 'success' });
+      toast.success('Password change request cancelled');
     },
     },
   });
   });
 
 

+ 2 - 11
src/client/modules/Settings/components/ChangePasswordForm/ChangePasswordForm.test.tsx

@@ -1,8 +1,6 @@
 import React from 'react';
 import React from 'react';
 import { server } from '@/client/mocks/server';
 import { server } from '@/client/mocks/server';
 import { getTRPCMock, getTRPCMockError } from '@/client/mocks/getTrpcMock';
 import { getTRPCMock, getTRPCMockError } from '@/client/mocks/getTrpcMock';
-import { useToastStore } from '@/client/state/toastStore';
-import { renderHook } from '@testing-library/react';
 import { faker } from '@faker-js/faker';
 import { faker } from '@faker-js/faker';
 import { render, screen, waitFor, fireEvent } from '../../../../../../tests/test-utils';
 import { render, screen, waitFor, fireEvent } from '../../../../../../tests/test-utils';
 import { ChangePasswordForm } from './ChangePasswordForm';
 import { ChangePasswordForm } from './ChangePasswordForm';
@@ -10,7 +8,6 @@ import { ChangePasswordForm } from './ChangePasswordForm';
 describe('<ChangePasswordForm />', () => {
 describe('<ChangePasswordForm />', () => {
   it('should show success toast upon password change', async () => {
   it('should show success toast upon password change', async () => {
     // arrange
     // arrange
-    const { result } = renderHook(() => useToastStore());
     server.use(getTRPCMock({ path: ['auth', 'changePassword'], type: 'mutation', response: true }));
     server.use(getTRPCMock({ path: ['auth', 'changePassword'], type: 'mutation', response: true }));
     render(<ChangePasswordForm />);
     render(<ChangePasswordForm />);
     const currentPasswordInput = screen.getByRole('textbox', { name: 'currentPassword' });
     const currentPasswordInput = screen.getByRole('textbox', { name: 'currentPassword' });
@@ -27,15 +24,12 @@ describe('<ChangePasswordForm />', () => {
 
 
     // assert
     // assert
     await waitFor(() => {
     await waitFor(() => {
-      expect(result.current.toasts).toHaveLength(1);
-      expect(result.current.toasts[0]?.status).toEqual('success');
-      expect(result.current.toasts[0]?.description).toEqual('Password successfully changed');
+      expect(screen.getByText('Password successfully changed')).toBeInTheDocument();
     });
     });
   });
   });
 
 
   it('should show error toast if change password failed', async () => {
   it('should show error toast if change password failed', async () => {
     // arrange
     // arrange
-    const { result } = renderHook(() => useToastStore());
     server.use(getTRPCMockError({ path: ['auth', 'changePassword'], type: 'mutation', message: 'Invalid password' }));
     server.use(getTRPCMockError({ path: ['auth', 'changePassword'], type: 'mutation', message: 'Invalid password' }));
     render(<ChangePasswordForm />);
     render(<ChangePasswordForm />);
     const currentPasswordInput = screen.getByRole('textbox', { name: 'currentPassword' });
     const currentPasswordInput = screen.getByRole('textbox', { name: 'currentPassword' });
@@ -52,10 +46,7 @@ describe('<ChangePasswordForm />', () => {
 
 
     // assert
     // assert
     await waitFor(() => {
     await waitFor(() => {
-      expect(result.current.toasts).toHaveLength(1);
-      expect(result.current.toasts[0]?.status).toEqual('error');
-      expect(result.current.toasts[0]?.title).toEqual('Error');
-      expect(result.current.toasts[0]?.description).toEqual('Invalid password');
+      expect(screen.getByText(/Invalid password/)).toBeInTheDocument();
     });
     });
   });
   });
 
 

+ 3 - 4
src/client/modules/Settings/components/ChangePasswordForm/ChangePasswordForm.tsx

@@ -2,11 +2,11 @@ import React from 'react';
 import { Input } from '@/components/ui/Input';
 import { Input } from '@/components/ui/Input';
 import { Button } from '@/components/ui/Button';
 import { Button } from '@/components/ui/Button';
 import { trpc } from '@/utils/trpc';
 import { trpc } from '@/utils/trpc';
-import { useToastStore } from '@/client/state/toastStore';
 import { useForm } from 'react-hook-form';
 import { useForm } from 'react-hook-form';
 import { z } from 'zod';
 import { z } from 'zod';
 import { zodResolver } from '@hookform/resolvers/zod';
 import { zodResolver } from '@hookform/resolvers/zod';
 import { useRouter } from 'next/router';
 import { useRouter } from 'next/router';
+import { toast } from 'react-hot-toast';
 
 
 const schema = z
 const schema = z
   .object({
   .object({
@@ -28,13 +28,12 @@ type FormValues = z.infer<typeof schema>;
 
 
 export const ChangePasswordForm = () => {
 export const ChangePasswordForm = () => {
   const router = useRouter();
   const router = useRouter();
-  const { addToast } = useToastStore();
   const changePassword = trpc.auth.changePassword.useMutation({
   const changePassword = trpc.auth.changePassword.useMutation({
     onError: (e) => {
     onError: (e) => {
-      addToast({ title: 'Error', description: e.message, status: 'error' });
+      toast.error(`Error changing password: ${e.message}`);
     },
     },
     onSuccess: () => {
     onSuccess: () => {
-      addToast({ title: 'Success', description: 'Password successfully changed', status: 'success' });
+      toast.success('Password successfully changed');
       router.push('/');
       router.push('/');
     },
     },
   });
   });

+ 5 - 27
src/client/modules/Settings/components/OtpForm/OptForm.test.tsx

@@ -1,8 +1,6 @@
 import React from 'react';
 import React from 'react';
 import { server } from '@/client/mocks/server';
 import { server } from '@/client/mocks/server';
 import { getTRPCMock, getTRPCMockError } from '@/client/mocks/getTrpcMock';
 import { getTRPCMock, getTRPCMockError } from '@/client/mocks/getTrpcMock';
-import { useToastStore } from '@/client/state/toastStore';
-import { renderHook } from '@testing-library/react';
 import { render, screen, waitFor, fireEvent } from '../../../../../../tests/test-utils';
 import { render, screen, waitFor, fireEvent } from '../../../../../../tests/test-utils';
 import { OtpForm } from './OtpForm';
 import { OtpForm } from './OtpForm';
 
 
@@ -48,7 +46,6 @@ describe('<OtpForm />', () => {
 
 
   it('should show show error toast if password is incorrect while enabling 2FA', async () => {
   it('should show show error toast if password is incorrect while enabling 2FA', async () => {
     // arrange
     // arrange
-    const { result } = renderHook(() => useToastStore());
     server.use(getTRPCMock({ path: ['auth', 'me'], response: { totp_enabled: false, id: 12, username: 'test' } }));
     server.use(getTRPCMock({ path: ['auth', 'me'], response: { totp_enabled: false, id: 12, username: 'test' } }));
     server.use(getTRPCMockError({ path: ['auth', 'getTotpUri'], type: 'mutation', message: 'Invalid password' }));
     server.use(getTRPCMockError({ path: ['auth', 'getTotpUri'], type: 'mutation', message: 'Invalid password' }));
     render(<OtpForm />);
     render(<OtpForm />);
@@ -71,16 +68,12 @@ describe('<OtpForm />', () => {
 
 
     // assert
     // assert
     await waitFor(() => {
     await waitFor(() => {
-      expect(result.current.toasts).toHaveLength(1);
-      expect(result.current.toasts[0]?.status).toEqual('error');
-      expect(result.current.toasts[0]?.title).toEqual('Error');
-      expect(result.current.toasts[0]?.description).toEqual('Invalid password');
+      expect(screen.getByText(/Invalid password/)).toBeInTheDocument();
     });
     });
   });
   });
 
 
   it('should show show error toast if password is incorrect while disabling 2FA', async () => {
   it('should show show error toast if password is incorrect while disabling 2FA', async () => {
     // arrange
     // arrange
-    const { result } = renderHook(() => useToastStore());
     server.use(getTRPCMock({ path: ['auth', 'me'], response: { totp_enabled: true, id: 12, username: 'test' } }));
     server.use(getTRPCMock({ path: ['auth', 'me'], response: { totp_enabled: true, id: 12, username: 'test' } }));
     server.use(getTRPCMockError({ path: ['auth', 'disableTotp'], type: 'mutation', message: 'Invalid password' }));
     server.use(getTRPCMockError({ path: ['auth', 'disableTotp'], type: 'mutation', message: 'Invalid password' }));
     render(<OtpForm />);
     render(<OtpForm />);
@@ -104,16 +97,12 @@ describe('<OtpForm />', () => {
 
 
     // assert
     // assert
     await waitFor(() => {
     await waitFor(() => {
-      expect(result.current.toasts).toHaveLength(1);
-      expect(result.current.toasts[0]?.status).toEqual('error');
-      expect(result.current.toasts[0]?.title).toEqual('Error');
-      expect(result.current.toasts[0]?.description).toEqual('Invalid password');
+      expect(screen.getByText(/Invalid password/)).toBeInTheDocument();
     });
     });
   });
   });
 
 
   it('should show success toast if password is correct while disabling 2FA', async () => {
   it('should show success toast if password is correct while disabling 2FA', async () => {
     // arrange
     // arrange
-    const { result } = renderHook(() => useToastStore());
     server.use(getTRPCMock({ path: ['auth', 'me'], response: { totp_enabled: true, id: 12, username: 'test' } }));
     server.use(getTRPCMock({ path: ['auth', 'me'], response: { totp_enabled: true, id: 12, username: 'test' } }));
     server.use(getTRPCMock({ path: ['auth', 'disableTotp'], type: 'mutation', response: true }));
     server.use(getTRPCMock({ path: ['auth', 'disableTotp'], type: 'mutation', response: true }));
 
 
@@ -138,10 +127,7 @@ describe('<OtpForm />', () => {
 
 
     // assert
     // assert
     await waitFor(() => {
     await waitFor(() => {
-      expect(result.current.toasts).toHaveLength(1);
-      expect(result.current.toasts[0]?.status).toEqual('success');
-      expect(result.current.toasts[0]?.title).toEqual('Success');
-      expect(result.current.toasts[0]?.description).toEqual('Two-factor authentication disabled');
+      expect(screen.getByText('Two-factor authentication disabled')).toBeInTheDocument();
     });
     });
   });
   });
 
 
@@ -174,7 +160,6 @@ describe('<OtpForm />', () => {
 
 
   it('should show error toast if submitted totp code is invalid', async () => {
   it('should show error toast if submitted totp code is invalid', async () => {
     // arrange
     // arrange
-    const { result } = renderHook(() => useToastStore());
     server.use(getTRPCMock({ path: ['auth', 'getTotpUri'], type: 'mutation', response: { key: 'test', uri: 'test' } }));
     server.use(getTRPCMock({ path: ['auth', 'getTotpUri'], type: 'mutation', response: { key: 'test', uri: 'test' } }));
     server.use(getTRPCMockError({ path: ['auth', 'setupTotp'], type: 'mutation', message: 'Invalid code' }));
     server.use(getTRPCMockError({ path: ['auth', 'setupTotp'], type: 'mutation', message: 'Invalid code' }));
 
 
@@ -210,16 +195,12 @@ describe('<OtpForm />', () => {
 
 
     // assert
     // assert
     await waitFor(() => {
     await waitFor(() => {
-      expect(result.current.toasts).toHaveLength(1);
-      expect(result.current.toasts[0]?.status).toEqual('error');
-      expect(result.current.toasts[0]?.title).toEqual('Error');
-      expect(result.current.toasts[0]?.description).toEqual('Invalid code');
+      expect(screen.getByText(/Invalid code/)).toBeInTheDocument();
     });
     });
   });
   });
 
 
   it('should show success toast if submitted totp code is valid', async () => {
   it('should show success toast if submitted totp code is valid', async () => {
     // arrange
     // arrange
-    const { result } = renderHook(() => useToastStore());
     server.use(getTRPCMock({ path: ['auth', 'getTotpUri'], type: 'mutation', response: { key: 'test', uri: 'test' } }));
     server.use(getTRPCMock({ path: ['auth', 'getTotpUri'], type: 'mutation', response: { key: 'test', uri: 'test' } }));
     server.use(getTRPCMock({ path: ['auth', 'setupTotp'], type: 'mutation', response: true }));
     server.use(getTRPCMock({ path: ['auth', 'setupTotp'], type: 'mutation', response: true }));
     render(<OtpForm />);
     render(<OtpForm />);
@@ -253,10 +234,7 @@ describe('<OtpForm />', () => {
 
 
     // assert
     // assert
     await waitFor(() => {
     await waitFor(() => {
-      expect(result.current.toasts).toHaveLength(1);
-      expect(result.current.toasts[0]?.status).toEqual('success');
-      expect(result.current.toasts[0]?.title).toEqual('Success');
-      expect(result.current.toasts[0]?.description).toEqual('Two-factor authentication enabled');
+      expect(screen.getByText('Two-factor authentication enabled')).toBeInTheDocument();
     });
     });
   });
   });
 
 

+ 6 - 7
src/client/modules/Settings/components/OtpForm/OtpForm.tsx

@@ -1,15 +1,14 @@
 import React from 'react';
 import React from 'react';
 import { trpc } from '@/utils/trpc';
 import { trpc } from '@/utils/trpc';
 import { Switch } from '@/components/ui/Switch';
 import { Switch } from '@/components/ui/Switch';
-import { useToastStore } from '@/client/state/toastStore';
 import { Button } from '@/components/ui/Button';
 import { Button } from '@/components/ui/Button';
 import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/Dialog';
 import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/Dialog';
 import { Input } from '@/components/ui/Input';
 import { Input } from '@/components/ui/Input';
 import { QRCodeSVG } from 'qrcode.react';
 import { QRCodeSVG } from 'qrcode.react';
 import { OtpInput } from '@/components/ui/OtpInput';
 import { OtpInput } from '@/components/ui/OtpInput';
+import { toast } from 'react-hot-toast';
 
 
 export const OtpForm = () => {
 export const OtpForm = () => {
-  const { addToast } = useToastStore();
   const [password, setPassword] = React.useState('');
   const [password, setPassword] = React.useState('');
   const [key, setKey] = React.useState('');
   const [key, setKey] = React.useState('');
   const [uri, setUri] = React.useState('');
   const [uri, setUri] = React.useState('');
@@ -28,7 +27,7 @@ export const OtpForm = () => {
     },
     },
     onError: (e) => {
     onError: (e) => {
       setPassword('');
       setPassword('');
-      addToast({ title: 'Error', description: e.message, status: 'error' });
+      toast.error(`Error getting TOTP URI: ${e.message}`);
     },
     },
     onSuccess: (data) => {
     onSuccess: (data) => {
       setKey(data.key);
       setKey(data.key);
@@ -40,13 +39,13 @@ export const OtpForm = () => {
     onMutate: () => {},
     onMutate: () => {},
     onError: (e) => {
     onError: (e) => {
       setTotpCode('');
       setTotpCode('');
-      addToast({ title: 'Error', description: e.message, status: 'error' });
+      toast.error(`Error setting up TOTP: ${e.message}`);
     },
     },
     onSuccess: () => {
     onSuccess: () => {
       setTotpCode('');
       setTotpCode('');
       setKey('');
       setKey('');
       setUri('');
       setUri('');
-      addToast({ title: 'Success', description: 'Two-factor authentication enabled', status: 'success' });
+      toast.success('Two-factor authentication enabled');
       ctx.auth.me.invalidate();
       ctx.auth.me.invalidate();
     },
     },
   });
   });
@@ -57,10 +56,10 @@ export const OtpForm = () => {
     },
     },
     onError: (e) => {
     onError: (e) => {
       setPassword('');
       setPassword('');
-      addToast({ title: 'Error', description: e.message, status: 'error' });
+      toast.error(`Error disabling TOTP: ${e.message}`);
     },
     },
     onSuccess: () => {
     onSuccess: () => {
-      addToast({ title: 'Success', description: 'Two-factor authentication disabled', status: 'success' });
+      toast.success('Two-factor authentication disabled');
       ctx.auth.me.invalidate();
       ctx.auth.me.invalidate();
     },
     },
   });
   });

+ 3 - 12
src/client/modules/Settings/containers/GeneralActions/GeneralActions.test.tsx

@@ -1,9 +1,8 @@
 import React from 'react';
 import React from 'react';
-import { useToastStore } from '@/client/state/toastStore';
 import { getTRPCMock, getTRPCMockError } from '@/client/mocks/getTrpcMock';
 import { getTRPCMock, getTRPCMockError } from '@/client/mocks/getTrpcMock';
 import { server } from '@/client/mocks/server';
 import { server } from '@/client/mocks/server';
 import { GeneralActions } from './GeneralActions';
 import { GeneralActions } from './GeneralActions';
-import { fireEvent, render, renderHook, screen, waitFor } from '../../../../../../tests/test-utils';
+import { fireEvent, render, screen, waitFor } from '../../../../../../tests/test-utils';
 
 
 describe('Test: GeneralActions', () => {
 describe('Test: GeneralActions', () => {
   it('should render without error', () => {
   it('should render without error', () => {
@@ -14,7 +13,6 @@ describe('Test: GeneralActions', () => {
 
 
   it('should show toast if update mutation fails', async () => {
   it('should show toast if update mutation fails', async () => {
     // arrange
     // arrange
-    const { result } = renderHook(() => useToastStore());
     server.use(getTRPCMock({ path: ['system', 'getVersion'], response: { current: '1.0.0', latest: '2.0.0' } }));
     server.use(getTRPCMock({ path: ['system', 'getVersion'], response: { current: '1.0.0', latest: '2.0.0' } }));
     server.use(getTRPCMockError({ path: ['system', 'update'], type: 'mutation', status: 500, message: 'Something went wrong' }));
     server.use(getTRPCMockError({ path: ['system', 'update'], type: 'mutation', status: 500, message: 'Something went wrong' }));
     render(<GeneralActions />);
     render(<GeneralActions />);
@@ -30,10 +28,7 @@ describe('Test: GeneralActions', () => {
 
 
     // assert
     // assert
     await waitFor(() => {
     await waitFor(() => {
-      expect(result.current.toasts).toHaveLength(1);
-      expect(result.current.toasts[0].status).toEqual('error');
-      expect(result.current.toasts[0].title).toEqual('Error');
-      expect(result.current.toasts[0].description).toEqual('Something went wrong');
+      expect(screen.getByText(/Something went wrong/)).toBeInTheDocument();
     });
     });
   });
   });
 
 
@@ -61,7 +56,6 @@ describe('Test: GeneralActions', () => {
 
 
   it('should show toast if restart mutation fails', async () => {
   it('should show toast if restart mutation fails', async () => {
     // arrange
     // arrange
-    const { result } = renderHook(() => useToastStore());
     server.use(getTRPCMockError({ path: ['system', 'restart'], type: 'mutation', status: 500, message: 'Something went wrong' }));
     server.use(getTRPCMockError({ path: ['system', 'restart'], type: 'mutation', status: 500, message: 'Something went wrong' }));
     render(<GeneralActions />);
     render(<GeneralActions />);
     const restartButton = screen.getByRole('button', { name: /Restart/i });
     const restartButton = screen.getByRole('button', { name: /Restart/i });
@@ -73,10 +67,7 @@ describe('Test: GeneralActions', () => {
 
 
     // assert
     // assert
     await waitFor(() => {
     await waitFor(() => {
-      expect(result.current.toasts).toHaveLength(1);
-      expect(result.current.toasts[0].status).toEqual('error');
-      expect(result.current.toasts[0].title).toEqual('Error');
-      expect(result.current.toasts[0].description).toEqual('Something went wrong');
+      expect(screen.getByText(/Something went wrong/)).toBeInTheDocument();
     });
     });
   });
   });
 
 

+ 3 - 4
src/client/modules/Settings/containers/GeneralActions/GeneralActions.tsx

@@ -1,8 +1,8 @@
 import React from 'react';
 import React from 'react';
 import semver from 'semver';
 import semver from 'semver';
+import { toast } from 'react-hot-toast';
 import { Button } from '../../../../components/ui/Button';
 import { Button } from '../../../../components/ui/Button';
 import { useDisclosure } from '../../../../hooks/useDisclosure';
 import { useDisclosure } from '../../../../hooks/useDisclosure';
-import { useToastStore } from '../../../../state/toastStore';
 import { RestartModal } from '../../components/RestartModal';
 import { RestartModal } from '../../components/RestartModal';
 import { UpdateModal } from '../../components/UpdateModal/UpdateModal';
 import { UpdateModal } from '../../components/UpdateModal/UpdateModal';
 import { trpc } from '../../../../utils/trpc';
 import { trpc } from '../../../../utils/trpc';
@@ -12,7 +12,6 @@ export const GeneralActions = () => {
   const versionQuery = trpc.system.getVersion.useQuery(undefined, { staleTime: 0 });
   const versionQuery = trpc.system.getVersion.useQuery(undefined, { staleTime: 0 });
 
 
   const [loading, setLoading] = React.useState(false);
   const [loading, setLoading] = React.useState(false);
-  const { addToast } = useToastStore();
   const { setPollStatus } = useSystemStore();
   const { setPollStatus } = useSystemStore();
   const restartDisclosure = useDisclosure();
   const restartDisclosure = useDisclosure();
   const updateDisclosure = useDisclosure();
   const updateDisclosure = useDisclosure();
@@ -30,7 +29,7 @@ export const GeneralActions = () => {
     },
     },
     onError: (error) => {
     onError: (error) => {
       updateDisclosure.close();
       updateDisclosure.close();
-      addToast({ title: 'Error', description: error.message, status: 'error' });
+      toast.error(`Error updating instance: ${error.message}`);
     },
     },
     onSettled: () => {
     onSettled: () => {
       setLoading(false);
       setLoading(false);
@@ -47,7 +46,7 @@ export const GeneralActions = () => {
     },
     },
     onError: (error) => {
     onError: (error) => {
       restartDisclosure.close();
       restartDisclosure.close();
-      addToast({ title: 'Error', description: error.message, status: 'error' });
+      toast.error(`Error restarting instance: ${error.message}`);
     },
     },
     onSettled: () => {
     onSettled: () => {
       setLoading(false);
       setLoading(false);

+ 3 - 9
src/client/modules/Settings/containers/SettingsContainer/SettingsContainer.test.tsx

@@ -1,9 +1,8 @@
 import React from 'react';
 import React from 'react';
 import { server } from '@/client/mocks/server';
 import { server } from '@/client/mocks/server';
 import { getTRPCMockError } from '@/client/mocks/getTrpcMock';
 import { getTRPCMockError } from '@/client/mocks/getTrpcMock';
-import { useToastStore } from '../../../../state/toastStore';
 import { SettingsContainer } from './SettingsContainer';
 import { SettingsContainer } from './SettingsContainer';
-import { fireEvent, render, renderHook, screen, waitFor } from '../../../../../../tests/test-utils';
+import { fireEvent, render, screen, waitFor } from '../../../../../../tests/test-utils';
 
 
 describe('Test: SettingsContainer', () => {
 describe('Test: SettingsContainer', () => {
   it('should render without error', () => {
   it('should render without error', () => {
@@ -14,7 +13,6 @@ describe('Test: SettingsContainer', () => {
 
 
   it('should show toast if updateSettings mutation fails', async () => {
   it('should show toast if updateSettings mutation fails', async () => {
     // arrange
     // arrange
-    const { result } = renderHook(() => useToastStore());
     server.use(getTRPCMockError({ path: ['system', 'updateSettings'], type: 'mutation', status: 500, message: 'Something went wrong' }));
     server.use(getTRPCMockError({ path: ['system', 'updateSettings'], type: 'mutation', status: 500, message: 'Something went wrong' }));
     render(<SettingsContainer />);
     render(<SettingsContainer />);
     const submitButton = screen.getByRole('button', { name: 'Save' });
     const submitButton = screen.getByRole('button', { name: 'Save' });
@@ -28,9 +26,7 @@ describe('Test: SettingsContainer', () => {
 
 
     // assert
     // assert
     await waitFor(() => {
     await waitFor(() => {
-      expect(result.current.toasts).toHaveLength(1);
-      expect(result.current.toasts[0].status).toEqual('error');
-      expect(result.current.toasts[0].title).toEqual('Error saving settings');
+      expect(screen.getByText(/Something went wrong/)).toBeInTheDocument();
     });
     });
   });
   });
 
 
@@ -50,7 +46,6 @@ describe('Test: SettingsContainer', () => {
 
 
   it('should show toast if updateSettings mutation succeeds', async () => {
   it('should show toast if updateSettings mutation succeeds', async () => {
     // arrange
     // arrange
-    const { result } = renderHook(() => useToastStore());
     render(<SettingsContainer />);
     render(<SettingsContainer />);
     const submitButton = screen.getByRole('button', { name: 'Save' });
     const submitButton = screen.getByRole('button', { name: 'Save' });
 
 
@@ -59,8 +54,7 @@ describe('Test: SettingsContainer', () => {
 
 
     // assert
     // assert
     await waitFor(() => {
     await waitFor(() => {
-      expect(result.current.toasts).toHaveLength(1);
-      expect(result.current.toasts[0].status).toEqual('success');
+      expect(screen.getByText(/Settings updated. Restart your instance to apply new settings./)).toBeInTheDocument();
     });
     });
   });
   });
 });
 });

+ 3 - 4
src/client/modules/Settings/containers/SettingsContainer/SettingsContainer.tsx

@@ -1,22 +1,21 @@
 import React, { useState } from 'react';
 import React, { useState } from 'react';
 import { trpc } from '@/utils/trpc';
 import { trpc } from '@/utils/trpc';
-import { useToastStore } from '../../../../state/toastStore';
+import { toast } from 'react-hot-toast';
 import { SettingsForm, SettingsFormValues } from '../../components/SettingsForm';
 import { SettingsForm, SettingsFormValues } from '../../components/SettingsForm';
 
 
 export const SettingsContainer = () => {
 export const SettingsContainer = () => {
   const [errors, setErrors] = useState<Record<string, string>>({});
   const [errors, setErrors] = useState<Record<string, string>>({});
-  const { addToast } = useToastStore();
   const getSettings = trpc.system.getSettings.useQuery();
   const getSettings = trpc.system.getSettings.useQuery();
   const updateSettings = trpc.system.updateSettings.useMutation({
   const updateSettings = trpc.system.updateSettings.useMutation({
     onSuccess: () => {
     onSuccess: () => {
-      addToast({ title: 'Settings updated', description: 'Restart your instance for settings to take effect', status: 'success' });
+      toast.success('Settings updated. Restart your instance to apply new settings.');
     },
     },
     onError: (e) => {
     onError: (e) => {
       if (e.shape?.data.zodError) {
       if (e.shape?.data.zodError) {
         setErrors(e.shape.data.zodError);
         setErrors(e.shape.data.zodError);
       }
       }
 
 
-      addToast({ title: 'Error saving settings', description: e.message, status: 'error' });
+      toast.error(`Error saving settings: ${e.message}`);
     },
     },
   });
   });