From 047932fb1d89b9d5384f96d3dd76d9f4c64ab529 Mon Sep 17 00:00:00 2001 From: eagle Date: Fri, 13 Oct 2023 19:28:06 +0530 Subject: [PATCH 01/21] feat: add passport logo in navbar --- packages/grant-explorer/src/features/common/Navbar.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/grant-explorer/src/features/common/Navbar.tsx b/packages/grant-explorer/src/features/common/Navbar.tsx index e98ce2f034..df1601fb89 100644 --- a/packages/grant-explorer/src/features/common/Navbar.tsx +++ b/packages/grant-explorer/src/features/common/Navbar.tsx @@ -1,11 +1,12 @@ 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 { ReactComponent as GitcoinPassportLogo } from "../../assets/passport-logo.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 { useEffect, useState } from "react"; import { useAccount } from "wagmi"; import { useCartStorage } from "../../store"; import { Link } from "react-router-dom"; @@ -38,6 +39,8 @@ export default function Navbar(props: NavbarProps) { const { address: walletAddress } = useAccount(); + const [passportWidgetOpen, setPassportWidgetOpen] = useState(false); + return ( ); } diff --git a/packages/grant-explorer/src/features/common/PassportWidget.tsx b/packages/grant-explorer/src/features/common/PassportWidget.tsx index cc31a3e14f..3fdd487f26 100644 --- a/packages/grant-explorer/src/features/common/PassportWidget.tsx +++ b/packages/grant-explorer/src/features/common/PassportWidget.tsx @@ -117,7 +117,7 @@ export function PassportWidget() { />
From 8316e0f72e7e27c9ecba4a7ee39130077ba9f8ae Mon Sep 17 00:00:00 2001 From: eagle Date: Thu, 19 Oct 2023 18:23:16 +0530 Subject: [PATCH 10/21] feat: make passport widget responsive --- .../src/features/common/PassportWidget.tsx | 107 +++++++++--------- 1 file changed, 55 insertions(+), 52 deletions(-) diff --git a/packages/grant-explorer/src/features/common/PassportWidget.tsx b/packages/grant-explorer/src/features/common/PassportWidget.tsx index 3fdd487f26..7f4f67ec94 100644 --- a/packages/grant-explorer/src/features/common/PassportWidget.tsx +++ b/packages/grant-explorer/src/features/common/PassportWidget.tsx @@ -104,7 +104,10 @@ export function PassportWidget() { }, [address, isConnected]); return ( <> -
handleClick()}> +
handleClick()} + > {passportState === PassportState.SCORE_AVAILABLE && (
@@ -115,58 +118,58 @@ export function PassportWidget() { className="inline mt-3" direction={open ? "up" : "down"} /> -
-
-
- - {passportState === PassportState.SCORE_AVAILABLE ? ( - <> -
-
-

Passport Score

-

{passportScore}

-
-
-

Donation Impact

-

+{donationImpact}%

+
+
+ + {passportState === PassportState.SCORE_AVAILABLE ? ( + <> +
+
+

Passport Score

+

{passportScore}

+
+
+

Donation Impact

+

+{donationImpact}%

+
-
-

- Your donation impact is a reflection of your Passport score. - This percentage ensures a fair and proportional match. You can - always 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. -

- - )} -
- +

+ Your donation impact is a reflection of your Passport score. + This percentage ensures a fair and proportional match. You can + always 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. +

+ + )} +
+ +
From b42d92b28977686e2639698f0dd3b4fa55c7c002 Mon Sep 17 00:00:00 2001 From: eagle Date: Thu, 19 Oct 2023 19:10:23 +0530 Subject: [PATCH 11/21] feat: update passport widget mobile view --- .../src/assets/passport-logo-bw.svg | 19 ++++++++++++++++++ .../src/features/common/PassportWidget.tsx | 20 +++++++++++++++---- 2 files changed, 35 insertions(+), 4 deletions(-) create mode 100644 packages/grant-explorer/src/assets/passport-logo-bw.svg 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/features/common/PassportWidget.tsx b/packages/grant-explorer/src/features/common/PassportWidget.tsx index 7f4f67ec94..3f13fa2b02 100644 --- a/packages/grant-explorer/src/features/common/PassportWidget.tsx +++ b/packages/grant-explorer/src/features/common/PassportWidget.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from "react"; import { PassportResponse, fetchPassport } 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"; @@ -108,18 +109,29 @@ export function PassportWidget() { className="flex flex-row gap-2 mt-1 relative" onClick={() => handleClick()} > - + {passportState === PassportState.SCORE_AVAILABLE ? ( +
+ +
+
+ ) : ( + + )} {passportState === PassportState.SCORE_AVAILABLE && ( -
+ )}
From 863bac1b93b2a8c5b410639b9ce555573bda3887 Mon Sep 17 00:00:00 2001 From: eagle Date: Fri, 20 Oct 2023 18:12:18 +0530 Subject: [PATCH 12/21] feat: passport cleanup --- packages/common/src/index.ts | 3 +- .../src/features/api/passport.ts | 66 ++- .../src/features/common/PassportBanner.tsx | 261 ----------- .../common/__tests__/PassportBanner.test.tsx | 213 --------- .../src/features/round/PassportConnect.tsx | 440 ------------------ .../round/__tests__/PassportConnect.test.tsx | 370 --------------- packages/grant-explorer/src/index.tsx | 7 - 7 files changed, 52 insertions(+), 1308 deletions(-) delete mode 100644 packages/grant-explorer/src/features/common/PassportBanner.tsx delete mode 100644 packages/grant-explorer/src/features/common/__tests__/PassportBanner.test.tsx delete mode 100644 packages/grant-explorer/src/features/round/PassportConnect.tsx delete mode 100644 packages/grant-explorer/src/features/round/__tests__/PassportConnect.test.tsx diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index ed7ac838a2..4dda61e433 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -10,8 +10,7 @@ export { ChainId }; export enum PassportState { NOT_CONNECTED, INVALID_PASSPORT, - MATCH_ELIGIBLE, - MATCH_INELIGIBLE, + SCORE_AVAILABLE, LOADING, ERROR, INVALID_RESPONSE, diff --git a/packages/grant-explorer/src/features/api/passport.ts b/packages/grant-explorer/src/features/api/passport.ts index e5b848ee4c..1459bc5fac 100644 --- a/packages/grant-explorer/src/features/api/passport.ts +++ b/packages/grant-explorer/src/features/api/passport.ts @@ -1,3 +1,4 @@ +import { datadogLogs } from "@datadog/browser-logs"; import { submitPassport, fetchPassport, @@ -9,50 +10,83 @@ import { useEffect, useState } from "react"; export { submitPassport, fetchPassport, PassportState }; export type { PassportResponse }; -export function usePassport({ address }: { address: string }) { - const [passport, setPassport] = useState(); +export function usePassport({ address }: { address: string | undefined }) { const [, setError] = useState(); const [passportState, setPassportState] = useState( PassportState.LOADING ); + const [passportScore, setPassportScore] = useState(); + const [passportColor, setPassportColor] = useState(""); + const [donationImpact, setDonationImpact] = useState(0); + useEffect(() => { setPassportState(PassportState.LOADING); 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 PASSPORT_THRESHOLD = 0; if (address && PASSPORT_COMMUNITY_ID) { const callFetchPassport = async () => { 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 json = await res.json(); + const scoreResponse: PassportResponse = await res.json(); - if (json.status === "PROCESSING") { + if (scoreResponse.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); + } + + if ( + !scoreResponse.score || + !scoreResponse.evidence || + scoreResponse.status === "ERROR" + ) { + datadogLogs.logger.error( + `error: callFetchPassport - invalid score response`, + scoreResponse + ); + setPassportState(PassportState.INVALID_RESPONSE); return; } - setPassport(json); - setPassportState( - json.score >= PASSPORT_THRESHOLD - ? PassportState.MATCH_ELIGIBLE - : PassportState.MATCH_INELIGIBLE - ); + const score = Number(scoreResponse.evidence.rawScore); + setPassportScore(score); + setPassportState(PassportState.SCORE_AVAILABLE); + if (score < 15) { + setPassportColor("orange"); + setDonationImpact(0); + } else if (score >= 15 && score < 25) { + setPassportColor("yellow"); + setDonationImpact(50); + } else { + setPassportColor("green"); + setDonationImpact(100); + } } 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); @@ -76,6 +110,8 @@ export function usePassport({ address }: { address: string }) { return { passportState, - passport, + passportScore, + passportColor, + donationImpact, }; } 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/__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/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/__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.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 */} - } - /> - } From 48948bee560fce49f7323918bfdad48c7e8628ae Mon Sep 17 00:00:00 2001 From: eagle Date: Fri, 20 Oct 2023 18:12:35 +0530 Subject: [PATCH 13/21] feat: update passport widget --- .../src/features/common/PassportWidget.tsx | 127 +++++------------- 1 file changed, 37 insertions(+), 90 deletions(-) diff --git a/packages/grant-explorer/src/features/common/PassportWidget.tsx b/packages/grant-explorer/src/features/common/PassportWidget.tsx index 3f13fa2b02..c2508e70c0 100644 --- a/packages/grant-explorer/src/features/common/PassportWidget.tsx +++ b/packages/grant-explorer/src/features/common/PassportWidget.tsx @@ -1,32 +1,17 @@ -import { useEffect, useState } from "react"; -import { PassportResponse, fetchPassport } from "../api/passport"; +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 enum PassportState { - NOT_CONNECTED, - INVALID_PASSPORT, - SCORE_AVAILABLE, - LOADING, - ERROR, - INVALID_RESPONSE, -} - export function PassportWidget() { - const [, setPassport] = useState(); - const [, setError] = useState(); - const { address, isConnected } = useAccount(); + const { address } = useAccount(); - const [passportState, setPassportState] = useState( - PassportState.LOADING - ); + const { passportState, passportScore, passportColor, donationImpact } = + usePassport({ address }); - const [passportScore, setPassportScore] = useState(); - const [textColor, setTextColor] = useState("gray"); - const [donationImpact, setDonationImpact] = useState(0); const [open, setOpen] = useState(false); function handleClick() { @@ -38,71 +23,6 @@ export function PassportWidget() { } } - 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); - setPassportScore(Number(scoreResponse.evidence.rawScore)); - setPassportState(PassportState.SCORE_AVAILABLE); - const score = Number(scoreResponse.evidence.rawScore); - if (score < 15) { - setTextColor("orange"); - setDonationImpact(0); - } else if (score >= 15 && score < 25) { - setTextColor("yellow"); - setDonationImpact(50); - } else { - setTextColor("green"); - setDonationImpact(100); - } - } 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]); return ( <>
) : ( @@ -121,7 +49,14 @@ export function PassportWidget() { )} {passportState === PassportState.SCORE_AVAILABLE && ( @@ -131,7 +66,7 @@ export function PassportWidget() { direction={open ? "up" : "down"} />
@@ -141,13 +76,25 @@ export function PassportWidget() { <>

Passport Score

{passportScore}

Donation Impact

+{donationImpact}%

From cedf572d31faa3a4d47789e1144841e60def04b1 Mon Sep 17 00:00:00 2001 From: eagle Date: Fri, 20 Oct 2023 18:33:28 +0530 Subject: [PATCH 14/21] test: update navbar tests --- packages/grant-explorer/src/features/common/Navbar.tsx | 2 +- .../src/features/common/__tests__/Navbar.test.tsx | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/grant-explorer/src/features/common/Navbar.tsx b/packages/grant-explorer/src/features/common/Navbar.tsx index 53bd9200e7..34a2645fe4 100644 --- a/packages/grant-explorer/src/features/common/Navbar.tsx +++ b/packages/grant-explorer/src/features/common/Navbar.tsx @@ -58,7 +58,7 @@ export default function Navbar(props: NavbarProps) {
{walletAddress && ( -
+
)} 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(); From 5714cbc1783e0e71c95721ba5db7d15791858421 Mon Sep 17 00:00:00 2001 From: Atris Date: Mon, 23 Oct 2023 18:03:18 +0200 Subject: [PATCH 15/21] fix: scale passport correctly --- packages/common/src/index.ts | 32 ++++--- packages/grant-explorer/package.json | 1 + .../src/features/api/passport.ts | 28 +++++- .../src/features/common/Passport.tsx | 49 ---------- .../round/ViewCartPage/RoundInCart.tsx | 14 +-- .../round/ViewCartPage/SummaryContainer.tsx | 16 +--- pnpm-lock.yaml | 95 ++++++++++--------- 7 files changed, 99 insertions(+), 136 deletions(-) delete mode 100644 packages/grant-explorer/src/features/common/Passport.tsx diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index 4dda61e433..8ac58c6689 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"; @@ -16,20 +16,22 @@ export enum PassportState { 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(), + rawScore: z.string(), + threshold: z.string(), +}); + +export type PassportResponse = z.infer; + +export const PassportResponseSchema = z.object({ + address: z.string().optional(), + score: z.string().optional(), + status: z.string().optional(), + evidence: PassportEvidenceSchema, + error: z.string().optional(), + detail: z.string().optional(), +}); /** * 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/src/features/api/passport.ts b/packages/grant-explorer/src/features/api/passport.ts index 1459bc5fac..2b214a7214 100644 --- a/packages/grant-explorer/src/features/api/passport.ts +++ b/packages/grant-explorer/src/features/api/passport.ts @@ -1,9 +1,10 @@ import { datadogLogs } from "@datadog/browser-logs"; import { - submitPassport, fetchPassport, PassportResponse, + PassportResponseSchema, PassportState, + submitPassport, } from "common"; import { useEffect, useState } from "react"; @@ -18,7 +19,9 @@ export function usePassport({ address }: { address: string | undefined }) { ); const [passportScore, setPassportScore] = useState(); - const [passportColor, setPassportColor] = useState(""); + const [passportColor, setPassportColor] = useState< + "orange" | "green" | "yellow" + >(); const [donationImpact, setDonationImpact] = useState(0); useEffect(() => { @@ -45,7 +48,7 @@ export function usePassport({ address }: { address: string | undefined }) { } if (res.ok) { - const scoreResponse: PassportResponse = await res.json(); + const scoreResponse = PassportResponseSchema.parse(await res.json()); if (scoreResponse.status === "PROCESSING") { console.log("processing, calling again in 3000 ms"); @@ -76,7 +79,7 @@ export function usePassport({ address }: { address: string | undefined }) { setDonationImpact(0); } else if (score >= 15 && score < 25) { setPassportColor("yellow"); - setDonationImpact(50); + setDonationImpact(10 * (score - 15)); } else { setPassportColor("green"); setDonationImpact(100); @@ -93,7 +96,7 @@ export function usePassport({ address }: { address: string | undefined }) { break; case 401: // invalid API key setPassportState(PassportState.ERROR); - console.error("invalid API key", res.json()); + console.error("invalid API key", await res.json()); break; default: setPassportState(PassportState.ERROR); @@ -115,3 +118,18 @@ export function usePassport({ address }: { address: string | undefined }) { donationImpact, }; } + +export type PassportColor = "gray" | "orange" | "yellow" | "green"; + +export function passportColorTextClass(color: PassportColor): string { + switch (color) { + case "gray": + return "text-gray-400"; + case "orange": + return "text-orange-400"; + case "yellow": + return "text-yellow-400"; + case "green": + return "text-green-400"; + } +} diff --git a/packages/grant-explorer/src/features/common/Passport.tsx b/packages/grant-explorer/src/features/common/Passport.tsx deleted file mode 100644 index 4de2dffdb6..0000000000 --- a/packages/grant-explorer/src/features/common/Passport.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { PassportResponse } from "common"; -import { useMemo } from "react"; - -export type PassportColor = "gray" | "orange" | "yellow" | "green"; - -export type PassportDisplay = { - score: number; - color: PassportColor; -}; - -export function passportColorTextClass(color: PassportColor): string { - switch (color) { - case "gray": - return "text-gray-400"; - case "orange": - return "text-orange-400"; - case "yellow": - return "text-yellow-400"; - case "green": - return "text-green-400"; - } -} - -function passportDisplayColorFromScore(score: number | null): PassportColor { - if (score === null) { - return "gray"; - } else if (score < 15) { - return "orange"; - } else if (score < 25) { - return "yellow"; - } - - return "green"; -} - -export function usePassportScore(score?: PassportResponse) { - const passportScore = useMemo(() => { - 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/round/ViewCartPage/RoundInCart.tsx b/packages/grant-explorer/src/features/round/ViewCartPage/RoundInCart.tsx index b36989c465..0fecb35bee 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 { passportColorTextClass, 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({ + const { passportColor } = usePassport({ address: address ?? "", }); - const passportScore = usePassportScore(passport); + const passportTextClass = passportColorTextClass(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..19db2794c0 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 { passportColorTextClass, 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 = passportColorTextClass(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/pnpm-lock.yaml b/pnpm-lock.yaml index a4652b6dfa..a3155a063f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -487,7 +487,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 +604,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) @@ -3968,7 +3971,6 @@ packages: - '@types/node' - postcss - typescript - dev: false /@craco/craco@7.1.0(@types/node@18.17.14)(postcss@8.4.29)(react-scripts@5.0.1)(typescript@4.9.5): resolution: {integrity: sha512-oRAcPIKYrfPXp9rSzlsDNeOaVtDiKhoyqSXUoqiK24jCkHr4T8m/a2f74yXIzCbIheoUWDOIfWZyRgFgT+cpqA==} @@ -3991,6 +3993,7 @@ packages: - '@types/node' - postcss - typescript + dev: false /@cspotcode/source-map-support@0.8.1: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} @@ -6158,8 +6161,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 +6370,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 +6402,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 +6414,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 @@ -7483,7 +7486,6 @@ packages: typescript: 5.2.2 transitivePeerDependencies: - supports-color - dev: false /@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.50.0)(typescript@4.9.5): resolution: {integrity: sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==} @@ -7553,6 +7555,7 @@ packages: transitivePeerDependencies: - supports-color - typescript + dev: false /@typescript-eslint/experimental-utils@5.62.0(eslint@8.48.0)(typescript@5.2.2): resolution: {integrity: sha512-RTXpeB3eMkpoclG3ZHft6vG/Z30azNHuqY6wKPBHlVMZFuEvrtlEDe8gMqDb+SO+9hjC/pLekeSCryf9vMZlCw==} @@ -7565,7 +7568,6 @@ packages: transitivePeerDependencies: - supports-color - typescript - dev: false /@typescript-eslint/experimental-utils@5.62.0(eslint@8.50.0)(typescript@4.9.5): resolution: {integrity: sha512-RTXpeB3eMkpoclG3ZHft6vG/Z30azNHuqY6wKPBHlVMZFuEvrtlEDe8gMqDb+SO+9hjC/pLekeSCryf9vMZlCw==} @@ -7617,7 +7619,6 @@ packages: typescript: 5.2.2 transitivePeerDependencies: - supports-color - dev: false /@typescript-eslint/parser@5.62.0(eslint@8.50.0)(typescript@4.9.5): resolution: {integrity: sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==} @@ -7712,7 +7713,6 @@ packages: typescript: 5.2.2 transitivePeerDependencies: - supports-color - dev: false /@typescript-eslint/type-utils@5.62.0(eslint@8.50.0)(typescript@4.9.5): resolution: {integrity: sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==} @@ -7802,7 +7802,6 @@ packages: typescript: 5.2.2 transitivePeerDependencies: - supports-color - dev: false /@typescript-eslint/typescript-estree@6.7.2(typescript@5.2.2): resolution: {integrity: sha512-kiJKVMLkoSciGyFU0TOY0fRxnp9qq1AzVOHNeN1+B9erKFCJ4Z8WdjAkKQPP+b1pWStGFqezMLltxO+308dJTQ==} @@ -7862,7 +7861,6 @@ packages: transitivePeerDependencies: - supports-color - typescript - dev: false /@typescript-eslint/utils@5.62.0(eslint@8.50.0)(typescript@4.9.5): resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==} @@ -8195,7 +8193,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 +8204,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 @@ -8314,7 +8312,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 +8321,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' @@ -9294,7 +9292,7 @@ packages: typescript: 5.2.2 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 +9302,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 +9317,7 @@ packages: optional: true dependencies: typescript: 5.2.2 + zod: 3.22.4 dev: false /abortable-iterator@3.0.2: @@ -10996,7 +10996,6 @@ packages: transitivePeerDependencies: - '@swc/core' - '@swc/wasm' - dev: false /cosmiconfig-typescript-loader@1.0.9(@types/node@18.17.14)(cosmiconfig@7.1.0)(typescript@4.9.5): resolution: {integrity: sha512-tRuMRhxN4m1Y8hP9SNYfz7jRwt8lZdWxdjg/ohg5esKmsndJIn4yT96oJVcf5x0eA11taXl+sIp+ielu529k6g==} @@ -11013,6 +11012,7 @@ packages: transitivePeerDependencies: - '@swc/core' - '@swc/wasm' + dev: false /cosmiconfig-typescript-loader@4.4.0(@types/node@20.4.7)(cosmiconfig@8.3.4)(ts-node@10.9.1)(typescript@5.2.2): resolution: {integrity: sha512-BabizFdC3wBHhbI4kJh0VkQP9GkBfoHPydD0COMce1nJ1kJAB3F2TmJ/I7diULBKtmEWSwEbuN/KDtgnmUUVmw==} @@ -11071,10 +11071,10 @@ packages: '@craco/craco': ^6.0.0 || ^7.0.0 || ^7.0.0-alpha react-scripts: ^5.0.0 dependencies: - '@craco/craco': 7.1.0(@types/node@18.17.14)(postcss@8.4.29)(react-scripts@5.0.1)(typescript@4.9.5) + '@craco/craco': 7.1.0(@types/node@17.0.45)(postcss@8.4.29)(react-scripts@5.0.1)(typescript@5.2.2) esbuild-jest: 0.5.0(esbuild@0.19.2) esbuild-loader: 2.21.0(webpack@5.88.2) - react-scripts: 5.0.1(@babel/plugin-syntax-flow@7.22.5)(@babel/plugin-transform-react-jsx@7.22.15)(esbuild@0.19.2)(eslint@8.48.0)(react@18.2.0)(ts-node@10.9.1)(typescript@4.9.5) + react-scripts: 5.0.1(@babel/plugin-syntax-flow@7.22.5)(@babel/plugin-transform-react-jsx@7.22.15)(esbuild@0.19.2)(eslint@8.48.0)(react@18.2.0)(ts-node@10.9.1)(typescript@5.2.2) transitivePeerDependencies: - esbuild - supports-color @@ -12442,6 +12442,7 @@ packages: - eslint-import-resolver-webpack - jest - supports-color + dev: false /eslint-config-react-app@7.0.1(@babel/plugin-syntax-flow@7.22.5)(@babel/plugin-transform-react-jsx@7.22.15)(eslint@8.48.0)(jest@27.5.1)(typescript@5.2.2): resolution: {integrity: sha512-K6rNzvkIeHaTd8m/QEh1Zko0KI7BACWkkneSs6s9cKZC/J27X3eZR6Upt1jkmZ/4FK+XUOPPxMEN7+lbUXfSlA==} @@ -12476,7 +12477,6 @@ packages: - eslint-import-resolver-webpack - jest - supports-color - dev: false /eslint-config-react-app@7.0.1(@babel/plugin-syntax-flow@7.22.5)(@babel/plugin-transform-react-jsx@7.22.15)(eslint@8.50.0)(jest@27.5.1)(typescript@4.9.5): resolution: {integrity: sha512-K6rNzvkIeHaTd8m/QEh1Zko0KI7BACWkkneSs6s9cKZC/J27X3eZR6Upt1jkmZ/4FK+XUOPPxMEN7+lbUXfSlA==} @@ -12706,6 +12706,7 @@ packages: transitivePeerDependencies: - supports-color - typescript + dev: false /eslint-plugin-jest@25.7.0(@typescript-eslint/eslint-plugin@5.62.0)(eslint@8.48.0)(jest@27.5.1)(typescript@5.2.2): resolution: {integrity: sha512-PWLUEXeeF7C9QGKqvdSbzLOiLTx+bno7/HC9eefePfEb257QFHg7ye3dh80AZVkaa/RQsBB1Q/ORQvg2X7F0NQ==} @@ -12727,7 +12728,6 @@ packages: transitivePeerDependencies: - supports-color - typescript - dev: false /eslint-plugin-jest@25.7.0(@typescript-eslint/eslint-plugin@5.62.0)(eslint@8.50.0)(jest@27.5.1)(typescript@4.9.5): resolution: {integrity: sha512-PWLUEXeeF7C9QGKqvdSbzLOiLTx+bno7/HC9eefePfEb257QFHg7ye3dh80AZVkaa/RQsBB1Q/ORQvg2X7F0NQ==} @@ -12894,6 +12894,7 @@ packages: transitivePeerDependencies: - supports-color - typescript + dev: false /eslint-plugin-testing-library@5.11.1(eslint@8.48.0)(typescript@5.2.2): resolution: {integrity: sha512-5eX9e1Kc2PqVRed3taaLnAAqPZGEX75C+M/rXzUAI3wIg/ZxzUm1OVAwfe/O+vE+6YXOLetSe9g5GKD2ecXipw==} @@ -12906,7 +12907,6 @@ packages: transitivePeerDependencies: - supports-color - typescript - dev: false /eslint-plugin-testing-library@5.11.1(eslint@8.50.0)(typescript@4.9.5): resolution: {integrity: sha512-5eX9e1Kc2PqVRed3taaLnAAqPZGEX75C+M/rXzUAI3wIg/ZxzUm1OVAwfe/O+vE+6YXOLetSe9g5GKD2ecXipw==} @@ -13650,6 +13650,7 @@ packages: tapable: 1.1.3 typescript: 4.9.5 webpack: 5.88.2(esbuild@0.19.2) + dev: false /fork-ts-checker-webpack-plugin@6.5.3(eslint@8.48.0)(typescript@5.2.2)(webpack@5.88.2): resolution: {integrity: sha512-SbH/l9ikmMWycd5puHJKTkZJKddF4iRLyW3DeZ08HTI7NGyLS38MXd/KGgeWumQO7YNQbW2u/NtPT2YowbPaGQ==} @@ -13681,7 +13682,6 @@ packages: tapable: 1.1.3 typescript: 5.2.2 webpack: 5.88.2(esbuild@0.19.2) - dev: false /fork-ts-checker-webpack-plugin@6.5.3(eslint@8.50.0)(typescript@4.9.5)(webpack@5.88.2): resolution: {integrity: sha512-SbH/l9ikmMWycd5puHJKTkZJKddF4iRLyW3DeZ08HTI7NGyLS38MXd/KGgeWumQO7YNQbW2u/NtPT2YowbPaGQ==} @@ -19858,6 +19858,7 @@ packages: - eslint - supports-color - vue-template-compiler + dev: false /react-dev-utils@12.0.1(eslint@8.48.0)(typescript@5.2.2)(webpack@5.88.2): resolution: {integrity: sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==} @@ -19899,7 +19900,6 @@ packages: - eslint - supports-color - vue-template-compiler - dev: false /react-dev-utils@12.0.1(eslint@8.50.0)(typescript@4.9.5)(webpack@5.88.2): resolution: {integrity: sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==} @@ -20275,6 +20275,7 @@ packages: - webpack-cli - webpack-hot-middleware - webpack-plugin-serve + dev: false /react-scripts@5.0.1(@babel/plugin-syntax-flow@7.22.5)(@babel/plugin-transform-react-jsx@7.22.15)(esbuild@0.19.2)(eslint@8.48.0)(react@18.2.0)(ts-node@10.9.1)(typescript@5.2.2): resolution: {integrity: sha512-8VAmEm/ZAwQzJ+GOMLbBsTdDKOpuZh7RPs0UymvBR2vRk4iZWCskjbFnxqjrzoIvlNNRZ3QJFx6/qDSi6zSnaQ==} @@ -20371,7 +20372,6 @@ packages: - webpack-cli - webpack-hot-middleware - webpack-plugin-serve - dev: false /react-scripts@5.0.1(@babel/plugin-syntax-flow@7.22.5)(@babel/plugin-transform-react-jsx@7.22.15)(esbuild@0.19.2)(eslint@8.50.0)(react@18.2.0)(ts-node@10.9.1)(typescript@4.9.5): resolution: {integrity: sha512-8VAmEm/ZAwQzJ+GOMLbBsTdDKOpuZh7RPs0UymvBR2vRk4iZWCskjbFnxqjrzoIvlNNRZ3QJFx6/qDSi6zSnaQ==} @@ -22299,7 +22299,6 @@ packages: typescript: 5.2.2 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 - dev: false /ts-node@10.9.1(@types/node@18.17.14)(typescript@4.9.5): resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} @@ -22330,6 +22329,7 @@ packages: typescript: 4.9.5 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 + dev: false /ts-node@10.9.1(@types/node@20.4.7)(typescript@5.2.2): resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} @@ -22396,7 +22396,6 @@ packages: dependencies: tslib: 1.14.1 typescript: 5.2.2 - dev: false /tulons@0.0.7: resolution: {integrity: sha512-JyL9Vn4PPG2TTEJS35yqQgAMLd3IX9pFIlbiLmv47HuHTo2F3ihYg2yfMqde4hqGY1nGk77iJ/lvsTbAURJ8rg==} @@ -22937,7 +22936,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 +22950,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 @@ -23330,7 +23329,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 +23342,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 +24147,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'} From a68fd1ec4c491fdc3ae028d836b31eed408e5885 Mon Sep 17 00:00:00 2001 From: Atris Date: Mon, 23 Oct 2023 18:11:18 +0200 Subject: [PATCH 16/21] fix: add zod package to common --- packages/common/package.json | 3 +- pnpm-lock.yaml | 58 +++++++++++++++++++++--------------- 2 files changed, 36 insertions(+), 25 deletions(-) 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/pnpm-lock.yaml b/pnpm-lock.yaml index a3155a063f..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 @@ -3971,6 +3974,7 @@ packages: - '@types/node' - postcss - typescript + dev: false /@craco/craco@7.1.0(@types/node@18.17.14)(postcss@8.4.29)(react-scripts@5.0.1)(typescript@4.9.5): resolution: {integrity: sha512-oRAcPIKYrfPXp9rSzlsDNeOaVtDiKhoyqSXUoqiK24jCkHr4T8m/a2f74yXIzCbIheoUWDOIfWZyRgFgT+cpqA==} @@ -3993,7 +3997,6 @@ packages: - '@types/node' - postcss - typescript - dev: false /@cspotcode/source-map-support@0.8.1: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} @@ -6116,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 @@ -7486,6 +7489,7 @@ packages: typescript: 5.2.2 transitivePeerDependencies: - supports-color + dev: false /@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.50.0)(typescript@4.9.5): resolution: {integrity: sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==} @@ -7555,7 +7559,6 @@ packages: transitivePeerDependencies: - supports-color - typescript - dev: false /@typescript-eslint/experimental-utils@5.62.0(eslint@8.48.0)(typescript@5.2.2): resolution: {integrity: sha512-RTXpeB3eMkpoclG3ZHft6vG/Z30azNHuqY6wKPBHlVMZFuEvrtlEDe8gMqDb+SO+9hjC/pLekeSCryf9vMZlCw==} @@ -7568,6 +7571,7 @@ packages: transitivePeerDependencies: - supports-color - typescript + dev: false /@typescript-eslint/experimental-utils@5.62.0(eslint@8.50.0)(typescript@4.9.5): resolution: {integrity: sha512-RTXpeB3eMkpoclG3ZHft6vG/Z30azNHuqY6wKPBHlVMZFuEvrtlEDe8gMqDb+SO+9hjC/pLekeSCryf9vMZlCw==} @@ -7619,6 +7623,7 @@ packages: typescript: 5.2.2 transitivePeerDependencies: - supports-color + dev: false /@typescript-eslint/parser@5.62.0(eslint@8.50.0)(typescript@4.9.5): resolution: {integrity: sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==} @@ -7713,6 +7718,7 @@ packages: typescript: 5.2.2 transitivePeerDependencies: - supports-color + dev: false /@typescript-eslint/type-utils@5.62.0(eslint@8.50.0)(typescript@4.9.5): resolution: {integrity: sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==} @@ -7802,6 +7808,7 @@ packages: typescript: 5.2.2 transitivePeerDependencies: - supports-color + dev: false /@typescript-eslint/typescript-estree@6.7.2(typescript@5.2.2): resolution: {integrity: sha512-kiJKVMLkoSciGyFU0TOY0fRxnp9qq1AzVOHNeN1+B9erKFCJ4Z8WdjAkKQPP+b1pWStGFqezMLltxO+308dJTQ==} @@ -7861,6 +7868,7 @@ packages: transitivePeerDependencies: - supports-color - typescript + dev: false /@typescript-eslint/utils@5.62.0(eslint@8.50.0)(typescript@4.9.5): resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==} @@ -8158,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' @@ -8174,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 @@ -8283,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' @@ -8293,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 @@ -9279,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: @@ -9290,6 +9298,7 @@ packages: optional: true dependencies: typescript: 5.2.2 + zod: 3.22.4 dev: false /abitype@0.8.7(typescript@5.2.2)(zod@3.22.4): @@ -10996,6 +11005,7 @@ packages: transitivePeerDependencies: - '@swc/core' - '@swc/wasm' + dev: false /cosmiconfig-typescript-loader@1.0.9(@types/node@18.17.14)(cosmiconfig@7.1.0)(typescript@4.9.5): resolution: {integrity: sha512-tRuMRhxN4m1Y8hP9SNYfz7jRwt8lZdWxdjg/ohg5esKmsndJIn4yT96oJVcf5x0eA11taXl+sIp+ielu529k6g==} @@ -11012,7 +11022,6 @@ packages: transitivePeerDependencies: - '@swc/core' - '@swc/wasm' - dev: false /cosmiconfig-typescript-loader@4.4.0(@types/node@20.4.7)(cosmiconfig@8.3.4)(ts-node@10.9.1)(typescript@5.2.2): resolution: {integrity: sha512-BabizFdC3wBHhbI4kJh0VkQP9GkBfoHPydD0COMce1nJ1kJAB3F2TmJ/I7diULBKtmEWSwEbuN/KDtgnmUUVmw==} @@ -11071,10 +11080,10 @@ packages: '@craco/craco': ^6.0.0 || ^7.0.0 || ^7.0.0-alpha react-scripts: ^5.0.0 dependencies: - '@craco/craco': 7.1.0(@types/node@17.0.45)(postcss@8.4.29)(react-scripts@5.0.1)(typescript@5.2.2) + '@craco/craco': 7.1.0(@types/node@18.17.14)(postcss@8.4.29)(react-scripts@5.0.1)(typescript@4.9.5) esbuild-jest: 0.5.0(esbuild@0.19.2) esbuild-loader: 2.21.0(webpack@5.88.2) - react-scripts: 5.0.1(@babel/plugin-syntax-flow@7.22.5)(@babel/plugin-transform-react-jsx@7.22.15)(esbuild@0.19.2)(eslint@8.48.0)(react@18.2.0)(ts-node@10.9.1)(typescript@5.2.2) + react-scripts: 5.0.1(@babel/plugin-syntax-flow@7.22.5)(@babel/plugin-transform-react-jsx@7.22.15)(esbuild@0.19.2)(eslint@8.48.0)(react@18.2.0)(ts-node@10.9.1)(typescript@4.9.5) transitivePeerDependencies: - esbuild - supports-color @@ -12442,7 +12451,6 @@ packages: - eslint-import-resolver-webpack - jest - supports-color - dev: false /eslint-config-react-app@7.0.1(@babel/plugin-syntax-flow@7.22.5)(@babel/plugin-transform-react-jsx@7.22.15)(eslint@8.48.0)(jest@27.5.1)(typescript@5.2.2): resolution: {integrity: sha512-K6rNzvkIeHaTd8m/QEh1Zko0KI7BACWkkneSs6s9cKZC/J27X3eZR6Upt1jkmZ/4FK+XUOPPxMEN7+lbUXfSlA==} @@ -12477,6 +12485,7 @@ packages: - eslint-import-resolver-webpack - jest - supports-color + dev: false /eslint-config-react-app@7.0.1(@babel/plugin-syntax-flow@7.22.5)(@babel/plugin-transform-react-jsx@7.22.15)(eslint@8.50.0)(jest@27.5.1)(typescript@4.9.5): resolution: {integrity: sha512-K6rNzvkIeHaTd8m/QEh1Zko0KI7BACWkkneSs6s9cKZC/J27X3eZR6Upt1jkmZ/4FK+XUOPPxMEN7+lbUXfSlA==} @@ -12706,7 +12715,6 @@ packages: transitivePeerDependencies: - supports-color - typescript - dev: false /eslint-plugin-jest@25.7.0(@typescript-eslint/eslint-plugin@5.62.0)(eslint@8.48.0)(jest@27.5.1)(typescript@5.2.2): resolution: {integrity: sha512-PWLUEXeeF7C9QGKqvdSbzLOiLTx+bno7/HC9eefePfEb257QFHg7ye3dh80AZVkaa/RQsBB1Q/ORQvg2X7F0NQ==} @@ -12728,6 +12736,7 @@ packages: transitivePeerDependencies: - supports-color - typescript + dev: false /eslint-plugin-jest@25.7.0(@typescript-eslint/eslint-plugin@5.62.0)(eslint@8.50.0)(jest@27.5.1)(typescript@4.9.5): resolution: {integrity: sha512-PWLUEXeeF7C9QGKqvdSbzLOiLTx+bno7/HC9eefePfEb257QFHg7ye3dh80AZVkaa/RQsBB1Q/ORQvg2X7F0NQ==} @@ -12894,7 +12903,6 @@ packages: transitivePeerDependencies: - supports-color - typescript - dev: false /eslint-plugin-testing-library@5.11.1(eslint@8.48.0)(typescript@5.2.2): resolution: {integrity: sha512-5eX9e1Kc2PqVRed3taaLnAAqPZGEX75C+M/rXzUAI3wIg/ZxzUm1OVAwfe/O+vE+6YXOLetSe9g5GKD2ecXipw==} @@ -12907,6 +12915,7 @@ packages: transitivePeerDependencies: - supports-color - typescript + dev: false /eslint-plugin-testing-library@5.11.1(eslint@8.50.0)(typescript@4.9.5): resolution: {integrity: sha512-5eX9e1Kc2PqVRed3taaLnAAqPZGEX75C+M/rXzUAI3wIg/ZxzUm1OVAwfe/O+vE+6YXOLetSe9g5GKD2ecXipw==} @@ -13650,7 +13659,6 @@ packages: tapable: 1.1.3 typescript: 4.9.5 webpack: 5.88.2(esbuild@0.19.2) - dev: false /fork-ts-checker-webpack-plugin@6.5.3(eslint@8.48.0)(typescript@5.2.2)(webpack@5.88.2): resolution: {integrity: sha512-SbH/l9ikmMWycd5puHJKTkZJKddF4iRLyW3DeZ08HTI7NGyLS38MXd/KGgeWumQO7YNQbW2u/NtPT2YowbPaGQ==} @@ -13682,6 +13690,7 @@ packages: tapable: 1.1.3 typescript: 5.2.2 webpack: 5.88.2(esbuild@0.19.2) + dev: false /fork-ts-checker-webpack-plugin@6.5.3(eslint@8.50.0)(typescript@4.9.5)(webpack@5.88.2): resolution: {integrity: sha512-SbH/l9ikmMWycd5puHJKTkZJKddF4iRLyW3DeZ08HTI7NGyLS38MXd/KGgeWumQO7YNQbW2u/NtPT2YowbPaGQ==} @@ -19858,7 +19867,6 @@ packages: - eslint - supports-color - vue-template-compiler - dev: false /react-dev-utils@12.0.1(eslint@8.48.0)(typescript@5.2.2)(webpack@5.88.2): resolution: {integrity: sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==} @@ -19900,6 +19908,7 @@ packages: - eslint - supports-color - vue-template-compiler + dev: false /react-dev-utils@12.0.1(eslint@8.50.0)(typescript@4.9.5)(webpack@5.88.2): resolution: {integrity: sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==} @@ -20275,7 +20284,6 @@ packages: - webpack-cli - webpack-hot-middleware - webpack-plugin-serve - dev: false /react-scripts@5.0.1(@babel/plugin-syntax-flow@7.22.5)(@babel/plugin-transform-react-jsx@7.22.15)(esbuild@0.19.2)(eslint@8.48.0)(react@18.2.0)(ts-node@10.9.1)(typescript@5.2.2): resolution: {integrity: sha512-8VAmEm/ZAwQzJ+GOMLbBsTdDKOpuZh7RPs0UymvBR2vRk4iZWCskjbFnxqjrzoIvlNNRZ3QJFx6/qDSi6zSnaQ==} @@ -20372,6 +20380,7 @@ packages: - webpack-cli - webpack-hot-middleware - webpack-plugin-serve + dev: false /react-scripts@5.0.1(@babel/plugin-syntax-flow@7.22.5)(@babel/plugin-transform-react-jsx@7.22.15)(esbuild@0.19.2)(eslint@8.50.0)(react@18.2.0)(ts-node@10.9.1)(typescript@4.9.5): resolution: {integrity: sha512-8VAmEm/ZAwQzJ+GOMLbBsTdDKOpuZh7RPs0UymvBR2vRk4iZWCskjbFnxqjrzoIvlNNRZ3QJFx6/qDSi6zSnaQ==} @@ -22299,6 +22308,7 @@ packages: typescript: 5.2.2 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 + dev: false /ts-node@10.9.1(@types/node@18.17.14)(typescript@4.9.5): resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} @@ -22329,7 +22339,6 @@ packages: typescript: 4.9.5 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 - dev: false /ts-node@10.9.1(@types/node@20.4.7)(typescript@5.2.2): resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} @@ -22396,6 +22405,7 @@ packages: dependencies: tslib: 1.14.1 typescript: 5.2.2 + dev: false /tulons@0.0.7: resolution: {integrity: sha512-JyL9Vn4PPG2TTEJS35yqQgAMLd3IX9pFIlbiLmv47HuHTo2F3ihYg2yfMqde4hqGY1nGk77iJ/lvsTbAURJ8rg==} @@ -23296,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' @@ -23309,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 From 51b3e7086c46566e3ff443aede9e962ff32bb4e2 Mon Sep 17 00:00:00 2001 From: Atris Date: Mon, 23 Oct 2023 18:32:07 +0200 Subject: [PATCH 17/21] fix: passport response schema, cursor on widget, undefined access, number formatting --- packages/common/src/index.ts | 18 +++++++++--------- .../src/features/common/PassportWidget.tsx | 8 ++++---- .../src/features/discovery/RoundCard.tsx | 2 +- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index 8ac58c6689..708531f024 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -17,20 +17,20 @@ export enum PassportState { } const PassportEvidenceSchema = z.object({ - type: z.string(), - rawScore: z.string(), - threshold: z.string(), + type: z.string().nullish(), + rawScore: z.string().nullish(), + threshold: z.string().nullish(), }); export type PassportResponse = z.infer; export const PassportResponseSchema = z.object({ - address: z.string().optional(), - score: z.string().optional(), - status: z.string().optional(), - evidence: PassportEvidenceSchema, - error: z.string().optional(), - detail: z.string().optional(), + address: z.string().nullish(), + score: z.string().nullish(), + status: z.string().nullish(), + evidence: PassportEvidenceSchema.nullish(), + error: z.string().nullish(), + detail: z.string().nullish(), }); /** diff --git a/packages/grant-explorer/src/features/common/PassportWidget.tsx b/packages/grant-explorer/src/features/common/PassportWidget.tsx index c2508e70c0..1f1134546f 100644 --- a/packages/grant-explorer/src/features/common/PassportWidget.tsx +++ b/packages/grant-explorer/src/features/common/PassportWidget.tsx @@ -26,7 +26,7 @@ export function PassportWidget() { return ( <>
handleClick()} > {passportState === PassportState.SCORE_AVAILABLE ? ( @@ -62,11 +62,11 @@ export function PassportWidget() {
)}
@@ -97,7 +97,7 @@ export function PassportWidget() { } w-40 p-4 justify-start rounded-2xl`} >

Donation Impact

-

+{donationImpact}%

+

+{donationImpact.toFixed(2)}%

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} /> - +

From b0dba5f6ba48d88d81beefc9f0f4a357979de2f0 Mon Sep 17 00:00:00 2001 From: Atris Date: Mon, 23 Oct 2023 18:35:37 +0200 Subject: [PATCH 18/21] fix: make estimated matching tooltip disappear faster --- .../src/features/common/MatchingEstimateTooltip.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 }) {
From 10ac6d3f9819d0078ebb5282dca8b4491d72b814 Mon Sep 17 00:00:00 2001 From: Atris Date: Tue, 24 Oct 2023 12:19:17 +0200 Subject: [PATCH 19/21] feat: update copy in passport, update fonts --- .../public/modern-era-medium.otf | Bin 0 -> 55068 bytes .../public/modern-era-regular.otf | Bin 0 -> 55308 bytes .../src/features/common/PassportWidget.tsx | 18 ++++++++------ packages/grant-explorer/src/index.css | 22 ++++++++++++++++++ 4 files changed, 33 insertions(+), 7 deletions(-) create mode 100644 packages/grant-explorer/public/modern-era-medium.otf create mode 100644 packages/grant-explorer/public/modern-era-regular.otf 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 0000000000000000000000000000000000000000..7907f0e82f6a82455cb4363788eebeed2a7c811d GIT binary patch literal 55068 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

QQ?~Qi7*Bsq^^JKV{^9Q({SXx1BYD>YgOkl8f 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..3b91916d7da201fc32221a492c4ce2dd5d20486a GIT binary patch literal 55308 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 BYH|Po literal 0 HcmV?d00001 diff --git a/packages/grant-explorer/src/features/common/PassportWidget.tsx b/packages/grant-explorer/src/features/common/PassportWidget.tsx index 1f1134546f..b43e93690a 100644 --- a/packages/grant-explorer/src/features/common/PassportWidget.tsx +++ b/packages/grant-explorer/src/features/common/PassportWidget.tsx @@ -66,7 +66,7 @@ export function PassportWidget() { direction={open ? "up" : "down"} />

@@ -85,7 +85,7 @@ export function PassportWidget() { } w-40 p-4 justify-start rounded-2xl`} >

Passport Score

-

{passportScore}

+

{passportScore}

Donation Impact

-

+{donationImpact.toFixed(2)}%

+

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

-

- Your donation impact is a reflection of your Passport score. - This percentage ensures a fair and proportional match. You can - always update your score by heading over to Passport. +

+ 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.

) : ( 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; From ea1217e04c8e02597622a9696474d27ecc6943cf Mon Sep 17 00:00:00 2001 From: Atris Date: Tue, 24 Oct 2023 16:55:59 +0200 Subject: [PATCH 20/21] feat: introduce swr into passport and refactor --- .../src/features/api/passport.ts | 177 +++++++++--------- .../src/features/common/PassportWidget.tsx | 2 +- 2 files changed, 88 insertions(+), 91 deletions(-) diff --git a/packages/grant-explorer/src/features/api/passport.ts b/packages/grant-explorer/src/features/api/passport.ts index 2b214a7214..178ef95534 100644 --- a/packages/grant-explorer/src/features/api/passport.ts +++ b/packages/grant-explorer/src/features/api/passport.ts @@ -6,110 +6,107 @@ import { 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 | undefined }) { - const [, setError] = useState(); +const PASSPORT_COMMUNITY_ID = process.env.REACT_APP_PASSPORT_API_COMMUNITY_ID; - const [passportState, setPassportState] = useState( - PassportState.LOADING +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); + + if (res.ok) { + return PassportResponseSchema.parse(await res.json()); + } else { + throw res; + } + } ); - const [passportScore, setPassportScore] = useState(); - const [passportColor, setPassportColor] = useState< - "orange" | "green" | "yellow" - >(); - const [donationImpact, setDonationImpact] = useState(0); + 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; + } + } - useEffect(() => { - setPassportState(PassportState.LOADING); + 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 PASSPORT_COMMUNITY_ID = - process.env.REACT_APP_PASSPORT_API_COMMUNITY_ID; + const passportScore = useMemo(() => { + if (swr.data?.evidence) { + return Number(swr.data.evidence.rawScore); + } + return 0; + }, [swr.data]); - if (PASSPORT_COMMUNITY_ID === undefined) { - throw new Error("passport community id not set"); + const PROCESSING_REFETCH_INTERVAL_MS = 3000; + /** If passport is still processing, refetch it every PROCESSING_REFETCH_INTERVAL_MS */ + useEffect(() => { + if (swr.data?.status === "PROCESSING") { + setTimeout(() => { + /* Revalidate */ + swr.mutate(); + }, PROCESSING_REFETCH_INTERVAL_MS); + } + }, [swr]); + + const passportColor = useMemo(() => { + if (passportScore < 15) { + return "orange"; + } else if (passportScore >= 15 && passportScore < 25) { + return "yellow"; + } else { + return "green"; } + }, [passportScore]); - if (address && PASSPORT_COMMUNITY_ID) { - const callFetchPassport = async () => { - 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 = PassportResponseSchema.parse(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; - } - - const score = Number(scoreResponse.evidence.rawScore); - setPassportScore(score); - setPassportState(PassportState.SCORE_AVAILABLE); - if (score < 15) { - setPassportColor("orange"); - setDonationImpact(0); - } else if (score >= 15 && score < 25) { - setPassportColor("yellow"); - setDonationImpact(10 * (score - 15)); - } else { - setPassportColor("green"); - setDonationImpact(100); - } - } 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", await res.json()); - break; - default: - setPassportState(PassportState.ERROR); - console.error("Error fetching passport", res); - } - } - }; - - callFetchPassport(); + 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, diff --git a/packages/grant-explorer/src/features/common/PassportWidget.tsx b/packages/grant-explorer/src/features/common/PassportWidget.tsx index b43e93690a..7492822665 100644 --- a/packages/grant-explorer/src/features/common/PassportWidget.tsx +++ b/packages/grant-explorer/src/features/common/PassportWidget.tsx @@ -106,7 +106,7 @@ export function PassportWidget() { 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 + score increases. You can update your score by heading over to{" "} Passport.

From 78d3c8c08233558cb84c81fc4990b93b16af3c9b Mon Sep 17 00:00:00 2001 From: Atris Date: Tue, 24 Oct 2023 18:30:48 +0200 Subject: [PATCH 21/21] feat: address feedback, improve type safety --- packages/common/src/index.ts | 2 +- .../features/api/__tests__/passport.test.tsx | 2 ++ .../src/features/api/passport.ts | 23 ++++++------ .../src/features/common/PassportWidget.tsx | 35 +++++++++++++------ .../round/ViewCartPage/RoundInCart.tsx | 6 ++-- .../round/ViewCartPage/SummaryContainer.tsx | 4 +-- 6 files changed, 42 insertions(+), 30 deletions(-) diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index 708531f024..f5a028177b 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -18,7 +18,7 @@ export enum PassportState { const PassportEvidenceSchema = z.object({ type: z.string().nullish(), - rawScore: z.string().nullish(), + rawScore: z.coerce.number(), threshold: z.string().nullish(), }); 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 178ef95534..29834b3df7 100644 --- a/packages/grant-explorer/src/features/api/passport.ts +++ b/packages/grant-explorer/src/features/api/passport.ts @@ -72,7 +72,7 @@ export function usePassport({ address }: { address: string | undefined }) { const passportScore = useMemo(() => { if (swr.data?.evidence) { - return Number(swr.data.evidence.rawScore); + return swr.data.evidence.rawScore; } return 0; }, [swr.data]); @@ -118,15 +118,12 @@ export function usePassport({ address }: { address: string | undefined }) { export type PassportColor = "gray" | "orange" | "yellow" | "green"; -export function passportColorTextClass(color: PassportColor): string { - switch (color) { - case "gray": - return "text-gray-400"; - case "orange": - return "text-orange-400"; - case "yellow": - return "text-yellow-400"; - case "green": - return "text-green-400"; - } -} +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/PassportWidget.tsx b/packages/grant-explorer/src/features/common/PassportWidget.tsx index 7492822665..baea582bb6 100644 --- a/packages/grant-explorer/src/features/common/PassportWidget.tsx +++ b/packages/grant-explorer/src/features/common/PassportWidget.tsx @@ -12,14 +12,14 @@ export function PassportWidget() { const { passportState, passportScore, passportColor, donationImpact } = usePassport({ address }); - const [open, setOpen] = useState(false); + const [isOpen, setIsOpen] = useState(false); function handleClick() { if ( passportState === PassportState.SCORE_AVAILABLE || passportState === PassportState.INVALID_PASSPORT ) { - setOpen(!open); + setIsOpen(!isOpen); } } @@ -41,7 +41,7 @@ export function PassportWidget() { ? "bg-yellow-500" : "bg-orange-500" } - absolute bottom-0.5 right-0 w-3 h-3 rounded-full sm:block md:hidden`} + absolute bottom-0.5 right-0 w-3 h-3 rounded-2xl sm:block md:hidden`} >
) : ( @@ -63,12 +63,13 @@ export function PassportWidget() { )}
@@ -102,12 +103,18 @@ export function PassportWidget() {

-

+

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 + + .

) : ( @@ -117,8 +124,14 @@ export function PassportWidget() {

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

)} diff --git a/packages/grant-explorer/src/features/round/ViewCartPage/RoundInCart.tsx b/packages/grant-explorer/src/features/round/ViewCartPage/RoundInCart.tsx index 0fecb35bee..9b90841b3a 100644 --- a/packages/grant-explorer/src/features/round/ViewCartPage/RoundInCart.tsx +++ b/packages/grant-explorer/src/features/round/ViewCartPage/RoundInCart.tsx @@ -11,7 +11,7 @@ import { useAccount } from "wagmi"; import { useCartStorage } from "../../../store"; import { Skeleton } from "@chakra-ui/react"; import { BoltIcon } from "@heroicons/react/24/outline"; -import { passportColorTextClass, usePassport } from "../../api/passport"; +import { getClassForPassportColor, usePassport } from "../../api/passport"; export function RoundInCart( props: React.ComponentProps<"div"> & { @@ -61,10 +61,10 @@ export function RoundInCart( const estimateText = matchingEstimatesToText(matchingEstimates); const { passportColor } = usePassport({ - address: address ?? "", + address, }); - const passportTextClass = passportColorTextClass(passportColor ?? "gray"); + const passportTextClass = getClassForPassportColor(passportColor ?? "gray"); return (
diff --git a/packages/grant-explorer/src/features/round/ViewCartPage/SummaryContainer.tsx b/packages/grant-explorer/src/features/round/ViewCartPage/SummaryContainer.tsx index 19db2794c0..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 { passportColorTextClass, 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"; @@ -249,7 +249,7 @@ export function SummaryContainer() { address: address ?? "", }); - const passportTextClass = passportColorTextClass(passportColor ?? "gray"); + const passportTextClass = getClassForPassportColor(passportColor ?? "gray"); const [totalDonationAcrossChainsInUSD, setTotalDonationAcrossChainsInUSD] = useState();