Skip to content

Commit

Permalink
feat(app): moving attestation issuance to hook - wip
Browse files Browse the repository at this point in the history
  • Loading branch information
lucianHymer committed Oct 2, 2024
1 parent 4b4cb5e commit 20776fe
Show file tree
Hide file tree
Showing 9 changed files with 354 additions and 171 deletions.
9 changes: 1 addition & 8 deletions app/components/DashboardScorePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { useAllOnChainStatus } from "../hooks/useOnChainStatus";
import { LoadButton } from "./LoadButton";
import { Hyperlink } from "@gitcoin/passport-platforms";
import { OnchainSidebar } from "./OnchainSidebar";
import { LoadingBar } from "./LoadingBar";

const PanelDiv = ({ className, children }: { className: string; children: React.ReactNode }) => {
return (
Expand Down Expand Up @@ -92,14 +93,6 @@ export const DashboardScorePanel = ({ className }: { className?: string }) => {
);
};

const LoadingBar = ({ className }: { className?: string }) => {
return (
<div
className={`h-10 w-full bg-size-400 animate-[loading-gradient_5s_ease-in-out_infinite] bg-gradient-to-r from-background via-foreground-5 to-background rounded-lg my-2 ${className}`}
/>
);
};

interface OnchainCTAProps {
setShowSidebar: (show: boolean) => void;
}
Expand Down
32 changes: 32 additions & 0 deletions app/components/LoadingBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from "react";

import { twMerge } from "tailwind-merge";

export const LoadingBar = ({ className }: { className?: string }) => {
return (
<div
className={twMerge(
"h-10 w-full bg-size-400 animate-[loading-gradient_5s_ease-in-out_infinite] bg-gradient-to-r from-background via-foreground-5 to-background rounded-lg my-2",
className
)}
/>
);
};

export type LoadingBarSectionProps = {
isLoading: boolean;
className?: string;
loadingBarClassName?: string;
children: React.ReactNode;
};

export const LoadingBarSection = ({ isLoading, className, loadingBarClassName, children }: LoadingBarSectionProps) => {
return (
<div className={twMerge("relative", className)}>
<LoadingBar
className={twMerge("absolute top-0 left-0", isLoading ? "visible" : "invisible", loadingBarClassName)}
/>
<div className={isLoading ? "invisible" : "visible"}>{children}</div>
</div>
);
};
175 changes: 139 additions & 36 deletions app/components/ScrollCampaign.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from "react";
import React, { useContext, useEffect, useMemo, useState } from "react";
import { useParams } from "react-router-dom";
import NotFound from "../pages/NotFound";
import PageRoot from "./PageRoot";
Expand All @@ -7,6 +7,17 @@ import { useWeb3ModalAccount } from "@web3modal/ethers/react";
import { useLoginFlow } from "../hooks/useLoginFlow";
import { LoadButton } from "./LoadButton";
import { useNextCampaignStep } from "../hooks/useNextCampaignStep";
import { CeramicContext } from "../context/ceramicContext";
import { ScorerContext } from "../context/scorerContext";
import { useWalletStore } from "../context/walletStore";
import { useDatastoreConnectionContext } from "../context/datastoreConnectionContext";
import { useNavigateToPage, useSetCustomizationKey } from "../hooks/useCustomization";
import { LoadingBarSection, LoadingBarSectionProps } from "./LoadingBar";
import { EasPayload, PROVIDER_ID } from "@gitcoin/passport-types";
import { useAttestation } from "../hooks/useSyncToChainButton";
import { chains } from "../utils/chains";
import { jsonRequest } from "../utils/AttestationProvider";
import { useMessage } from "../hooks/useMessage";

const SCROLL_STEP_NAMES = ["Connect Wallet", "Connect to Github", "Mint Badge"];

Expand Down Expand Up @@ -192,50 +203,142 @@ const ScrollConnectGithub = () => {
return <div>Scroll Connect Github</div>;
};

// TODO env?
const SCROLL_BADGE_PROVIDER_NAMES: Partial<Record<PROVIDER_ID, string>> = {
NFT: "Rust Developer Badge (Level 1)",
"githubContributionActivityGte#120": "GitHub Contributor Badge (Level 2)",
};

const ScrollLoadingBarSection = (props: LoadingBarSectionProps) => (
<LoadingBarSection loadingBarClassName="h-10 via-[#FFEEDA] brightness-50" {...props} />
);

const ScrollMintBadge = () => {
const nextStep = useNextCampaignStep();
const { isLoggingIn, signIn, loginStep } = useLoginFlow({ onLoggedIn: nextStep });
const { failure } = useMessage();
const { passport } = useContext(CeramicContext);
const { stampScores, scoreState, refreshScore } = useContext(ScorerContext);
const address = useWalletStore((state) => state.address);
const { dbAccessToken } = useDatastoreConnectionContext();
const scrollChain = chains.find((chain) => chain.label === "Scroll");
const { nonce, issueAttestation } = useAttestation({ chain: scrollChain });
const [syncingToChain, setSyncingToChain] = useState(false);

useEffect(() => {
address && dbAccessToken && refreshScore(address, dbAccessToken);
}, [address, dbAccessToken]);

const badgeCredentials = useMemo(
() =>
passport && scoreState === "DONE"
? passport.stamps.filter(({ provider }) => Object.keys(SCROLL_BADGE_PROVIDER_NAMES).includes(provider))
: [],
[passport, scoreState]
);

const deduplicatedBadgeCredentials = useMemo(
() => badgeCredentials.filter(({ provider }) => stampScores[provider] && parseFloat(stampScores[provider]) > 0),
[badgeCredentials, stampScores]
);

const hasBadge = deduplicatedBadgeCredentials.length > 0;
const hasMultipleBadges = deduplicatedBadgeCredentials.length > 1;
const hasIgnoredBadges = badgeCredentials.length > deduplicatedBadgeCredentials.length;
const loading = !(passport && scoreState === "DONE");

const onMint = async () => {
try {
setSyncingToChain(true);
const { data }: { data: EasPayload } = await jsonRequest("/TODO", {
recipient: address || "",
badgeCredentials,
nonce,
});

if (data.error) {
console.error("error syncing credentials to chain: ", data.error, "nonce:", nonce);
failure({
title: "Error",
message: "An unexpected error occurred while generating attestations.",
});
}

issueAttestation({ data });
} catch (error) {
console.error("Error minting badge", error);
failure({
title: "Error",
message: "An unexpected error occurred while trying to bring the data onchain.",
});
}
setSyncingToChain(false);
};

return (
<ScrollCampaignPage fadeBackgroundImage emblemSrc="/assets/scrollCampaignMint.svg">
<div className="text-5xl text-[#FFEEDA]">Developer Badge</div>
<div className="text-xl mt-2">
Connect your GitHub account to prove the number of contributions you have made, then mint your badge to prove
you are a Rust developer.
</div>
<div className="mt-8">
<LoadButton
data-testid="connectWalletButton"
variant="custom"
onClick={signIn}
isLoading={isLoggingIn}
className="text-color-1 text-lg font-bold bg-[#FF684B] hover:brightness-150 py-3 transition-all duration-200"
>
<div className="flex flex-col items-center justify-center">
{isLoggingIn ? (
<>
<div>Connecting...</div>
<div className="text-sm font-base">
(
{loginStep === "PENDING_WALLET_CONNECTION"
? "Connect your wallet"
: loginStep === "PENDING_DATABASE_CONNECTION"
? "Sign message in wallet"
: ""}
)
</div>
</>
) : (
"Connect Wallet"
)}
<ScrollCampaignPage
fadeBackgroundImage={loading || hasBadge}
emblemSrc={hasBadge ? "/assets/scrollCampaignMint.svg" : undefined}
>
<ScrollLoadingBarSection
isLoading={loading}
className={`text-5xl ${hasBadge ? "text-[#FFEEDA]" : "text-[#FF684B]"}`}
>
{hasBadge ? "Congratulations!" : "We're sorry!"}
</ScrollLoadingBarSection>
<ScrollLoadingBarSection isLoading={loading} className="text-xl mt-2">
{hasBadge ? (
<div>
You qualify for:
<ul className="list-disc list-inside">
{deduplicatedBadgeCredentials.map(({ provider }) => (
<li key={provider}>{SCROLL_BADGE_PROVIDER_NAMES[provider] || provider}</li>
))}
</ul>
Mint your badge
{hasMultipleBadges ? "s" : ""} and get a chance to work with us.
{hasIgnoredBadges
? " (Some badge credentials could not be validated because they have already been claimed on another address.)"
: ""}
</div>
</LoadButton>
</div>
) : hasIgnoredBadges ? (
"Your badge credentials have already been claimed with another address."
) : (
"You don't qualify for any badges."
)}
</ScrollLoadingBarSection>

{hasBadge && (
<div className="mt-8">
<LoadButton
variant="custom"
onClick={onMint}
isLoading={loading || syncingToChain}
className="text-color-1 text-lg font-bold bg-[#FF684B] hover:brightness-150 py-3 transition-all duration-200"
>
<div className="flex flex-col items-center justify-center">
{syncingToChain ? "Minting..." : "Mint Badge"}
</div>
</LoadButton>
</div>
)}
</ScrollCampaignPage>
);
};

export const ScrollCampaign = ({ step }: { step: number }) => {
const { dbAccessToken } = useDatastoreConnectionContext();
const navigateToPage = useNavigateToPage();
const setCustomizationKey = useSetCustomizationKey();

useEffect(() => {
setCustomizationKey("scroll");
}, [setCustomizationKey]);

useEffect(() => {
if (!dbAccessToken && step > 0) {
navigateToPage("campaign/scroll");
}
}, [dbAccessToken, navigateToPage]);

if (step === 0) {
return <ScrollLogin />;
} else if (step === 1) {
Expand Down
2 changes: 2 additions & 0 deletions app/context/scorerContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export interface ScorerContextState {
refreshScore: (address: string | undefined, dbAccessToken: string, forceRescore?: boolean) => Promise<void>;
fetchStampWeights: () => Promise<void>;
stampWeights: Partial<Weights>;
stampScores: Partial<StampScores>;
// submitPassport: (address: string | undefined) => Promise<void>;
}

Expand All @@ -64,6 +65,7 @@ const startingState: ScorerContextState = {
): Promise<void> => {},
fetchStampWeights: async (): Promise<void> => {},
stampWeights: {},
stampScores: {},
// submitPassport: async (address: string | undefined): Promise<void> => {},
};

Expand Down
5 changes: 3 additions & 2 deletions app/hooks/useCustomization.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,9 @@ export const useNavigateToPage = () => {
return navigateToPage;
};

// Don't use this directly, use the CustomizationUrlLayoutRoute component instead
// This is only exported for testing purposes
// Generally use the CustomizationUrlLayoutRoute component instead
// This is only exported for testing purposes,or when you need to
// override the customization key in
export const useSetCustomizationKey = (): ((customizationKey: string | undefined) => Promise<void>) => {
const setCustomizationConfig = useSetAtom(customizationConfigAtom);

Expand Down
Loading

0 comments on commit 20776fe

Please sign in to comment.