diff --git a/README.md b/README.md index 1185d8047..3dba05b94 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,18 @@ CoreUI is meant to be the UX game changer. Pure & transparent code is devoid of * [Support CoreUI Development](#support-coreui-development) * [Copyright and License](#copyright-and-license) +## Login and Connect Wallet + +If you want to add login and connect wallet functionality to your application, you can use the Wagmi library. Wagmi is a collection of React Hooks that makes it easy to work with Ethereum, including connecting wallets, displaying ENS and balance information, signing messages, interacting with contracts, and more. + +To get started with Wagmi, you can follow these steps: + +1. Install the Wagmi library and the necessary dependencies in your React project: + + ```bash + npm install wagmi viem + You can read more about that [here](https://wagmi.sh) + ## Versions * [CoreUI Free Bootstrap Admin Template](https://github.com/coreui/coreui-free-bootstrap-admin-template) diff --git a/package.json b/package.json index d6e40d776..a92a84a13 100644 --- a/package.json +++ b/package.json @@ -26,20 +26,37 @@ "@coreui/coreui": "^4.2.6", "@coreui/icons": "^3.0.1", "@coreui/icons-react": "^2.1.0", - "@coreui/react": "^4.9.0-rc.0", - "@coreui/react-chartjs": "^2.1.3", - "@coreui/utils": "^2.0.2", + "@coreui/react": "^4.6.0", + "@coreui/react-chartjs": "^2.1.2", + "@coreui/utils": "^2.0.1", + "@web3-react/core": "^8.2.0", + "@web3-react/injected-connector": "^6.0.7", + "@web3-react/walletconnect": "^8.2.0", + "@web3modal/ethereum": "^2.4.5", + "@web3modal/react": "^2.4.5", + "@web3modal/sign-react": "^2.4.7", "chart.js": "^3.9.1", "classnames": "^2.3.2", - "core-js": "^3.31.0", + "core-js": "^3.29.0", + "ethers": "^6.6.0", "prop-types": "^15.8.1", "react": "^18.2.0", "react-app-polyfill": "^3.0.0", "react-dom": "^18.2.0", + "react-ranger": "^2.1.0", "react-redux": "^8.1.1", "react-router-dom": "^6.14.0", - "redux": "4.2.1", - "simplebar-react": "^2.4.3" + "react-toastify": "^9.1.3", + "redux": "^4.2.1", + "redux-persist": "^6.0.0", + "redux-saga": "^1.2.3", + "redux-thunk": "^2.4.2", + "simplebar-react": "^2.4.3", + "viem": "^1.0.7", + "wagmi": "^1.2.0", + "web3": "^4.0.2", + "web3-core": "^4.0.1", + "web3-errors": "^1.0.0" }, "devDependencies": { "@testing-library/jest-dom": "^5.16.5", @@ -56,4 +73,4 @@ "node": ">=10", "npm": ">=6" } -} +} \ No newline at end of file diff --git a/src/App.js b/src/App.js index 7c2488188..8fbf1e85a 100644 --- a/src/App.js +++ b/src/App.js @@ -1,6 +1,9 @@ import React, { Component, Suspense } from 'react' +import { Provider } from 'react-redux' import { HashRouter, Route, Routes } from 'react-router-dom' import './scss/style.scss' +import store from './redux/store' + const loading = (
@@ -20,17 +23,19 @@ const Page500 = React.lazy(() => import('./views/pages/page500/Page500')) class App extends Component { render() { return ( - - - - } /> - } /> - } /> - } /> - } /> - - - + + + + + } /> + } /> + } /> + } /> + } /> + + + + ) } } diff --git a/src/assets/images/tokens/arbitrum.svg b/src/assets/images/tokens/arbitrum.svg new file mode 100644 index 000000000..d439bc6f6 --- /dev/null +++ b/src/assets/images/tokens/arbitrum.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/images/tokens/celo.svg b/src/assets/images/tokens/celo.svg new file mode 100644 index 000000000..bd24e97d7 --- /dev/null +++ b/src/assets/images/tokens/celo.svg @@ -0,0 +1,11 @@ + + + + + + + diff --git a/src/assets/images/tokens/eth.png b/src/assets/images/tokens/eth.png new file mode 100644 index 000000000..760946e76 Binary files /dev/null and b/src/assets/images/tokens/eth.png differ diff --git a/src/assets/images/tokens/polygon.svg b/src/assets/images/tokens/polygon.svg new file mode 100644 index 000000000..a5bb6124f --- /dev/null +++ b/src/assets/images/tokens/polygon.svg @@ -0,0 +1,16 @@ + + + + + + + + diff --git a/src/components/AppHeader.js b/src/components/AppHeader.js index dd5f544e3..e0d226a67 100644 --- a/src/components/AppHeader.js +++ b/src/components/AppHeader.js @@ -1,6 +1,7 @@ -import React from 'react' -import { NavLink } from 'react-router-dom' -import { useSelector, useDispatch } from 'react-redux' +import React from 'react'; +import { NavLink } from 'react-router-dom'; +import { useSelector, useDispatch } from 'react-redux'; +import { setSidebarShow } from '../redux/actions'; import { CContainer, CHeader, @@ -10,24 +11,24 @@ import { CHeaderToggler, CNavLink, CNavItem, -} from '@coreui/react' -import CIcon from '@coreui/icons-react' -import { cilBell, cilEnvelopeOpen, cilList, cilMenu } from '@coreui/icons' +} from '@coreui/react'; +import CIcon from '@coreui/icons-react'; +import { cilBell, cilEnvelopeOpen, cilList, cilMenu } from '@coreui/icons'; -import { AppBreadcrumb } from './index' -import { AppHeaderDropdown } from './header/index' -import { logo } from 'src/assets/brand/logo' +import { AppBreadcrumb } from './index'; +import { logo } from 'src/assets/brand/logo'; +import WagmiAuth from './header/wagmi'; // Import the WagmiAuth component const AppHeader = () => { - const dispatch = useDispatch() - const sidebarShow = useSelector((state) => state.sidebarShow) + const dispatch = useDispatch(); + const sidebarShow = useSelector((state) => state.sidebarShow); return ( dispatch({ type: 'set', sidebarShow: !sidebarShow })} + onClick={() => dispatch(setSidebarShow(!sidebarShow))} > @@ -47,6 +48,10 @@ const AppHeader = () => { Settings + + + {/* Render the WagmiAuth component */} + @@ -64,16 +69,13 @@ const AppHeader = () => { - - - - ) -} + ); +}; -export default AppHeader +export default AppHeader; diff --git a/src/components/header/auth/ChainDropdown.js b/src/components/header/auth/ChainDropdown.js new file mode 100644 index 000000000..1465f203c --- /dev/null +++ b/src/components/header/auth/ChainDropdown.js @@ -0,0 +1,33 @@ +import React from 'react'; +import supportedNetwork from './SupportedNetwork'; +import { CAvatar, CDropdown, CDropdownToggle, CDropdownMenu, CDropdownItem } from '@coreui/react'; + +const ChainDropdown = ({ chains, chain, switchNetwork, isLoading, pendingChainId }) => { + const findToken = (name) => { + return supportedNetwork.find((x) => x.name === name); + }; + + return ( + + + {findToken(chain.name) ? '' : 'Unsupported Network Choosen'} + + + + {chains.map((x) => ( + switchNetwork?.(x.id)} + > + + {x.name} + {isLoading && pendingChainId === x.id && ' (switching)'} + + ))} + + + ); +}; + +export default ChainDropdown; diff --git a/src/components/header/auth/SupportedNetwork.js b/src/components/header/auth/SupportedNetwork.js new file mode 100644 index 000000000..9f17a65ed --- /dev/null +++ b/src/components/header/auth/SupportedNetwork.js @@ -0,0 +1,21 @@ +// supportedNetwork.js +import ethIcon from '../../../assets/images/tokens/eth.png'; +import arbitIcon from '../../../assets/images/tokens/arbitrum.svg'; +import maticIcon from '../../../assets/images/tokens/polygon.svg'; + +const supportedNetwork = [ + { + name: 'Ethereum', + cover: ethIcon, + }, + { + name: 'Arbitrum One', + cover: arbitIcon, + }, + { + name: 'Polygon', + cover: maticIcon, + }, +]; + +export default supportedNetwork; diff --git a/src/components/header/auth/WalletDropdown.js b/src/components/header/auth/WalletDropdown.js new file mode 100644 index 000000000..12031c207 --- /dev/null +++ b/src/components/header/auth/WalletDropdown.js @@ -0,0 +1,31 @@ +import React from 'react'; +import { CAvatar, CBadge, CDropdown, CDropdownMenu, CDropdownItem, CDropdownToggle } from '@coreui/react'; + +const WalletDropdown = ({ address, open, signIn, visible, status }) => { + return ( + + {visible && ( + + New alerts + + )} + + + {address} + + {/* Dropdown Menu */} + + Account + Sign in wallet + + + ); +}; + +export default WalletDropdown; diff --git a/src/components/header/auth/index.js b/src/components/header/auth/index.js new file mode 100644 index 000000000..abeefb042 --- /dev/null +++ b/src/components/header/auth/index.js @@ -0,0 +1,115 @@ +import React, { useState, useEffect } from 'react'; +import { useWeb3Modal } from '@web3modal/react'; +import { useNetwork, useSwitchNetwork, useAccount } from 'wagmi'; +import { useDispatch, useSelector } from 'react-redux'; +import { setToken } from '../../../redux/actions'; +import { useSignMessage } from 'wagmi'; +import ChainDropdown from './ChainDropdown'; +import WalletDropdown from './WalletDropdown'; +import supportedNetwork from './SupportedNetwork'; +import { getNonce, login } from '../../../services/apiServices'; +import { CButton } from '@coreui/react'; +import { ToastContainer, toast } from 'react-toastify'; +import 'react-toastify/dist/ReactToastify.css'; + +const ConnectWalletButton = () => { + const [visible, setVisible] = useState(false); + const { chain } = useNetwork(); + const { open } = useWeb3Modal(); + const { chains, isLoading, pendingChainId, switchNetwork } = useSwitchNetwork(); + const { address, status } = useAccount(); + const { data, error, signMessage } = useSignMessage(); + const dispatch = useDispatch(); + const token = useSelector((state) => state.token); + + const findToken = (name) => { + return supportedNetwork.find((x) => x.name === name); + }; + + const handleDisconnect = () => { + setVisible(false); + dispatch(setToken('')); // Reset token in store + }; + + useEffect(() => { + if (!address) { + handleDisconnect(); + } + }, [address, handleDisconnect]); + + const signIn = async () => { + try { + const response = await getNonce(address); + if (response) { + try { + const message = response.message; + await signMessage({ message: message }); + if (!error && data !== undefined) { + getToken(data, address); + } + } catch (error) { + console.log('Error:', error); + toast.error('An error occurred. Please try again later.'); + } + } + } catch (error) { + console.log('Error:', error); + toast.error('An error occurred. Please try again later.'); + } + }; + + const getToken = async (signature, address) => { + try { + if (signature) { + const response = await login(address, signature); + if (response) { + setVisible(true); + dispatch(setToken(response.access_token)); + } else { + // Handle the error condition if needed + } + } + } catch (error) { + console.log('Error:', error); + } + }; + + return ( + <> + +
+ {!address && ( + open()} + color="primary" + className={address ? 'w-50 text-truncate h-75 rounded-pill' : 'w-auto rounded-pill'} + > + Connect + + )} + {address && ( + + )} + {chain && ( + + )} +
+ + ); +}; + +export default ConnectWalletButton; diff --git a/src/components/header/wagmi/index.js b/src/components/header/wagmi/index.js new file mode 100644 index 000000000..3245fbc2c --- /dev/null +++ b/src/components/header/wagmi/index.js @@ -0,0 +1,29 @@ +import React from 'react'; +import ConnectWalletButton from '../auth'; +import { EthereumClient, w3mConnectors, w3mProvider } from '@web3modal/ethereum'; +import { Web3Modal } from '@web3modal/react'; +import { configureChains, createConfig, WagmiConfig } from 'wagmi'; +import { arbitrum, mainnet, polygon } from 'wagmi/chains'; + +const chains = [arbitrum, mainnet, polygon]; +const projectId = 'Your Projcet ID'; + +const { publicClient } = configureChains(chains, [w3mProvider({ projectId })]); +const wagmiConfig = createConfig({ + autoConnect: true, + connectors: w3mConnectors({ projectId, version: 1, chains }), + publicClient +}); + +const ethereumClient = new EthereumClient(wagmiConfig, chains); + +const WagmiAuth = () => { + return ( + + + + + ); +}; + +export default WagmiAuth; diff --git a/src/index.js b/src/index.js index d19a3bcd3..24e08e724 100644 --- a/src/index.js +++ b/src/index.js @@ -5,7 +5,7 @@ import { createRoot } from 'react-dom/client' import App from './App' import reportWebVitals from './reportWebVitals' import { Provider } from 'react-redux' -import store from './store' +import store from './redux/store' createRoot(document.getElementById('root')).render( diff --git a/src/redux/actionTypes.js b/src/redux/actionTypes.js new file mode 100644 index 000000000..ba3b8573f --- /dev/null +++ b/src/redux/actionTypes.js @@ -0,0 +1,4 @@ +// actionTypes.js +export const SET_TOKEN = 'SET_TOKEN'; +export const SET_USER = 'SET_USER'; +export const SET_SIDEBAR_SHOW = 'SET_SIDEBAR_SHOW'; diff --git a/src/redux/actions.js b/src/redux/actions.js new file mode 100644 index 000000000..9c3758240 --- /dev/null +++ b/src/redux/actions.js @@ -0,0 +1,22 @@ +// actions.js +// Import action types +import { SET_TOKEN, SET_USER, SET_SIDEBAR_SHOW } from './actionTypes'; + + +// Action creators +export const setToken = (token) => ({ + type: SET_TOKEN, + payload: token +}); + +export const setUser = (user) => ({ + type: SET_USER, + payload: user +}); + +export const setSidebarShow = (show) => { + return { + type: SET_SIDEBAR_SHOW, + payload: show, + }; +}; \ No newline at end of file diff --git a/src/redux/reducer.js b/src/redux/reducer.js new file mode 100644 index 000000000..a33c65b30 --- /dev/null +++ b/src/redux/reducer.js @@ -0,0 +1,35 @@ +// reducer.js +// Import action types +import { SET_TOKEN, SET_USER, SET_SIDEBAR_SHOW } from './actionTypes'; + +// Initial state +const initialState = { + sidebarShow: true, + token: null, + user: null +}; + +// Reducer +const reducer = (state = initialState, action) => { + switch (action.type) { + case SET_TOKEN: + return { + ...state, + token: action.payload + }; + case SET_USER: + return { + ...state, + user: action.payload + }; + case SET_SIDEBAR_SHOW: + return { + ...state, + sidebarShow: action.payload + }; + default: + return state; + } +}; + +export default reducer; diff --git a/src/redux/store.js b/src/redux/store.js new file mode 100644 index 000000000..1ee3a019c --- /dev/null +++ b/src/redux/store.js @@ -0,0 +1,8 @@ +import { createStore, applyMiddleware } from 'redux'; +import thunk from 'redux-thunk'; +import reducer from './reducer'; + +// Create the store +const store = createStore(reducer, applyMiddleware(thunk)); + +export default store; diff --git a/src/scss/_custom.scss b/src/scss/_custom.scss index 15d367af4..c53e63d02 100644 --- a/src/scss/_custom.scss +++ b/src/scss/_custom.scss @@ -1 +1,8 @@ // Here you can add other styles + + +// Size // +.w-15-icon { + width: 15px; + height: 15px; +} \ No newline at end of file diff --git a/src/services/api.js b/src/services/api.js new file mode 100644 index 000000000..9e8ba152a --- /dev/null +++ b/src/services/api.js @@ -0,0 +1,52 @@ +import axios from 'axios'; + +class ReactApiPlugin { + setAuthToken(token) { + axios.defaults.headers.common['Authorization'] = `Bearer ${token}`; + } + + clearAuthToken() { + delete axios.defaults.headers.common['Authorization']; + } + + async putData(url, data, config = {}) { + try { + const response = await axios.put(url, data, config); + return response.data; + } catch (error) { + if (error.response) { + throw new Error(error.response.data.message); + } else { + throw new Error('Network error'); + } + } + } + + async getData(url, config = {}) { + try { + const response = await axios.get(url, config); + return response.data; + } catch (error) { + if (error.response) { + throw new Error(error.response.data.message); + } else { + throw new Error('Network error'); + } + } + } + + async postData(url, data, config = {}) { + try { + const response = await axios.post(url, data, config); + return response.data; + } catch (error) { + if (error.response) { + throw new Error(error.response.data.message); + } else { + throw new Error('Network error'); + } + } + } +} + +export default new ReactApiPlugin(); \ No newline at end of file diff --git a/src/services/apiServices.js b/src/services/apiServices.js new file mode 100644 index 000000000..5777a078c --- /dev/null +++ b/src/services/apiServices.js @@ -0,0 +1,24 @@ +import apiPlugin from './api'; +const API_URL = "https://nestapi.treejer.com" + +export const getNonce = async (address) => { + try { + const response = await apiPlugin.getData(`${API_URL}/nonce/${address}`); + return response; + } catch (error) { + console.log('Error:', error); + throw error; + } +}; + +export const login = async (address, signature) => { + try { + const response = await apiPlugin.postData(`${API_URL}/login/${address}`, { + signature: signature, + }); + return response; + } catch (error) { + console.log('Error:', error); + throw error; + } +}; diff --git a/src/store.js b/src/store.js deleted file mode 100644 index ab446364c..000000000 --- a/src/store.js +++ /dev/null @@ -1,17 +0,0 @@ -import { createStore } from 'redux' - -const initialState = { - sidebarShow: true, -} - -const changeState = (state = initialState, { type, ...rest }) => { - switch (type) { - case 'set': - return { ...state, ...rest } - default: - return state - } -} - -const store = createStore(changeState) -export default store