diff --git a/packages/common/package.json b/packages/common/package.json index 9b1f27a623..994cad53ba 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -24,7 +24,8 @@ "swr": "^2.0.1", "tailwind-styled-components": "^2.2.0", "typescript": "latest", - "wagmi": "0.12.19" + "wagmi": "0.12.19", + "zod": "^3.22.4" }, "devDependencies": { "@types/dompurify": "^2.4.0", diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index ed7ac838a2..f5a028177b 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -1,7 +1,7 @@ import useSWR from "swr"; import { useMemo, useState } from "react"; import { ChainId } from "./chains"; - +import z from "zod"; export * from "./icons"; export * from "./markdown"; @@ -10,27 +10,28 @@ export { ChainId }; export enum PassportState { NOT_CONNECTED, INVALID_PASSPORT, - MATCH_ELIGIBLE, - MATCH_INELIGIBLE, + SCORE_AVAILABLE, LOADING, ERROR, INVALID_RESPONSE, } -type PassportEvidence = { - type: string; - rawScore: string; - threshold: string; -}; - -export type PassportResponse = { - address?: string; - score?: string; - status?: string; - evidence?: PassportEvidence; - error?: string; - detail?: string; -}; +const PassportEvidenceSchema = z.object({ + type: z.string().nullish(), + rawScore: z.coerce.number(), + threshold: z.string().nullish(), +}); + +export type PassportResponse = z.infer; + +export const PassportResponseSchema = z.object({ + address: z.string().nullish(), + score: z.string().nullish(), + status: z.string().nullish(), + evidence: PassportEvidenceSchema.nullish(), + error: z.string().nullish(), + detail: z.string().nullish(), +}); /** * Endpoint used to fetch the passport score for a given address diff --git a/packages/grant-explorer/package.json b/packages/grant-explorer/package.json index a054bcfca8..9a35672271 100644 --- a/packages/grant-explorer/package.json +++ b/packages/grant-explorer/package.json @@ -95,6 +95,7 @@ "viem": "^1.5.3", "wagmi": "1.4.1", "web-vitals": "^2.1.0", + "zod": "^3.22.4", "zustand": "^4.4.0" }, "devDependencies": { diff --git a/packages/grant-explorer/public/modern-era-medium.otf b/packages/grant-explorer/public/modern-era-medium.otf new file mode 100644 index 0000000000..7907f0e82f Binary files /dev/null and b/packages/grant-explorer/public/modern-era-medium.otf differ diff --git a/packages/grant-explorer/public/modern-era-regular.otf b/packages/grant-explorer/public/modern-era-regular.otf new file mode 100644 index 0000000000..3b91916d7d Binary files /dev/null and b/packages/grant-explorer/public/modern-era-regular.otf differ diff --git a/packages/grant-explorer/src/assets/passport-logo-bw.svg b/packages/grant-explorer/src/assets/passport-logo-bw.svg new file mode 100644 index 0000000000..9aedc84528 --- /dev/null +++ b/packages/grant-explorer/src/assets/passport-logo-bw.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/packages/grant-explorer/src/assets/passport-logo-full.svg b/packages/grant-explorer/src/assets/passport-logo-full.svg new file mode 100644 index 0000000000..f73772a5a0 --- /dev/null +++ b/packages/grant-explorer/src/assets/passport-logo-full.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/grant-explorer/src/assets/passport-logo.svg b/packages/grant-explorer/src/assets/passport-logo.svg index 628217d257..0440c95ce8 100644 --- a/packages/grant-explorer/src/assets/passport-logo.svg +++ b/packages/grant-explorer/src/assets/passport-logo.svg @@ -1,16 +1,14 @@ - - - - - - - - - + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/grant-explorer/src/features/api/__tests__/passport.test.tsx b/packages/grant-explorer/src/features/api/__tests__/passport.test.tsx index 4d08518096..81d00edeab 100644 --- a/packages/grant-explorer/src/features/api/__tests__/passport.test.tsx +++ b/packages/grant-explorer/src/features/api/__tests__/passport.test.tsx @@ -31,6 +31,7 @@ describe("fetchPassport", () => { vi.clearAllMocks(); }); + /* TODO: this doesn't test anything */ it("should return a response", async () => { (fetchPassport as Mock).mockResolvedValue({ ok: true, @@ -48,6 +49,7 @@ describe("submitPassport", () => { vi.clearAllMocks(); }); + /* TODO: again, this doesn't test anything */ it("should return a response", async () => { (submitPassport as Mock).mockResolvedValue({ ok: true, diff --git a/packages/grant-explorer/src/features/api/passport.ts b/packages/grant-explorer/src/features/api/passport.ts index e5b848ee4c..29834b3df7 100644 --- a/packages/grant-explorer/src/features/api/passport.ts +++ b/packages/grant-explorer/src/features/api/passport.ts @@ -1,81 +1,129 @@ +import { datadogLogs } from "@datadog/browser-logs"; import { - submitPassport, fetchPassport, PassportResponse, + PassportResponseSchema, PassportState, + submitPassport, } from "common"; -import { useEffect, useState } from "react"; +import { useEffect, useMemo } from "react"; +import useSWR from "swr"; export { submitPassport, fetchPassport, PassportState }; export type { PassportResponse }; -export function usePassport({ address }: { address: string }) { - const [passport, setPassport] = useState(); - const [, setError] = useState(); +const PASSPORT_COMMUNITY_ID = process.env.REACT_APP_PASSPORT_API_COMMUNITY_ID; + +export function usePassport({ address }: { address: string | undefined }) { + const swr = useSWR [string, string] | null>( + () => + address && PASSPORT_COMMUNITY_ID + ? [address, PASSPORT_COMMUNITY_ID] + : null, + async (args) => { + const res = await fetchPassport(...args); - const [passportState, setPassportState] = useState( - PassportState.LOADING + if (res.ok) { + return PassportResponseSchema.parse(await res.json()); + } else { + throw res; + } + } ); + const passportState = useMemo(() => { + if (swr.error) { + switch (swr.error.status) { + case 400: // unregistered/nonexistent passport address + return PassportState.INVALID_PASSPORT; + case 401: // invalid API key + swr.error.json().then((json) => { + console.error("invalid API key", json); + }); + return PassportState.ERROR; + default: + console.error("Error fetching passport", swr.error); + return PassportState.ERROR; + } + } + + if (swr.data) { + if ( + !swr.data.score || + !swr.data.evidence || + swr.data.status === "ERROR" + ) { + datadogLogs.logger.error( + `error: callFetchPassport - invalid score response`, + swr.data + ); + return PassportState.INVALID_RESPONSE; + } + + return PassportState.SCORE_AVAILABLE; + } + + if (!address) { + return PassportState.NOT_CONNECTED; + } + + return PassportState.LOADING; + }, [swr.error, swr.data, address]); + + const passportScore = useMemo(() => { + if (swr.data?.evidence) { + return swr.data.evidence.rawScore; + } + return 0; + }, [swr.data]); + + const PROCESSING_REFETCH_INTERVAL_MS = 3000; + /** If passport is still processing, refetch it every PROCESSING_REFETCH_INTERVAL_MS */ useEffect(() => { - setPassportState(PassportState.LOADING); + if (swr.data?.status === "PROCESSING") { + setTimeout(() => { + /* Revalidate */ + swr.mutate(); + }, PROCESSING_REFETCH_INTERVAL_MS); + } + }, [swr]); - const PASSPORT_COMMUNITY_ID = - process.env.REACT_APP_PASSPORT_API_COMMUNITY_ID; - if (PASSPORT_COMMUNITY_ID === undefined) { - throw new Error("passport community id not set"); + const passportColor = useMemo(() => { + if (passportScore < 15) { + return "orange"; + } else if (passportScore >= 15 && passportScore < 25) { + return "yellow"; + } else { + return "green"; } - const PASSPORT_THRESHOLD = 0; - - if (address && PASSPORT_COMMUNITY_ID) { - const callFetchPassport = async () => { - const res = await fetchPassport(address, PASSPORT_COMMUNITY_ID); - if (res.ok) { - const json = await res.json(); - - if (json.status === "PROCESSING") { - console.log("processing, calling again in 3000 ms"); - setTimeout(async () => { - await callFetchPassport(); - }, 3000); - return; - } else if (json.status === "ERROR") { - // due to error at passport end - setPassportState(PassportState.ERROR); - return; - } - - setPassport(json); - setPassportState( - json.score >= PASSPORT_THRESHOLD - ? PassportState.MATCH_ELIGIBLE - : PassportState.MATCH_INELIGIBLE - ); - } else { - setError(res); - switch (res.status) { - case 400: // unregistered/nonexistent passport address - setPassportState(PassportState.INVALID_PASSPORT); - break; - case 401: // invalid API key - setPassportState(PassportState.ERROR); - console.error("invalid API key", res.json()); - break; - default: - setPassportState(PassportState.ERROR); - console.error("Error fetching passport", res); - } - } - }; - - callFetchPassport(); + }, [passportScore]); + + const donationImpact = useMemo(() => { + if (passportScore < 15) { + return 0; + } else if (passportScore >= 15 && passportScore < 25) { + return 10 * (passportScore - 15); } else { - setPassportState(PassportState.NOT_CONNECTED); + return 100; } - }, [address]); + }, [passportScore]); return { passportState, - passport, + passportScore, + passportColor, + donationImpact, }; } + +export type PassportColor = "gray" | "orange" | "yellow" | "green"; + +const passportColorToClassName: Record = { + gray: "text-gray-400", + orange: "text-orange-400", + yellow: "text-yellow-400", + green: "text-green-400", +}; + +export const getClassForPassportColor = (color: PassportColor) => + passportColorToClassName[color]; diff --git a/packages/grant-explorer/src/features/common/MatchingEstimateTooltip.tsx b/packages/grant-explorer/src/features/common/MatchingEstimateTooltip.tsx index 4d756ac094..f45d8ec96d 100644 --- a/packages/grant-explorer/src/features/common/MatchingEstimateTooltip.tsx +++ b/packages/grant-explorer/src/features/common/MatchingEstimateTooltip.tsx @@ -7,7 +7,7 @@ export function MatchingEstimateTooltip(props: { isEligible: boolean }) {
diff --git a/packages/grant-explorer/src/features/common/Navbar.tsx b/packages/grant-explorer/src/features/common/Navbar.tsx index e98ce2f034..34a2645fe4 100644 --- a/packages/grant-explorer/src/features/common/Navbar.tsx +++ b/packages/grant-explorer/src/features/common/Navbar.tsx @@ -1,14 +1,15 @@ import { ConnectButton } from "@rainbow-me/rainbowkit"; import { ReactComponent as GitcoinLogo } from "../../assets/gitcoinlogo-black.svg"; import { ReactComponent as GrantsExplorerLogo } from "../../assets/topbar-logos-black.svg"; -import { useNavigate } from "react-router-dom"; -import { RoundsSubNav } from "../discovery/RoundsSubNav"; import NavbarCart from "./NavbarCart"; import { UserCircleIcon } from "@heroicons/react/24/outline"; import { useEffect } from "react"; import { useAccount } from "wagmi"; import { useCartStorage } from "../../store"; import { Link } from "react-router-dom"; +import { PassportWidget } from "./PassportWidget"; +import { useNavigate } from "react-router-dom"; +import { RoundsSubNav } from "../discovery/RoundsSubNav"; export interface NavbarProps { customBackground?: string; @@ -56,6 +57,11 @@ export default function Navbar(props: NavbarProps) {
+ {walletAddress && ( +
+ +
+ )} {showWalletInteraction && (
{ - if (score?.evidence?.rawScore === undefined) { - return null; - } - - return Number(score.evidence.rawScore); - }, [score]); - - return { - score: passportScore, - color: passportDisplayColorFromScore(passportScore), - }; -} diff --git a/packages/grant-explorer/src/features/common/PassportBanner.tsx b/packages/grant-explorer/src/features/common/PassportBanner.tsx deleted file mode 100644 index d6c4eac133..0000000000 --- a/packages/grant-explorer/src/features/common/PassportBanner.tsx +++ /dev/null @@ -1,261 +0,0 @@ -import { - ArrowRightIcon, - CheckBadgeIcon, - ExclamationCircleIcon, -} from "@heroicons/react/24/solid"; -import { ChainId, getUTCDateTime } from "common"; -import { useEffect, useState } from "react"; -import { useNavigate } from "react-router-dom"; -import { useAccount } from "wagmi"; -import { ReactComponent as PassportLogo } from "../../assets/passport-logo.svg"; -import { - PassportResponse, - PassportState, - fetchPassport, -} from "../api/passport"; -import { Round } from "../api/types"; -/*TODO: use usePassport hook and refactor */ -export default function PassportBanner(props: { - chainId: ChainId; - round: Round; -}) { - const chainId = props.chainId; - const roundId = props.round.id; - - const navigate = useNavigate(); - - const [, setPassport] = useState(); - const [, setError] = useState(); - const { address, isConnected } = useAccount(); - - const [passportState, setPassportState] = useState( - PassportState.LOADING - ); - - useEffect(() => { - setPassportState(PassportState.LOADING); - - const PASSPORT_COMMUNITY_ID = - process.env.REACT_APP_PASSPORT_API_COMMUNITY_ID; - - if (isConnected && address && PASSPORT_COMMUNITY_ID) { - const callFetchPassport = async () => { - const res = await fetchPassport(address, PASSPORT_COMMUNITY_ID); - if (res.ok) { - const scoreResponse = await res.json(); - - if (scoreResponse.status === "PROCESSING") { - console.log("processing, calling again in 3000 ms"); - setTimeout(async () => { - await callFetchPassport(); - }, 3000); - return; - } - - if (scoreResponse.status === "ERROR") { - // due to error at passport end - setPassportState(PassportState.ERROR); - return; - } - - setPassport(scoreResponse); - setPassportState( - Number(scoreResponse.evidence.rawScore) >= - Number(scoreResponse.evidence.threshold) - ? PassportState.MATCH_ELIGIBLE - : PassportState.MATCH_INELIGIBLE - ); - } else { - setError(res); - switch (res.status) { - case 400: // unregistered/nonexistent passport address - setPassportState(PassportState.INVALID_PASSPORT); - break; - case 401: // invalid API key - setPassportState(PassportState.ERROR); - console.error("invalid API key", res.json()); - break; - default: - setPassportState(PassportState.ERROR); - console.error("Error fetching passport", res); - } - } - }; - - callFetchPassport(); - } else { - setPassportState(PassportState.NOT_CONNECTED); - } - - // call fetch passport - // check passport - }, [address, isConnected]); - - const ViewScoreButton = () => ( -
- -
- -
-
- ); - - const UpdateScoreButton = () => ( -
- -
- -
-
- ); - - const CreatePassportButton = () => ( -
- -
- -
-
- ); - - const ConnectWalletButton = () => ( -
- -
- -
-
- ); - - const AlertIcon = () => { - return ( -
- -
- ); - }; - - const bannerConfig = { - [PassportState.NOT_CONNECTED]: { - icon: ( - - ), - color: "bg-white", - testId: "wallet-not-connected", - body: `Want to make sure your donations get matched? Verify your Gitcoin Passport by ${getUTCDateTime( - props.round.roundEndTime - )}`, - button: , - }, - [PassportState.MATCH_ELIGIBLE]: { - icon: ( - - ), - color: "bg-white", - testId: "match-eligible", - body: "Gitcoin Passport score verified. Your donation will be matched!", - button: , - }, - [PassportState.MATCH_INELIGIBLE]: { - icon: , - color: "bg-white", - testId: "match-ineligible", - body: `Your Gitcoin Passport is not currently eligible for donation matching. Please update by ${getUTCDateTime( - props.round.roundEndTime - )}.`, - button: , - }, - [PassportState.LOADING]: { - icon: , - color: "bg-white", - testId: "loading-passport-score", - body: "Loading Passport...", - button: null, - }, - [PassportState.INVALID_PASSPORT]: { - icon: , - color: "bg-white", - testId: "invalid-passport", - body: `You don't have a Gitcoin Passport. Please create one by ${getUTCDateTime( - props.round.roundEndTime - )}.`, - button: , - }, - [PassportState.ERROR]: { - icon: ( - - ), - color: "bg-white", - testId: "error-loading-passport", - body: "An error occurred while loading your Gitcoin Passport. Please try again later.", - button: null, - }, - [PassportState.INVALID_RESPONSE]: { - icon: ( - - ), - color: "bg-white", - testId: "error-loading-passport", - body: "Passport Profile not detected. Please open Passport to troubleshoot.", - button: null, - }, - }; - - return ( -
-
-
-
-
- {bannerConfig[passportState].icon} -
-
- - {bannerConfig[passportState].body} - - {bannerConfig[passportState].button} -
-
-
-
-
- ); -} diff --git a/packages/grant-explorer/src/features/common/PassportWidget.tsx b/packages/grant-explorer/src/features/common/PassportWidget.tsx new file mode 100644 index 0000000000..baea582bb6 --- /dev/null +++ b/packages/grant-explorer/src/features/common/PassportWidget.tsx @@ -0,0 +1,154 @@ +import { useState } from "react"; +import { PassportState, usePassport } from "../api/passport"; +import { useAccount } from "wagmi"; +import { ReactComponent as GitcoinPassportLogo } from "../../assets/passport-logo.svg"; +import { ReactComponent as GitcoinPassportBWLogo } from "../../assets/passport-logo-bw.svg"; +import { ReactComponent as GitcoinPassportLogoFull } from "../../assets/passport-logo-full.svg"; +import { Dropdown as DropdownIcon } from "common/src/icons/Dropdown"; + +export function PassportWidget() { + const { address } = useAccount(); + + const { passportState, passportScore, passportColor, donationImpact } = + usePassport({ address }); + + const [isOpen, setIsOpen] = useState(false); + + function handleClick() { + if ( + passportState === PassportState.SCORE_AVAILABLE || + passportState === PassportState.INVALID_PASSPORT + ) { + setIsOpen(!isOpen); + } + } + + return ( + <> +
handleClick()} + > + {passportState === PassportState.SCORE_AVAILABLE ? ( +
+ +
+
+ ) : ( + + )} + {passportState === PassportState.SCORE_AVAILABLE && ( + + )} + +
+
+ + {passportState === PassportState.SCORE_AVAILABLE ? ( + <> +
+
+

Passport Score

+

{passportScore}

+
+
+

Donation Impact

+

+ +{donationImpact.toFixed(2)}% +

+
+
+

+ Your donation impact is calculated based on your Passport + score. Scores higher than 15 will begin to be eligible for + matching, and your donation impact scales as your Passport + score increases. You can update your score by heading over to{" "} + + Passport + + . +

+ + ) : ( + <> +

+ Passport score not detected. +

+

+ You either do not have a Passport or no stamps added to your + Passport yet. Please head over to{" "} + + Passport + {" "} + to configure your score. +

+ + )} +
+ +
+
+
+
+ + ); +} diff --git a/packages/grant-explorer/src/features/common/__tests__/Navbar.test.tsx b/packages/grant-explorer/src/features/common/__tests__/Navbar.test.tsx index 05419bbdc2..d8f848d6f4 100644 --- a/packages/grant-explorer/src/features/common/__tests__/Navbar.test.tsx +++ b/packages/grant-explorer/src/features/common/__tests__/Navbar.test.tsx @@ -33,6 +33,10 @@ vi.mock("@rainbow-me/rainbowkit", () => ({ vi.mock("../Auth"); +vi.mock("../PassportWidget", () => ({ + PassportWidget: vi.fn(), +})); + const navigateMock = vi.fn(); vi.mock("react-router-dom", async () => { @@ -63,6 +67,11 @@ describe("", () => { expect(screen.getByTestId("connect-wallet-button")).toBeInTheDocument(); }); + it("SHOULD display passport widget", () => { + renderWithContext(); + expect(screen.getByTestId("passport-widget")).toBeInTheDocument(); + }); + it("SHOULD display cart if round has not ended", () => { renderWithContext(); expect(screen.getByTestId("navbar-cart")).toBeInTheDocument(); diff --git a/packages/grant-explorer/src/features/common/__tests__/PassportBanner.test.tsx b/packages/grant-explorer/src/features/common/__tests__/PassportBanner.test.tsx deleted file mode 100644 index 49df518d83..0000000000 --- a/packages/grant-explorer/src/features/common/__tests__/PassportBanner.test.tsx +++ /dev/null @@ -1,213 +0,0 @@ -import { render, screen, waitFor } from "@testing-library/react"; -import PassportBanner from "../PassportBanner"; -import { BrowserRouter } from "react-router-dom"; -import { fetchPassport } from "../../api/passport"; -import { faker } from "@faker-js/faker"; -import { - makeRoundData, - mockBalance, - mockNetwork, - mockSigner, -} from "../../../test-utils"; - -const userAddress = faker.finance.ethereumAddress(); - -const mockAccount = { - address: userAddress, - isConnected: false, -}; - -vi.mock("wagmi", () => ({ - useAccount: () => mockAccount, - useBalance: () => mockBalance, - useSigner: () => mockSigner, - useNetwork: () => mockNetwork, -})); - -vi.mock("../../../features/api/passport"); - -process.env.REACT_APP_PASSPORT_API_COMMUNITY_ID = "12"; - -describe("PassportBanner", () => { - const mockRound = makeRoundData(); - - describe("renders the correct banner", () => { - it("WHEN user is not connected to passport THEN it shows the not connected banner", () => { - mockAccount.isConnected = false; - render(, { - wrapper: BrowserRouter, - }); - expect(screen.getByTestId("wallet-not-connected")).toBeInTheDocument(); - expect(screen.getByTestId("connect-wallet-button")).toBeInTheDocument(); - }); - - it("WHEN user is connected to passport and is ELIGIBLE for match THEN it shows the eligible for matching banner", async () => { - mockAccount.isConnected = true; - - const mockJsonPromise = Promise.resolve({ - score: "1", - address: userAddress, - evidence: { - rawScore: 1, - threshold: 1, - }, - }); - - const mockFetchPassportPromise = { - ok: true, - json: () => mockJsonPromise, - } as unknown as Response; - - vitest - .mocked(fetchPassport) - .mockResolvedValueOnce(mockFetchPassportPromise); - - render(, { - wrapper: BrowserRouter, - }); - - await waitFor(() => { - expect(screen.getByTestId("match-eligible")).toBeInTheDocument(); - expect(screen.getByTestId("view-score-button")).toBeInTheDocument(); - }); - }); - - it("WHEN user is connected to passport and is not ELIGIBLE for match THEN it shows the not eligible for matching banner", async () => { - mockAccount.isConnected = true; - - const mockJsonPromise = Promise.resolve({ - score: "-1", - address: userAddress, - evidence: { - rawScore: -1, - threshold: 1, - }, - }); - - const mockFetchPassportPromise = { - ok: true, - json: () => mockJsonPromise, - } as unknown as Response; - - vitest.mocked(fetchPassport).mockResolvedValue(mockFetchPassportPromise); - - render(, { - wrapper: BrowserRouter, - }); - - await waitFor(() => { - expect(screen.getByTestId("match-ineligible")).toBeInTheDocument(); - expect(screen.getByTestId("view-score-button")).toBeInTheDocument(); - }); - }); - - it("WHEN user is connected to passport and is LOADING for match THEN it shows the passport loading banner", () => { - mockAccount.isConnected = true; - - const mockJsonPromise = Promise.resolve({ - score: "1", - address: userAddress, - evidence: { - rawScore: 1, - threshold: 1, - }, - }); - - const mockFetchPassportPromise = { - ok: true, - json: () => mockJsonPromise, - } as unknown as Response; - - vitest.mocked(fetchPassport).mockResolvedValue(mockFetchPassportPromise); - - render(, { - wrapper: BrowserRouter, - }); - - expect(screen.getByTestId("loading-passport-score")).toBeInTheDocument(); - }); - - it("WHEN user is connected to passport and is an invalid passport THEN it shows the invalid matching banner", async () => { - mockAccount.isConnected = true; - - const mockJsonPromise = Promise.resolve({ - address: userAddress, - evidence: { - rawScore: 1, - threshold: 1, - }, - }); - - const mockFetchPassportPromise = { - ok: false, - json: () => mockJsonPromise, - status: 400, - } as unknown as Response; - - vitest.mocked(fetchPassport).mockResolvedValue(mockFetchPassportPromise); - - render(, { - wrapper: BrowserRouter, - }); - - await waitFor(() => { - expect(screen.getByTestId("invalid-passport")).toBeInTheDocument(); - }); - }); - - it("WHEN user is connected to passport and it errors out THEN it shows the error banner", async () => { - mockAccount.isConnected = true; - - const mockJsonPromise = Promise.resolve({}); - - const mockFetchPassportPromise = { - ok: false, - json: () => mockJsonPromise, - status: 401, - } as unknown as Response; - - vitest.mocked(fetchPassport).mockResolvedValue(mockFetchPassportPromise); - - render(, { - wrapper: BrowserRouter, - }); - - await waitFor(() => { - expect( - screen.getByTestId("error-loading-passport") - ).toBeInTheDocument(); - }); - }); - - it("WHEN user is connected to passport and it errors out THEN it shows the error banner", async () => { - mockAccount.isConnected = true; - - const mockJsonPromise = Promise.resolve({ - address: userAddress, - status: "ERROR", - evidence: { - rawScore: 1, - threshold: 1, - }, - }); - - const mockFetchPassportPromise = { - ok: false, - json: () => mockJsonPromise, - status: 200, - } as unknown as Response; - - vitest.mocked(fetchPassport).mockResolvedValue(mockFetchPassportPromise); - - render(, { - wrapper: BrowserRouter, - }); - - await waitFor(() => { - expect( - screen.getByTestId("error-loading-passport") - ).toBeInTheDocument(); - }); - }); - }); -}); diff --git a/packages/grant-explorer/src/features/discovery/RoundCard.tsx b/packages/grant-explorer/src/features/discovery/RoundCard.tsx index ecd7f50c5e..b80033e780 100644 --- a/packages/grant-explorer/src/features/discovery/RoundCard.tsx +++ b/packages/grant-explorer/src/features/discovery/RoundCard.tsx @@ -108,7 +108,7 @@ const RoundCard = ({ round }: RoundCardProps) => { isValidRoundEndTime={isValidRoundEndTime} /> - +
diff --git a/packages/grant-explorer/src/features/round/PassportConnect.tsx b/packages/grant-explorer/src/features/round/PassportConnect.tsx deleted file mode 100644 index 81d2d306ed..0000000000 --- a/packages/grant-explorer/src/features/round/PassportConnect.tsx +++ /dev/null @@ -1,440 +0,0 @@ -import { datadogLogs } from "@datadog/browser-logs"; -import { - ArrowPathIcon, - ChevronRightIcon, - ChevronDownIcon, - ChevronUpIcon, -} from "@heroicons/react/24/solid"; -import { Button } from "common/src/styles"; -import { useEffect, useState } from "react"; -import { Link, useNavigate, useParams } from "react-router-dom"; -import { useAccount } from "wagmi"; -import { ReactComponent as PassportLogo } from "../../assets/passport-logo.svg"; -import { - fetchPassport, - PassportResponse, - PassportState, - submitPassport, -} from "../api/passport"; -import Footer from "common/src/components/Footer"; -import Navbar from "../common/Navbar"; -import { useRoundById } from "../../context/RoundContext"; -import { formatUTCDateAsISOString, getUTCTime } from "common"; - -export default function PassportConnect() { - datadogLogs.logger.info( - "====> Route: /round/:chainId/:roundId/passport/connect" - ); - datadogLogs.logger.info(`====> URL: ${window.location.href}`); - - const navigate = useNavigate(); - - const { chainId, roundId } = useParams(); - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const { round, isLoading } = useRoundById(chainId!, roundId!); - - const PASSPORT_COMMUNITY_ID = process.env.REACT_APP_PASSPORT_API_COMMUNITY_ID; - - const [passport, setPassport] = useState(); - const [, setError] = useState(); - const { address, isConnected } = useAccount(); - - const [passportState, setPassportState] = useState( - PassportState.LOADING - ); - - const [expanded, setExpanded] = useState(true); - - useEffect(() => { - if (passportState === PassportState.MATCH_ELIGIBLE) { - setExpanded(false); - } - }, [passportState]); - - const callFetchPassport = async () => { - if (!address || !PASSPORT_COMMUNITY_ID) { - setPassportState(PassportState.NOT_CONNECTED); - return; - } - - const res = await fetchPassport(address, PASSPORT_COMMUNITY_ID); - - if (!res) { - datadogLogs.logger.error(`error: callFetchPassport - fetch failed`, res); - setPassportState(PassportState.ERROR); - return; - } - - if (res.ok) { - const scoreResponse: PassportResponse = await res.json(); - - if (scoreResponse.status === "PROCESSING") { - console.log("processing, calling again in 3000 ms"); - setTimeout(async () => { - await callFetchPassport(); - }, 3000); - return; - } - - if ( - !scoreResponse.score || - !scoreResponse.evidence || - scoreResponse.status === "ERROR" - ) { - datadogLogs.logger.error( - `error: callFetchPassport - invalid score response`, - scoreResponse - ); - setPassportState(PassportState.INVALID_RESPONSE); - return; - } - - setPassport(scoreResponse); - setPassportState( - Number(scoreResponse.evidence.rawScore) >= - Number(scoreResponse.evidence.threshold) - ? PassportState.MATCH_ELIGIBLE - : PassportState.MATCH_INELIGIBLE - ); - } else { - setError(res); - datadogLogs.logger.error( - `error: callFetchPassport - passport NOT OK`, - res - ); - switch (res.status) { - case 400: // unregistered/nonexistent passport address - setPassportState(PassportState.INVALID_PASSPORT); - break; - case 401: // invalid API key - setPassportState(PassportState.ERROR); - console.error("invalid API key", res.json()); - break; - default: - setPassportState(PassportState.ERROR); - console.error("Error fetching passport", res); - } - } - }; - - useEffect(() => { - setPassportState(PassportState.LOADING); - if (isConnected) { - callFetchPassport(); - } else { - setPassportState(PassportState.NOT_CONNECTED); - } - }, [address, isConnected]); // eslint-disable-line react-hooks/exhaustive-deps - - const updatePassportScore = async () => { - if (!address || !PASSPORT_COMMUNITY_ID) return; - - setPassportState(PassportState.LOADING); - submitPassport(address, PASSPORT_COMMUNITY_ID) - .then(() => { - callFetchPassport(); - }) - .catch((err) => { - console.error("Error submitting passport", err); - setPassportState(PassportState.ERROR); - }); - }; - - function HaveAPassportInstructions() { - return ( -
- -
-
-
Instructions
- -
- {expanded && ( - <> -
-
-
1
-
-
- Create a Gitcoin Passport if you don’t have one already. You - will be taken to a new window to begin verifying your - identity. -
-
-
-
-
2
-
-
- Verify your identity by connecting to various stamps. -
-
-
-
-
3
-
-
- Return back to this screen and recalculate your score. -
-
-
-
-
4
-
-
- If ineligible, you will have the chance to verify more stamps - to raise your score. Once you have, recalculate your score. -
-
-
-
-
5
-
-
- If eligible, your donation will be matched. -
-
- - )} -
-
- ); - } - - function PassportButtons() { - return ( - <> -
-
-

- {passportState === PassportState.LOADING && ( - - - - )} - {(passportState === PassportState.MATCH_ELIGIBLE || - passportState === PassportState.MATCH_INELIGIBLE) && ( - <> - - {(passport?.evidence?.rawScore && - Number(passport?.evidence?.rawScore).toFixed(2)) || - 0} - - / - - {(passport?.evidence?.threshold && - Number(passport?.evidence?.threshold).toFixed(2)) || - 0} - - - )} -

- - -
- - {passportState === PassportState.LOADING && ( -
-

Checking eligibility

-

Fetching score from passport!

-
- )} - - {passportState === PassportState.MATCH_ELIGIBLE && ( -
-

Eligible for matching

-

You are eligible for matching. Happy donating!

-
- )} - - {passportState === PassportState.MATCH_INELIGIBLE && ( -
-

Ineligible for matching

-

- Reach {Number(passport?.evidence?.threshold).toFixed(2) || 0} to - have your donation matched. -

-
- )} - - {passportState === PassportState.NOT_CONNECTED && ( -
-

Ineligible for matching

-

Please create a Gitcoin Passport in order to continue.

-
- )} - - {passportState === PassportState.ERROR && ( -
-

Error In fetching passport

-

Please try again later.

-
- )} - - {passportState === PassportState.INVALID_RESPONSE && ( -
-

- Passport Profile not detected. -

-

Please open Passport to troubleshoot.

-
- )} -
- -
- - - {passportState === PassportState.INVALID_PASSPORT && ( - - )} - {passportState === PassportState.MATCH_INELIGIBLE && ( - - )} - {passportState === PassportState.MATCH_ELIGIBLE && ( - - )} -
- - {passportState === PassportState.MATCH_INELIGIBLE && ( -
-

- Make sure to update your score before the round ends{" "} - {!isLoading && round - ? "on " + - formatUTCDateAsISOString(round.roundEndTime) + - " " + - getUTCTime(round.roundEndTime) - : ""} - . -

-
- )} - - ); - } - - return ( - <> - -
-
-
- - Home - - - Connect to Passport -
-
- -
-
-

- Amplify - your donation -

-

- Unlock matching for your donation by verifying your identity -

-

- Connect your wallet to Gitcoin Passport to check your identity - score and maximize your donation power. -

-

- Passport is designed to proactively verify users’ identities to - protect against Sybil attacks. -

- -

- - What is Gitcoin Passport and how does it work? - -

- -
- -
-
-
-
-
-
-
- - ); -} diff --git a/packages/grant-explorer/src/features/round/ViewCartPage/RoundInCart.tsx b/packages/grant-explorer/src/features/round/ViewCartPage/RoundInCart.tsx index b36989c465..9b90841b3a 100644 --- a/packages/grant-explorer/src/features/round/ViewCartPage/RoundInCart.tsx +++ b/packages/grant-explorer/src/features/round/ViewCartPage/RoundInCart.tsx @@ -11,11 +11,7 @@ import { useAccount } from "wagmi"; import { useCartStorage } from "../../../store"; import { Skeleton } from "@chakra-ui/react"; import { BoltIcon } from "@heroicons/react/24/outline"; -import { usePassport } from "../../api/passport"; -import { - passportColorTextClass, - usePassportScore, -} from "../../common/Passport"; +import { getClassForPassportColor, usePassport } from "../../api/passport"; export function RoundInCart( props: React.ComponentProps<"div"> & { @@ -64,11 +60,11 @@ export function RoundInCart( const estimateText = matchingEstimatesToText(matchingEstimates); - const { passport } = usePassport({ - address: address ?? "", + const { passportColor } = usePassport({ + address, }); - const passportScore = usePassportScore(passport); + const passportTextClass = getClassForPassportColor(passportColor ?? "gray"); return (
@@ -94,9 +90,7 @@ export function RoundInCart(
{matchingEstimateError === undefined && matchingEstimates !== undefined && ( diff --git a/packages/grant-explorer/src/features/round/ViewCartPage/SummaryContainer.tsx b/packages/grant-explorer/src/features/round/ViewCartPage/SummaryContainer.tsx index 04e53c544a..ee8052a6a8 100644 --- a/packages/grant-explorer/src/features/round/ViewCartPage/SummaryContainer.tsx +++ b/packages/grant-explorer/src/features/round/ViewCartPage/SummaryContainer.tsx @@ -13,7 +13,7 @@ import { Button } from "common/src/styles"; import { InformationCircleIcon } from "@heroicons/react/24/solid"; import { BoltIcon } from "@heroicons/react/24/outline"; -import { usePassport } from "../../api/passport"; +import { getClassForPassportColor, usePassport } from "../../api/passport"; import useSWR from "swr"; import { groupBy, uniqBy } from "lodash-es"; import { getRoundById } from "../../api/round"; @@ -29,10 +29,6 @@ import { import { Skeleton } from "@chakra-ui/react"; import { MatchingEstimateTooltip } from "../../common/MatchingEstimateTooltip"; import { parseChainId } from "common/src/chains"; -import { - passportColorTextClass, - usePassportScore, -} from "../../common/Passport"; export function SummaryContainer() { const { projects, getVotingTokenForChain, chainToVotingToken } = @@ -249,11 +245,11 @@ export function SummaryContainer() { } } - const { passportState, passport } = usePassport({ + const { passportColor, passportScore, passportState } = usePassport({ address: address ?? "", }); - const passportScore = usePassportScore(passport); + const passportTextClass = getClassForPassportColor(passportColor ?? "gray"); const [totalDonationAcrossChainsInUSD, setTotalDonationAcrossChainsInUSD] = useState(); @@ -333,9 +329,7 @@ export function SummaryContainer() {

Summary

{matchingEstimateError === undefined && matchingEstimates !== undefined && ( @@ -344,7 +338,7 @@ export function SummaryContainer() {

Estimated match

= 15 + passportScore !== undefined && passportScore >= 15 } />
diff --git a/packages/grant-explorer/src/features/round/ViewProjectDetails.tsx b/packages/grant-explorer/src/features/round/ViewProjectDetails.tsx index 14a56a2c67..50c7936450 100644 --- a/packages/grant-explorer/src/features/round/ViewProjectDetails.tsx +++ b/packages/grant-explorer/src/features/round/ViewProjectDetails.tsx @@ -27,7 +27,6 @@ import { } from "../api/types"; import Footer from "common/src/components/Footer"; import Navbar from "../common/Navbar"; -import PassportBanner from "../common/PassportBanner"; import { ProjectBanner } from "../common/ProjectBanner"; import RoundEndedBanner from "../common/RoundEndedBanner"; import Breadcrumb, { BreadcrumbItem } from "../common/Breadcrumb"; @@ -126,10 +125,6 @@ export default function ViewProjectDetails() { return ( <> - - {round && !isDirectRound(round) && isBeforeRoundEndDate && ( - - )} {isAfterRoundEndDate && (
diff --git a/packages/grant-explorer/src/features/round/ViewRoundPage.tsx b/packages/grant-explorer/src/features/round/ViewRoundPage.tsx index 8e19650162..ed464e6556 100644 --- a/packages/grant-explorer/src/features/round/ViewRoundPage.tsx +++ b/packages/grant-explorer/src/features/round/ViewRoundPage.tsx @@ -28,7 +28,6 @@ import { import Footer from "common/src/components/Footer"; import Navbar from "../common/Navbar"; import NotFoundPage from "../common/NotFoundPage"; -import PassportBanner from "../common/PassportBanner"; import { ProjectBanner } from "../common/ProjectBanner"; import RoundEndedBanner from "../common/RoundEndedBanner"; import { Spinner } from "../common/Spinner"; @@ -250,9 +249,6 @@ function AfterRoundStart(props: { <> {showCartNotification && renderCartNotification()} - {round && !isDirectRound(round) && props.isBeforeRoundEndDate && ( - - )} {props.isAfterRoundEndDate && (
diff --git a/packages/grant-explorer/src/features/round/__tests__/PassportConnect.test.tsx b/packages/grant-explorer/src/features/round/__tests__/PassportConnect.test.tsx deleted file mode 100644 index 968134e8e4..0000000000 --- a/packages/grant-explorer/src/features/round/__tests__/PassportConnect.test.tsx +++ /dev/null @@ -1,370 +0,0 @@ -import { faker } from "@faker-js/faker"; -import { fireEvent, screen, waitFor } from "@testing-library/react"; -import { - makeRoundData, - mockBalance, - mockNetwork, - mockSigner, - renderWithContext, -} from "../../../test-utils"; -import { fetchPassport, submitPassport } from "../../api/passport"; -import PassportConnect from "../PassportConnect"; -import { Round } from "../../api/types"; -import { votingTokens } from "../../api/utils"; -import { Mock } from "vitest"; - -const chainId = 5; -const roundId = faker.finance.ethereumAddress(); - -vi.mock("../../api/passport"); -vi.mock("../../common/Navbar"); -vi.mock("../../common/Auth"); - -vi.mock("react-router-dom", async () => { - const useParamsFn = () => ({ - chainId: chainId, - roundId: roundId, - }); - const actual = await vi.importActual( - "react-router-dom" - ); - return { - ...actual, - useNavigate: () => vi.fn(), - useParams: useParamsFn, - }; -}); - -const userAddress = faker.finance.ethereumAddress(); -const mockAccount = { - address: userAddress, - isConnected: true, -}; - -const mockJsonPromise = Promise.resolve({ - score: "1", - address: userAddress, - status: "DONE", - evidence: { - threshold: "0", - rawScore: "1", - }, -}); - -const mockPassportPromise = { - ok: true, - json: () => mockJsonPromise, -} as unknown as Response; - -vi.mock("wagmi", () => ({ - useAccount: () => mockAccount, - useBalance: () => mockBalance, - useSigner: () => mockSigner, - useNetwork: () => mockNetwork, -})); - -process.env.REACT_APP_PASSPORT_API_COMMUNITY_ID = "12"; - -describe("", () => { - describe("Navigation Buttons", () => { - beforeEach(() => { - vi.clearAllMocks(); - - let stubRound: Round; - const roundStartTime = faker.date.recent(); - const applicationsEndTime = faker.date.past(1, roundStartTime); - const applicationsStartTime = faker.date.past(1, applicationsEndTime); - const roundEndTime = faker.date.soon(); - const token = votingTokens[0].address; - - // eslint-disable-next-line prefer-const - stubRound = makeRoundData({ - id: roundId, - applicationsStartTime, - applicationsEndTime, - roundStartTime, - roundEndTime, - token: token, - }); - - renderWithContext(, { rounds: [stubRound] }); - }); - - it("shows Home and Connect to Passport breadcrumb", async () => { - await waitFor(() => { - expect(screen.getByTestId("breadcrumb")).toBeInTheDocument(); - expect(screen.getByText("Home")).toBeInTheDocument(); - expect(screen.getByText("Connect to Passport")).toBeInTheDocument(); - }); - }); - - it("shows back to browsing button on page load", async () => { - await waitFor(() => { - expect( - screen.getByTestId("back-to-browsing-button") - ).toBeInTheDocument(); - }); - }); - - it("shows what is passport link on page load", async () => { - await waitFor(() => { - expect(screen.getByTestId("what-is-passport-link")).toBeInTheDocument(); - }); - }); - }); - - describe("Passport Connect", () => { - beforeEach(() => { - vi.clearAllMocks(); - - let stubRound: Round; - const roundStartTime = faker.date.recent(); - const applicationsEndTime = faker.date.past(1, roundStartTime); - const applicationsStartTime = faker.date.past(1, applicationsEndTime); - const roundEndTime = faker.date.soon(); - const token = votingTokens[0].address; - - // eslint-disable-next-line prefer-const - stubRound = makeRoundData({ - id: roundId, - applicationsStartTime, - applicationsEndTime, - roundStartTime, - roundEndTime, - token: token, - }); - - const mockJsonPromise = Promise.resolve({ - score: "-1", - address: userAddress, - status: "DONE", - evidence: { - threshold: "0", - rawScore: "-1", - }, - }); - - const mockPassportPromise = { - ok: true, - json: () => mockJsonPromise, - } as unknown as Response; - - (fetchPassport as Mock).mockResolvedValueOnce(mockPassportPromise); - (submitPassport as Mock).mockResolvedValueOnce(vi.fn()); - - renderWithContext(, { - rounds: [stubRound], - isLoading: false, - }); - }); - - it("Should show the Create Passport button", async () => { - await waitFor(async () => { - expect( - screen.getByTestId("create-passport-button") - ).toBeInTheDocument(); - }); - }); - - it("Should show the Recalculate Score button", async () => { - await waitFor(() => { - expect( - screen.getByTestId("recalculate-score-button") - ).toBeInTheDocument(); - }); - }); - - it("Clicking the Recalculate Score button invokes submitPassport", async () => { - await waitFor(async () => { - fireEvent.click(await screen.findByTestId("recalculate-score-button")); - - expect(submitPassport).toHaveBeenCalled(); - expect(fetchPassport).toHaveBeenCalled(); - }); - }); - }); -}); - -describe("", () => { - describe("PassportConnect Passport State", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it("IF passport state return error status THEN it shows issue in fetching passport", async () => { - let stubRound: Round; - const roundStartTime = faker.date.recent(); - const applicationsEndTime = faker.date.past(1, roundStartTime); - const applicationsStartTime = faker.date.past(1, applicationsEndTime); - const roundEndTime = faker.date.soon(); - const token = votingTokens[0].address; - - // eslint-disable-next-line prefer-const - stubRound = makeRoundData({ - id: roundId, - applicationsStartTime, - applicationsEndTime, - roundStartTime, - roundEndTime, - token: token, - }); - - const mockJsonPromise = Promise.resolve({ - score: "-1", - address: userAddress, - status: "ERROR", - evidence: { - threshold: "0", - rawScore: "-1", - }, - }); - - const mockPassportPromise = { - ok: true, - json: () => mockJsonPromise, - } as unknown as Response; - - (fetchPassport as Mock).mockResolvedValueOnce(mockPassportPromise); - - renderWithContext(, { - rounds: [stubRound], - isLoading: false, - }); - - await waitFor(() => { - expect( - screen.getByText("Passport Profile not detected.") - ).toBeInTheDocument(); - expect( - screen.getByText("Please open Passport to troubleshoot.") - ).toBeInTheDocument(); - }); - }); - - it("IF passport state is match inelgible THEN it shows ineligible for matching", async () => { - let stubRound: Round; - const roundStartTime = faker.date.recent(); - const applicationsEndTime = faker.date.past(1, roundStartTime); - const applicationsStartTime = faker.date.past(1, applicationsEndTime); - const roundEndTime = faker.date.soon(); - const token = votingTokens[0].address; - - // eslint-disable-next-line prefer-const - stubRound = makeRoundData({ - id: roundId, - applicationsStartTime, - applicationsEndTime, - roundStartTime, - roundEndTime, - token: token, - }); - - const mockJsonPromise = Promise.resolve({ - score: "1", - address: userAddress, - status: "DONE", - evidence: { - threshold: "2", - rawScore: "1", - }, - }); - - const mockPassportPromise = { - ok: true, - json: () => mockJsonPromise, - } as unknown as Response; - - (fetchPassport as Mock).mockResolvedValueOnce(mockPassportPromise); - - renderWithContext(, { - rounds: [stubRound], - isLoading: false, - }); - - await waitFor(() => { - expect(screen.getByText("Ineligible for matching")).toBeInTheDocument(); - }); - }); - - it("IF passport state is match eligible THEN it shows eligible for matching", async () => { - let stubRound: Round; - const roundStartTime = faker.date.recent(); - const applicationsEndTime = faker.date.past(1, roundStartTime); - const applicationsStartTime = faker.date.past(1, applicationsEndTime); - const roundEndTime = faker.date.soon(); - const token = votingTokens[0].address; - - // eslint-disable-next-line prefer-const - stubRound = makeRoundData({ - id: roundId, - applicationsStartTime, - applicationsEndTime, - roundStartTime, - roundEndTime, - token: token, - }); - - const mockJsonPromise = Promise.resolve({ - score: "2", - address: userAddress, - status: "DONE", - evidence: { - threshold: "2", - rawScore: "2", - }, - }); - - const mockPassportPromise = { - ok: true, - json: () => mockJsonPromise, - } as unknown as Response; - - (fetchPassport as Mock).mockResolvedValueOnce(mockPassportPromise); - - renderWithContext(, { - rounds: [stubRound], - isLoading: false, - }); - - await waitFor(() => { - expect(screen.getByText("Eligible for matching")).toBeInTheDocument(); - }); - }); - - it("IF passport state is not connected THEN it shows ineligible for matching", async () => { - let stubRound: Round; - const roundStartTime = faker.date.recent(); - const applicationsEndTime = faker.date.past(1, roundStartTime); - const applicationsStartTime = faker.date.past(1, applicationsEndTime); - const roundEndTime = faker.date.soon(); - const token = votingTokens[0].address; - - // eslint-disable-next-line prefer-const - stubRound = makeRoundData({ - id: roundId, - applicationsStartTime, - applicationsEndTime, - roundStartTime, - roundEndTime, - token: token, - }); - - (fetchPassport as Mock).mockResolvedValueOnce(mockPassportPromise); - mockAccount.isConnected = false; - - renderWithContext(, { - rounds: [stubRound], - isLoading: false, - }); - - await waitFor(() => { - expect(screen.getByText("Ineligible for matching")).toBeInTheDocument(); - expect( - screen.getByText( - "Please create a Gitcoin Passport in order to continue." - ) - ).toBeInTheDocument(); - }); - }); - }); -}); diff --git a/packages/grant-explorer/src/index.css b/packages/grant-explorer/src/index.css index bf89f00442..5504bb7cfe 100644 --- a/packages/grant-explorer/src/index.css +++ b/packages/grant-explorer/src/index.css @@ -1,9 +1,31 @@ @import url("https://fonts.googleapis.com/css2?family=Libre+Franklin:wght@400;500;600;700&family=Miriam+Libre:wght@400;700&display=swap"); +@import url('https://fonts.googleapis.com/css2?family=DM+Mono&display=swap'); + +@font-face { + font-family: ModernEraMedium; + src: url(../public/modern-era-medium.otf); +} + +@font-face { + font-family: ModernEraRegular; + src: url(../public/modern-era-regular.otf); +} @tailwind base; @tailwind components; @tailwind utilities; +.font-dm-mono { + font-family: 'DM Mono', monospace; +} + +.font-modern-era-medium { + font-family: 'ModernEraMedium', serif; +} +.font-modern-era-regular { + font-family: 'ModernEraRegular', serif; +} + @layer base { html { @apply text-grey-500; diff --git a/packages/grant-explorer/src/index.tsx b/packages/grant-explorer/src/index.tsx index 913afaf13b..720c601959 100644 --- a/packages/grant-explorer/src/index.tsx +++ b/packages/grant-explorer/src/index.tsx @@ -20,7 +20,6 @@ import Auth from "./features/common/Auth"; import NotFound from "./features/common/NotFoundPage"; import ApplyNowPage from "./features/discovery/ApplyNowPage"; import LandingPage from "./features/discovery/LandingPage"; -import PassportConnect from "./features/round/PassportConnect"; import ThankYou from "./features/round/ThankYou"; import ViewProjectDetails from "./features/round/ViewProjectDetails"; import ViewRound from "./features/round/ViewRoundPage"; @@ -72,12 +71,6 @@ root.render( } /> - {/* Passport Connect */} - } - /> - } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a4652b6dfa..ed03e33464 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -366,7 +366,10 @@ importers: version: 5.2.2 wagmi: specifier: 0.12.19 - version: 0.12.19(@types/react@18.2.21)(ethers@5.7.2)(react-dom@18.2.0)(react@18.2.0)(typescript@5.2.2) + version: 0.12.19(@types/react@18.2.21)(ethers@5.7.2)(react-dom@18.2.0)(react@18.2.0)(typescript@5.2.2)(zod@3.22.4) + zod: + specifier: ^3.22.4 + version: 3.22.4 devDependencies: '@types/dompurify': specifier: ^2.4.0 @@ -487,7 +490,7 @@ importers: version: 4.0.4(vite@4.4.9) '@wagmi/core': specifier: 1.4.1 - version: 1.4.1(@types/react@18.2.21)(react@18.2.0)(typescript@5.2.2)(viem@1.10.7) + version: 1.4.1(@types/react@18.2.21)(react@18.2.0)(typescript@5.2.2)(viem@1.10.7)(zod@3.22.4) '@walletconnect/modal': specifier: ^2.5.9 version: 2.6.1(react@18.2.0) @@ -604,13 +607,16 @@ importers: version: link:../verify-env viem: specifier: ^1.5.3 - version: 1.10.7(typescript@5.2.2) + version: 1.10.7(typescript@5.2.2)(zod@3.22.4) wagmi: specifier: 1.4.1 - version: 1.4.1(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0)(typescript@5.2.2)(viem@1.10.7) + version: 1.4.1(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0)(typescript@5.2.2)(viem@1.10.7)(zod@3.22.4) web-vitals: specifier: ^2.1.0 version: 2.1.4 + zod: + specifier: ^3.22.4 + version: 3.22.4 zustand: specifier: ^4.4.0 version: 4.4.1(@types/react@18.2.21)(react@18.2.0) @@ -6113,7 +6119,7 @@ packages: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) react-remove-scroll: 2.5.4(@types/react@18.2.21)(react@18.2.0) - wagmi: 0.12.19(@types/react@18.2.21)(ethers@5.7.2)(react-dom@18.2.0)(react@18.2.0)(typescript@5.2.2) + wagmi: 0.12.19(@types/react@18.2.21)(ethers@5.7.2)(react-dom@18.2.0)(react@18.2.0)(typescript@5.2.2)(zod@3.22.4) transitivePeerDependencies: - '@types/react' dev: false @@ -6158,8 +6164,8 @@ packages: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) react-remove-scroll: 2.5.4(@types/react@18.2.21)(react@18.2.0) - viem: 1.10.7(typescript@5.2.2) - wagmi: 1.4.1(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0)(typescript@5.2.2)(viem@1.10.7) + viem: 1.10.7(typescript@5.2.2)(zod@3.22.4) + wagmi: 1.4.1(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0)(typescript@5.2.2)(viem@1.10.7)(zod@3.22.4) transitivePeerDependencies: - '@types/react' dev: false @@ -6367,10 +6373,10 @@ packages: - utf-8-validate dev: false - /@safe-global/safe-apps-provider@0.17.1(typescript@5.2.2): + /@safe-global/safe-apps-provider@0.17.1(typescript@5.2.2)(zod@3.22.4): resolution: {integrity: sha512-lYfRqrbbK1aKU1/UGkYWc/X7PgySYcumXKc5FB2uuwAs2Ghj8uETuW5BrwPqyjBknRxutFbTv+gth/JzjxAhdQ==} dependencies: - '@safe-global/safe-apps-sdk': 8.0.0(typescript@5.2.2) + '@safe-global/safe-apps-sdk': 8.0.0(typescript@5.2.2)(zod@3.22.4) events: 3.3.0 transitivePeerDependencies: - bufferutil @@ -6399,11 +6405,11 @@ packages: - utf-8-validate dev: false - /@safe-global/safe-apps-sdk@8.0.0(typescript@5.2.2): + /@safe-global/safe-apps-sdk@8.0.0(typescript@5.2.2)(zod@3.22.4): resolution: {integrity: sha512-gYw0ki/EAuV1oSyMxpqandHjnthZjYYy+YWpTAzf8BqfXM3ItcZLpjxfg+3+mXW8HIO+3jw6T9iiqEXsqHaMMw==} dependencies: '@safe-global/safe-gateway-typescript-sdk': 3.12.0 - viem: 1.10.7(typescript@5.2.2) + viem: 1.10.7(typescript@5.2.2)(zod@3.22.4) transitivePeerDependencies: - bufferutil - typescript @@ -6411,11 +6417,11 @@ packages: - zod dev: false - /@safe-global/safe-apps-sdk@8.1.0(typescript@5.2.2): + /@safe-global/safe-apps-sdk@8.1.0(typescript@5.2.2)(zod@3.22.4): resolution: {integrity: sha512-XJbEPuaVc7b9n23MqlF6c+ToYIS3f7P2Sel8f3cSBQ9WORE4xrSuvhMpK9fDSFqJ7by/brc+rmJR/5HViRr0/w==} dependencies: '@safe-global/safe-gateway-typescript-sdk': 3.12.0 - viem: 1.10.7(typescript@5.2.2) + viem: 1.10.7(typescript@5.2.2)(zod@3.22.4) transitivePeerDependencies: - bufferutil - typescript @@ -8160,7 +8166,7 @@ packages: - zod dev: false - /@wagmi/connectors@0.3.24(@wagmi/core@0.10.17)(ethers@5.7.2)(react@18.2.0)(typescript@5.2.2): + /@wagmi/connectors@0.3.24(@wagmi/core@0.10.17)(ethers@5.7.2)(react@18.2.0)(typescript@5.2.2)(zod@3.22.4): resolution: {integrity: sha512-1pI0G9HRblc651dCz9LXuEu/zWQk23XwOUYqJEINb/c2TTLtw5TnTRIcefxxK6RnxeJvcKfnmK0rdZp/4ujFAA==} peerDependencies: '@wagmi/core': '>=0.9.x' @@ -8176,11 +8182,11 @@ packages: '@ledgerhq/connect-kit-loader': 1.1.2 '@safe-global/safe-apps-provider': 0.15.2 '@safe-global/safe-apps-sdk': 7.11.0 - '@wagmi/core': 0.10.17(@types/react@18.2.21)(ethers@5.7.2)(react@18.2.0)(typescript@5.2.2) + '@wagmi/core': 0.10.17(@types/react@18.2.21)(ethers@5.7.2)(react@18.2.0)(typescript@5.2.2)(zod@3.22.4) '@walletconnect/ethereum-provider': 2.9.0(@walletconnect/modal@2.6.1)(lokijs@1.5.12) '@walletconnect/legacy-provider': 2.0.0 '@walletconnect/modal': 2.6.1(react@18.2.0) - abitype: 0.3.0(typescript@5.2.2) + abitype: 0.3.0(typescript@5.2.2)(zod@3.22.4) ethers: 5.7.2 eventemitter3: 4.0.7 typescript: 5.2.2 @@ -8195,7 +8201,7 @@ packages: - zod dev: false - /@wagmi/connectors@3.1.1(react@18.2.0)(typescript@5.2.2)(viem@1.10.7): + /@wagmi/connectors@3.1.1(react@18.2.0)(typescript@5.2.2)(viem@1.10.7)(zod@3.22.4): resolution: {integrity: sha512-ewOV40AlrXcX018qckU0V9OCsDgHhs+KZjQJZhlplqRtc2ijjS62B5kcypXkcTtfU5qXUBA9KEwPsSTxGdT4ag==} peerDependencies: typescript: '>=5.0.4' @@ -8206,16 +8212,16 @@ packages: dependencies: '@coinbase/wallet-sdk': 3.7.1 '@ledgerhq/connect-kit-loader': 1.1.2 - '@safe-global/safe-apps-provider': 0.17.1(typescript@5.2.2) - '@safe-global/safe-apps-sdk': 8.1.0(typescript@5.2.2) + '@safe-global/safe-apps-provider': 0.17.1(typescript@5.2.2)(zod@3.22.4) + '@safe-global/safe-apps-sdk': 8.1.0(typescript@5.2.2)(zod@3.22.4) '@walletconnect/ethereum-provider': 2.10.0(@walletconnect/modal@2.6.1)(lokijs@1.5.12) '@walletconnect/legacy-provider': 2.0.0 '@walletconnect/modal': 2.6.1(react@18.2.0) '@walletconnect/utils': 2.10.0(lokijs@1.5.12) - abitype: 0.8.7(typescript@5.2.2) + abitype: 0.8.7(typescript@5.2.2)(zod@3.22.4) eventemitter3: 4.0.7 typescript: 5.2.2 - viem: 1.10.7(typescript@5.2.2) + viem: 1.10.7(typescript@5.2.2)(zod@3.22.4) transitivePeerDependencies: - '@react-native-async-storage/async-storage' - bufferutil @@ -8285,7 +8291,7 @@ packages: - zod dev: false - /@wagmi/core@0.10.17(@types/react@18.2.21)(ethers@5.7.2)(react@18.2.0)(typescript@5.2.2): + /@wagmi/core@0.10.17(@types/react@18.2.21)(ethers@5.7.2)(react@18.2.0)(typescript@5.2.2)(zod@3.22.4): resolution: {integrity: sha512-qud45y3IlHp7gYWzoFeyysmhyokRie59Xa5tcx5F1E/v4moD5BY0kzD26mZW/ZQ3WZuVK/lZwiiPRqpqWH52Gw==} peerDependencies: ethers: '>=5.5.1 <6' @@ -8295,8 +8301,8 @@ packages: optional: true dependencies: '@wagmi/chains': 0.2.22(typescript@5.2.2) - '@wagmi/connectors': 0.3.24(@wagmi/core@0.10.17)(ethers@5.7.2)(react@18.2.0)(typescript@5.2.2) - abitype: 0.3.0(typescript@5.2.2) + '@wagmi/connectors': 0.3.24(@wagmi/core@0.10.17)(ethers@5.7.2)(react@18.2.0)(typescript@5.2.2)(zod@3.22.4) + abitype: 0.3.0(typescript@5.2.2)(zod@3.22.4) ethers: 5.7.2 eventemitter3: 4.0.7 typescript: 5.2.2 @@ -8314,7 +8320,7 @@ packages: - zod dev: false - /@wagmi/core@1.4.1(@types/react@18.2.21)(react@18.2.0)(typescript@5.2.2)(viem@1.10.7): + /@wagmi/core@1.4.1(@types/react@18.2.21)(react@18.2.0)(typescript@5.2.2)(viem@1.10.7)(zod@3.22.4): resolution: {integrity: sha512-b6LDFL0vZSCNcIHjnJzv++hakavTTt1/2WEQg2S5eEnaHTp7UoQlwfCyjKeiBhRih4yF34N06ea8cyEVjyjXrw==} peerDependencies: typescript: '>=5.0.4' @@ -8323,11 +8329,11 @@ packages: typescript: optional: true dependencies: - '@wagmi/connectors': 3.1.1(react@18.2.0)(typescript@5.2.2)(viem@1.10.7) - abitype: 0.8.7(typescript@5.2.2) + '@wagmi/connectors': 3.1.1(react@18.2.0)(typescript@5.2.2)(viem@1.10.7)(zod@3.22.4) + abitype: 0.8.7(typescript@5.2.2)(zod@3.22.4) eventemitter3: 4.0.7 typescript: 5.2.2 - viem: 1.10.7(typescript@5.2.2) + viem: 1.10.7(typescript@5.2.2)(zod@3.22.4) zustand: 4.4.1(@types/react@18.2.21)(react@18.2.0) transitivePeerDependencies: - '@react-native-async-storage/async-storage' @@ -9281,7 +9287,7 @@ packages: typescript: 4.9.5 dev: false - /abitype@0.3.0(typescript@5.2.2): + /abitype@0.3.0(typescript@5.2.2)(zod@3.22.4): resolution: {integrity: sha512-0YokyAV4hKMcy97Pl+6QgZBlBdZJN2llslOs7kiFY+cu7kMlVXDBpxMExfv0krzBCQt2t7hNovpQ3y/zvEm18A==} engines: {pnpm: '>=7'} peerDependencies: @@ -9292,9 +9298,10 @@ packages: optional: true dependencies: typescript: 5.2.2 + zod: 3.22.4 dev: false - /abitype@0.8.7(typescript@5.2.2): + /abitype@0.8.7(typescript@5.2.2)(zod@3.22.4): resolution: {integrity: sha512-wQ7hV8Yg/yKmGyFpqrNZufCxbszDe5es4AZGYPBitocfSqXtjrTG9JMWFcc4N30ukl2ve48aBTwt7NJxVQdU3w==} peerDependencies: typescript: '>=5.0.4' @@ -9304,9 +9311,10 @@ packages: optional: true dependencies: typescript: 5.2.2 + zod: 3.22.4 dev: false - /abitype@0.9.8(typescript@5.2.2): + /abitype@0.9.8(typescript@5.2.2)(zod@3.22.4): resolution: {integrity: sha512-puLifILdm+8sjyss4S+fsUN09obiT1g2YW6CtcQF+QDzxR0euzgEB29MZujC6zMk2a6SVmtttq1fc6+YFA7WYQ==} peerDependencies: typescript: '>=5.0.4' @@ -9318,6 +9326,7 @@ packages: optional: true dependencies: typescript: 5.2.2 + zod: 3.22.4 dev: false /abortable-iterator@3.0.2: @@ -22937,7 +22946,7 @@ packages: extsprintf: 1.3.0 dev: false - /viem@1.10.7(typescript@5.2.2): + /viem@1.10.7(typescript@5.2.2)(zod@3.22.4): resolution: {integrity: sha512-yuaYSHgV1g794nfxhn+V89qgK5ziFTLBSNqSDt4KW8YpjLu0Ah6LLZTtpOj3+MRWKKDwJ1YL2rENb8cuXstUzg==} peerDependencies: typescript: '>=5.0.4' @@ -22951,7 +22960,7 @@ packages: '@scure/bip32': 1.3.2 '@scure/bip39': 1.2.1 '@types/ws': 8.5.5 - abitype: 0.9.8(typescript@5.2.2) + abitype: 0.9.8(typescript@5.2.2)(zod@3.22.4) isomorphic-ws: 5.0.0(ws@8.13.0) typescript: 5.2.2 ws: 8.13.0 @@ -23297,7 +23306,7 @@ packages: - zod dev: false - /wagmi@0.12.19(@types/react@18.2.21)(ethers@5.7.2)(react-dom@18.2.0)(react@18.2.0)(typescript@5.2.2): + /wagmi@0.12.19(@types/react@18.2.21)(ethers@5.7.2)(react-dom@18.2.0)(react@18.2.0)(typescript@5.2.2)(zod@3.22.4): resolution: {integrity: sha512-S/el9BDb/HNeQWh1v8TvntMPX/CgKLDAoJqDb8i7jifLfWPqFL7gor3vnI1Vs6ZlB8uh7m+K1Qyg+mKhbITuDQ==} peerDependencies: ethers: '>=5.5.1 <6' @@ -23310,8 +23319,8 @@ packages: '@tanstack/query-sync-storage-persister': 4.35.0 '@tanstack/react-query': 4.35.0(react-dom@18.2.0)(react@18.2.0) '@tanstack/react-query-persist-client': 4.35.0(@tanstack/react-query@4.35.0) - '@wagmi/core': 0.10.17(@types/react@18.2.21)(ethers@5.7.2)(react@18.2.0)(typescript@5.2.2) - abitype: 0.3.0(typescript@5.2.2) + '@wagmi/core': 0.10.17(@types/react@18.2.21)(ethers@5.7.2)(react@18.2.0)(typescript@5.2.2)(zod@3.22.4) + abitype: 0.3.0(typescript@5.2.2)(zod@3.22.4) ethers: 5.7.2 react: 18.2.0 typescript: 5.2.2 @@ -23330,7 +23339,7 @@ packages: - zod dev: false - /wagmi@1.4.1(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0)(typescript@5.2.2)(viem@1.10.7): + /wagmi@1.4.1(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0)(typescript@5.2.2)(viem@1.10.7)(zod@3.22.4): resolution: {integrity: sha512-v3xd+uYZfLCAs1I4fLU7U9hg/gCw+Ud005J7kNR0mi20BcFAEU1EDN1LxHxpjUV0qKhOzSlMlrLjJyBCmSYhFA==} peerDependencies: react: '>=17.0.0' @@ -23343,12 +23352,12 @@ packages: '@tanstack/query-sync-storage-persister': 4.35.0 '@tanstack/react-query': 4.35.0(react-dom@18.2.0)(react@18.2.0) '@tanstack/react-query-persist-client': 4.35.0(@tanstack/react-query@4.35.0) - '@wagmi/core': 1.4.1(@types/react@18.2.21)(react@18.2.0)(typescript@5.2.2)(viem@1.10.7) - abitype: 0.8.7(typescript@5.2.2) + '@wagmi/core': 1.4.1(@types/react@18.2.21)(react@18.2.0)(typescript@5.2.2)(viem@1.10.7)(zod@3.22.4) + abitype: 0.8.7(typescript@5.2.2)(zod@3.22.4) react: 18.2.0 typescript: 5.2.2 use-sync-external-store: 1.2.0(react@18.2.0) - viem: 1.10.7(typescript@5.2.2) + viem: 1.10.7(typescript@5.2.2)(zod@3.22.4) transitivePeerDependencies: - '@react-native-async-storage/async-storage' - '@types/react' @@ -24148,6 +24157,10 @@ packages: ethers: 5.7.2 dev: true + /zod@3.22.4: + resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} + dev: false + /zustand@4.4.1(@types/react@18.2.21)(react@18.2.0): resolution: {integrity: sha512-QCPfstAS4EBiTQzlaGP1gmorkh/UL1Leaj2tdj+zZCZ/9bm0WS7sI2wnfD5lpOszFqWJ1DcPnGoY8RDL61uokw==} engines: {node: '>=12.7.0'}