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

feat: hook to check status of all chains #2805

Merged
merged 3 commits into from
Aug 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 118 additions & 2 deletions app/__tests__/components/DashboardScorePanel.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
// Added for document.getElementById since just testing invocation
/* eslint-disable testing-library/no-node-access */
import React from "react";
import { screen } from "@testing-library/react";
import { DashboardScorePanel } from "../../components/DashboardScorePanel";
import { screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { DashboardScorePanel, OnchainCTA } from "../../components/DashboardScorePanel";
import { ScorerContext } from "../../context/scorerContext";
import { useAllOnChainStatus } from "../../hooks/useOnChainStatus";
import { PlatformScoreSpec } from "../../context/scorerContext";

import { renderWithContext, makeTestCeramicContext } from "../../__test-fixtures__/contextTestHelpers";
import { CeramicContextState } from "../../context/ceramicContext";
Expand Down Expand Up @@ -32,6 +38,16 @@ jest.mock("../../hooks/useCustomization", () => ({
useCustomization: jest.fn(),
}));

// Add type definition for MockedFunction
type MockedFunction<T extends (...args: any[]) => any> = jest.MockedFunction<T>;

// Mock useAllOnChainStatus
jest.mock("../../hooks/useOnChainStatus", () => ({
useAllOnChainStatus: jest.fn(),
}));

const mockedUseAllOnChainStatus = useAllOnChainStatus as MockedFunction<typeof useAllOnChainStatus>;

describe("DashboardScorePanel", () => {
it("should indicate the loading state", () => {
renderWithContext(mockCeramicContext, <DashboardScorePanel className="test" />);
Expand All @@ -46,3 +62,103 @@ describe("DashboardScorePanel", () => {
expect(screen.getByText("0")).toBeInTheDocument(); // Adjust this based on your actual UI
});
});

describe("OnchainCTA", () => {
const mockSetShowSidebar = jest.fn();
const cardListProps = {}; // Add any necessary props for CardList

const scorerContext = {
scoredPlatforms: [
{
icon: "./assets/star-light.svg",
platform: "AllowList",
name: "Guest List",
description: "Verify you are part of a community",
connectMessage: "Verify",
isEVM: true,
possiblePoints: 100,
earnedPoints: 100,
},
] as PlatformScoreSpec[],
};

beforeEach(() => {
jest.clearAllMocks();
});

it("renders content for above threshold and all chains up to date", async () => {
mockedUseAllOnChainStatus.mockReturnValue({ allChainsUpToDate: true });

renderWithContext(
mockCeramicContext,
<OnchainCTA setShowSidebar={mockSetShowSidebar} />,
{},
{ ...scorerContext, rawScore: 25, threshold: 20 }
);
expect(screen.getByText("Congratulations. Your Passport is onchain.")).toBeInTheDocument();
expect(screen.getByText("See onchain passport")).toBeInTheDocument();
});

it("renders content for above threshold but chains not up to date", async () => {
mockedUseAllOnChainStatus.mockReturnValue({ allChainsUpToDate: false });

renderWithContext(
mockCeramicContext,
<OnchainCTA setShowSidebar={mockSetShowSidebar} />,
{},
{ ...scorerContext, rawScore: 25, threshold: 20 }
);

expect(screen.getByText("Congratulations. You have a passing score")).toBeInTheDocument();
expect(screen.getByText("Mint onchain")).toBeInTheDocument();
});

it("renders content for below threshold", async () => {
mockedUseAllOnChainStatus.mockReturnValue({ allChainsUpToDate: false });

renderWithContext(
mockCeramicContext,
<OnchainCTA setShowSidebar={mockSetShowSidebar} />,
{},
{ ...scorerContext, rawScore: 15, threshold: 20 }
);

expect(screen.getByText("Let's increase that score")).toBeInTheDocument();
expect(screen.getByText("Verify Stamps")).toBeInTheDocument();
});

it("calls setShowSidebar when 'See onchain passport' button is clicked", async () => {
mockedUseAllOnChainStatus.mockReturnValue({ allChainsUpToDate: true });

renderWithContext(
mockCeramicContext,
<OnchainCTA setShowSidebar={mockSetShowSidebar} />,
{},
{ ...scorerContext, rawScore: 25, threshold: 20 }
);

await userEvent.click(screen.getByText("See onchain passport"));

await waitFor(() => {
expect(mockSetShowSidebar).toHaveBeenCalledWith(true);
});
});

it("scrolls to 'add-stamps' element when 'Verify Stamps' button is clicked", async () => {
mockedUseAllOnChainStatus.mockReturnValue({ allChainsUpToDate: false });

const mockScrollIntoView = jest.fn();
document.getElementById = jest.fn().mockReturnValue({ scrollIntoView: mockScrollIntoView });

renderWithContext(
mockCeramicContext,
<OnchainCTA setShowSidebar={mockSetShowSidebar} />,
{},
{ ...scorerContext, rawScore: 15, threshold: 20 }
);

await userEvent.click(screen.getByText("Verify Stamps"));
expect(document.getElementById).toHaveBeenCalledWith("add-stamps");
expect(mockScrollIntoView).toHaveBeenCalledWith({ behavior: "smooth" });
});
});
2 changes: 1 addition & 1 deletion app/__tests__/components/OnChainSidbar.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { parseValidChains } from "../../components/OnchainSidebar";
import { parseValidChains } from "../../hooks/useOnChainStatus";
import { Customization } from "../../utils/customizationUtils";

const customization = {
Expand Down
2 changes: 1 addition & 1 deletion app/components/Confetti.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export const Confetti: React.FC = () => {
<div data-testid="react-confetti">
<ReactConfetti
width={windowDimensions.width}
height={windowDimensions.height}
height={windowDimensions.height - 100}
numberOfPieces={4000}
recycle={false}
run={true}
Expand Down
137 changes: 117 additions & 20 deletions app/components/DashboardScorePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import { useCustomization } from "../hooks/useCustomization";
import { useAtom } from "jotai";
import { mutableUserVerificationAtom } from "../context/userState";
import Tooltip from "./Tooltip";
import { useAllOnChainStatus } from "../hooks/useOnChainStatus";
import { LoadButton } from "./LoadButton";
import { Hyperlink } from "@gitcoin/passport-platforms";
import { OnchainSidebar } from "./OnchainSidebar";

const PanelDiv = ({ className, children }: { className: string; children: React.ReactNode }) => {
return (
Expand All @@ -18,8 +22,6 @@ const PanelDiv = ({ className, children }: { className: string; children: React.

const LoadingScoreImage = () => <img src="/assets/scoreLogoLoading.svg" alt="loading" className="h-20 w-auto" />;

const SuccessScoreImage = () => <img src="/assets/scoreLogoSuccess.svg" alt="success" className="h-20 w-auto" />;

const Ellipsis = () => {
return (
<div className="flex">
Expand All @@ -29,6 +31,10 @@ const Ellipsis = () => {
);
};

const borderStyle = (color: string) => {
return `shadow-${color} shadow-even-md border-${color} shadow-${color} border`;
};

export const DashboardScorePanel = ({ className }: { className?: string }) => {
const { rawScore, passportSubmissionState, threshold } = React.useContext(ScorerContext);
const [verificationState] = useAtom(mutableUserVerificationAtom);
Expand All @@ -47,9 +53,11 @@ export const DashboardScorePanel = ({ className }: { className?: string }) => {
const customTitle = customization?.scorerPanel?.title;

const loading = passportSubmissionState === "APP_REQUEST_PENDING" || verificationState.loading;
const aboveThreshold = rawScore >= threshold;
const highlightColor = aboveThreshold ? "foreground-2" : "background-5";

return (
<PanelDiv className={`text-color-2 font-heading ${className}`}>
<PanelDiv className={`${borderStyle(highlightColor)} text-color-2 font-heading ${className}`}>
<div className="flex items-center w-full">
<span className="grow">{customTitle || "Unique Humanity Score"}</span>
<Tooltip className="px-0">
Expand All @@ -58,7 +66,16 @@ export const DashboardScorePanel = ({ className }: { className?: string }) => {
</Tooltip>
</div>
<div className="flex grow items-center align-middle text-xl mt-6 mb-10">
<div className="m-4">{loading ? <LoadingScoreImage /> : <SuccessScoreImage />}</div>
<div className="m-4">
{loading ? (
<LoadingScoreImage />
) : (
<img
src={aboveThreshold ? "/assets/scoreLogoSuccess.svg" : "/assets/scoreLogoBelow.svg"}
alt={aboveThreshold ? "Above threshold Passport Logo" : "Below threshold Passport logo"}
/>
)}
</div>
{loading ? (
<div className="leading-none">
Updating
Expand All @@ -68,7 +85,7 @@ export const DashboardScorePanel = ({ className }: { className?: string }) => {
</div>
</div>
) : (
<span>{+displayScore.toFixed(2)}</span>
<span className={`text-${highlightColor} text-5xl`}>{+displayScore.toFixed(2)}</span>
)}
</div>
</PanelDiv>
Expand All @@ -83,27 +100,107 @@ const LoadingBar = ({ className }: { className?: string }) => {
);
};

interface OnchainCTAProps {
setShowSidebar: (show: boolean) => void;
}

export const OnchainCTA: React.FC<OnchainCTAProps> = ({ setShowSidebar }) => {
const { rawScore, threshold } = React.useContext(ScorerContext);
const { allChainsUpToDate } = useAllOnChainStatus();
const customization = useCustomization();

const aboveThreshold = rawScore >= threshold;
const customText = customization?.scorerPanel?.text;

const renderContent = (title: string, description?: string, linkText?: string, linkHref?: string) => (
<div className="flex flex-col h-full w-full pt-10">
<h2 className={`text-xl text-foreground-2 ${!description && "mb-4"}`}>{title}</h2>
{description && <p className="py-2">{description}</p>}
{linkText && linkHref && <Hyperlink href={linkHref}>{linkText}</Hyperlink>}
</div>
);

const renderButton = (text: string, onClick: () => void, className: string = "w-auto mt-4") => (
<div className="flex w-full justify-end px-4">
<LoadButton className={className} onClick={onClick}>
{text}
</LoadButton>
</div>
);

if (customText) {
return renderContent(customText);
}

if (aboveThreshold && allChainsUpToDate) {
return (
<>
{renderContent(
"Congratulations. Your Passport is onchain.",
undefined,
// Once link is available, add it here - issue #2763
undefined,
undefined
)}
{renderButton("See onchain passport", () => setShowSidebar(true))}
</>
);
}

if (aboveThreshold) {
return (
<>
{renderContent(
"Congratulations. You have a passing score",
"Next up, mint your passport onchain!",
// Once link is available, add it here - issue #2763
undefined,
undefined
)}
{renderButton("Mint onchain", () => setShowSidebar(true))}
</>
);
}

return (
<>
{renderContent(
"Let's increase that score",
undefined,
"Here's some tips on how to raise your score to a minimum of 20.",
"https://support.passport.xyz/passport-knowledge-base/stamps/scoring-20-for-humans"
)}
{renderButton("Verify Stamps", () => {
const addStamps = document.getElementById("add-stamps");
if (addStamps) {
addStamps.scrollIntoView({ behavior: "smooth" });
}
})}
</>
);
};

export const DashboardScoreExplanationPanel = ({ className }: { className?: string }) => {
const { passportSubmissionState } = React.useContext(ScorerContext);
const [verificationState] = useAtom(mutableUserVerificationAtom);
const customization = useCustomization();
const [showSidebar, setShowSidebar] = React.useState(false);

const loading = passportSubmissionState === "APP_REQUEST_PENDING" || verificationState.loading;

// TODO Do we display this instead of the standard success text when available? Or drop it from the customization in the scorer?
const customText = customization?.scorerPanel?.text;

return (
<PanelDiv className={`${className}`}>
{loading ? (
<div className="p-2 flex w-full flex-col gap-4">
<LoadingBar className="w-full" />
<LoadingBar className="w-full" />
<LoadingBar className="w-2/3" />
</div>
) : (
"Placeholder"
)}
</PanelDiv>
<>
<PanelDiv className={`${className}`}>
{loading ? (
<div className="p-2 flex w-full flex-col gap-4">
<LoadingBar className="w-full" />
<LoadingBar className="w-full" />
<LoadingBar className="w-2/3" />
</div>
) : (
<OnchainCTA setShowSidebar={() => setShowSidebar(true)} />
)}
</PanelDiv>
<OnchainSidebar isOpen={showSidebar} onClose={() => setShowSidebar(false)} />
</>
);
};
10 changes: 1 addition & 9 deletions app/components/OnchainSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,14 @@ import { Drawer, DrawerContent, DrawerHeader, DrawerBody, DrawerOverlay, DrawerC
import { chains } from "../utils/chains";
import { NetworkCard } from "./NetworkCard";
import { useCustomization } from "../hooks/useCustomization";
import { Customization } from "../utils/customizationUtils";
import { mintFee } from "../config/mintFee";
import { parseValidChains } from "../hooks/useOnChainStatus";

type OnchainSidebarProps = {
isOpen: boolean;
onClose: () => void;
};

export const parseValidChains = (customization: Customization, id: string) => {
if (customization.includedChainIds && customization.includedChainIds?.length > 0) {
return customization.includedChainIds.includes(id);
} else {
return true;
}
};

export function OnchainSidebar({ isOpen, onClose }: OnchainSidebarProps) {
const customization = useCustomization();
const validChains = chains.filter(
Expand Down
Loading
Loading