Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
3 changes: 3 additions & 0 deletions src/app/dashboard/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
"use client";

import { MetamaskStateProvider } from "use-metamask";
import "./style.css";
import Link from "next/link";

Expand Down
5 changes: 4 additions & 1 deletion src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { MetamaskProvider } from "@/hooks/useMetamask";
import "./globals.css";
import type { Metadata } from "next";
import { Inter } from "next/font/google";
Expand All @@ -16,7 +17,9 @@ export default function RootLayout({
}) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
<MetamaskProvider>
<body className={inter.className}>{children}</body>
</MetamaskProvider>
</html>
);
}
36 changes: 35 additions & 1 deletion src/app/test/page.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,46 @@
"use client";

import Wallet from "@/components/Wallet";
import { useListen } from "@/hooks/useListen";
import { useMetamask } from "@/hooks/useMetamask";
import Image from "next/image";
import React, { useState } from "react";
import React, { useEffect, useState } from "react";

export default function Page() {
const [isLoggedIn, setIsLoggedIn] = useState(false);

const [issues, setIssues] = useState([]);
const [bounties, setBounties] = useState([]);

const { dispatch } = useMetamask();
const listen = useListen();

useEffect(() => {
if (typeof window !== undefined) {
// start by checking if window.ethereum is present, indicating a wallet extension
const ethereumProviderInjected = typeof window.ethereum !== "undefined";
// this could be other wallets so we can verify if we are dealing with metamask
// using the boolean constructor to be explecit and not let this be used as a falsy value (optional)
const isMetamaskInstalled =
ethereumProviderInjected && Boolean(window.ethereum.isMetaMask);

const local = window.localStorage.getItem("metamaskState");

// user was previously connected, start listening to MM
if (local) {
listen();
}

// local could be null if not present in LocalStorage
const { wallet, balance } = local
? JSON.parse(local)
: // backup if local storage is empty
{ wallet: null, balance: null };

dispatch({ type: "pageLoaded", isMetamaskInstalled, wallet, balance });
}
}, []);

const handleLoginSubmit = async (
e: React.FormEvent<HTMLFormElement>
): Promise<void> => {
Expand Down Expand Up @@ -152,6 +184,8 @@ export default function Page() {
<form onSubmit={handleGetBounties}>
<button type="submit">Get Bounites</button>
</form>

<Wallet />
</>
);
}
135 changes: 135 additions & 0 deletions src/components/Wallet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import Link from "next/link";
import { useListen } from "@/hooks/useListen";
import { useMetamask } from "@/hooks/useMetamask";

function Loading() {
return (
<div>
<h1>Loading</h1>
</div>
);
}

export default function Wallet() {
const {
dispatch,
state: { status, isMetamaskInstalled, wallet, balance },
} = useMetamask();
const listen = useListen();

const showInstallMetamask =
status !== "pageNotLoaded" && !isMetamaskInstalled;
const showConnectButton =
status !== "pageNotLoaded" && isMetamaskInstalled && !wallet;

const isConnected = status !== "pageNotLoaded" && typeof wallet === "string";

const handleConnect = async () => {
dispatch({ type: "loading" });
const accounts = await window.ethereum.request({
method: "eth_requestAccounts",
});

if (accounts.length > 0) {
const balance = await window.ethereum!.request({
method: "eth_getBalance",
params: [accounts[0], "latest"],
});
dispatch({ type: "connect", wallet: accounts[0], balance });

// we can register an event listener for changes to the users wallet
listen();
}
};

const handleDisconnect = () => {
dispatch({ type: "disconnect" });
};

const handleAddUsdc = async () => {
dispatch({ type: "loading" });

await window.ethereum.request({
method: "wallet_watchAsset",
params: {
type: "ERC20",
options: {
address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
symbol: "USDC",
decimals: 18,
image: "https://cryptologos.cc/logos/usd-coin-usdc-logo.svg?v=023",
},
},
});
dispatch({ type: "idle" });
};

return (
<div className="bg-truffle">
<div className="mx-auto max-w-2xl py-16 px-4 text-center sm:py-20 sm:px-6 lg:px-8">
<h2 className="text-3xl font-bold tracking-tight text-black sm:text-4xl">
<span className="block">Metamask API intro</span>
</h2>

{wallet && balance && (
<div className=" px-4 py-5 sm:px-6">
<div className="-ml-4 -mt-4 flex flex-wrap items-center justify-between sm:flex-nowrap">
<div className="ml-4 mt-4">
<div className="flex items-center">
<div className="ml-4">
<h3 className="text-lg font-medium leading-6 text-black">
Address: <span>{wallet}</span>
</h3>
<p className="text-sm text-black">
Balance:{" "}
<span>
{(parseInt(balance) / 1000000000000000000).toFixed(4)}{" "}
ETH
</span>
</p>
</div>
</div>
</div>
</div>
</div>
)}

{showConnectButton && (
<button
onClick={handleConnect}
className="mt-8 inline-flex w-full items-center justify-center rounded-md border border-transparent bg-ganache text-black px-5 py-3 text-base font-medium sm:w-auto"
>
{status === "loading" ? <Loading /> : "Connect Wallet"}
</button>
)}

{showInstallMetamask && (
<Link
href="https://metamask.io/"
target="_blank"
className="mt-8 inline-flex w-full items-center justify-center rounded-md border border-transparent bg-ganache text-black px-5 py-3 text-base font-medium sm:w-auto"
>
Install Metamask
</Link>
)}

{isConnected && (
<div className="flex w-full justify-center space-x-2">
<button
onClick={handleAddUsdc}
className="mt-8 inline-flex w-full items-center justify-center rounded-md border border-transparent bg-ganache text-black px-5 py-3 text-base font-medium sm:w-auto"
>
{status === "loading" ? <Loading /> : "Add Token"}
</button>
<button
onClick={handleDisconnect}
className="mt-8 inline-flex w-full items-center justify-center rounded-md border border-transparent bg-ganache text-black px-5 py-3 text-base font-medium sm:w-auto"
>
Disconnect
</button>
</div>
)}
</div>
</div>
);
}
26 changes: 26 additions & 0 deletions src/hooks/useListen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { useMetamask } from "./useMetamask";

export const useListen = () => {
const { dispatch } = useMetamask();

return () => {
window.ethereum.on("accountsChanged", async (newAccounts: string[]) => {
if (newAccounts.length > 0) {
// uppon receiving a new wallet, we'll request again the balance to synchronize the UI.
const newBalance = await window.ethereum!.request({
method: "eth_getBalance",
params: [newAccounts[0], "latest"],
});

dispatch({
type: "connect",
wallet: newAccounts[0],
balance: newBalance,
});
} else {
// if the length is 0, then the user has disconnected from the wallet UI
dispatch({ type: "disconnect" });
}
});
};
};
98 changes: 98 additions & 0 deletions src/hooks/useMetamask.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"use client";

import React, { useEffect, type PropsWithChildren } from "react";

type ConnectAction = { type: "connect"; wallet: string; balance: string };
type DisconnectAction = { type: "disconnect" };
type PageLoadedAction = {
type: "pageLoaded";
isMetamaskInstalled: boolean;
wallet: string | null;
balance: string | null;
};
type LoadingAction = { type: "loading" };
type IdleAction = { type: "idle" };

type Action =
| ConnectAction
| DisconnectAction
| PageLoadedAction
| LoadingAction
| IdleAction;

type Dispatch = (action: Action) => void;

type Status = "loading" | "idle" | "pageNotLoaded";

type State = {
wallet: string | null;
isMetamaskInstalled: boolean;
status: Status;
balance: string | null;
};

const initialState: State = {
wallet: null,
isMetamaskInstalled: false,
status: "loading",
balance: null,
} as const;

function metamaskReducer(state: State, action: Action): State {
switch (action.type) {
case "connect": {
const { wallet, balance } = action;
const newState = { ...state, wallet, balance, status: "idle" } as State;
const info = JSON.stringify(newState);
window.localStorage.setItem("metamaskState", info);

return newState;
}
case "disconnect": {
window.localStorage.removeItem("metamaskState");
if (typeof window.ethereum !== undefined) {
window.ethereum.removeAllListeners(["accountsChanged"]);
}
return { ...state, wallet: null, balance: null };
}
case "pageLoaded": {
const { isMetamaskInstalled, balance, wallet } = action;
return { ...state, isMetamaskInstalled, status: "idle", wallet, balance };
}
case "loading": {
return { ...state, status: "loading" };
}
case "idle": {
return { ...state, status: "idle" };
}

default: {
throw new Error("Unhandled action type");
}
}
}

const MetamaskContext = React.createContext<
{ state: State; dispatch: Dispatch } | undefined
>(undefined);

function MetamaskProvider({ children }: PropsWithChildren) {
const [state, dispatch] = React.useReducer(metamaskReducer, initialState);
const value = { state, dispatch };

return (
<MetamaskContext.Provider value={value}>
{children}
</MetamaskContext.Provider>
);
}

function useMetamask() {
const context = React.useContext(MetamaskContext);
if (context === undefined) {
throw new Error("useMetamask must be used within a MetamaskProvider");
}
return context;
}

export { MetamaskProvider, useMetamask };