Skip to content

Commit

Permalink
feat(app): when attempting to re-claim a valid credential that is now…
Browse files Browse the repository at this point in the history
… unclaimable, leave the existing credential in place (passportxyz#2274)

* feat(app): when attempting to re-claim a valid credential that is now unclaimable, leave the existing credential in place

* test(app): added mock check to test
  • Loading branch information
lucianHymer authored Mar 11, 2024
1 parent 28df451 commit bb4cd1f
Show file tree
Hide file tree
Showing 8 changed files with 115 additions and 25 deletions.
4 changes: 2 additions & 2 deletions app/__test-fixtures__/verifiableCredentialResults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ export const SUCCESFUL_ENS_RESULTS: CredentialResponseBody = {
};

export const UN_SUCCESSFUL_ENS_RESULT: CredentialResponseBody = {
record: { type: "Ens", version: "0.0.0", ens: "jimmyjim.eth" },
credential,
code: 403,
error: "You can't claim this stamp",
};

export const SUCCESFUL_POAP_RESULT: VerifiableCredentialRecord = {
Expand Down
66 changes: 63 additions & 3 deletions app/__tests__/components/GenericPlatform.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { closeAllToasts } from "../../__test-fixtures__/toastTestHelpers";
import { PlatformScoreSpec } from "../../context/scorerContext";
import { getPlatformSpec } from "../../config/platforms";
import { PlatformSpec } from "@gitcoin/passport-platforms";
import { PROVIDER_ID } from "@gitcoin/passport-types";

jest.mock("@didtools/cacao", () => ({
Cacao: {
Expand All @@ -32,9 +33,13 @@ jest.mock("../../utils/helpers.tsx", () => ({
createSignedPayload: jest.fn(),
generateUID: jest.fn(),
getProviderSpec: jest.fn(),
difference: (setA: any, setB: any) => ({
size: 1,
}),
difference: (setA: any, setB: any) => {
const _difference = new Set(setA);
setB.forEach((elem: any) => {
_difference.delete(elem);
});
return _difference;
},
}));

jest.mock("next/router", () => ({
Expand Down Expand Up @@ -162,6 +167,61 @@ describe("when user has not verified with EnsProvider", () => {
});
});

describe("when user has previously verified with EnsProvider", () => {
beforeEach(async () => {
await closeAllToasts();
(fetchVerifiableCredential as jest.Mock).mockResolvedValue({
credentials: [UN_SUCCESSFUL_ENS_RESULT],
});
});

it("should show re-verified toast when credential is selected but no longer able to be re-claimed", async () => {
const extraProvider = "FakeExtraProviderRequiredForCanSubmitLogic" as PROVIDER_ID;
const drawer = () => (
<ChakraProvider>
<Drawer isOpen={true} placement="right" size="sm" onClose={() => {}}>
<DrawerOverlay />
<GenericPlatform
platform={new Ens.EnsPlatform()}
platFormGroupSpec={[
{
...Ens.ProviderConfig[0],
providers: [...Ens.ProviderConfig[0].providers, { title: "Extra", name: extraProvider }],
},
]}
platformScoreSpec={EnsScoreSpec}
onClose={() => {}}
/>
</Drawer>
</ChakraProvider>
);

const handlePatchStampsMock = jest.fn();
renderWithContext(
{
...mockCeramicContext,
verifiedProviderIds: ["Ens"],
handlePatchStamps: handlePatchStampsMock,
},
drawer()
);

const firstSwitch = screen.queryByTestId("select-all");
fireEvent.click(firstSwitch as HTMLElement);
const initialVerifyButton = screen.queryByTestId("button-verify-Ens");
fireEvent.click(initialVerifyButton as HTMLElement);

// Wait to see the done toast
await waitFor(() => {
// Empty b/c don't qualify for any stamps but also don't want to delete any stamps
expect(handlePatchStampsMock).toHaveBeenCalledWith([]);

expect(screen.getByText("Successfully re-verified Ens data point.")).toBeInTheDocument();
expect(fetchVerifiableCredential).toHaveBeenCalled();
});
});
});

describe("Mulitple EVM plaftorms", () => {
it("Should show no stamp modal if the platform isEVM and no stamps were found", async () => {
(fetchVerifiableCredential as jest.Mock).mockResolvedValue({
Expand Down
31 changes: 24 additions & 7 deletions app/components/GenericPlatform.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import { datadogLogs } from "@datadog/browser-logs";

// --- Identity tools
import {
VerifiableCredential,
CredentialResponseBody,
PROVIDER_ID,
PLATFORM_ID,
StampPatch,
ValidResponseBody,
} from "@gitcoin/passport-types";
import { fetchVerifiableCredential } from "@gitcoin/passport-identity";

Expand Down Expand Up @@ -198,16 +198,23 @@ export const GenericPlatform = ({

const verifiedCredentials =
selectedProviders.length > 0
? verifyCredentialsResponse.credentials?.filter((cred: any) => !cred.error) || []
? verifyCredentialsResponse.credentials?.filter((cred: any): cred is ValidResponseBody => !cred.error) || []
: [];

setVerificationResponse(verifyCredentialsResponse.credentials || []);

const stampPatches: StampPatch[] = platformProviderIds.map((provider: PROVIDER_ID) => {
const cred = verifiedCredentials.find((cred: any) => cred.record?.type === provider);
if (cred) return { provider, credential: cred.credential as VerifiableCredential };
else return { provider };
});
// If the stamp was selected and can be claimed, return the {provider, credential} to add the stamp
// If the stamp was not selected, return {provider} to delete the stamp
// If the stamp was selected but cannot be claimed, return null to do nothing and
// therefore keep any existing valid stamp if it exists
const stampPatches: StampPatch[] = platformProviderIds
.map((provider: PROVIDER_ID) => {
const cred = verifiedCredentials.find((cred: any) => cred.record?.type === provider);
if (cred) return { provider, credential: cred.credential };
else if (!selectedProviders.includes(provider)) return { provider };
else return null;
})
.filter((patch): patch is StampPatch => Boolean(patch));

await handlePatchStamps(stampPatches);

Expand All @@ -217,6 +224,16 @@ export const GenericPlatform = ({
(providerId: any) =>
!!stampPatches.find((stampPatch) => stampPatch?.credential?.credentialSubject?.provider === providerId)
);

// `verifiedProviders` still holds the previously verified providers. If the user
// can no longer claim the credential, but they still have a valid credential that
// was previously verified, AND they had selected it, we want to keep it
verifiedProviders.forEach((provider) => {
if (!actualVerifiedProviders.includes(provider) && selectedProviders.includes(provider)) {
actualVerifiedProviders.push(provider);
}
});

// both verified and selected should look the same after save
setVerifiedProviders([...actualVerifiedProviders]);
setSelectedProviders([...actualVerifiedProviders]);
Expand Down
5 changes: 3 additions & 2 deletions app/context/stampClaimingContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import React, { createContext, useContext, useState } from "react";
import { datadogLogs } from "@datadog/browser-logs";

// --- Identity tools
import { VerifiableCredential, PROVIDER_ID, PLATFORM_ID, StampPatch } from "@gitcoin/passport-types";
import { VerifiableCredential, PROVIDER_ID, PLATFORM_ID, StampPatch, ValidResponseBody } from "@gitcoin/passport-types";
import { Platform, ProviderPayload } from "@gitcoin/passport-platforms";
import { fetchVerifiableCredential } from "@gitcoin/passport-identity";

Expand Down Expand Up @@ -204,7 +204,8 @@ export const StampClaimingContextProvider = ({ children }: { children: any }) =>

const verifiedCredentials =
selectedProviders.length > 0
? verifyCredentialsResponse.credentials?.filter((cred: any) => !cred.error) || []
? verifyCredentialsResponse.credentials?.filter((cred: any): cred is ValidResponseBody => !cred.error) ||
[]
: [];

const stampPatches: StampPatch[] = selectedProviders.map((provider: PROVIDER_ID) => {
Expand Down
15 changes: 9 additions & 6 deletions app/utils/helpers.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
// import React from "react";

// --- Types
import { PLATFORM_ID } from "@gitcoin/passport-types";
import { CredentialResponseBody, PROVIDER_ID, VerifiableCredential } from "@gitcoin/passport-types";
import {
PLATFORM_ID,
ValidResponseBody,
CredentialResponseBody,
PROVIDER_ID,
VerifiableCredential,
} from "@gitcoin/passport-types";
import axios, { AxiosResponse } from "axios";
import { ProviderSpec, STAMP_PROVIDERS } from "../config/providers";
import { datadogRum } from "@datadog/browser-rum";
Expand Down Expand Up @@ -32,10 +37,8 @@ export function generateUID(length: number) {
export function reduceStampResponse(providerIDs: PROVIDER_ID[], verifiedCredentials?: CredentialResponseBody[]) {
if (!verifiedCredentials) return [];
return verifiedCredentials
.filter(
(credential) =>
!credential.error && providerIDs.find((providerId: PROVIDER_ID) => credential?.record?.type === providerId)
)
.filter((credential): credential is ValidResponseBody => !("error" in credential && credential.error))
.filter((credential) => providerIDs.find((providerId: PROVIDER_ID) => credential?.record?.type === providerId))
.map((credential) => ({
provider: credential.record?.type as PROVIDER_ID,
credential: credential.credential as VerifiableCredential,
Expand Down
3 changes: 2 additions & 1 deletion iam/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,8 @@ app.post("/api/v0.0.0/verify", (req: Request, res: Response): void => {

if (singleType) {
const response = responses[0];
if (response.code && response.error) return errorRes(res, response.error, response.code);
if ("error" in response && response.code && response.error)
return errorRes(res, response.error, response.code);
else return res.json(response);
} else {
return res.json(responses);
Expand Down
14 changes: 11 additions & 3 deletions identity/src/credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
IssuedChallenge,
CredentialResponseBody,
SignatureType,
ValidResponseBody,
} from "@gitcoin/passport-types";

// --- Node/Browser http req library
Expand Down Expand Up @@ -315,9 +316,16 @@ export const fetchChallengeCredential = async (iamUrl: string, payload: RequestP
}
);

return {
challenge: response.data.credential,
} as IssuedChallenge;
const data = response.data;

if ("error" in data && data.error) {
console.error("Error fetching challenge credential", data.error);
throw new Error("Unable to fetch challenge credential");
} else {
return {
challenge: (data as ValidResponseBody).credential,
} as IssuedChallenge;
}
};

// Fetch a verifiableCredential
Expand Down
2 changes: 1 addition & 1 deletion types/src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ export type ErrorResponseBody = {
error?: string;
code?: number;
};
export type CredentialResponseBody = ValidResponseBody & ErrorResponseBody;
export type CredentialResponseBody = ValidResponseBody | ErrorResponseBody;

// Issued Credential response
export type IssuedChallenge = {
Expand Down

0 comments on commit bb4cd1f

Please sign in to comment.