Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Matching estimates on checkout page #2022

Merged
merged 33 commits into from
Oct 23, 2023
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
e7340a7
chore: update indexer client version
vacekj Sep 29, 2023
080802d
feat: matching estimates on checkout and hook
vacekj Sep 29, 2023
68fb3ab
chore: fix lint
vacekj Sep 29, 2023
b28b93b
chore: total matching
vacekj Sep 29, 2023
ed9a0cc
chore: rename ge scripts and fix matching estimates
vacekj Sep 29, 2023
2af3ef6
feat: cleanup
vacekj Oct 2, 2023
c3a58e3
feat: loading and error states for estimates
vacekj Oct 3, 2023
a6b1dab
chore: comments
vacekj Oct 3, 2023
49c7e2d
feat: per-round matching, hide per-project matching
vacekj Oct 4, 2023
39d438c
chore: revert some unrelated changes
vacekj Oct 4, 2023
b43001b
feat: take passport into account in matching estimates
vacekj Oct 5, 2023
19d58c4
feat: take passport into account in matching estimates
vacekj Oct 5, 2023
e0de288
chore: update lock
vacekj Oct 5, 2023
01edc1d
chore: address feedback from self-review
vacekj Oct 5, 2023
3a6fee6
Merge branch 'main' into 1931-spike-matching-estimates-for-donations
vacekj Oct 5, 2023
2801fc3
feat(common): update closeDelay in MatchingEstimateTooltip component …
vacekj Oct 5, 2023
d310d54
chore: add common and verify-env tests to CI, address feedback from PR
vacekj Oct 6, 2023
ac8f4b2
Merge branch 'main' into 1931-spike-matching-estimates-for-donations
vacekj Oct 6, 2023
f360090
fix: test
vacekj Oct 6, 2023
2f1dd5a
Merge branch 'main' into 1931-spike-matching-estimates-for-donations
vacekj Oct 6, 2023
2b4662e
chore: drop a todoˆ
vacekj Oct 6, 2023
1412e78
feat: update frontend matching estimates
vacekj Oct 12, 2023
ebc5b74
Merge branch 'main' into 1931-spike-matching-estimates-for-donations
vacekj Oct 13, 2023
5c54fb3
fix: don't pass undefined as voter in matching estimates, pass zeroAd…
vacekj Oct 13, 2023
bd61965
fix: lint
vacekj Oct 13, 2023
a6e82cb
fix: minor fixes for feedback from review
vacekj Oct 13, 2023
a8c9f89
fix: don't estimate when round is not loaded, fix total matching bugˆ
vacekj Oct 13, 2023
e12ee25
fix: don't estimate when round is not loaded, fix total matching bug,…
vacekj Oct 13, 2023
f419f66
Merge branch 'main' into 1931-spike-matching-estimates-for-donations
vacekj Oct 13, 2023
86b16eb
Merge branch 'main' into 1931-spike-matching-estimates-for-donations
vacekj Oct 17, 2023
e431d64
feat: implement colors for matching estimates based on passport
boudra Oct 19, 2023
f47756b
Merge branch 'main' into 1931-spike-matching-estimates-for-donations
boudra Oct 20, 2023
e3894d6
Merge branch 'main' into 1931-spike-matching-estimates-for-donations
vacekj Oct 23, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/grant-explorer.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,12 @@ jobs:

- name: Lint Explorer
run: |
pnpm re-lint
pnpm ge-lint

- name: Test Explorer
run: |
pnpm re-test
pnpm ge-test

- name: Typecheck Explorer
run: |
pnpm re-typecheck
pnpm ge-typecheck
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@
"rm-lint": "turbo run lint:ci --filter=round-manager",
"rm-typecheck": "turbo run typecheck --filter=round-manager",
"// grant explorer script": "====== packages/grant-explorer specific ======",
"re-build": "turbo run build --filter=grant-explorer",
"re-test": "turbo run test --filter=grant-explorer",
"re-start": "pnpm --filter grant-explorer run start",
"re-typecheck": "turbo run typecheck --filter=grant-explorer",
"re-lint": "turbo run lint:ci --filter=grant-explorer",
"ge-build": "turbo run build --filter=grant-explorer",
"ge-test": "turbo run test --filter=grant-explorer",
"ge-start": "pnpm --filter grant-explorer run start",
"ge-typecheck": "turbo run typecheck --filter=grant-explorer",
"ge-lint": "turbo run lint:ci --filter=grant-explorer",
"// builder script": "====== packages/builder specific ======",
"b-start": "pnpm --filter builder run start",
"b-lint": "turbo run lint:ci --filter=builder",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { InformationCircleIcon } from "@heroicons/react/24/solid";
import React from "react";
import { Tooltip } from "@chakra-ui/react";

export function MatchingEstimateTooltip(props: { isEligible: boolean }) {
return (
<div>
<Tooltip
vacekj marked this conversation as resolved.
Show resolved Hide resolved
hasArrow
closeDelay={2000}
placement={"bottom-end"}
label={
<p className="text-xs p-1 pointer-events-auto select-all">
vacekj marked this conversation as resolved.
Show resolved Hide resolved
{props.isEligible ? (
<>
Due to the nature of quadratic funding, this estimated match is
subject to change as the round progresses. Your match may start
at $0, but can change as the project receives more donations.
Read more about how quadratic funding works{" "}
<a href="https://wtfisqf.com">here</a>.
vacekj marked this conversation as resolved.
Show resolved Hide resolved
</>
) : (
<>
Keep in mind that this is a potential match. By connecting to
Gitcoin Passport, you can update your score before or after
submitting your donation.
<a href="https://passport.gitcoin.co" target="_blank">
Click here
</a>{" "}
to configure your score.
</>
)}
</p>
}
id="matching-estimate-tooltip"
className={"max-w-sm bg-gray-500 text-gray-50"}
>
<InformationCircleIcon
data-background-color="#5932C4"
className="inline w-4 h-4 ml-2"
data-testid={"matching-estiamte-tooltip"}
vacekj marked this conversation as resolved.
Show resolved Hide resolved
/>
</Tooltip>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export function ProjectInCart(
selectedPayoutToken: VotingToken;
payoutTokenPrice: number;
removeProjectFromCart: (grantApplicationId: string) => void;
matchingEstimateUSD: number | undefined;
}
) {
const { project, roundRoutePath } = props;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@ import React from "react";
import { CartProject, VotingToken } from "../../api/types";
import { useRoundById } from "../../../context/RoundContext";
import { ProjectInCart } from "./ProjectInCart";
import { useMatchingEstimates } from "../../../hooks/matchingEstimate";
import { getAddress, parseUnits, zeroAddress } from "viem";
import { useAccount } from "wagmi";
import { useCartStorage } from "../../../store";
import { Skeleton } from "@chakra-ui/react";
import { BoltIcon } from "@heroicons/react/24/outline";
import { PassportState, usePassport } from "../../api/passport";
import { MatchingEstimateTooltip } from "../../common/MatchingEstimateTooltip";

export function RoundInCart(
props: React.ComponentProps<"div"> & {
Expand All @@ -15,14 +23,84 @@ export function RoundInCart(
String(props.roundCart[0].chainId),
props.roundCart[0].roundId
).round;

const minDonationThresholdAmount =
round?.roundMetadata?.quadraticFundingConfig?.minDonationThresholdAmount ??
1;

const { address } = useAccount();
const votingTokenForChain = useCartStorage((state) =>
state.getVotingTokenForChain(props.roundCart[0].chainId)
vacekj marked this conversation as resolved.
Show resolved Hide resolved
);

const {
data: matchingEstimates,
error: matchingEstimateError,
isLoading: matchingEstimateLoading,
} = useMatchingEstimates([
{
roundId: getAddress(round?.id ?? zeroAddress),
chainid: props.roundCart[0].chainId,
vacekj marked this conversation as resolved.
Show resolved Hide resolved
potentialVotes: props.roundCart.map((proj) => ({
amount: parseUnits(
proj.amount ?? "0",
votingTokenForChain.decimal ?? 18
),
recipient: proj.recipient,
contributor: address ?? zeroAddress,
token: votingTokenForChain.address.toLowerCase(),
})),
},
]);

const estimateText = matchingEstimates
?.flat()
.map((est) => est.differenceInUSD ?? 0)
.filter((diff) => diff > 0)
.reduce((acc, b) => acc + b, 0)
.toFixed(2);
vacekj marked this conversation as resolved.
Show resolved Hide resolved

const { passportState, passport } = usePassport({
address: address ?? "",
});

const isNotEligibleForMatching =
passportState === PassportState.NOT_CONNECTED ||
(passport?.score !== undefined && Number(passport.score) < 1);

return (
<div className="my-4 bg-grey-50 rounded-xl">
<div className="flex flex-row pt-4 sm:px-4 px-2">
<p className="text-xl font-semibold">{round?.roundMetadata?.name}</p>
<p className="text-lg font-bold ml-2">({props.roundCart.length})</p>
<div className="flex flex-row items-center pt-4 sm:px-4 px-2 justify-between">
<div className={"flex"}>
vacekj marked this conversation as resolved.
Show resolved Hide resolved
<p className="text-xl font-semibold">{round?.roundMetadata?.name}</p>
<p className="text-lg font-bold ml-2">({props.roundCart.length})</p>
</div>
<div
className={`flex flex-row gap-4 items-center justify-between font-semibold italic ${
isNotEligibleForMatching ? "text-red-400" : "text-teal-500"
}`}
>
{matchingEstimateError === undefined &&
matchingEstimates !== undefined && (
<>
<div className="flex flex-row items-center">
<p>Estimated match</p>
<MatchingEstimateTooltip
isEligible={!isNotEligibleForMatching}
/>
</div>
<div className="flex justify-end ">
<Skeleton isLoaded={!matchingEstimateLoading}>
<p>
<BoltIcon className={"w-4 h-4 inline"} />
~$
{estimateText}
</p>
</Skeleton>
</div>
</>
)}
</div>
</div>
{minDonationThresholdAmount && (
<div>
Expand All @@ -41,6 +119,15 @@ export function RoundInCart(
removeProjectFromCart={props.handleRemoveProjectFromCart}
project={project}
index={key}
matchingEstimateUSD={
matchingEstimates
?.flat()
.find(
(est) =>
getAddress(est.recipient ?? zeroAddress) ===
getAddress(project.recipient ?? zeroAddress)
)?.differenceInUSD
vacekj marked this conversation as resolved.
Show resolved Hide resolved
}
roundRoutePath={`/round/${props.roundCart[0].chainId}/${props.roundCart[0].roundId}`}
last={key === props.roundCart.length - 1}
payoutTokenPrice={props.payoutTokenPrice}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ChainId, PassportState, getTokenPrice } from "common";
import { ChainId, getTokenPrice, PassportState } from "common";
import { useCartStorage } from "../../../store";
import React, { useEffect, useMemo, useState } from "react";
import { Summary } from "./Summary";
Expand All @@ -8,18 +8,23 @@ import { ChainConfirmationModalBody } from "./ChainConfirmationModalBody";
import { ProgressStatus } from "../../api/types";
import { modalDelayMs } from "../../../constants";
import { useNavigate } from "react-router-dom";
import { useAccount, useWalletClient, usePublicClient } from "wagmi";
import { useAccount, usePublicClient, useWalletClient } from "wagmi";
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 useSWR from "swr";
import { round, groupBy, uniqBy } from "lodash-es";
import { groupBy, uniqBy } from "lodash-es";
import { getRoundById } from "../../api/round";
import MRCProgressModal from "../../common/MRCProgressModal";
import { MRCProgressModalBody } from "./MRCProgressModalBody";
import { useCheckoutStore } from "../../../checkoutStore";
import { formatUnits, parseUnits } from "viem";
import { Address, formatUnits, getAddress, parseUnits } from "viem";
import { useConnectModal } from "@rainbow-me/rainbowkit";
import { useMatchingEstimates } from "../../../hooks/matchingEstimate";
import { Skeleton } from "@chakra-ui/react";
import { MatchingEstimateTooltip } from "../../common/MatchingEstimateTooltip";

export function SummaryContainer() {
const { projects, getVotingTokenForChain, chainToVotingToken } =
Expand Down Expand Up @@ -214,7 +219,7 @@ export function SummaryContainer() {

async function handleSubmitDonation() {
try {
if (!round || !walletClient) {
if (!walletClient) {
return;
}

Expand All @@ -236,7 +241,7 @@ export function SummaryContainer() {
}
}

const { passportState } = usePassport({
const { passportState, passport } = usePassport({
address: address ?? "",
});

Expand Down Expand Up @@ -275,13 +280,78 @@ export function SummaryContainer() {
}
}, [totalDonationAcrossChainsInUSDData]);

/* Matching estimates are calculated per-round */
const matchingEstimateParamsPerRound =
rounds?.map((round) => {
const projs = projects.filter((project) => project.roundId === round.id);
return {
vacekj marked this conversation as resolved.
Show resolved Hide resolved
roundId: getAddress(round.id ?? ""),
chainid: projs[0].chainId,
potentialVotes: projects.map((proj) => ({
vacekj marked this conversation as resolved.
Show resolved Hide resolved
amount: parseUnits(
proj.amount ?? "0",
getVotingTokenForChain(Number(proj.chainId) as ChainId).decimal ??
18
vacekj marked this conversation as resolved.
Show resolved Hide resolved
),
recipient: proj.recipient,
contributor: address as Address,
token: getVotingTokenForChain(
Number(proj.chainId) as ChainId
).address.toLowerCase(),
})),
};
}) ?? [];

const {
data: matchingEstimates,
error: matchingEstimateError,
isLoading: matchingEstimateLoading,
} = useMatchingEstimates(matchingEstimateParamsPerRound);

const estimateText = matchingEstimates
?.flat()
.map((est) => est.differenceInUSD ?? 0)
.filter((diff) => diff > 0)
.reduce((acc, b) => acc + b, 0)
.toFixed(2);

if (projects.length === 0) {
vacekj marked this conversation as resolved.
Show resolved Hide resolved
return null;
}

const isNotEligibleForMatching =
passportState === PassportState.NOT_CONNECTED ||
(passport?.score !== undefined && Number(passport.score) < 1);

return (
<div className="mb-5 block px-[16px] py-4 rounded-lg shadow-lg bg-white border border-violet-400 font-semibold">
<h2 className="text-xl border-b-2 pb-2">Summary</h2>
<div
className={`flex flex-row items-center justify-between mt-4 font-semibold italic ${
isNotEligibleForMatching ? "text-red-400" : "text-teal-500"
}`}
>
{matchingEstimateError === undefined &&
matchingEstimates !== undefined && (
<>
<div className="flex flex-row mt-4 items-center">
<p>Estimated match</p>
<MatchingEstimateTooltip
isEligible={!isNotEligibleForMatching}
/>
</div>
<div className="flex justify-end mt-4">
<Skeleton isLoaded={!matchingEstimateLoading}>
<p>
<BoltIcon className={"w-4 h-4 inline"} />
~$
{estimateText}
</p>
</Skeleton>
</div>
</>
)}
</div>
<div>
{Object.keys(projectsByChain).map((chainId) => (
<Summary
Expand Down
Loading
Loading