Skip to content

Commit

Permalink
Refactored getPassport to return a clear status, building types to al…
Browse files Browse the repository at this point in the history
…low for enums
  • Loading branch information
lucianHymer committed Jan 24, 2023
1 parent 4ce9bfc commit 947d386
Show file tree
Hide file tree
Showing 13 changed files with 195 additions and 171 deletions.
3 changes: 2 additions & 1 deletion app/__test-fixtures__/contextTestHelpers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -202,13 +202,14 @@ export const makeTestCeramicContext = (initialState?: Partial<CeramicContextStat
stamp: undefined,
},
},
ceramicErrors: { error: false },
passportLoadResponse: undefined,
handleAddStamp: jest.fn(),
handleAddStamps: jest.fn(),
handleCreatePassport: jest.fn(),
handleDeleteStamp: jest.fn(),
handleDeleteStamps: jest.fn(),
handleCheckRefreshPassport: () => Promise.resolve(true),
passportHasErrors: () => false,
...initialState,
};
};
Expand Down
8 changes: 5 additions & 3 deletions app/components/PlatformCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export const PlatformCard = ({
getUpdatedPlatforms,
}: PlatformCardProps): JSX.Element => {
// import all providers
const { allProvidersState, ceramicErrors, handleDeleteStamps } = useContext(CeramicContext);
const { allProvidersState, passportHasErrors, handleDeleteStamps } = useContext(CeramicContext);

// useDisclosure to control JSON modal
const {
Expand All @@ -57,6 +57,8 @@ export const PlatformCard = ({
onClose: onCloseRemoveStampModal,
} = useDisclosure();

const disabled = passportHasErrors();

// returns a single Platform card
return (
<div className="w-1/2 p-2 md:w-1/2 xl:w-1/4" key={`${platform.name}${i}`}>
Expand Down Expand Up @@ -93,7 +95,7 @@ export const PlatformCard = ({
{selectedProviders[platform.platform].length > 0 ? (
<>
<Menu>
<MenuButton disabled={ceramicErrors?.error} className="verify-btn flex" data-testid="card-menu-button">
<MenuButton disabled={disabled} className="verify-btn flex" data-testid="card-menu-button">
<div className="m-auto flex justify-center">
<svg
className="m-1 mr-2"
Expand Down Expand Up @@ -179,7 +181,7 @@ export const PlatformCard = ({
) : (
<button
className="verify-btn"
disabled={ceramicErrors?.error}
disabled={disabled}
ref={btnRef.current}
onClick={(e) => {
if (platform.enablePlatformCardUpdate) {
Expand Down
122 changes: 57 additions & 65 deletions app/context/ceramicContext.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { createContext, useContext, useEffect, useMemo, useState } from "react";
import { Passport, PassportWithErrors, PassportError, PLATFORM_ID, PROVIDER_ID, Stamp } from "@gitcoin/passport-types";
import {
Passport,
PassportLoadResponse,
PassportLoadStatus,
PLATFORM_ID,
PROVIDER_ID,
Stamp,
} from "@gitcoin/passport-types";
import { ProviderSpec, STAMP_PROVIDERS } from "../config/providers";
import { CeramicDatabase } from "@gitcoin/passport-database-client";
import { useViewerConnection } from "@self.id/framework";
Expand Down Expand Up @@ -45,7 +52,8 @@ export interface CeramicContextState {
handleDeleteStamps: (providerIds: PROVIDER_ID[]) => Promise<void>;
handleCheckRefreshPassport: () => Promise<boolean>;
userDid: string | undefined;
ceramicErrors: PassportError | undefined;
passportHasErrors: () => boolean;
passportLoadResponse?: PassportLoadResponse;
}

export const platforms = new Map<PLATFORM_ID, PlatformProps>();
Expand Down Expand Up @@ -441,8 +449,9 @@ const startingState: CeramicContextState = {
handleDeleteStamp: async (streamId: string) => {},
handleDeleteStamps: async () => {},
handleCheckRefreshPassport: async () => false,
passportHasErrors: () => false,
userDid: undefined,
ceramicErrors: undefined,
passportLoadResponse: undefined,
};

export const CeramicContext = createContext(startingState);
Expand All @@ -453,7 +462,7 @@ export const CeramicContextProvider = ({ children }: { children: any }) => {
const [isLoadingPassport, setIsLoadingPassport] = useState<IsLoadingPassportState>(IsLoadingPassportState.Loading);
const [passport, setPassport] = useState<Passport | undefined>(undefined);
const [userDid, setUserDid] = useState<string | undefined>();
const [ceramicErrors, setCeramicErrors] = useState<PassportError | undefined>();
const [passportLoadResponse, setPassportLoadResponse] = useState<PassportLoadResponse | undefined>();
const [viewerConnection] = useViewerConnection();

const { address } = useContext(UserContext);
Expand All @@ -464,7 +473,7 @@ export const CeramicContextProvider = ({ children }: { children: any }) => {
setCeramicDatabase(undefined);
setPassport(undefined);
setUserDid(undefined);
setCeramicErrors(undefined);
setPassportLoadResponse(undefined);
};
}, [address]);

Expand Down Expand Up @@ -502,63 +511,37 @@ export const CeramicContextProvider = ({ children }: { children: any }) => {
}, [ceramicDatabase]);

const fetchPassport = async (database: CeramicDatabase, skipLoadingState?: boolean): Promise<void> => {
let passportHasError = false;
let failedStamps: string[] = [];

if (!skipLoadingState) setIsLoadingPassport(IsLoadingPassportState.Loading);
// fetch, clean and set the new Passport state
const passportResponse = (await database.getPassport()) as PassportWithErrors | undefined | false;

// Ceramic error being thrown relies on the false response from getPassport. This is a temporary fix
let passport;
if (passportResponse === undefined || passportResponse === false) {
passport = passportResponse;
} else {
passport = passportResponse.passport;
}

if (passportResponse && passportResponse?.errors?.passport) {
const passportCacaoError = await database.checkPassportCACAOError();
if (passportCacaoError) {
datadogRum.addError("Passport CACAO error -- error thrown on initial fetch within getPassport", { address });
passportHasError = true;
}
}
if (passportResponse && passportResponse?.errors?.stamps) failedStamps = passportResponse.errors.stamps;
// fetch, clean and set the new Passport state
const { status, errorDetails, passport } = await database.getPassport();

if (passport) {
passport = cleanPassport(passport, database) as Passport;
hydrateAllProvidersState(passport);
setPassport(passport);
if (!skipLoadingState) setIsLoadingPassport(IsLoadingPassportState.Idle);
} else if (passportResponse === false) {
const passportCacaoError = await database.checkPassportCACAOError();
if (passportCacaoError) {
datadogRum.addError(
"Passport CACAO error -- undefined or null return while fetching getPassport from ceramic",
{ address }
);
passportHasError = true;
} else {
handleCreatePassport();
}
} else {
const passportCacaoError = await database.checkPassportCACAOError();
if (passportCacaoError) {
datadogRum.addError("Passport CACAO error -- checking before throwing IsLoadingPassportState.FailedToConnect", {
address,
});
passportHasError = true;
} else {
// something is wrong with Ceramic...
datadogRum.addError("Ceramic connection failed", { address });
setPassport(undefined);
if (!skipLoadingState) setIsLoadingPassport(IsLoadingPassportState.FailedToConnect);
}
switch (status) {
case (PassportLoadStatus.PassportError, PassportLoadStatus.DoesNotExist):
const passportCacaoError = await database.checkPassportCACAOError();
if (passportCacaoError) {
datadogRum.addError("Passport CACAO error -- error thrown on initial fetch", { address });
} else {
if (status === PassportLoadStatus.DoesNotExist) handleCreatePassport();
else {
// something is wrong with Ceramic...
datadogRum.addError("Ceramic connection failed", { address });
setPassport(undefined);
if (!skipLoadingState) setIsLoadingPassport(IsLoadingPassportState.FailedToConnect);
}
}
break;
case PassportLoadStatus.PassportStampError:
// Handled in the refresh
break;
case PassportLoadStatus.Success:
default:
const cleanedPassport = cleanPassport(passport, database) as Passport;
hydrateAllProvidersState(cleanedPassport);
setPassport(cleanedPassport);
}
const error = passportHasError || failedStamps.length > 0;

setCeramicErrors({ passport: passportHasError, stamps: failedStamps, error });
setPassportLoadResponse({ passport, status, errorDetails });
};

const cleanPassport = (
Expand All @@ -585,22 +568,22 @@ export const CeramicContextProvider = ({ children }: { children: any }) => {

const handleCheckRefreshPassport = async (): Promise<boolean> => {
let success = true;
if (ceramicDatabase && ceramicErrors) {
let passportHasError = ceramicErrors.passport;
let failedStamps = ceramicErrors.stamps || [];
if (ceramicDatabase && passportLoadResponse) {
let passportHasError = passportLoadResponse.status === PassportLoadStatus.PassportError;
let failedStamps = passportLoadResponse.errorDetails?.stampStreamIds || [];
try {
if (ceramicErrors.passport) {
if (passportHasError) {
passportHasError = !(await ceramicDatabase.refreshPassport());
}

if (ceramicErrors.stamps && ceramicErrors.stamps.length) {
if (failedStamps && failedStamps.length) {
try {
await ceramicDatabase.deleteStampIDs(ceramicErrors.stamps);
await ceramicDatabase.deleteStampIDs(failedStamps);
failedStamps = [];
} catch {}
}

// fetchpassport to reset passport state
// fetchPassport to reset passport state
await fetchPassport(ceramicDatabase);

success = !passportHasError && !failedStamps.length;
Expand Down Expand Up @@ -677,6 +660,14 @@ export const CeramicContextProvider = ({ children }: { children: any }) => {
setAllProviderState(startingAllProvidersState);
};

const passportHasErrors = (): boolean => {
if (passportLoadResponse)
return [PassportLoadStatus.PassportError || PassportLoadStatus.PassportStampError].includes(
passportLoadResponse.status
);
else return false;
};

const stateMemo = useMemo(
() => ({
passport,
Expand Down Expand Up @@ -705,7 +696,8 @@ export const CeramicContextProvider = ({ children }: { children: any }) => {
handleDeleteStamp,
handleCheckRefreshPassport,
userDid,
ceramicErrors,
passportLoadResponse,
passportHasErrors,
};

return <CeramicContext.Provider value={providerProps}>{children}</CeramicContext.Provider>;
Expand Down
4 changes: 2 additions & 2 deletions app/pages/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { getExpiredStamps } from "../utils/helpers";
import { RefreshStampModal } from "../components/RefreshStampModal";

export default function Dashboard() {
const { passport, isLoadingPassport, ceramicErrors } = useContext(CeramicContext);
const { passport, isLoadingPassport, passportHasErrors } = useContext(CeramicContext);
const { wallet, toggleConnection, handleDisconnection } = useContext(UserContext);

const { isOpen, onOpen, onClose } = useDisclosure();
Expand Down Expand Up @@ -129,7 +129,7 @@ export default function Dashboard() {
</div>
)}

{ceramicErrors && ceramicErrors.error && (
{passportHasErrors() && (
<Banner>
<div className="flex w-full justify-center">
We have detected some broken stamps in your passport. Your passport is currently locked because of this. We
Expand Down
24 changes: 13 additions & 11 deletions database-client/__tests__/CeramicDatabase.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { CeramicClient } from "@ceramicnetwork/http-client";
import { PassportWithErrors } from "./../../types/src/index.d";
import { PassportLoadStatus } from "@gitcoin/passport-types";
import { Passport, VerifiableCredential, Stamp, PROVIDER_ID } from "@gitcoin/passport-types";
import { DID } from "dids";
import { Ed25519Provider } from "key-did-provider-ed25519";
Expand Down Expand Up @@ -42,10 +41,11 @@ describe("Verify Ceramic Database", () => {
return null;
});

const passport = await ceramicDatabase.getPassport();
const { passport, status } = await ceramicDatabase.getPassport();

// We do not expect to have any passport, hence `false` should be returned
expect(passport).toEqual(false);
expect(status).toEqual(PassportLoadStatus.DoesNotExist);
expect(passport).toEqual(undefined);
expect(spyStoreGet).toBeCalledTimes(1);
expect(spyStoreGet).toBeCalledWith("Passport");
});
Expand All @@ -55,10 +55,11 @@ describe("Verify Ceramic Database", () => {
return {};
});

const passport = await ceramicDatabase.getPassport();
const { passport, status } = await ceramicDatabase.getPassport();

// We do not expect to have any passport, hence `false` should be returned
expect(passport).toEqual(false);
expect(status).toEqual(PassportLoadStatus.DoesNotExist);
expect(passport).toEqual(undefined);
expect(spyStoreGet).toBeCalledTimes(1);
expect(spyStoreGet).toBeCalledWith("Passport");
});
Expand Down Expand Up @@ -114,7 +115,7 @@ describe("Verify Ceramic Database", () => {
return;
});

const passport = (await ceramicDatabase.getPassport()) as PassportWithErrors;
const passport = await ceramicDatabase.getPassport();

// We do not expect to have any passport, hence `false` should be returned
expect(spyStoreGet).toBeCalledTimes(1);
Expand Down Expand Up @@ -212,7 +213,7 @@ describe("Verify Ceramic Database", () => {
throw "Error loading stamp!";
});

const passport = (await ceramicDatabase.getPassport()) as PassportWithErrors;
const passport = await ceramicDatabase.getPassport();

// We do not expect to have any passport, hence `false` should be returned
expect(spyStoreGet).toBeCalledTimes(1);
Expand Down Expand Up @@ -257,7 +258,7 @@ describe("Verify Ceramic Database", () => {
};
});

const passport = (await ceramicDatabase.getPassport()) as PassportWithErrors;
const passport = await ceramicDatabase.getPassport();

expect(spyAxiosGet).toBeCalledTimes(3);

Expand All @@ -274,10 +275,11 @@ describe("Verify Ceramic Database", () => {
},
]);

expect(passport.errors?.error).toBe(true);
expect(passport.errors?.stamps).toEqual(["ceramic://credential-1"]);
expect(passport.status).toBe(PassportLoadStatus.PassportStampError);
expect(passport.errorDetails?.stampStreamIds).toEqual(["ceramic://credential-1"]);
});
});

it("checkPassportCACAOError should indicate if a passport stream is throwing a CACAO error", async () => {
jest
.spyOn(ceramicDatabase.store, "getRecordID")
Expand Down
8 changes: 4 additions & 4 deletions database-client/integration-tests/ceramicDatabaseTest.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { PassportWithErrors } from "./../../types/src/index.d";
// import { PassportWithErrors } from "./../../types/src/index.d";
import { Passport, VerifiableCredential, Stamp, PROVIDER_ID } from "@gitcoin/passport-types";
import { DID } from "dids";
import { Ed25519Provider } from "key-did-provider-ed25519";
Expand Down Expand Up @@ -83,7 +83,7 @@ describe("when there is an existing passport without stamps for the given did",
});

it("getPassport retrieves the passport from ceramic", async () => {
const { passport } = (await ceramicDatabase.getPassport()) as PassportWithErrors;
const { passport } = await ceramicDatabase.getPassport();

expect(passport).toBeDefined();
expect(passport).toEqual(existingPassport);
Expand Down Expand Up @@ -210,7 +210,7 @@ describe("when there is an existing passport with stamps for the given did", ()
});

it("getPassport retrieves the passport and stamps from ceramic", async () => {
const actualPassport = (await ceramicDatabase.getPassport()) as PassportWithErrors;
const actualPassport = await ceramicDatabase.getPassport();

const formattedDate = new Date(actualPassport.passport["issuanceDate"]);

Expand Down Expand Up @@ -489,7 +489,7 @@ describe("when loading a stamp from a passport fails", () => {
});

it("ignores the failed stamp and only returns the successfully loaded stamps", async () => {
const passport = (await ceramicDatabase.getPassport()) as PassportWithErrors;
const passport = await ceramicDatabase.getPassport();

// We only expect 2 results: Ens and Google stamps
expect(passport.passport.stamps.length).toEqual(2);
Expand Down
Loading

0 comments on commit 947d386

Please sign in to comment.