From f151660d7c620fa6f0b6710be6306147b10f0350 Mon Sep 17 00:00:00 2001 From: schultztimothy Date: Mon, 26 Aug 2024 15:46:49 -0600 Subject: [PATCH 1/3] feat: hook to check status of all chains --- .../components/OnChainSidbar.test.tsx | 2 +- app/components/OnchainSidebar.tsx | 10 +--- app/hooks/useOnChainStatus.tsx | 48 ++++++++++++++++++- 3 files changed, 49 insertions(+), 11 deletions(-) diff --git a/app/__tests__/components/OnChainSidbar.test.tsx b/app/__tests__/components/OnChainSidbar.test.tsx index d757952f6a..00896f2fef 100644 --- a/app/__tests__/components/OnChainSidbar.test.tsx +++ b/app/__tests__/components/OnChainSidbar.test.tsx @@ -1,4 +1,4 @@ -import { parseValidChains } from "../../components/OnchainSidebar"; +import { parseValidChains } from "../../hooks/useOnChainStatus"; import { Customization } from "../../utils/customizationUtils"; const customization = { diff --git a/app/components/OnchainSidebar.tsx b/app/components/OnchainSidebar.tsx index cd88747116..d3909809bf 100644 --- a/app/components/OnchainSidebar.tsx +++ b/app/components/OnchainSidebar.tsx @@ -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( diff --git a/app/hooks/useOnChainStatus.tsx b/app/hooks/useOnChainStatus.tsx index 8b37a2404f..cbb584dd83 100644 --- a/app/hooks/useOnChainStatus.tsx +++ b/app/hooks/useOnChainStatus.tsx @@ -1,10 +1,20 @@ -import { useContext, useEffect, useState } from "react"; +import { useContext, useEffect, useState, useMemo } from "react"; import { CeramicContext } from "../context/ceramicContext"; import { ScorerContext } from "../context/scorerContext"; import { Chain, chains } from "../utils/chains"; import { AttestationProvider } from "../utils/AttestationProvider"; import { OnChainStatus } from "../utils/onChainStatus"; import { useOnChainData } from "./useOnChainData"; +import { useCustomization } from "./useCustomization"; +import { Customization } from "../utils/customizationUtils"; + +export const parseValidChains = (customization: Customization, id: string) => { + if (customization.includedChainIds && customization.includedChainIds?.length > 0) { + return customization.includedChainIds.includes(id); + } else { + return true; + } +}; export const useOnChainStatus = ({ chain }: { chain?: Chain }): OnChainStatus => { const { allProvidersState } = useContext(CeramicContext); @@ -40,3 +50,39 @@ export const useOnChainStatus = ({ chain }: { chain?: Chain }): OnChainStatus => return onChainStatus; }; + +export const useAllOnChainStatus = () => { + const { allProvidersState } = useContext(CeramicContext); + const { data, isPending } = useOnChainData(); + const { rawScore, scoreState } = useContext(ScorerContext); + const customization = useCustomization(); + + const allChainStatus = useMemo(() => { + if (isPending) return false; + return chains + .filter( + ({ attestationProvider, id }) => + (attestationProvider?.status === "comingSoon" || attestationProvider?.status === "enabled") && + parseValidChains(customization, id) + ) + .every((activeChain) => { + const { score, providers, expirationDate } = data[activeChain.id] || { score: 0, providers: [] }; + const attestationProvider = activeChain.attestationProvider; + + if (!attestationProvider) return false; + + const status = attestationProvider.checkOnChainStatus( + allProvidersState, + providers, + rawScore, + scoreState, + score, + expirationDate + ); + + return status === OnChainStatus.MOVED_UP_TO_DATE; + }); + }, [allProvidersState, customization, data, isPending, rawScore, scoreState]); + + return { allChainStatus }; +}; From 56ac7ecdfa22cd0240d314d1cd13b412f47e7882 Mon Sep 17 00:00:00 2001 From: schultztimothy Date: Tue, 27 Aug 2024 17:19:33 -0600 Subject: [PATCH 2/3] feat: incidate below thrshold scor and remaining cta logic --- .../components/DashboardScorePanel.test.tsx | 120 +++++++++++++++- app/components/Confetti.tsx | 2 +- app/components/DashboardScorePanel.tsx | 135 +++++++++++++++--- app/hooks/useOnChainStatus.tsx | 4 +- app/pages/Dashboard.tsx | 6 +- app/public/assets/scoreLogoBelow.svg | 4 + app/tailwind.config.js | 1 + 7 files changed, 245 insertions(+), 27 deletions(-) create mode 100644 app/public/assets/scoreLogoBelow.svg diff --git a/app/__tests__/components/DashboardScorePanel.test.tsx b/app/__tests__/components/DashboardScorePanel.test.tsx index bda151f0e4..a25f5835c5 100644 --- a/app/__tests__/components/DashboardScorePanel.test.tsx +++ b/app/__tests__/components/DashboardScorePanel.test.tsx @@ -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"; @@ -32,6 +38,16 @@ jest.mock("../../hooks/useCustomization", () => ({ useCustomization: jest.fn(), })); +// Add type definition for MockedFunction +type MockedFunction any> = jest.MockedFunction; + +// Mock useAllOnChainStatus +jest.mock("../../hooks/useOnChainStatus", () => ({ + useAllOnChainStatus: jest.fn(), +})); + +const mockedUseAllOnChainStatus = useAllOnChainStatus as MockedFunction; + describe("DashboardScorePanel", () => { it("should indicate the loading state", () => { renderWithContext(mockCeramicContext, ); @@ -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, + , + {}, + { ...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, + , + {}, + { ...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, + , + {}, + { ...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, + , + {}, + { ...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, + , + {}, + { ...scorerContext, rawScore: 15, threshold: 20 } + ); + + await userEvent.click(screen.getByText("Verify Stamps")); + expect(document.getElementById).toHaveBeenCalledWith("add-stamps"); + expect(mockScrollIntoView).toHaveBeenCalledWith({ behavior: "smooth" }); + }); +}); diff --git a/app/components/Confetti.tsx b/app/components/Confetti.tsx index a6083bd65b..8678464f76 100644 --- a/app/components/Confetti.tsx +++ b/app/components/Confetti.tsx @@ -33,7 +33,7 @@ export const Confetti: React.FC = () => {
{ return ( @@ -18,8 +22,6 @@ const PanelDiv = ({ className, children }: { className: string; children: React. const LoadingScoreImage = () => loading; -const SuccessScoreImage = () => success; - const Ellipsis = () => { return (
@@ -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); @@ -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 ( - +
{customTitle || "Unique Humanity Score"} @@ -58,7 +66,16 @@ export const DashboardScorePanel = ({ className }: { className?: string }) => {
-
{loading ? : }
+
+ {loading ? ( + + ) : ( + {aboveThreshold + )} +
{loading ? (
Updating @@ -68,7 +85,7 @@ export const DashboardScorePanel = ({ className }: { className?: string }) => {
) : ( - {+displayScore.toFixed(2)} + {+displayScore.toFixed(2)} )}
@@ -83,27 +100,105 @@ const LoadingBar = ({ className }: { className?: string }) => { ); }; +interface OnchainCTAProps { + setShowSidebar: (show: boolean) => void; +} + +export const OnchainCTA: React.FC = ({ 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) => ( +
+

{title}

+ {description &&

{description}

} + {linkText && linkHref && {linkText}} +
+ ); + + const renderButton = (text: string, onClick: () => void, className: string = "w-auto mt-4") => ( +
+ + {text} + +
+ ); + + if (customText) { + return renderContent(customText); + } + + if (aboveThreshold && allChainsUpToDate) { + return ( + <> + {renderContent( + "Congratulations. Your Passport is onchain.", + undefined, + "Here's what you can do with your passport!", + "www.tbd.com" + )} + {renderButton("See onchain passport", () => setShowSidebar(true))} + + ); + } + + if (aboveThreshold) { + return ( + <> + {renderContent( + "Congratulations. You have a passing score", + "Next up, mint your passport onchain!", + "Here's what you can do with your passport!", + "www.tbd.com" + )} + {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 ( - - {loading ? ( -
- - - -
- ) : ( - "Placeholder" - )} -
+ <> + + {loading ? ( +
+ + + +
+ ) : ( + setShowSidebar(true)} /> + )} +
+ setShowSidebar(false)} /> + ); }; diff --git a/app/hooks/useOnChainStatus.tsx b/app/hooks/useOnChainStatus.tsx index cbb584dd83..bc97b4f97d 100644 --- a/app/hooks/useOnChainStatus.tsx +++ b/app/hooks/useOnChainStatus.tsx @@ -57,7 +57,7 @@ export const useAllOnChainStatus = () => { const { rawScore, scoreState } = useContext(ScorerContext); const customization = useCustomization(); - const allChainStatus = useMemo(() => { + const allChainsUpToDate = useMemo(() => { if (isPending) return false; return chains .filter( @@ -84,5 +84,5 @@ export const useAllOnChainStatus = () => { }); }, [allProvidersState, customization, data, isPending, rawScore, scoreState]); - return { allChainStatus }; + return { allChainsUpToDate }; }; diff --git a/app/pages/Dashboard.tsx b/app/pages/Dashboard.tsx index 796d587ef4..a88c72218c 100644 --- a/app/pages/Dashboard.tsx +++ b/app/pages/Dashboard.tsx @@ -272,7 +272,7 @@ export default function Dashboard() {
-
+
@@ -281,7 +281,9 @@ export default function Dashboard() { )}
- Add Stamps + + Add Stamps + + + + diff --git a/app/tailwind.config.js b/app/tailwind.config.js index 1dcf63e1ea..3ddc353c11 100644 --- a/app/tailwind.config.js +++ b/app/tailwind.config.js @@ -8,6 +8,7 @@ module.exports = { // Without this, classNames used only in /platforms wouldn't be included "../platforms/src/**/*.tsx", ], + safelist: ["shadow-background-5"], theme: { screens: { md: "480px", From 7b12336c080ecef42183a10843aa24284fa51487 Mon Sep 17 00:00:00 2001 From: schultztimothy Date: Wed, 28 Aug 2024 10:14:59 -0600 Subject: [PATCH 3/3] chore(app): hide link until it is ready --- app/components/DashboardScorePanel.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/components/DashboardScorePanel.tsx b/app/components/DashboardScorePanel.tsx index e8aa57794d..bed89e7050 100644 --- a/app/components/DashboardScorePanel.tsx +++ b/app/components/DashboardScorePanel.tsx @@ -138,8 +138,9 @@ export const OnchainCTA: React.FC = ({ setShowSidebar }) => { {renderContent( "Congratulations. Your Passport is onchain.", undefined, - "Here's what you can do with your passport!", - "www.tbd.com" + // Once link is available, add it here - issue #2763 + undefined, + undefined )} {renderButton("See onchain passport", () => setShowSidebar(true))} @@ -152,8 +153,9 @@ export const OnchainCTA: React.FC = ({ setShowSidebar }) => { {renderContent( "Congratulations. You have a passing score", "Next up, mint your passport onchain!", - "Here's what you can do with your passport!", - "www.tbd.com" + // Once link is available, add it here - issue #2763 + undefined, + undefined )} {renderButton("Mint onchain", () => setShowSidebar(true))}