Skip to content

Commit

Permalink
1479 on chain sidebar (passportxyz#1516)
Browse files Browse the repository at this point in the history
* chore(app): pulled lateston chain button

* feat(app): on chain sidebar markup

* feat(app): on chain logos

* feat(app): chain status

* feat(app): updates card borders and adds tests

* chore(app): test onchain - db provider check

---------

Co-authored-by: schultztimothy <[email protected]>
  • Loading branch information
aminah-io and tim-schultz authored Jul 27, 2023
1 parent ed96a23 commit bbca98e
Show file tree
Hide file tree
Showing 15 changed files with 349 additions and 25 deletions.
1 change: 1 addition & 0 deletions app/.env-example.env
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,4 @@ NEXT_PUBLIC_MAINTENANCE_MODE_ON=["2023-06-07T21:00:00.000Z", "2023-06-08T22:15:0
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=YOUR_WALLET_CONNECT_PROJECT_ID
NEXT_PUBLIC_WEB3_ONBOARD_EXPLORE_URL=http://localhost:3000/

NEXT_PUBLIC_ACTIVE_ON_CHAIN_PASSPORT_CHAINIDS=["0x14a33"]
27 changes: 27 additions & 0 deletions app/__test-fixtures__/contextTestHelpers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import React from "react";
import { render } from "@testing-library/react";
import { PLATFORM_ID } from "@gitcoin/passport-types";
import { PlatformProps } from "../components/GenericPlatform";
import { OnChainContextState } from "../context/onChainContext";

jest.mock("@didtools/cacao", () => ({
Cacao: {
Expand Down Expand Up @@ -260,3 +261,29 @@ export const renderWithContext = (
<CeramicContext.Provider value={ceramicContext}>{ui}</CeramicContext.Provider>
</UserContext.Provider>
);

export const testOnChainContextState = (initialState?: Partial<OnChainContextState>): OnChainContextState => {
return {
onChainProviders: [
{
providerName: "githubAccountCreationGte#90",
credentialHash: "v0.0.0:rnutMGjNA2yPx/8xzJdn6sXDsY46lLUNV3DHAHoPJJg=",
expirationDate: new Date("2090-07-31T11:49:51.433Z"),
issuanceDate: new Date("2023-07-02T11:49:51.433Z"),
},
{
providerName: "githubAccountCreationGte#180",
credentialHash: "v0.0.0:rnutMGjNA2yPx/8xzJdn6sXDsY46lLUNV3DHAHoPJJg=",
expirationDate: new Date("2090-07-31T11:49:51.433Z"),
issuanceDate: new Date("2023-07-02T11:49:51.433Z"),
},
{
providerName: "githubAccountCreationGte#365",
credentialHash: "v0.0.0:rnutMGjNA2yPx/8xzJdn6sXDsY46lLUNV3DHAHoPJJg=",
expirationDate: new Date("2090-07-31T11:49:51.433Z"),
issuanceDate: new Date("2023-07-02T11:49:51.433Z"),
},
],
refreshOnChainProviders: jest.fn(),
};
};
121 changes: 121 additions & 0 deletions app/__tests__/components/NetworkCard.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import React from "react";
import { screen } from "@testing-library/react";

import { NetworkCard, checkOnChainStatus, OnChainStatus } from "../../components/NetworkCard";

import {
makeTestCeramicContext,
makeTestUserContext,
renderWithContext,
} from "../../__test-fixtures__/contextTestHelpers";

import { UserContextState } from "../../context/userContext";
import { CeramicContextState, AllProvidersState, ProviderState } from "../../context/ceramicContext";
import { OnChainProviderType } from "../../context/onChainContext";
import { Drawer, DrawerOverlay } from "@chakra-ui/react";

jest.mock("../../utils/onboard.ts");

jest.mock("next/router", () => ({
useRouter: () => ({
query: { filter: "" },
}),
}));

const chains = [
{
id: "12345",
token: "SEP",
label: "Sepolia Testnet",
rpcUrl: "http://www.sepolia.com",
icon: "sepolia.svg",
},
{
id: "67899",
token: "ETH",
label: "Ethereum Testnet",
rpcUrl: "http://www.etherum.com",
icon: "ethereum.svg",
},
];

const mockUserContext: UserContextState = makeTestUserContext();
const mockCeramicContext: CeramicContextState = makeTestCeramicContext();

describe("OnChainSidebar", () => {
it("renders", () => {
const drawer = () => (
<Drawer isOpen={true} placement="right" size="sm" onClose={() => {}}>
<DrawerOverlay />
<NetworkCard key={4} chain={chains[0]} activeChains={[chains[0].id, chains[1].id]} />
</Drawer>
);
renderWithContext(mockUserContext, mockCeramicContext, drawer());
expect(screen.getByText("Sepolia Testnet")).toBeInTheDocument();
});
});

describe("checkOnChainStatus", () => {
const mockAllProvidersState: AllProvidersState = {
["Google"]: {
stamp: {
provider: "Google",
credential: {
credentialSubject: {
hash: "hash1",
},
},
},
} as unknown as ProviderState,
["Ens"]: {
stamp: {
provider: "Ens",
credential: {
credentialSubject: {
hash: "hash2",
},
},
},
} as unknown as ProviderState,
};

const mockOnChainProviders: OnChainProviderType[] = [
{
providerName: "Google",
credentialHash: "hash1",
expirationDate: new Date(),
issuanceDate: new Date(),
},
{
providerName: "Ens",
credentialHash: "hash2",
expirationDate: new Date(),
issuanceDate: new Date(),
},
];

it("should return NOT_MOVED when onChainProviders is an empty array", () => {
expect(checkOnChainStatus(mockAllProvidersState, [])).toBe(OnChainStatus.NOT_MOVED);
});

it("should return MOVED_UP_TO_DATE when onChainProviders matches with allProvidersState", () => {
expect(checkOnChainStatus(mockAllProvidersState, mockOnChainProviders)).toBe(OnChainStatus.MOVED_UP_TO_DATE);
});

it("should return MOVED_OUT_OF_DATE when there are differences between onChainProviders and allProvidersState", () => {
const diffMockAllProviderState: AllProvidersState = {
...mockAllProvidersState,
["Github"]: {
stamp: {
provider: "Github",
credential: {
credentialSubject: {
hash: "hash2",
},
},
},
} as unknown as ProviderState,
};
expect(checkOnChainStatus(diffMockAllProviderState, mockOnChainProviders)).toBe(OnChainStatus.MOVED_OUT_OF_DATE);
});
});
96 changes: 96 additions & 0 deletions app/components/NetworkCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { Stamp } from "@gitcoin/passport-types";
import { useContext, useEffect, useState } from "react";
import { CeramicContext, AllProvidersState, ProviderState } from "../context/ceramicContext";
import { OnChainContext, OnChainProviderType } from "../context/onChainContext";

type Chain = {
id: string;
token: string;
label: string;
rpcUrl: string;
icon: string;
};

export enum OnChainStatus {
NOT_MOVED,
MOVED_OUT_OF_DATE,
MOVED_UP_TO_DATE,
}

type ProviderWithStamp = ProviderState & { stamp: Stamp };

export const checkOnChainStatus = (
allProvidersState: AllProvidersState,
onChainProviders: OnChainProviderType[]
): OnChainStatus => {
if (onChainProviders.length === 0) {
return OnChainStatus.NOT_MOVED;
}
const verifiedDbProviders: ProviderWithStamp[] = Object.values(allProvidersState).filter(
(provider): provider is ProviderWithStamp => provider.stamp !== undefined
);

const onChainDifference = verifiedDbProviders.filter(
(provider) =>
!onChainProviders.some(
(onChainProvider) =>
onChainProvider.providerName === provider.stamp.provider &&
onChainProvider.credentialHash === provider.stamp.credential.credentialSubject?.hash
)
);

return onChainDifference.length > 0 ? OnChainStatus.MOVED_OUT_OF_DATE : OnChainStatus.MOVED_UP_TO_DATE;
};

export function getButtonMsg(onChainStatus: OnChainStatus): string {
switch (onChainStatus) {
case OnChainStatus.NOT_MOVED:
return "Up to date";
case OnChainStatus.MOVED_OUT_OF_DATE:
return "Update";
case OnChainStatus.MOVED_UP_TO_DATE:
return "Up to date";
}
}

export function NetworkCard({ chain, activeChains }: { chain: Chain; activeChains: string[] }) {
const { allProvidersState } = useContext(CeramicContext);
const { onChainProviders } = useContext(OnChainContext);
const [isActive, setIsActive] = useState(false);
const [onChainStatus, setOnChainStatus] = useState<OnChainStatus>(OnChainStatus.NOT_MOVED);

useEffect(() => {
setIsActive(activeChains.includes(chain.id));
}, [activeChains, chain.id]);

useEffect(() => {
const checkStatus = async () => {
const stampStatus = await checkOnChainStatus(allProvidersState, onChainProviders);
setOnChainStatus(stampStatus);
};
checkStatus();
}, [allProvidersState, onChainProviders]);

return (
<div className="border border-accent-2 bg-background-2 p-0">
<div className="mx-4 my-2">
<div className="flex w-full">
<div className="mr-4">
<img className="max-h-6" src={chain.icon} alt={`${chain.label} logo`} />
</div>
<div>
<div className="flex w-full flex-col">
<h1 className="text-lg text-color-1">{chain.label}</h1>
<p className="mt-2 text-color-4 md:inline-block">Not moved yet</p>
</div>
</div>
</div>
</div>
<button className="verify-btn center" data-testid="card-menu-button">
<span className="mx-2 translate-y-[1px] text-muted">
{isActive ? getButtonMsg(onChainStatus) : "Coming Soon"}
</span>
</button>
</div>
);
}
51 changes: 51 additions & 0 deletions app/components/OnchainSidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Drawer, DrawerContent, DrawerHeader, DrawerBody, DrawerOverlay, DrawerCloseButton } from "@chakra-ui/react";
import { useEffect, useState } from "react";
import { chains } from "../utils/onboard";
import { NetworkCard } from "./NetworkCard";

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

export function OnchainSidebar({ isOpen, onClose }: OnchainSidebarProps) {
const activeOnChainPassportChains = process.env.NEXT_PUBLIC_ACTIVE_ON_CHAIN_PASSPORT_CHAINIDS;
const [activeChains, setActiveChains] = useState<string[]>([]);

useEffect(() => {
if (activeOnChainPassportChains) {
const chainsArray = JSON.parse(activeOnChainPassportChains);
setActiveChains(chainsArray);
}
}, [activeOnChainPassportChains]);

return (
<Drawer isOpen={isOpen} placement="right" size="sm" onClose={onClose}>
<DrawerOverlay />
<DrawerContent
style={{
backgroundColor: "var(--color-background-2)",
border: "1px solid var(--color-accent-2)",
borderRadius: "6px",
}}
>
<DrawerCloseButton className="text-color-1" />
<DrawerHeader className="text-center text-color-1">
<div className="mt-10 justify-center">
<h2 className="mt-4 text-2xl">Go On-Chain</h2>
<p className="text-base font-normal">
Moving your passport on-chain creates a tamper-proof record of your stamps. This is only required if
you&apos;re using applications that fetch Passport data from on-chain. Note: This involves blockchain
network fees and a $2 Gitcoin minting fee.
</p>
</div>
</DrawerHeader>
<DrawerBody>
{chains.map((chain) => (
<NetworkCard key={chain.id} chain={chain} activeChains={activeChains} />
))}
</DrawerBody>
</DrawerContent>
</Drawer>
);
}
30 changes: 17 additions & 13 deletions app/components/SyncToChainButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { OnChainContext } from "../context/onChainContext";

// --- Style Components
import { DoneToastContent } from "./DoneToastContent";
import { OnchainSidebar } from "./OnchainSidebar";

export type ErrorDetailsProps = {
msg: string;
Expand Down Expand Up @@ -72,6 +73,7 @@ const SyncToChainButton = () => {
const { wallet, address } = useContext(UserContext);
const { refreshOnChainProviders } = useContext(OnChainContext);
const [syncingToChain, setSyncingToChain] = useState(false);
const [showSidebar, setShowSidebar] = useState<boolean>(false);
const toast = useToast();

const onSyncToChain = useCallback(async (wallet, passport) => {
Expand Down Expand Up @@ -249,19 +251,21 @@ const SyncToChainButton = () => {
}, []);

return (
<button
className="h-10 w-10 rounded-md border border-muted"
// Add on-chain sidebar onOpen disclosure
// onClick={() => onOpen()}
disabled={syncingToChain}
>
<div className={`${syncingToChain ? "block" : "hidden"} relative top-1`}>
<Spinner thickness="2px" speed="0.65s" emptyColor="darkGray" color="gray" size="md" />
</div>
<div className={`${syncingToChain ? "hidden" : "block"} flex justify-center`}>
<LinkIcon width="24" />
</div>
</button>
<>
<button
className="h-10 w-10 rounded-md border border-muted"
onClick={() => setShowSidebar(true)}
disabled={syncingToChain}
>
<div className={`${syncingToChain ? "block" : "hidden"} relative top-1`}>
<Spinner thickness="2px" speed="0.65s" emptyColor="darkGray" color="gray" size="md" />
</div>
<div className={`${syncingToChain ? "hidden" : "block"} flex justify-center`}>
<LinkIcon width="24" />
</div>
</button>
<OnchainSidebar isOpen={showSidebar} onClose={() => setShowSidebar(false)} />
</>
);
};

Expand Down
10 changes: 6 additions & 4 deletions app/context/ceramicContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -234,11 +234,13 @@ export enum IsLoadingPassportState {
FailedToConnect,
}

export type ProviderState = {
providerSpec: ProviderSpec;
stamp?: Stamp;
};

export type AllProvidersState = {
[provider in PROVIDER_ID]?: {
providerSpec: ProviderSpec;
stamp?: Stamp;
};
[provider in PROVIDER_ID]?: ProviderState;
};

// Generate {<stampName>: {providerSpec, stamp}} for all stamps
Expand Down
Loading

0 comments on commit bbca98e

Please sign in to comment.