Skip to content
This repository has been archived by the owner on Apr 20, 2022. It is now read-only.

Improved the navbar and the metamask events handling #13

Merged
merged 5 commits into from
Mar 10, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions web/app/components/AccountMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { LinkOff } from "@mui/icons-material";
import {
Box,
Button,
ListItemIcon,
ListItemText,
Menu,
MenuItem,
Stack,
Typography,
} from "@mui/material";
import React, { FC, useState } from "react";
import { useEthereum } from "~/context/EthereumContext";
import { useVoter } from "~/context/VoterContext";
import { formatAccountString } from "~/lib/utils";

export const AccountMenu: FC = () => {
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

si tu fais useState<HTMLElement>(); automatiquement le type ça va être HTMLElement | undefined

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sick! 😀

const open = Boolean(anchorEl);
const { account, logoutEthereumAccount, loading } = useEthereum();
const { voter } = useVoter();

const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget);
};

const handleClose = () => {
setAnchorEl(null);
};

return (
<>
<Button
id="account-menu"
aria-controls={open ? "account-menu" : undefined}
aria-haspopup="true"
aria-expanded={open ? "true" : undefined}
onClick={handleClick}
color="neutral"
>
<Stack direction="row" spacing={1}>
<Typography>
{voter && account ? `${voter.firstName} ${voter.lastName}` : ""}
vcheeney marked this conversation as resolved.
Show resolved Hide resolved
</Typography>
<Box
component="img"
src="/metamask-fox.svg"
sx={{
width: 22,
}}
/>
<Typography
sx={{
fontWeight: "bold",
minWidth: 120,
display: "inline-block",
textAlign: "left",
}}
>
{loading
? "loading..."
: (account && formatAccountString(account)) || "not connected"}
</Typography>
</Stack>
</Button>
<Menu
id="basic-menu"
anchorEl={anchorEl}
open={open}
onClose={handleClose}
MenuListProps={{
"aria-labelledby": "basic-button",
}}
anchorOrigin={{
vertical: "bottom",
horizontal: "right",
}}
transformOrigin={{
vertical: "top",
horizontal: "right",
}}
sx={{
marginTop: 1,
}}
>
<MenuItem
onClick={() => {
logoutEthereumAccount();
handleClose();
}}
>
<ListItemIcon>
<LinkOff />
</ListItemIcon>
<ListItemText>Disconnect</ListItemText>
</MenuItem>
</Menu>
</>
);
};
2 changes: 1 addition & 1 deletion web/app/components/Layout.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Box, Container } from "@mui/material";
import { FC } from "react";
import { useLocation } from "remix";
import { Navbar } from "../../public/Navbar";
import { Navbar } from "./Navbar";

export const Layout: FC = ({ children }) => {
const location = useLocation();
Expand Down
36 changes: 16 additions & 20 deletions web/public/Navbar.tsx → web/app/components/Navbar.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Language } from "@mui/icons-material";
import { Box, Container, Stack, Typography } from "@mui/material";
import { FC } from "react";
import { Link } from "remix";
import { useEthereum } from "~/context/EthereumContext";
import { useVoter } from "../app/context/VoterContext";
import { useVoter } from "../context/VoterContext";
import { AccountMenu } from "./AccountMenu";

export const Navbar: FC = () => {
const { account, network } = useEthereum();
Expand Down Expand Up @@ -54,31 +56,25 @@ export const Navbar: FC = () => {
</Stack>
</Box>
<Box>
<Typography
<Stack
direction="row"
spacing={2}
sx={{
fontSize: "0.5rem",
alignItems: "center",
}}
>
Account: {account || "(not connected)"}
</Typography>
{network && (
<Typography
<AccountMenu />
<Stack
direction="row"
spacing={1}
sx={{
fontSize: "0.5rem",
minWidth: 120,
}}
>
Chain: {network.name} {network.chainId}{" "}
{network.connected ? "🟢" : "🔴"}
</Typography>
)}
<Typography
sx={{
fontSize: "0.5rem",
}}
>
Voter:{" "}
{voter ? `${voter.firstName} ${voter.lastName}` : "unverified"}
</Typography>
<Language />
{network && <Typography>{network.name}</Typography>}
</Stack>
</Stack>
</Box>
</Container>
</Box>
Expand Down
42 changes: 32 additions & 10 deletions web/app/context/BallotContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
useRef,
useState,
} from "react";
import { useNavigate } from "remix";
import { useLocation, useNavigate } from "remix";
import invariant from "tiny-invariant";
import { VoteEvent, VoterAllowedEvent } from "types/ethers-contracts/Ballot";
import { TypedListener } from "types/ethers-contracts/common";
Expand Down Expand Up @@ -43,7 +43,8 @@ const UNPROTECTED_ROUTES = [
"/errors/no-ethereum-provider",
"/errors/nonce-too-high",
];
const isProtected = (route: string) => !UNPROTECTED_ROUTES.includes(route);
const routeRequiresBallot = (route: string) =>
!UNPROTECTED_ROUTES.includes(route);

const BallotContext = createContext<BallotContextInterface>({
loading: true,
Expand All @@ -56,15 +57,22 @@ const BallotContext = createContext<BallotContextInterface>({
});

export const BallotProvider: FC = ({ children }) => {
const [ballotExists, setBallotExists] = useState<boolean>(false);
const [loading, setLoading] = useState<boolean>(true);
const [proposals, setProposals] = useState<Proposal[]>([]);
const [voteRightReceived, setVoteRightReceived] = useState(false);
const [currentVoterVoteStatus, setCurrentVoterVoteStatus] =
useState<VoterVoteStatus>("unknown");
const ballotRef = useRef<Ballot | null>(null);
const providerRef = useRef<ethers.providers.Web3Provider | null>(null);
const { ethereumExists, loading: ethereumLoading, account } = useEthereum();
const {
ethereumExists,
loading: ethereumLoading,
account,
network,
} = useEthereum();
const navigate = useNavigate();
const location = useLocation();

const voteEventsListener: TypedListener<VoteEvent> = (voterAccount) => {
fetchProposals();
Expand Down Expand Up @@ -103,12 +111,11 @@ export const BallotProvider: FC = ({ children }) => {
try {
await ballot.chairperson();
console.log("[BallotContext] Contract is accessible 👍");
setBallotExists(true);
fetchProposals();
} catch (error) {
console.error(error);
if (isProtected(window.location.pathname)) {
navigate("/errors/ballot-not-found");
}
console.log("[BallotContext] Contract is NOT accessible 👎");
setBallotExists(false);
} finally {
setLoading(false);
}
Expand All @@ -121,7 +128,22 @@ export const BallotProvider: FC = ({ children }) => {
ballot.off("Vote", voteEventsListener);
ballot.off("VoterAllowed", voterAllowedEventsListener);
};
}, [ethereumLoading, ethereumExists]);
}, [ethereumLoading, ethereumExists, network]);

useEffect(() => {
if (!loading) {
vcheeney marked this conversation as resolved.
Show resolved Hide resolved
if (ballotExists) {
if (location.pathname === "/errors/ballot-not-found") {
navigate("/getstarted");
}
} else {
// alert(`ballotExists: ${ballotExists}, loading: ${loading}`);
if (routeRequiresBallot(location.pathname)) {
navigate("/errors/ballot-not-found");
}
}
}
}, [loading, location.pathname, ballotExists]);

function fetchProposals() {
const ballot = ballotRef.current;
Expand Down Expand Up @@ -175,9 +197,9 @@ export const BallotProvider: FC = ({ children }) => {
};
}

const value = {
const value: BallotContextInterface = {
loading,
ballotExists: ballotRef.current !== null,
ballotExists,
proposals,
voteRightReceived,
currentVoterVoteStatus,
Expand Down
23 changes: 20 additions & 3 deletions web/app/context/EthereumContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ interface EthereumContextInterface {
account: string | null;
signer?: JsonRpcSigner;
connectWithMetamask: () => void;
logoutEthereumAccount: () => void;
}

export const HARDHAT_CHAIN_ID = 31337;
Expand All @@ -40,6 +41,7 @@ const EthereumContext = createContext<EthereumContextInterface>({
network: null,
account: null,
connectWithMetamask: () => {},
logoutEthereumAccount: () => {},
});

export const EthereumProvider: FC = ({ children }) => {
Expand Down Expand Up @@ -68,11 +70,11 @@ export const EthereumProvider: FC = ({ children }) => {
});

provider.on("network", handleNetworkChange);

// TODO: Detect les account changes
ethereum.on("accountsChanged", handleAccountsChanged);

return () => {
provider.off("network", handleNetworkChange);
ethereum.removeListener("accountsChanged", handleAccountsChanged);
};
} else {
setLoading(false);
Expand Down Expand Up @@ -109,6 +111,20 @@ export const EthereumProvider: FC = ({ children }) => {
});
}

function handleAccountsChanged(accounts: string[]) {
if (!accounts.length) {
logoutEthereumAccount();
} else {
setAccount(accounts[0]);
// TODO: Destroy session / login the other account and setup new session?
}
}

function logoutEthereumAccount() {
setAccount(null);
// TODO: Destroy session
}

async function connectWithMetamask() {
const provider = providerRef.current;
invariant(provider, "Provider should be defined");
Expand All @@ -130,13 +146,14 @@ export const EthereumProvider: FC = ({ children }) => {
}
}

const value = {
const value: EthereumContextInterface = {
loading,
ethereumExists,
network,
account,
signer,
connectWithMetamask,
logoutEthereumAccount,
};

return (
Expand Down
1 change: 1 addition & 0 deletions web/app/context/VoterContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const VoterContext = createContext<VoterContextInterface>({

const UNPROTECTED_ROUTES = [
"/",
"/results",
"/getstarted/register",
"/getstarted/verify",
"/errors/ballot-not-found",
Expand Down
3 changes: 3 additions & 0 deletions web/app/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const formatAccountString = (address: string) => {
return address.slice(0, 6) + "..." + address.slice(-4);
};
19 changes: 19 additions & 0 deletions web/app/mui/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,32 @@ declare module "@mui/material/Typography" {
}
}

declare module "@mui/material/styles" {
interface Palette {
neutral: Palette["primary"];
}
interface PaletteOptions {
neutral: PaletteOptions["primary"];
}
}

// Update the Button's color prop options
declare module "@mui/material/Button" {
interface ButtonPropsColorOverrides {
neutral: true;
}
}

// Create a theme instance.
const theme = createTheme({
palette: {
grey: grey,
primary: {
main: blue[800],
},
neutral: {
main: grey[800],
},
secondary: {
main: grey[800],
},
Expand Down
4 changes: 2 additions & 2 deletions web/app/routes/errors/ballot-not-found.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Box, Typography } from "@mui/material";
import { GenericPageLayoutWithBackButton } from "~/components/GenericPageLayout";

function NoEthereumProvider() {
function BallotNotFound() {
return (
<GenericPageLayoutWithBackButton>
<Typography variant="pageTitle">
Expand All @@ -20,4 +20,4 @@ function NoEthereumProvider() {
);
}

export default NoEthereumProvider;
export default BallotNotFound;
Loading