Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/refactor app #66

Merged
merged 15 commits into from
Sep 1, 2024
35 changes: 27 additions & 8 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,37 @@ module.exports = {
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
project: ['./tsconfig.json', './tsconfig.node.json', "./tsconfig.app.json"],
project: ['./tsconfig.json', './tsconfig.node.json', './tsconfig.app.json'],
},
plugins: ['react', '@typescript-eslint', 'prettier'],
plugins: ['react', '@typescript-eslint', 'prettier', 'simple-import-sort'],
rules: {
'simple-import-sort/imports': [
'error',
{
groups: [
// 1. Side effect imports at the start. For me this is important because I want to import reset.css and global styles at the top of my main file.
['^\\u0000'],
// 2. `react` and packages: Things that start with a letter (or digit or underscore), or `@` followed by a letter.
['^react$', '^@?\\w'],
// 3. Absolute imports and other imports such as Vue-style `@/foo`.
// Anything not matched in another group. (also relative imports starting with "../")
['^@', '^'],
// 4. relative imports from same folder "./" (I like to have them grouped together)
['^\\./'],
// 5. style module imports always come last, this helps to avoid CSS order issues
['^.+\\.(module.css|module.scss)$'],
// 6. media imports
['^.+\\.(gif|png|svg|jpg)$'],
],
},
],
'react/react-in-jsx-scope': 0,
'import/no-extraneous-dependencies': 'off',
'import/prefer-default-export': 'warn',
"import/prefer-default-export": "off",
"import/order": 'warn',
"import/extensions": 'warn',
'import/order': 'warn',
'import/extensions': 'warn',
'@typescript-eslint/no-exp': 'off',
"react/no-array": 'off',
"react/function-component-definition": "off"
'react/no-array': 'off',
'react/function-component-definition': 'off',
},
};
};
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-react": "^7.34.3",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-simple-import-sort": "^12.1.1",
"globals": "^15.8.0",
"husky": "^9.0.11",
"jsdom": "^24.1.0",
Expand Down
97 changes: 15 additions & 82 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,31 @@
import React, { useEffect, useState } from 'react';
import './App.css';
import '@rainbow-me/rainbowkit/styles.css';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ThemeProvider } from '@mui/material/styles';

import React from 'react';
import { LitNetwork } from '@lit-protocol/constants';
import CssBaseline from '@mui/material/CssBaseline';
import { Route, Routes, Navigate, useNavigate } from 'react-router-dom';
import { WagmiProvider } from 'wagmi';
import { ThemeProvider } from '@mui/material/styles';
import {
AuthenticationStatus,
createAuthenticationAdapter,
getDefaultConfig,
RainbowKitAuthenticationProvider,
RainbowKitProvider,
} from '@rainbow-me/rainbowkit';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { Navigate, Route, Routes } from 'react-router-dom';
import { WagmiProvider } from 'wagmi';
import { sepolia } from 'wagmi/chains';
import { getAddress } from 'viem';
import { createSiweMessage } from 'viem/siwe';
import { LitNetwork } from '@lit-protocol/constants';
import Login from './pages/Auth/Login';
import theme from './libs/theme';
import { api } from './services/api';

import { CustomSnackbar } from './components/shared/CustomSnackbar';
import { LitProvider } from './hooks/LitProvider';
import useSiweAuth from './hooks/useSiweAuth';
import DefaultLayout from './layouts/DefaultLayout';

// import Dashboard from './pages/Dashboard';
import theme from './libs/theme';
import Login from './pages/Auth/Login';
import Callback from './pages/Callback';
import Identifiers from './pages/Identifiers';
import Permissions from './pages/Permissions';
import Attestation from './pages/Identifiers/Attestation';
import Callback from './pages/Callback';
import Permissions from './pages/Permissions';
import ProtectedRoute from './ProtectedRoute';
import { LitProvider } from './hooks/LitProvider';
import { CustomSnackbar } from './components/shared/CustomSnackbar';

const queryClient = new QueryClient({
defaultOptions: {
Expand All @@ -50,69 +45,7 @@ const config = getDefaultConfig({
});

const App: React.FC = () => {
const navigate = useNavigate();
const [authStatus, setAuthStatus] =
useState<AuthenticationStatus>('unauthenticated');

const authenticationAdapter = createAuthenticationAdapter({
getNonce: async () => {
const { data } = await api.get('auth/siwe/nonce');
return data.nonce;
},
createMessage: ({ nonce, address, chainId }) => {
return createSiweMessage({
address: getAddress(address),
chainId,
domain: window.location.host,
nonce,
uri: window.location.origin,
version: '1',
statement: 'Sign in with Ethereum to the app.',
});
},
getMessageBody: ({ message }) => message,
verify: async ({ message, signature }) => {
const { data } = await api.post('auth/siwe/verify', {
message,
signature,
chainId: 11155111,
});

if (!data) {
throw new Error('Verification response data is empty');
}

if (data?.jwt) {
localStorage.setItem('OCI_TOKEN', data.jwt);
setAuthStatus('authenticated');
return true;
}

return false;
},
signOut: async () => {
localStorage.removeItem('OCI_TOKEN');
navigate('/auth/login');
setAuthStatus('unauthenticated');
},
});

useEffect(() => {
const checkStoredToken = () => {
const OCI_TOKEN = localStorage.getItem('OCI_TOKEN');
if (OCI_TOKEN) {
setAuthStatus('authenticated');
} else {
setAuthStatus('unauthenticated');
}
};

checkStoredToken();
}, []);

useEffect(() => {
console.log('authStatus', authStatus);
}, [authStatus]);
const { authStatus, authenticationAdapter } = useSiweAuth();

globalThis.Buffer = Buffer;

Expand Down
24 changes: 10 additions & 14 deletions src/ProtectedRoute.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react';
import { Navigate } from 'react-router-dom';
import CircularProgress from '@mui/material/CircularProgress';
import Backdrop from '@mui/material/Backdrop';
import CircularProgress from '@mui/material/CircularProgress';
import { Navigate } from 'react-router-dom';
import { useAccount } from 'wagmi';

interface ProtectedRouteProps {
Expand All @@ -15,23 +15,19 @@ const ProtectedRoute = ({ children }: ProtectedRouteProps) => {
const { isConnected } = useAccount();

useEffect(() => {
if (!isConnected) {
setIsAuthenticated(false);
localStorage.removeItem('OCI_TOKEN');
}
}, [isConnected]);

useEffect(() => {
const checkAuthStatus = async () => {
const token = localStorage.getItem('OCI_TOKEN');
if (token) {
setIsAuthenticated(true);
const checkAuthStatus = () => {
if (isConnected) {
const token = localStorage.getItem('OCI_TOKEN');
setIsAuthenticated(!!token);
} else {
setIsAuthenticated(false);
localStorage.removeItem('OCI_TOKEN');
}
setLoading(false);
};

checkAuthStatus();
}, []);
}, [isConnected]);

if (loading) {
return (
Expand Down
5 changes: 3 additions & 2 deletions src/components/layouts/AccountPopover.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { render, screen, fireEvent } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { fireEvent, render, screen } from '@testing-library/react';
import { describe, expect, it } from 'vitest';

import AccountPopover from './AccountPopover';

describe('AccountPopover', () => {
Expand Down
10 changes: 5 additions & 5 deletions src/components/layouts/AccountPopover.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { useState, MouseEvent } from 'react';
import { MouseEvent, useState } from 'react';
import AccountCircleIcon from '@mui/icons-material/AccountCircle';
import LogoutIcon from '@mui/icons-material/Logout';
import {
Avatar,
Popover,
MenuItem,
IconButton,
MenuItem,
Popover,
Typography,
} from '@mui/material';
import { alpha } from '@mui/material/styles';
import AccountCircleIcon from '@mui/icons-material/AccountCircle';
import LogoutIcon from '@mui/icons-material/Logout';

function AccountPopover() {
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
Expand Down
3 changes: 2 additions & 1 deletion src/components/layouts/AppbarApp.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { render, screen } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { describe, expect, it, vi } from 'vitest';

import AppbarApp from './AppbarApp';

// Mocking the ConnectButton from RainbowKit
Expand Down
8 changes: 5 additions & 3 deletions src/components/layouts/SidebarApp.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { render, screen, fireEvent } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { fireEvent, render, screen } from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom';
import SidebarApp from './SidebarApp';
import { describe, expect, it, vi } from 'vitest';

import { SIDEBAR_MENU } from '../../libs/constants';

import SidebarApp from './SidebarApp';

// Mock the constants
vi.mock('../../libs/constants', () => ({
DRAWER_WIDTH: 240,
Expand Down
5 changes: 3 additions & 2 deletions src/components/layouts/SidebarApp.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import {
Box,
Divider,
Drawer,
List,
ListItemButton,
ListItemIcon,
ListItemText,
Box,
Toolbar,
Divider,
Typography,
} from '@mui/material';
import { useLocation, useNavigate } from 'react-router-dom';

import { DRAWER_WIDTH, SIDEBAR_MENU } from '../../libs/constants';

function SidebarApp() {
Expand Down
74 changes: 44 additions & 30 deletions src/components/shared/AccessControlButton.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,51 @@
// AccessControlButton.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';

import { fireEvent, render, screen } from '@testing-library/react';
import { vi } from 'vitest';

import AccessControlButton from './AccessControlButton';

describe('AccessControlButton', () => {
it('renders with "Grant Access" when hasAccess is false', () => {
const handleToggleAccess = vi.fn();
render(<AccessControlButton hasAccess={false} onToggleAccess={handleToggleAccess} />);

const button = screen.getByRole('button', { name: /grant access/i });
expect(button).toBeInTheDocument();
expect(button).toHaveTextContent('Grant Access');
expect(button).toHaveClass('MuiButton-containedPrimary');
});

it('renders with "Revoke Access" when hasAccess is true', () => {
const handleToggleAccess = vi.fn();
render(<AccessControlButton hasAccess={true} onToggleAccess={handleToggleAccess} />);

const button = screen.getByRole('button', { name: /revoke access/i });
expect(button).toBeInTheDocument();
expect(button).toHaveTextContent('Revoke Access');
expect(button).toHaveClass('MuiButton-outlinedError');
});

it('calls onToggleAccess when button is clicked', () => {
const handleToggleAccess = vi.fn();
render(<AccessControlButton hasAccess={false} onToggleAccess={handleToggleAccess} />);

const button = screen.getByRole('button', { name: /grant access/i });
fireEvent.click(button);

expect(handleToggleAccess).toHaveBeenCalledTimes(1);
});
it('renders with "Grant Access" when hasAccess is false', () => {
const handleToggleAccess = vi.fn();
render(
<AccessControlButton
hasAccess={false}
onToggleAccess={handleToggleAccess}
/>
);

const button = screen.getByRole('button', { name: /grant access/i });
expect(button).toBeInTheDocument();
expect(button).toHaveTextContent('Grant Access');
expect(button).toHaveClass('MuiButton-containedPrimary');
});

it('renders with "Revoke Access" when hasAccess is true', () => {
const handleToggleAccess = vi.fn();
render(
<AccessControlButton hasAccess onToggleAccess={handleToggleAccess} />
);

const button = screen.getByRole('button', { name: /revoke access/i });
expect(button).toBeInTheDocument();
expect(button).toHaveTextContent('Revoke Access');
expect(button).toHaveClass('MuiButton-outlinedError');
});

it('calls onToggleAccess when button is clicked', () => {
const handleToggleAccess = vi.fn();
render(
<AccessControlButton
hasAccess={false}
onToggleAccess={handleToggleAccess}
/>
);

const button = screen.getByRole('button', { name: /grant access/i });
fireEvent.click(button);

expect(handleToggleAccess).toHaveBeenCalledTimes(1);
});
});
Loading
Loading