diff --git a/packages/app/src/background/cryptKeeper.ts b/packages/app/src/background/cryptKeeper.ts index 45b6641f7..8bef5c06b 100644 --- a/packages/app/src/background/cryptKeeper.ts +++ b/packages/app/src/background/cryptKeeper.ts @@ -233,6 +233,11 @@ export default class CryptKeeperController { this.lockService.ensure, this.verifiableCredentialsService.generateVerifiablePresentation, ); + this.handler.add( + RPCAction.GENERATE_VERIFIABLE_PRESENTATION_WITH_CRYPTKEEPER, + this.lockService.ensure, + this.verifiableCredentialsService.generateVerifiablePresentationWithCryptkeeper, + ); this.handler.add( RPCAction.GENERATE_VERIFIABLE_PRESENTATION_REQUEST, this.lockService.ensure, diff --git a/packages/app/src/background/services/credentials/__tests__/credentials.test.ts b/packages/app/src/background/services/credentials/__tests__/credentials.test.ts index 9bb4b16cd..941207237 100644 --- a/packages/app/src/background/services/credentials/__tests__/credentials.test.ts +++ b/packages/app/src/background/services/credentials/__tests__/credentials.test.ts @@ -1,4 +1,6 @@ -import { IVerifiableCredential } from "@cryptkeeperzk/types"; +/* eslint-disable @typescript-eslint/unbound-method */ +import { EventName } from "@cryptkeeperzk/providers"; +import browser from "webextension-polyfill"; import VerifiableCredentialsService from "@src/background/services/credentials"; import { @@ -7,7 +9,14 @@ import { serializeVerifiableCredential, } from "@src/background/services/credentials/utils"; import SimpleStorage from "@src/background/services/storage"; -import { ICryptkeeperVerifiableCredential } from "@src/types"; +import pushMessage from "@src/util/pushMessage"; + +import type { + IVerifiablePresentation, + IVerifiableCredential, + IVerifiablePresentationRequest, +} from "@cryptkeeperzk/types"; +import type { ICryptkeeperVerifiableCredential } from "@src/types"; jest.mock("@src/background/services/crypto", (): unknown => ({ ...jest.requireActual("@src/background/services/crypto"), @@ -19,6 +28,15 @@ jest.mock("@src/background/services/crypto", (): unknown => ({ })), })); +const exampleSignature = "ck-signature"; +jest.mock("@src/background/services/wallet", (): unknown => ({ + getInstance: jest.fn(() => ({ + signMessage: jest.fn(() => Promise.resolve(exampleSignature)), + })), +})); + +jest.mock("@src/util/pushMessage"); + jest.mock("@src/background/services/storage"); interface MockStorage { @@ -86,9 +104,28 @@ describe("background/services/credentials", () => { credentialsMap.set(exampleCredentialHashTwo, exampleCryptkeeperCredentialStringTwo); const credentialsStorageString = JSON.stringify(Array.from(credentialsMap)); + const exampleVerifiablePresentationRequest: IVerifiablePresentationRequest = { + request: "example request", + }; + const exampleVerifiablePresentation: IVerifiablePresentation = { + context: ["https://www.w3.org/2018/credentials/v1"], + type: ["VerifiablePresentation"], + verifiableCredential: [exampleCredential], + }; + + const defaultTabs = [{ id: 1 }]; + + const defaultPopupTab = { id: 1, active: true, highlighted: true }; + const verifiableCredentialsService = VerifiableCredentialsService.getInstance(); beforeEach(() => { + (browser.tabs.create as jest.Mock).mockResolvedValue(defaultPopupTab); + + (browser.tabs.query as jest.Mock).mockResolvedValue(defaultTabs); + + (browser.tabs.sendMessage as jest.Mock).mockRejectedValueOnce(false).mockResolvedValue(true); + (SimpleStorage as jest.Mock).mock.instances.forEach((instance: MockStorage) => { instance.get.mockReturnValue(credentialsStorageString); instance.set.mockReturnValue(undefined); @@ -102,6 +139,95 @@ describe("background/services/credentials", () => { instance.set.mockClear(); instance.clear.mockClear(); }); + + (pushMessage as jest.Mock).mockClear(); + + (browser.tabs.sendMessage as jest.Mock).mockClear(); + }); + + describe("add and reject verifiable credential requests", () => { + test("should successfully create an add verifiable credential request", async () => { + await verifiableCredentialsService.addVerifiableCredentialRequest(exampleCredentialString); + + expect(browser.tabs.query).toBeCalledWith({ lastFocusedWindow: true }); + + const defaultOptions = { + tabId: defaultPopupTab.id, + type: "popup", + focused: true, + width: 385, + height: 610, + }; + + expect(browser.windows.create).toBeCalledWith(defaultOptions); + }); + + test("should successfully reject a verifiable credential request", async () => { + await verifiableCredentialsService.rejectVerifiableCredentialRequest(); + + expect(browser.tabs.query).toBeCalledWith({ lastFocusedWindow: true }); + expect(browser.tabs.sendMessage).toBeCalledWith(defaultTabs[0].id, { + type: EventName.REJECT_VERIFIABLE_CREDENTIAL, + }); + }); + }); + + describe("generate verifiable presentations", () => { + test("should successfully create a generate verifiable presentation request", async () => { + await verifiableCredentialsService.generateVerifiablePresentationRequest(exampleVerifiablePresentationRequest); + + expect(browser.tabs.query).toBeCalledWith({ lastFocusedWindow: true }); + + const defaultOptions = { + tabId: defaultPopupTab.id, + type: "popup", + focused: true, + width: 385, + height: 610, + }; + + expect(browser.windows.create).toBeCalledWith(defaultOptions); + }); + + test("should successfully generate a verifiable presentation", async () => { + await verifiableCredentialsService.generateVerifiablePresentation(exampleVerifiablePresentation); + + expect(browser.tabs.query).toBeCalledWith({ lastFocusedWindow: true }); + expect(browser.tabs.sendMessage).toBeCalledWith(defaultTabs[0].id, { + type: EventName.GENERATE_VERIFIABLE_PRESENTATION, + payload: { verifiablePresentation: exampleVerifiablePresentation }, + }); + }); + + test("should successfully generate a verifiable presentation with cryptkeeper", async () => { + const exampleAddress = "0x123"; + const ETHEREUM_SIGNATURE_SPECIFICATION_TYPE = "EthereumEip712Signature2021"; + const VERIFIABLE_CREDENTIAL_PROOF_PURPOSE = "assertionMethod"; + + await verifiableCredentialsService.generateVerifiablePresentationWithCryptkeeper({ + verifiablePresentation: exampleVerifiablePresentation, + address: exampleAddress, + }); + + const signedVerifiablePresentation = { + ...exampleVerifiablePresentation, + proof: [ + { + type: [ETHEREUM_SIGNATURE_SPECIFICATION_TYPE], + proofPurpose: VERIFIABLE_CREDENTIAL_PROOF_PURPOSE, + verificationMethod: exampleAddress, + created: new Date(), + proofValue: exampleSignature, + }, + ], + }; + + expect(browser.tabs.query).toBeCalledWith({ lastFocusedWindow: true }); + expect(browser.tabs.sendMessage).toBeCalledWith(defaultTabs[0].id, { + type: EventName.GENERATE_VERIFIABLE_PRESENTATION, + payload: { verifiablePresentation: signedVerifiablePresentation }, + }); + }); }); describe("add and retrieve verifiable credentials", () => { diff --git a/packages/app/src/background/services/credentials/index.ts b/packages/app/src/background/services/credentials/index.ts index 559044c78..f93a2690b 100644 --- a/packages/app/src/background/services/credentials/index.ts +++ b/packages/app/src/background/services/credentials/index.ts @@ -6,9 +6,13 @@ import CryptoService, { ECryptMode } from "@src/background/services/crypto"; import HistoryService from "@src/background/services/history"; import NotificationService from "@src/background/services/notification"; import SimpleStorage from "@src/background/services/storage"; +import WalletService from "@src/background/services/wallet"; import { Paths } from "@src/constants"; import { OperationType, IRenameVerifiableCredentialArgs, ICryptkeeperVerifiableCredential } from "@src/types"; -import { IAddVerifiableCredentialArgs } from "@src/types/verifiableCredentials"; +import { + IAddVerifiableCredentialArgs, + IGenerateVerifiablePresentationWithCryptkeeperArgs, +} from "@src/types/verifiableCredentials"; import type { IVerifiablePresentation, IVerifiablePresentationRequest } from "@cryptkeeperzk/types"; import type { BackupData, IBackupable } from "@src/background/services/backup"; @@ -19,9 +23,12 @@ import { deserializeVerifiableCredential, deserializeCryptkeeperVerifiableCredential, validateSerializedVerifiableCredential, + serializeVerifiablePresentation, } from "./utils"; const VERIFIABLE_CREDENTIALS_KEY = "@@VERIFIABLE-CREDENTIALS@@"; +const ETHEREUM_SIGNATURE_SPECIFICATION_TYPE = "EthereumEip712Signature2021"; +const VERIFIABLE_CREDENTIAL_PROOF_PURPOSE = "assertionMethod"; export default class VerifiableCredentialsService implements IBackupable { private static INSTANCE?: VerifiableCredentialsService; @@ -30,6 +37,8 @@ export default class VerifiableCredentialsService implements IBackupable { private cryptoService: CryptoService; + private walletService: WalletService; + private historyService: HistoryService; private notificationService: NotificationService; @@ -39,6 +48,7 @@ export default class VerifiableCredentialsService implements IBackupable { private constructor() { this.verifiableCredentialsStore = new SimpleStorage(VERIFIABLE_CREDENTIALS_KEY); this.cryptoService = CryptoService.getInstance(); + this.walletService = WalletService.getInstance(); this.historyService = HistoryService.getInstance(); this.notificationService = NotificationService.getInstance(); this.browserController = BrowserUtils.getInstance(); @@ -232,6 +242,50 @@ export default class VerifiableCredentialsService implements IBackupable { ); }; + generateVerifiablePresentationWithCryptkeeper = async ({ + verifiablePresentation, + address, + }: IGenerateVerifiablePresentationWithCryptkeeperArgs): Promise => { + const serializedVerifiablePresentation = serializeVerifiablePresentation(verifiablePresentation); + const signature = await this.walletService.signMessage({ + message: serializedVerifiablePresentation, + address, + }); + const signedVerifiablePresentation = { + ...verifiablePresentation, + proof: [ + { + type: [ETHEREUM_SIGNATURE_SPECIFICATION_TYPE], + proofPurpose: VERIFIABLE_CREDENTIAL_PROOF_PURPOSE, + verificationMethod: address, + created: new Date(), + proofValue: signature, + }, + ], + }; + + await this.historyService.trackOperation(OperationType.GENERATE_VERIFIABLE_PRESENTATION, {}); + await this.notificationService.create({ + options: { + title: "Verifiable Presentation generated.", + message: `Generated 1 Verifiable Presentation.`, + iconUrl: browser.runtime.getURL("/icons/logo.png"), + type: "basic", + }, + }); + const tabs = await browser.tabs.query({ active: true }); + await Promise.all( + tabs.map((tab) => + browser.tabs + .sendMessage(tab.id!, { + type: EventName.GENERATE_VERIFIABLE_PRESENTATION, + payload: { verifiablePresentation: signedVerifiablePresentation }, + }) + .catch(() => undefined), + ), + ); + }; + rejectVerifiablePresentationRequest = async (): Promise => { await this.historyService.trackOperation(OperationType.REJECT_VERIFIABLE_PRESENTATION_REQUEST, {}); await this.notificationService.create({ diff --git a/packages/app/src/types/index.ts b/packages/app/src/types/index.ts index 748c2671a..984eaa568 100644 --- a/packages/app/src/types/index.ts +++ b/packages/app/src/types/index.ts @@ -6,9 +6,10 @@ export { ConnectorNames, type IUseWalletData } from "./hooks"; export { type ISecretArgs, type ICheckPasswordArgs } from "./lock"; export { InitializationStep } from "./misc"; export { type DeferredPromise } from "./utility"; -export { - type IVerifiableCredentialMetadata, - type ICryptkeeperVerifiableCredential, - type IRenameVerifiableCredentialArgs, +export type { + IVerifiableCredentialMetadata, + ICryptkeeperVerifiableCredential, + IRenameVerifiableCredentialArgs, + IGenerateVerifiablePresentationWithCryptkeeperArgs, } from "./verifiableCredentials"; export { type ISignMessageArgs, type ICheckMnemonicArgs } from "./wallet"; diff --git a/packages/app/src/types/verifiableCredentials/index.ts b/packages/app/src/types/verifiableCredentials/index.ts index 6aa89ce4d..700f356c0 100644 --- a/packages/app/src/types/verifiableCredentials/index.ts +++ b/packages/app/src/types/verifiableCredentials/index.ts @@ -1,4 +1,4 @@ -import { IVerifiableCredential } from "@cryptkeeperzk/types"; +import type { IVerifiableCredential, IVerifiablePresentation } from "@cryptkeeperzk/types"; export interface IVerifiableCredentialMetadata { name: string; @@ -19,3 +19,8 @@ export interface IRenameVerifiableCredentialArgs { verifiableCredentialHash: string; newVerifiableCredentialName: string; } + +export interface IGenerateVerifiablePresentationWithCryptkeeperArgs { + verifiablePresentation: IVerifiablePresentation; + address: string; +} diff --git a/packages/app/src/ui/ducks/__tests__/verifiableCredentials.test.tsx b/packages/app/src/ui/ducks/__tests__/verifiableCredentials.test.tsx index 7770fc66c..63096017c 100644 --- a/packages/app/src/ui/ducks/__tests__/verifiableCredentials.test.tsx +++ b/packages/app/src/ui/ducks/__tests__/verifiableCredentials.test.tsx @@ -17,47 +17,52 @@ import { deleteVerifiableCredential, useVerifiableCredentials, fetchVerifiableCredentials, + generateVerifiablePresentation, + generateVerifiablePresentationWithCryptkeeper, + rejectVerifiablePresentationRequest, } from "../verifiableCredentials"; jest.unmock("@src/ui/ducks/hooks"); describe("ui/ducks/verifiableCredentials", () => { + const mockVerifiableCredentialOne = { + context: ["https://www.w3.org/2018/credentials/v1"], + id: "http://example.edu/credentials/3732", + type: ["VerifiableCredential"], + issuer: "did:example:123", + issuanceDate: new Date("2020-03-10T04:24:12.164Z"), + credentialSubject: { + id: "did:example:456", + claims: { + type: "BachelorDegree", + name: "Bachelor of Science and Arts", + }, + }, + }; + const mockVerifiableCredentialTwo = { + context: ["https://www.w3.org/2018/credentials/v1"], + id: "http://example.edu/credentials/3733", + type: ["VerifiableCredential"], + issuer: "did:example:12345", + issuanceDate: new Date("2020-03-10T04:24:12.164Z"), + credentialSubject: { + id: "did:example:123", + claims: { + type: "BachelorDegree", + name: "Bachelor of Science and Arts", + }, + }, + }; const mockCryptkeeperVerifiableCredentials = [ { - verifiableCredential: { - context: ["https://www.w3.org/2018/credentials/v1"], - id: "http://example.edu/credentials/3732", - type: ["VerifiableCredential"], - issuer: "did:example:123", - issuanceDate: new Date("2020-03-10T04:24:12.164Z"), - credentialSubject: { - id: "did:example:456", - claims: { - type: "BachelorDegree", - name: "Bachelor of Science and Arts", - }, - }, - }, + verifiableCredential: mockVerifiableCredentialOne, metadata: { hash: "0x123", name: "Credential #0", }, }, { - verifiableCredential: { - context: ["https://www.w3.org/2018/credentials/v1"], - id: "http://example.edu/credentials/3733", - type: ["VerifiableCredential"], - issuer: "did:example:12345", - issuanceDate: new Date("2020-03-10T04:24:12.164Z"), - credentialSubject: { - id: "did:example:123", - claims: { - type: "BachelorDegree", - name: "Bachelor of Science and Arts", - }, - }, - }, + verifiableCredential: mockVerifiableCredentialTwo, metadata: { hash: "0x1234", name: "Credential #1", @@ -65,6 +70,12 @@ describe("ui/ducks/verifiableCredentials", () => { }, ]; + const mockVerifiablePresentation = { + context: ["https://www.w3.org/2018/credentials/v1"], + type: ["VerifiablePresentation"], + verifiableCredential: [mockVerifiableCredentialOne, mockVerifiableCredentialTwo], + }; + const mockSerializedVerifiableCredentials = [ serializeCryptkeeperVerifiableCredential(mockCryptkeeperVerifiableCredentials[0]), serializeCryptkeeperVerifiableCredential(mockCryptkeeperVerifiableCredentials[1]), @@ -104,6 +115,15 @@ describe("ui/ducks/verifiableCredentials", () => { }); }); + test("should reject verifiable credential request properly", async () => { + await Promise.resolve(store.dispatch(rejectVerifiableCredentialRequest())); + + expect(postMessage).toBeCalledTimes(1); + expect(postMessage).toBeCalledWith({ + method: RPCAction.REJECT_VERIFIABLE_CREDENTIAL_REQUEST, + }); + }); + test("should rename verifiable credential properly", async () => { const mockPayload = { verifiableCredentialHash: "hash", newVerifiableCredentialName: "name" }; @@ -128,12 +148,40 @@ describe("ui/ducks/verifiableCredentials", () => { }); }); - test("should reject verifiable credential request properly", async () => { - await Promise.resolve(store.dispatch(rejectVerifiableCredentialRequest())); + test("should generate verfifiable presentation properly", async () => { + await Promise.resolve(store.dispatch(generateVerifiablePresentation(mockVerifiablePresentation))); expect(postMessage).toBeCalledTimes(1); expect(postMessage).toBeCalledWith({ - method: RPCAction.REJECT_VERIFIABLE_CREDENTIAL_REQUEST, + method: RPCAction.GENERATE_VERIFIABLE_PRESENTATION, + payload: mockVerifiablePresentation, + }); + }); + + test("should generate verifiable presentation with cryptkeeper properly", async () => { + const mockAddress = "0x123"; + await Promise.resolve( + store.dispatch( + generateVerifiablePresentationWithCryptkeeper({ + verifiablePresentation: mockVerifiablePresentation, + address: mockAddress, + }), + ), + ); + + expect(postMessage).toBeCalledTimes(1); + expect(postMessage).toBeCalledWith({ + method: RPCAction.GENERATE_VERIFIABLE_PRESENTATION_WITH_CRYPTKEEPER, + payload: { verifiablePresentation: mockVerifiablePresentation, address: mockAddress }, + }); + }); + + test("should reject a verfifiable presentation request properly", async () => { + await Promise.resolve(store.dispatch(rejectVerifiablePresentationRequest())); + + expect(postMessage).toBeCalledTimes(1); + expect(postMessage).toBeCalledWith({ + method: RPCAction.REJECT_VERIFIABLE_PRESENTATION_REQUEST, }); }); }); diff --git a/packages/app/src/ui/ducks/verifiableCredentials.ts b/packages/app/src/ui/ducks/verifiableCredentials.ts index 0a944a7f6..69ec725a0 100644 --- a/packages/app/src/ui/ducks/verifiableCredentials.ts +++ b/packages/app/src/ui/ducks/verifiableCredentials.ts @@ -3,10 +3,14 @@ import { RPCAction } from "@cryptkeeperzk/providers"; import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { serializeCryptkeeperVerifiableCredential } from "@src/background/services/credentials/utils"; -import { ICryptkeeperVerifiableCredential, IRenameVerifiableCredentialArgs } from "@src/types"; import postMessage from "@src/util/postMessage"; import type { IVerifiablePresentation } from "@cryptkeeperzk/types"; +import type { + ICryptkeeperVerifiableCredential, + IGenerateVerifiablePresentationWithCryptkeeperArgs, + IRenameVerifiableCredentialArgs, +} from "@src/types"; import type { TypedThunk } from "@src/ui/store/configureAppStore"; import { useAppSelector } from "./hooks"; @@ -70,6 +74,15 @@ export const generateVerifiablePresentation = }); }; +export const generateVerifiablePresentationWithCryptkeeper = + (generateVerifiablePresentationArgs: IGenerateVerifiablePresentationWithCryptkeeperArgs) => + async (): Promise => { + await postMessage({ + method: RPCAction.GENERATE_VERIFIABLE_PRESENTATION_WITH_CRYPTKEEPER, + payload: generateVerifiablePresentationArgs, + }); + }; + export const rejectVerifiablePresentationRequest = () => async (): Promise => { await postMessage({ method: RPCAction.REJECT_VERIFIABLE_PRESENTATION_REQUEST, diff --git a/packages/app/src/ui/pages/PresentVerifiableCredential/PresentVerifiableCredential.tsx b/packages/app/src/ui/pages/PresentVerifiableCredential/PresentVerifiableCredential.tsx index cc6908f41..b1fea2ed9 100644 --- a/packages/app/src/ui/pages/PresentVerifiableCredential/PresentVerifiableCredential.tsx +++ b/packages/app/src/ui/pages/PresentVerifiableCredential/PresentVerifiableCredential.tsx @@ -1,5 +1,20 @@ -import VerifiableCredentialSelector from "./components/VerifiableCredentialSelector"; -import VerifiablePresentationSigner from "./components/VerifiablePresentationSigner"; +import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown"; +import InfoIcon from "@mui/icons-material/Info"; +import Box from "@mui/material/Box"; +import Button from "@mui/material/Button"; +import ButtonGroup from "@mui/material/ButtonGroup"; +import ClickAwayListener from "@mui/material/ClickAwayListener"; +import Grow from "@mui/material/Grow"; +import MenuItem from "@mui/material/MenuItem"; +import MenuList from "@mui/material/MenuList"; +import Paper from "@mui/material/Paper"; +import Popper from "@mui/material/Popper"; +import Tooltip from "@mui/material/Tooltip"; +import Typography from "@mui/material/Typography"; + +import { FullModalHeader, FullModalContent, FullModalFooter } from "@src/ui/components/FullModal"; +import { VerifiableCredentialItem } from "@src/ui/components/VerifiableCredential/Item"; + import { usePresentVerifiableCredential } from "./usePresentVerifiableCredential"; const PresentVerifiableCredential = (): JSX.Element => { @@ -9,45 +24,142 @@ const PresentVerifiableCredential = (): JSX.Element => { verifiablePresentationRequest, cryptkeeperVerifiableCredentials, selectedVerifiableCredentialHashes, - verifiablePresentation, error, + isMenuOpen, + menuSelectedIndex, + menuRef, onCloseModal, onRejectRequest, onToggleSelection, - onConfirmSelection, - onReturnToSelection, - onConnectWallet, - onSubmitWithSignature, - onSubmitWithoutSignature, + onToggleMenu, + onMenuItemClick, + onSubmitVerifiablePresentation, } = usePresentVerifiableCredential(); - if (!verifiablePresentation) { - return ( - - ); - } + const menuOptions = [ + isWalletConnected ? "Sign with Metamask" : "Connect to Metamask", + "Sign with Cryptkeeper", + "Proceed without Signing", + ]; return ( - + + Request for Verifiable Credentials + + + + You have received a request to present Verifiable Credentials: + + + {verifiablePresentationRequest} + + {cryptkeeperVerifiableCredentials.map(({ verifiableCredential, metadata }) => ( + + ))} + + + {error && ( + + {error} + + )} + + + Choose Verifiable Credential signer + + + + + + + + + + + + + + + + + + {({ TransitionProps }) => ( + + + + + {menuOptions.map((option, index) => ( + { + onMenuItemClick(index); + }} + > + {option} + + ))} + + + + + )} + + + + ); }; diff --git a/packages/app/src/ui/pages/PresentVerifiableCredential/__tests__/PresentVerifiableCredential.test.tsx b/packages/app/src/ui/pages/PresentVerifiableCredential/__tests__/PresentVerifiableCredential.test.tsx index 0a6bd5f4e..12c7ed778 100644 --- a/packages/app/src/ui/pages/PresentVerifiableCredential/__tests__/PresentVerifiableCredential.test.tsx +++ b/packages/app/src/ui/pages/PresentVerifiableCredential/__tests__/PresentVerifiableCredential.test.tsx @@ -2,11 +2,7 @@ * @jest-environment jsdom */ -import { render, waitFor } from "@testing-library/react"; - -import { createModalRoot, deleteModalRoot } from "@src/config/mock/modal"; - -import type { IVerifiablePresentation } from "@cryptkeeperzk/types"; +import { fireEvent, render, waitFor } from "@testing-library/react"; import PresentVerifiableCredential from "../PresentVerifiableCredential"; import { IUsePresentVerifiableCredentialData, usePresentVerifiableCredential } from "../usePresentVerifiableCredential"; @@ -60,63 +56,33 @@ describe("ui/pages/PresentVerifiableCredential", () => { }, ]; - const mockVerifiablePresentation: IVerifiablePresentation = { - context: ["https://www.w3.org/2018/credentials/v1"], - type: ["VerifiablePresentation"], - verifiableCredential: [ - { - context: ["https://www.w3.org/2018/credentials/v1"], - id: "http://example.edu/credentials/3732", - type: ["VerifiableCredential"], - issuer: "did:example:123", - issuanceDate: new Date("2020-03-10T04:24:12.164Z"), - credentialSubject: { - id: "did:example:456", - claims: { - type: "BachelorDegree", - name: "Bachelor of Science and Arts", - }, - }, - }, - ], - }; - const defaultHookData: IUsePresentVerifiableCredentialData = { isWalletConnected: true, isWalletInstalled: true, verifiablePresentationRequest: "example presentation request", cryptkeeperVerifiableCredentials: mockCryptkeeperVerifiableCredentials, selectedVerifiableCredentialHashes: ["0x123"], - verifiablePresentation: undefined, error: undefined, + isMenuOpen: false, + menuSelectedIndex: 0, + menuRef: { current: document.createElement("div") }, onCloseModal: jest.fn(), onRejectRequest: jest.fn(), onToggleSelection: jest.fn(), - onConfirmSelection: jest.fn(), - onReturnToSelection: jest.fn(), - onConnectWallet: jest.fn(), - onSubmitWithSignature: jest.fn(), - onSubmitWithoutSignature: jest.fn(), - }; - - const verifiablePresentationHookData = { - ...defaultHookData, - verifiablePresentation: mockVerifiablePresentation, + onToggleMenu: jest.fn(), + onMenuItemClick: jest.fn(), + onSubmitVerifiablePresentation: jest.fn(), }; beforeEach(() => { - createModalRoot(); + (usePresentVerifiableCredential as jest.Mock).mockReturnValue(defaultHookData); }); afterEach(() => { jest.clearAllMocks(); - - deleteModalRoot(); }); test("should render present verifiable credential page properly", async () => { - (usePresentVerifiableCredential as jest.Mock).mockReturnValue(defaultHookData); - const { container, findByText } = render(); await waitFor(() => container.firstChild !== null); @@ -128,19 +94,97 @@ describe("ui/pages/PresentVerifiableCredential", () => { expect(credentialTwo).toBeInTheDocument(); }); - test("should render a verifiable presentation correctly", async () => { - (usePresentVerifiableCredential as jest.Mock).mockReturnValue(verifiablePresentationHookData); + test("should render an error properly", async () => { + const newError = "My Error"; + + (usePresentVerifiableCredential as jest.Mock).mockReturnValueOnce({ ...defaultHookData, error: newError }); const { container, findByText } = render(); await waitFor(() => container.firstChild !== null); - const header = await findByText("Sign Verifiable Presentation"); - const metamask = await findByText("Metamask"); - const proceedWithoutSigning = await findByText("Proceed Without Signing"); + const error = await findByText(newError); + + expect(error).toBeInTheDocument(); + }); + + test("should display connect to metamask", async () => { + (usePresentVerifiableCredential as jest.Mock).mockReturnValueOnce({ + ...defaultHookData, + isWalletConnected: false, + }); + + const { container, findAllByText } = render(); + + await waitFor(() => container.firstChild !== null); + + const metamask = await findAllByText("Connect to Metamask"); + + expect(metamask[0]).toBeInTheDocument(); + }); + + test("should reject a verifiable presentation request correctly", async () => { + const { container, findByTestId } = render(); + + await waitFor(() => container.firstChild !== null); + + const button = await findByTestId("reject-verifiable-presentation-request"); + fireEvent.click(button); + + expect(defaultHookData.onRejectRequest).toBeCalledTimes(1); + }); + + test("should toggle menu", async () => { + const { container, findAllByText, findByTestId } = render(); + + await waitFor(() => container.firstChild !== null); + + const metamask = await findAllByText("Sign with Metamask"); + + expect(metamask[0]).toBeInTheDocument(); + + const button = await findByTestId("sign-verifiable-presentation-selector"); + fireEvent.click(button); + + expect(defaultHookData.onToggleMenu).toBeCalledTimes(1); + }); + + test("should sign with metamask", async () => { + const { container, findAllByText, findByTestId } = render(); + + await waitFor(() => container.firstChild !== null); + + const metamask = await findAllByText("Sign with Metamask"); + + expect(metamask[0]).toBeInTheDocument(); + + const button = await findByTestId("sign-verifiable-presentation-button"); + fireEvent.click(button); + + expect(defaultHookData.onSubmitVerifiablePresentation).toBeCalledTimes(1); + }); + + test("should select menu items", async () => { + (usePresentVerifiableCredential as jest.Mock).mockReturnValueOnce({ + ...defaultHookData, + isMenuOpen: true, + menuRef: { current: document.createElement("div") }, + }); + + const { container, findByText, findByTestId } = render(); + + await waitFor(() => container.firstChild !== null); + + const cryptkeeper = await findByText("Sign with Cryptkeeper"); + const proceed = await findByText("Proceed without Signing"); + + expect(cryptkeeper).toBeInTheDocument(); + expect(proceed).toBeInTheDocument(); + + const button = await findByTestId("sign-verifiable-presentation-menu-2"); + fireEvent.click(button); - expect(header).toBeInTheDocument(); - expect(metamask).toBeInTheDocument(); - expect(proceedWithoutSigning).toBeInTheDocument(); + expect(defaultHookData.onMenuItemClick).toBeCalledTimes(1); + expect(defaultHookData.onMenuItemClick).toBeCalledWith(2); }); }); diff --git a/packages/app/src/ui/pages/PresentVerifiableCredential/__tests__/usePresentVerifiableCredential.test.ts b/packages/app/src/ui/pages/PresentVerifiableCredential/__tests__/usePresentVerifiableCredential.test.ts index 60fa3d4a9..e32e22716 100644 --- a/packages/app/src/ui/pages/PresentVerifiableCredential/__tests__/usePresentVerifiableCredential.test.ts +++ b/packages/app/src/ui/pages/PresentVerifiableCredential/__tests__/usePresentVerifiableCredential.test.ts @@ -9,10 +9,11 @@ import { closePopup } from "@src/ui/ducks/app"; import { useAppDispatch } from "@src/ui/ducks/hooks"; import { generateVerifiablePresentation, + generateVerifiablePresentationWithCryptkeeper, rejectVerifiablePresentationRequest, } from "@src/ui/ducks/verifiableCredentials"; import { useCryptkeeperVerifiableCredentials } from "@src/ui/hooks/verifiableCredentials"; -import { useEthWallet } from "@src/ui/hooks/wallet"; +import { useCryptKeeperWallet, useEthWallet } from "@src/ui/hooks/wallet"; import type { BrowserProvider } from "ethers"; @@ -81,6 +82,7 @@ jest.mock("@src/ui/ducks/verifiableCredentials", (): unknown => ({ fetchVerifiableCredentials: jest.fn(), useVerifiableCredentials: jest.fn(), generateVerifiablePresentation: jest.fn(), + generateVerifiablePresentationWithCryptkeeper: jest.fn(), rejectVerifiablePresentationRequest: jest.fn(), })); @@ -100,217 +102,453 @@ describe("ui/pages/PresentVerifiableCredential/usePresentVerifiableCredential", }), } as unknown as BrowserProvider; - const savedWindow = window; + const menuIndexMap = { + Metamask: 0, + Cryptkeeper: 1, + NoSignature: 2, + }; - beforeEach(() => { - (useAppDispatch as jest.Mock).mockReturnValue(mockDispatch); + const oldHref = window.location.href; - (useCryptkeeperVerifiableCredentials as jest.Mock).mockReturnValue(mockCryptkeeperVerifiableCredentials); + Object.defineProperty(window, "location", { + value: { + href: oldHref, + }, + writable: true, + }); - (useEthWallet as jest.Mock).mockReturnValue({ ...defaultWalletHookData, provider: mockProvider, isActive: true }); + describe("basic hook functionality", () => { + beforeEach(() => { + (useAppDispatch as jest.Mock).mockReturnValue(mockDispatch); - // eslint-disable-next-line no-global-assign - window = Object.create(window) as Window & typeof globalThis; - const url = `http://localhost:3000/generate-verifiable-presentation-request?request=${exampleRequest}`; - Object.defineProperty(window, "location", { - value: { - href: url, - }, + (useCryptkeeperVerifiableCredentials as jest.Mock).mockReturnValue(mockCryptkeeperVerifiableCredentials); + + (useEthWallet as jest.Mock).mockReturnValue({ ...defaultWalletHookData, provider: mockProvider, isActive: true }); + + (useCryptKeeperWallet as jest.Mock).mockReturnValue({ ...defaultWalletHookData, isActive: true }); + + window.location.href = `http://localhost:3000/generate-verifiable-presentation-request?request=${exampleRequest}`; }); - }); - afterEach(() => { - jest.clearAllMocks(); + afterEach(() => { + jest.clearAllMocks(); - // eslint-disable-next-line no-global-assign - window = savedWindow; - }); + window.location.href = oldHref; + }); + + test("should return initial data", async () => { + const { result } = renderHook(() => usePresentVerifiableCredential()); + + await waitFor(() => { + expect(result.current.verifiablePresentationRequest).toStrictEqual(exampleRequest); + expect(result.current.selectedVerifiableCredentialHashes).toStrictEqual([]); + expect(result.current.error).toBe(undefined); + }); + }); + + test("should have no initial data if request is empty", async () => { + window.location.href = `http://localhost:3000/generate-verifiable-presentation-request`; + + const { result } = renderHook(() => usePresentVerifiableCredential()); + + await waitFor(() => { + expect(result.current.verifiablePresentationRequest).toStrictEqual(undefined); + }); + }); + + test("should close the modal properly", async () => { + const { result } = renderHook(() => usePresentVerifiableCredential()); + + await waitFor(() => { + expect(result.current.verifiablePresentationRequest).toStrictEqual(exampleRequest); + }); + + act(() => result.current.onCloseModal()); + + expect(closePopup).toBeCalledTimes(1); + expect(mockDispatch).toBeCalledTimes(2); + }); + + test("should reject a verifiable presentation request", async () => { + const { result } = renderHook(() => usePresentVerifiableCredential()); + + await waitFor(() => { + expect(result.current.verifiablePresentationRequest).toStrictEqual(exampleRequest); + }); + + await act(async () => Promise.resolve(result.current.onRejectRequest())); + + expect(rejectVerifiablePresentationRequest).toBeCalledTimes(1); + expect(closePopup).toBeCalledTimes(1); + expect(mockDispatch).toBeCalledTimes(3); + }); + + test("should toggle selecting a verifiable credential", async () => { + const hash = "0x123"; - test("should return initial data", async () => { - const { result } = renderHook(() => usePresentVerifiableCredential()); + const { result } = renderHook(() => usePresentVerifiableCredential()); + + await waitFor(() => { + expect(result.current.verifiablePresentationRequest).toStrictEqual(exampleRequest); + }); + + act(() => result.current.onToggleSelection(hash)); + + expect(result.current.selectedVerifiableCredentialHashes).toStrictEqual([hash]); + + act(() => result.current.onToggleSelection(hash)); - await waitFor(() => { - expect(result.current.verifiablePresentationRequest).toStrictEqual(exampleRequest); expect(result.current.selectedVerifiableCredentialHashes).toStrictEqual([]); - expect(result.current.verifiablePresentation).toBe(undefined); - expect(result.current.error).toBe(undefined); }); - }); - test("should close the modal properly", async () => { - const { result } = renderHook(() => usePresentVerifiableCredential()); + test("should erase error after toggling select", async () => { + const hash = "0x123"; + + const { result } = renderHook(() => usePresentVerifiableCredential()); - await waitFor(() => { - expect(result.current.verifiablePresentationRequest).toStrictEqual(exampleRequest); + await waitFor(() => { + expect(result.current.verifiablePresentationRequest).toStrictEqual(exampleRequest); + }); + + act(() => result.current.onMenuItemClick(menuIndexMap.NoSignature)); + await act(() => result.current.onSubmitVerifiablePresentation()); + + act(() => result.current.onToggleSelection(hash)); + + expect(result.current.error).toBe(undefined); }); - act(() => result.current.onCloseModal()); + test("should toggle the menu properly", async () => { + const { result } = renderHook(() => usePresentVerifiableCredential()); - expect(closePopup).toBeCalledTimes(1); - expect(mockDispatch).toBeCalledTimes(2); - }); + await waitFor(() => { + expect(result.current.verifiablePresentationRequest).toStrictEqual(exampleRequest); + }); + + expect(result.current.isMenuOpen).toBe(false); - test("should reject a verifiable presentation request", async () => { - const { result } = renderHook(() => usePresentVerifiableCredential()); + act(() => result.current.onToggleMenu()); - await waitFor(() => { - expect(result.current.verifiablePresentationRequest).toStrictEqual(exampleRequest); + expect(result.current.isMenuOpen).toBe(true); }); - await act(async () => Promise.resolve(result.current.onRejectRequest())); + test("should select a menu item properly", async () => { + const { result } = renderHook(() => usePresentVerifiableCredential()); - expect(rejectVerifiablePresentationRequest).toBeCalledTimes(1); - expect(closePopup).toBeCalledTimes(1); - expect(mockDispatch).toBeCalledTimes(3); - }); + await waitFor(() => { + expect(result.current.verifiablePresentationRequest).toStrictEqual(exampleRequest); + }); + + expect(result.current.menuSelectedIndex).toBe(0); + + act(() => result.current.onMenuItemClick(menuIndexMap.Cryptkeeper)); + + expect(result.current.menuSelectedIndex).toBe(1); - test("should toggle selecting a verifiable credential", async () => { - const hash = "0x123"; + act(() => result.current.onMenuItemClick(menuIndexMap.NoSignature)); - const { result } = renderHook(() => usePresentVerifiableCredential()); + expect(result.current.menuSelectedIndex).toBe(2); - await waitFor(() => { - expect(result.current.verifiablePresentationRequest).toStrictEqual(exampleRequest); + act(() => result.current.onMenuItemClick(menuIndexMap.Metamask)); + + expect(result.current.menuSelectedIndex).toBe(0); }); - act(() => result.current.onToggleSelection(hash)); + test("should close the menu properly", async () => { + const { result } = renderHook(() => usePresentVerifiableCredential()); - expect(result.current.selectedVerifiableCredentialHashes).toStrictEqual([hash]); + await waitFor(() => { + expect(result.current.verifiablePresentationRequest).toStrictEqual(exampleRequest); + }); - act(() => result.current.onToggleSelection(hash)); + expect(result.current.isMenuOpen).toBe(false); - expect(result.current.selectedVerifiableCredentialHashes).toStrictEqual([]); - }); + act(() => result.current.onToggleMenu()); - test("should confirm selection of verifiable credentials", async () => { - const hash = "0x123"; + expect(result.current.isMenuOpen).toBe(true); - const { result } = renderHook(() => usePresentVerifiableCredential()); + act(() => result.current.onToggleMenu()); - await waitFor(() => { - expect(result.current.verifiablePresentationRequest).toStrictEqual(exampleRequest); + expect(result.current.isMenuOpen).toBe(false); }); - act(() => result.current.onConfirmSelection()); + test("should submit verifiable presentation without signature properly", async () => { + const hash = "0x123"; - expect(result.current.error).toBe("Please select at least one credential."); + const { result } = renderHook(() => usePresentVerifiableCredential()); - act(() => result.current.onToggleSelection(hash)); - act(() => result.current.onConfirmSelection()); + await waitFor(() => { + expect(result.current.verifiablePresentationRequest).toStrictEqual(exampleRequest); + }); - expect(result.current.verifiablePresentation?.verifiableCredential?.length).toBe(1); - }); + act(() => result.current.onToggleSelection(hash)); + act(() => result.current.onMenuItemClick(menuIndexMap.NoSignature)); + await act(() => result.current.onSubmitVerifiablePresentation()); - test("should return to selection of verifiable credentials", async () => { - const hash = "0x123"; + expect(generateVerifiablePresentation).toBeCalledTimes(1); + expect(closePopup).toBeCalledTimes(1); + expect(mockDispatch).toBeCalledTimes(3); + }); + + test("should fail to submit an empty verifiable presentation", async () => { + const { result } = renderHook(() => usePresentVerifiableCredential()); + + await waitFor(() => { + expect(result.current.verifiablePresentationRequest).toStrictEqual(exampleRequest); + }); - const { result } = renderHook(() => usePresentVerifiableCredential()); + act(() => result.current.onMenuItemClick(menuIndexMap.NoSignature)); + await act(() => result.current.onSubmitVerifiablePresentation()); - await waitFor(() => { - expect(result.current.verifiablePresentationRequest).toStrictEqual(exampleRequest); + expect(generateVerifiablePresentation).toBeCalledTimes(0); + expect(result.current.error).toBe("Please select at least one credential."); }); - act(() => result.current.onToggleSelection(hash)); - act(() => result.current.onConfirmSelection()); - act(() => result.current.onReturnToSelection()); + test("should submit verifiable presentation with cryptkeeper signature properly", async () => { + const hash = "0x123"; - expect(result.current.verifiablePresentation).toBe(undefined); - }); + const { result } = renderHook(() => usePresentVerifiableCredential()); + + await waitFor(() => { + expect(result.current.verifiablePresentationRequest).toStrictEqual(exampleRequest); + }); - test("should connect eth wallet properly", async () => { - const { result } = renderHook(() => usePresentVerifiableCredential()); + act(() => result.current.onToggleSelection(hash)); + act(() => result.current.onMenuItemClick(menuIndexMap.Cryptkeeper)); + await act(() => result.current.onSubmitVerifiablePresentation()); - await waitFor(() => { - expect(result.current.verifiablePresentationRequest).toStrictEqual(exampleRequest); + expect(generateVerifiablePresentationWithCryptkeeper).toBeCalledTimes(1); + expect(closePopup).toBeCalledTimes(1); + expect(mockDispatch).toBeCalledTimes(3); }); - await act(async () => Promise.resolve(result.current.onConnectWallet())); + test("should fail to submit an empty verifiable presentation with cryptkeeper", async () => { + const { result } = renderHook(() => usePresentVerifiableCredential()); - expect(defaultWalletHookData.onConnect).toBeCalledTimes(1); - }); + await waitFor(() => { + expect(result.current.verifiablePresentationRequest).toStrictEqual(exampleRequest); + }); - test("should handle error when trying to connect with eth wallet", async () => { - (useEthWallet as jest.Mock).mockReturnValue({ - ...defaultWalletHookData, - onConnect: jest.fn(() => Promise.reject()), + act(() => result.current.onMenuItemClick(menuIndexMap.Cryptkeeper)); + await act(() => result.current.onSubmitVerifiablePresentation()); + + expect(generateVerifiablePresentationWithCryptkeeper).toBeCalledTimes(0); + expect(result.current.error).toBe("Please select at least one credential."); }); - const { result } = renderHook(() => usePresentVerifiableCredential()); + test("should submit verifiable presentation with Metamask signature properly", async () => { + const hash = "0x123"; + + const { result } = renderHook(() => usePresentVerifiableCredential()); + + await waitFor(() => { + expect(result.current.verifiablePresentationRequest).toStrictEqual(exampleRequest); + }); + + act(() => result.current.onToggleSelection(hash)); + await act(() => result.current.onSubmitVerifiablePresentation()); - await waitFor(() => { - expect(result.current.verifiablePresentationRequest).toStrictEqual(exampleRequest); + expect(generateVerifiablePresentation).toBeCalledTimes(1); + expect(closePopup).toBeCalledTimes(1); + expect(mockDispatch).toBeCalledTimes(3); }); - await act(async () => Promise.resolve(result.current.onConnectWallet())); + test("should fail to submit an empty verifiable presentation with metamask", async () => { + const { result } = renderHook(() => usePresentVerifiableCredential()); - expect(result.current.error).toBe("Wallet connection error"); + await waitFor(() => { + expect(result.current.verifiablePresentationRequest).toStrictEqual(exampleRequest); + }); + + await act(() => result.current.onSubmitVerifiablePresentation()); + + expect(generateVerifiablePresentationWithCryptkeeper).toBeCalledTimes(0); + expect(result.current.error).toBe("Please select at least one credential."); + }); + + test("should create error upon invalid menu index", async () => { + const { result } = renderHook(() => usePresentVerifiableCredential()); + + await waitFor(() => { + expect(result.current.verifiablePresentationRequest).toStrictEqual(exampleRequest); + }); + + act(() => result.current.onMenuItemClick(3)); + await act(() => result.current.onSubmitVerifiablePresentation()); + + expect(generateVerifiablePresentation).toBeCalledTimes(0); + expect(generateVerifiablePresentationWithCryptkeeper).toBeCalledTimes(0); + expect(result.current.error).toBe("Invalid menu index."); + }); }); - test("should submit verifiable presentation without signature properly", async () => { - const hash = "0x123"; + describe("wallet connection error", () => { + beforeEach(() => { + (useAppDispatch as jest.Mock).mockReturnValue(mockDispatch); + + (useCryptkeeperVerifiableCredentials as jest.Mock).mockReturnValue(mockCryptkeeperVerifiableCredentials); - const { result } = renderHook(() => usePresentVerifiableCredential()); + (useEthWallet as jest.Mock).mockReturnValue({ + ...defaultWalletHookData, + onConnect: () => { + throw Error("error"); + }, + provider: { + getSigner: () => undefined, + }, + isActive: false, + }); - await waitFor(() => { - expect(result.current.verifiablePresentationRequest).toStrictEqual(exampleRequest); + (useCryptKeeperWallet as jest.Mock).mockReturnValue({ ...defaultWalletHookData, isActive: true }); + + window.location.href = `http://localhost:3000/generate-verifiable-presentation-request?request=${exampleRequest}`; }); - act(() => result.current.onToggleSelection(hash)); - act(() => result.current.onConfirmSelection()); - await act(async () => Promise.resolve(result.current.onSubmitWithoutSignature())); + afterEach(() => { + jest.clearAllMocks(); + + window.location.href = oldHref; + }); + + test("should fail to connect wallet if connection is invalid", async () => { + const hash = "0x123"; + + const { result } = renderHook(() => usePresentVerifiableCredential()); - expect(generateVerifiablePresentation).toBeCalledTimes(1); - expect(closePopup).toBeCalledTimes(1); - expect(mockDispatch).toBeCalledTimes(3); + await waitFor(() => { + expect(result.current.verifiablePresentationRequest).toStrictEqual(exampleRequest); + }); + + act(() => result.current.onToggleSelection(hash)); + await act(() => result.current.onSubmitVerifiablePresentation()); + + expect(result.current.error).toStrictEqual("Wallet connection error"); + }); }); - test("should submit verifiable presentation with signature properly", async () => { - const hash = "0x123"; + describe("wallet provider error", () => { + beforeEach(() => { + (useAppDispatch as jest.Mock).mockReturnValue(mockDispatch); + + (useCryptkeeperVerifiableCredentials as jest.Mock).mockReturnValue(mockCryptkeeperVerifiableCredentials); + + (useEthWallet as jest.Mock).mockReturnValue({ + ...defaultWalletHookData, + provider: { + getSigner: () => undefined, + }, + isActive: true, + }); + + (useCryptKeeperWallet as jest.Mock).mockReturnValue({ ...defaultWalletHookData, isActive: true }); + + window.location.href = `http://localhost:3000/generate-verifiable-presentation-request?request=${exampleRequest}`; + }); - const { result } = renderHook(() => usePresentVerifiableCredential()); + afterEach(() => { + jest.clearAllMocks(); - await waitFor(() => { - expect(result.current.verifiablePresentationRequest).toStrictEqual(exampleRequest); + window.location.href = oldHref; }); - act(() => result.current.onToggleSelection(hash)); - act(() => result.current.onConfirmSelection()); - await act(async () => Promise.resolve(result.current.onSubmitWithSignature())); + test("should fail to submit verifiable presentation if wallet is invalid", async () => { + const hash = "0x123"; + + const { result } = renderHook(() => usePresentVerifiableCredential()); - expect(generateVerifiablePresentation).toBeCalledTimes(1); - expect(closePopup).toBeCalledTimes(1); - expect(mockDispatch).toBeCalledTimes(3); + await waitFor(() => { + expect(result.current.verifiablePresentationRequest).toStrictEqual(exampleRequest); + }); + + act(() => result.current.onToggleSelection(hash)); + await act(() => result.current.onSubmitVerifiablePresentation()); + + expect(result.current.error).toStrictEqual("Could not connect to Ethereum account."); + }); }); - test("should fail to submit an empty verifiable presentation", async () => { - const { result } = renderHook(() => usePresentVerifiableCredential()); + describe("wallet signer error", () => { + beforeEach(() => { + (useAppDispatch as jest.Mock).mockReturnValue(mockDispatch); + + (useCryptkeeperVerifiableCredentials as jest.Mock).mockReturnValue(mockCryptkeeperVerifiableCredentials); + + (useEthWallet as jest.Mock).mockReturnValue({ + ...defaultWalletHookData, + provider: { + getSigner: () => ({ + signMessage: () => { + throw new Error("error"); + }, + }), + }, + isActive: true, + }); + + (useCryptKeeperWallet as jest.Mock).mockReturnValue({ ...defaultWalletHookData, isActive: true }); + + window.location.href = `http://localhost:3000/generate-verifiable-presentation-request?request=${exampleRequest}`; + }); + + afterEach(() => { + jest.clearAllMocks(); - await waitFor(() => { - expect(result.current.verifiablePresentationRequest).toStrictEqual(exampleRequest); + window.location.href = oldHref; }); - await act(async () => Promise.resolve(result.current.onSubmitWithoutSignature())); + test("should fail to submit verifiable presentation if signing errors", async () => { + const hash = "0x123"; + + const { result } = renderHook(() => usePresentVerifiableCredential()); + + await waitFor(() => { + expect(result.current.verifiablePresentationRequest).toStrictEqual(exampleRequest); + }); + + act(() => result.current.onToggleSelection(hash)); + await act(() => result.current.onSubmitVerifiablePresentation()); - expect(generateVerifiablePresentation).toBeCalledTimes(0); - expect(result.current.error).toBe("Failed to generate Verifiable Presentation."); + expect(result.current.error).toStrictEqual("Failed to sign Verifiable Presentation."); + }); }); - test("should fail to sign a verifiable presentation with an Ethereum connection error", async () => { - const hash = "0x123"; + describe("cryptkeeper address error", () => { + beforeEach(() => { + (useAppDispatch as jest.Mock).mockReturnValue(mockDispatch); + + (useCryptkeeperVerifiableCredentials as jest.Mock).mockReturnValue(mockCryptkeeperVerifiableCredentials); - (useEthWallet as jest.Mock).mockReturnValue({ ...defaultWalletHookData, address: undefined }); + (useEthWallet as jest.Mock).mockReturnValue({ ...defaultWalletHookData, provider: mockProvider, isActive: true }); - const { result } = renderHook(() => usePresentVerifiableCredential()); + (useCryptKeeperWallet as jest.Mock).mockReturnValue({ + ...defaultWalletHookData, + isActive: true, + address: undefined, + }); - await waitFor(() => { - expect(result.current.verifiablePresentationRequest).toStrictEqual(exampleRequest); + window.location.href = `http://localhost:3000/generate-verifiable-presentation-request?request=${exampleRequest}`; }); - act(() => result.current.onToggleSelection(hash)); - act(() => result.current.onConfirmSelection()); - await act(async () => Promise.resolve(result.current.onSubmitWithSignature())); + afterEach(() => { + jest.clearAllMocks(); - expect(generateVerifiablePresentation).toBeCalledTimes(0); - expect(result.current.error).toBe("Could not connect to Ethereum account."); + window.location.href = oldHref; + }); + + test("should fail to submit verifiable presentation if cryptkeeper address is invalid", async () => { + const hash = "0x123"; + + const { result } = renderHook(() => usePresentVerifiableCredential()); + + await waitFor(() => { + expect(result.current.verifiablePresentationRequest).toStrictEqual(exampleRequest); + }); + + act(() => result.current.onToggleSelection(hash)); + act(() => result.current.onMenuItemClick(menuIndexMap.Cryptkeeper)); + await act(() => result.current.onSubmitVerifiablePresentation()); + + expect(result.current.error).toStrictEqual("Could not connect to Cryptkeeper account."); + }); }); }); diff --git a/packages/app/src/ui/pages/PresentVerifiableCredential/components/VerifiableCredentialSelector/VerifiableCredentialSelector.tsx b/packages/app/src/ui/pages/PresentVerifiableCredential/components/VerifiableCredentialSelector/VerifiableCredentialSelector.tsx deleted file mode 100644 index b39ea749b..000000000 --- a/packages/app/src/ui/pages/PresentVerifiableCredential/components/VerifiableCredentialSelector/VerifiableCredentialSelector.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import Box from "@mui/material/Box"; -import Button from "@mui/material/Button"; -import Typography from "@mui/material/Typography"; - -import { ICryptkeeperVerifiableCredential } from "@src/types"; -import { FullModal, FullModalHeader, FullModalContent, FullModalFooter } from "@src/ui/components/FullModal"; -import { VerifiableCredentialItem } from "@src/ui/components/VerifiableCredential/Item"; - -export interface IVerifiableCredentialSelectorProps { - verifiablePresentationRequest?: string; - cryptkeeperVerifiableCredentials: ICryptkeeperVerifiableCredential[]; - selectedVerifiableCredentialHashes: string[]; - error?: string; - onCloseModal: () => void; - onRejectVerifiablePresentationRequest: () => void; - onToggleSelectVerifiableCredential: (hash: string) => void; - onConfirmSelection: () => void; -} - -const VerifiableCredentialSelector = ({ - verifiablePresentationRequest = undefined, - cryptkeeperVerifiableCredentials, - selectedVerifiableCredentialHashes, - error = undefined, - onCloseModal, - onRejectVerifiablePresentationRequest, - onToggleSelectVerifiableCredential, - onConfirmSelection, -}: IVerifiableCredentialSelectorProps): JSX.Element => ( - - - Request for Verifiable Credentials - - - - You have received a request to present Verifiable Credentials: - - - {verifiablePresentationRequest} - - {cryptkeeperVerifiableCredentials.map(({ verifiableCredential, metadata }) => ( - - ))} - - - {error && ( - - {error} - - )} - - - - - - - - - - -); - -export default VerifiableCredentialSelector; diff --git a/packages/app/src/ui/pages/PresentVerifiableCredential/components/VerifiableCredentialSelector/__tests__/VerifiableCredentialSelector.test.tsx b/packages/app/src/ui/pages/PresentVerifiableCredential/components/VerifiableCredentialSelector/__tests__/VerifiableCredentialSelector.test.tsx deleted file mode 100644 index d880aa6c5..000000000 --- a/packages/app/src/ui/pages/PresentVerifiableCredential/components/VerifiableCredentialSelector/__tests__/VerifiableCredentialSelector.test.tsx +++ /dev/null @@ -1,120 +0,0 @@ -/** - * @jest-environment jsdom - */ - -import { waitFor, fireEvent, render } from "@testing-library/react"; - -import { createModalRoot, deleteModalRoot } from "@src/config/mock/modal"; - -import VerifiableCredentialSelector, { IVerifiableCredentialSelectorProps } from "../VerifiableCredentialSelector"; - -describe("ui/pages/PresentVerifiableCredential/components/VerifiableCredentialSelector", () => { - const defaultProps: IVerifiableCredentialSelectorProps = { - verifiablePresentationRequest: "example presentation request", - cryptkeeperVerifiableCredentials: [ - { - verifiableCredential: { - context: ["https://www.w3.org/2018/credentials/v1"], - id: "http://example.edu/credentials/3732", - type: ["VerifiableCredential"], - issuer: "did:example:123", - issuanceDate: new Date("2020-03-10T04:24:12.164Z"), - credentialSubject: { - id: "did:example:456", - claims: { - type: "BachelorDegree", - name: "Bachelor of Science and Arts", - }, - }, - }, - metadata: { - hash: "0x123", - name: "Credential #0", - }, - }, - { - verifiableCredential: { - context: ["https://www.w3.org/2018/credentials/v1"], - id: "http://example.edu/credentials/3733", - type: ["VerifiableCredential"], - issuer: "did:example:12345", - issuanceDate: new Date("2020-03-10T04:24:12.164Z"), - credentialSubject: { - id: "did:example:123", - claims: { - type: "BachelorDegree", - name: "Bachelor of Science and Arts", - }, - }, - }, - metadata: { - hash: "0x1234", - name: "Credential #1", - }, - }, - ], - selectedVerifiableCredentialHashes: ["0x123"], - error: undefined, - onCloseModal: jest.fn(), - onRejectVerifiablePresentationRequest: jest.fn(), - onToggleSelectVerifiableCredential: jest.fn(), - onConfirmSelection: jest.fn(), - }; - - beforeEach(() => { - createModalRoot(); - }); - - afterEach(() => { - jest.clearAllMocks(); - - deleteModalRoot(); - }); - - test("should render properly", async () => { - const { container, findByTestId, findByText } = render(); - - await waitFor(() => container.firstChild !== null); - - const page = await findByTestId("select-verifiable-credential-page"); - const request = await findByText(defaultProps.verifiablePresentationRequest!); - const item = await findByText(defaultProps.cryptkeeperVerifiableCredentials[0].metadata.name); - - expect(page).toBeInTheDocument(); - expect(request).toBeInTheDocument(); - expect(item).toBeInTheDocument(); - }); - - test("should render an error properly", async () => { - const newError = "My Error"; - const { container, findByText } = render(); - - await waitFor(() => container.firstChild !== null); - - const error = await findByText(newError); - - expect(error).toBeInTheDocument(); - }); - - test("should reject a verifiable presentation request correctly", async () => { - const { container, findByTestId } = render(); - - await waitFor(() => container.firstChild !== null); - - const button = await findByTestId("reject-verifiable-presentation-request"); - fireEvent.click(button); - - expect(defaultProps.onRejectVerifiablePresentationRequest).toBeCalledTimes(1); - }); - - test("should confirm a credential selection correctly", async () => { - const { container, findByTestId } = render(); - - await waitFor(() => container.firstChild !== null); - - const button = await findByTestId("confirm-verifiable-presentation-request"); - fireEvent.click(button); - - expect(defaultProps.onConfirmSelection).toBeCalledTimes(1); - }); -}); diff --git a/packages/app/src/ui/pages/PresentVerifiableCredential/components/VerifiableCredentialSelector/index.ts b/packages/app/src/ui/pages/PresentVerifiableCredential/components/VerifiableCredentialSelector/index.ts deleted file mode 100644 index fbf0b718b..000000000 --- a/packages/app/src/ui/pages/PresentVerifiableCredential/components/VerifiableCredentialSelector/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { lazy } from "react"; - -export type { IVerifiableCredentialSelectorProps } from "./VerifiableCredentialSelector"; - -export default lazy(() => import("./VerifiableCredentialSelector")); diff --git a/packages/app/src/ui/pages/PresentVerifiableCredential/components/VerifiablePresentationSigner/VerifiablePresentationSigner.tsx b/packages/app/src/ui/pages/PresentVerifiableCredential/components/VerifiablePresentationSigner/VerifiablePresentationSigner.tsx deleted file mode 100644 index a0b3b06d8..000000000 --- a/packages/app/src/ui/pages/PresentVerifiableCredential/components/VerifiablePresentationSigner/VerifiablePresentationSigner.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import Box from "@mui/material/Box"; -import Button from "@mui/material/Button"; -import Typography from "@mui/material/Typography"; - -import { ICryptkeeperVerifiableCredential } from "@src/types"; -import { FullModal, FullModalHeader, FullModalContent, FullModalFooter } from "@src/ui/components/FullModal"; -import { VerifiableCredentialItem } from "@src/ui/components/VerifiableCredential/Item"; - -export interface IVerifiablePresentationSignerProps { - isWalletConnected: boolean; - isWalletInstalled: boolean; - cryptkeeperVerifiableCredentials: ICryptkeeperVerifiableCredential[]; - selectedVerifiableCredentialHashes: string[]; - onCloseModal: () => void; - onReturnToSelection: () => void; - onConnectWallet: () => Promise; - onSubmitWithSignature: () => void; - onSubmitWithoutSignature: () => void; -} - -const VerifiablePresentationSigner = ({ - isWalletConnected, - isWalletInstalled, - cryptkeeperVerifiableCredentials, - selectedVerifiableCredentialHashes, - onCloseModal, - onReturnToSelection, - onConnectWallet, - onSubmitWithSignature, - onSubmitWithoutSignature, -}: IVerifiablePresentationSignerProps): JSX.Element => { - const selectedVerifableCredentials = cryptkeeperVerifiableCredentials.filter(({ metadata }) => - selectedVerifiableCredentialHashes.includes(metadata.hash), - ); - - const ethWalletTitle = isWalletConnected ? "Metamask" : "Connect to Metamask"; - - return ( - - - Sign Verifiable Presentation - - - - You have selected the following Verifiable Credentials. You may now sign them with your Metamask wallet, or - send them unsigned. - - - {selectedVerifableCredentials.map(({ verifiableCredential, metadata }) => ( - - ))} - - - - - - - - - - - - ); -}; - -export default VerifiablePresentationSigner; diff --git a/packages/app/src/ui/pages/PresentVerifiableCredential/components/VerifiablePresentationSigner/__tests__/VerifiablePresentationSigner.test.tsx b/packages/app/src/ui/pages/PresentVerifiableCredential/components/VerifiablePresentationSigner/__tests__/VerifiablePresentationSigner.test.tsx deleted file mode 100644 index 26f5a2001..000000000 --- a/packages/app/src/ui/pages/PresentVerifiableCredential/components/VerifiablePresentationSigner/__tests__/VerifiablePresentationSigner.test.tsx +++ /dev/null @@ -1,131 +0,0 @@ -/** - * @jest-environment jsdom - */ - -import { waitFor, fireEvent, render } from "@testing-library/react"; - -import { createModalRoot, deleteModalRoot } from "@src/config/mock/modal"; - -import VerifiablePresentationSigner, { IVerifiablePresentationSignerProps } from "../VerifiablePresentationSigner"; - -describe("ui/pages/PresentVerifiableCredential/components/VerifiablePresentationSigner", () => { - const defaultProps: IVerifiablePresentationSignerProps = { - isWalletConnected: true, - isWalletInstalled: true, - cryptkeeperVerifiableCredentials: [ - { - verifiableCredential: { - context: ["https://www.w3.org/2018/credentials/v1"], - id: "http://example.edu/credentials/3732", - type: ["VerifiableCredential"], - issuer: "did:example:123", - issuanceDate: new Date("2020-03-10T04:24:12.164Z"), - credentialSubject: { - id: "did:example:456", - claims: { - type: "BachelorDegree", - name: "Bachelor of Science and Arts", - }, - }, - }, - metadata: { - hash: "0x123", - name: "Credential #0", - }, - }, - { - verifiableCredential: { - context: ["https://www.w3.org/2018/credentials/v1"], - id: "http://example.edu/credentials/3733", - type: ["VerifiableCredential"], - issuer: "did:example:12345", - issuanceDate: new Date("2020-03-10T04:24:12.164Z"), - credentialSubject: { - id: "did:example:123", - claims: { - type: "BachelorDegree", - name: "Bachelor of Science and Arts", - }, - }, - }, - metadata: { - hash: "0x1234", - name: "Credential #1", - }, - }, - ], - selectedVerifiableCredentialHashes: ["0x123"], - onCloseModal: jest.fn(), - onReturnToSelection: jest.fn(), - onConnectWallet: jest.fn(), - onSubmitWithSignature: jest.fn(), - onSubmitWithoutSignature: jest.fn(), - }; - - beforeEach(() => { - createModalRoot(); - }); - - afterEach(() => { - jest.clearAllMocks(); - - deleteModalRoot(); - }); - - test("should render properly", async () => { - const { container, findByTestId } = render(); - - await waitFor(() => container.firstChild !== null); - - const page = await findByTestId("sign-verifiable-credential-page"); - - expect(page).toBeInTheDocument(); - }); - - test("should submit without signing correctly", async () => { - const { container, findByTestId } = render(); - - await waitFor(() => container.firstChild !== null); - - const button = await findByTestId("submit-verifiable-presentation-without-signing"); - fireEvent.click(button); - - expect(defaultProps.onSubmitWithoutSignature).toBeCalledTimes(1); - }); - - test("should submit with signing correctly", async () => { - const { container, findByTestId } = render(); - - await waitFor(() => container.firstChild !== null); - - const button = await findByTestId("sign-verifiable-presentation-metamask"); - fireEvent.click(button); - - expect(defaultProps.onSubmitWithSignature).toBeCalledTimes(1); - }); - - test("should connect to metamask correctly", async () => { - const { container, findByTestId } = render( - , - ); - - await waitFor(() => container.firstChild !== null); - - const button = await findByTestId("sign-verifiable-presentation-metamask"); - fireEvent.click(button); - - expect(defaultProps.onConnectWallet).toBeCalledTimes(1); - }); - - test("should display install metamask message", async () => { - const { container, findByText } = render( - , - ); - - await waitFor(() => container.firstChild !== null); - - const text = await findByText("Install MetaMask"); - - expect(text).toBeInTheDocument(); - }); -}); diff --git a/packages/app/src/ui/pages/PresentVerifiableCredential/components/VerifiablePresentationSigner/index.ts b/packages/app/src/ui/pages/PresentVerifiableCredential/components/VerifiablePresentationSigner/index.ts deleted file mode 100644 index 3e8cf5e3f..000000000 --- a/packages/app/src/ui/pages/PresentVerifiableCredential/components/VerifiablePresentationSigner/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { lazy } from "react"; - -export type { IVerifiablePresentationSignerProps } from "./VerifiablePresentationSigner"; - -export default lazy(() => import("./VerifiablePresentationSigner")); diff --git a/packages/app/src/ui/pages/PresentVerifiableCredential/usePresentVerifiableCredential.ts b/packages/app/src/ui/pages/PresentVerifiableCredential/usePresentVerifiableCredential.ts index c4fa9a1af..0ffaf09fc 100644 --- a/packages/app/src/ui/pages/PresentVerifiableCredential/usePresentVerifiableCredential.ts +++ b/packages/app/src/ui/pages/PresentVerifiableCredential/usePresentVerifiableCredential.ts @@ -1,20 +1,23 @@ -import { IVerifiablePresentation } from "@cryptkeeperzk/types"; +import * as React from "react"; import { useCallback, useEffect, useState } from "react"; import { generateVerifiablePresentationFromVerifiableCredentials, serializeVerifiablePresentation, } from "@src/background/services/credentials/utils"; -import { ICryptkeeperVerifiableCredential } from "@src/types"; import { closePopup } from "@src/ui/ducks/app"; import { useAppDispatch } from "@src/ui/ducks/hooks"; import { fetchVerifiableCredentials, rejectVerifiablePresentationRequest, generateVerifiablePresentation, + generateVerifiablePresentationWithCryptkeeper, } from "@src/ui/ducks/verifiableCredentials"; import { useCryptkeeperVerifiableCredentials } from "@src/ui/hooks/verifiableCredentials"; -import { useEthWallet } from "@src/ui/hooks/wallet"; +import { useCryptKeeperWallet, useEthWallet } from "@src/ui/hooks/wallet"; + +import type { IVerifiablePresentation } from "@cryptkeeperzk/types"; +import type { ICryptkeeperVerifiableCredential } from "@src/types"; const ETHEREUM_SIGNATURE_SPECIFICATION_TYPE = "EthereumEip712Signature2021"; const VERIFIABLE_CREDENTIAL_PROOF_PURPOSE = "assertionMethod"; @@ -25,27 +28,31 @@ export interface IUsePresentVerifiableCredentialData { verifiablePresentationRequest?: string; cryptkeeperVerifiableCredentials: ICryptkeeperVerifiableCredential[]; selectedVerifiableCredentialHashes: string[]; - verifiablePresentation?: IVerifiablePresentation; error?: string; + isMenuOpen: boolean; + menuSelectedIndex: number; + menuRef: React.RefObject; onCloseModal: () => void; onRejectRequest: () => void; onToggleSelection: (hash: string) => void; - onConfirmSelection: () => void; - onReturnToSelection: () => void; - onConnectWallet: () => Promise; - onSubmitWithSignature: () => Promise; - onSubmitWithoutSignature: () => Promise; + onToggleMenu: () => void; + onMenuItemClick: (index: number) => void; + onSubmitVerifiablePresentation: () => Promise; } export const usePresentVerifiableCredential = (): IUsePresentVerifiableCredentialData => { const [verifiablePresentationRequest, setVerifiablePresentationRequest] = useState(); const cryptkeeperVerifiableCredentials = useCryptkeeperVerifiableCredentials(); const [selectedVerifiableCredentialHashes, setSelectedVerifiableCredentialHashes] = useState([]); - const [verifiablePresentation, setVerifiablePresentation] = useState(); + const [isMenuOpen, setIsMenuOpen] = React.useState(false); + const menuRef = React.useRef(null); + const [menuSelectedIndex, setMenuSelectedIndex] = React.useState(0); const [error, setError] = useState(); const ethWallet = useEthWallet(); + const cryptKeeperWallet = useCryptKeeperWallet(); const dispatch = useAppDispatch(); + const isWalletConnected = ethWallet.isActive; useEffect(() => { const { searchParams } = new URL(window.location.href.replace("#", "")); @@ -84,10 +91,10 @@ export const usePresentVerifiableCredential = (): IUsePresentVerifiableCredentia [selectedVerifiableCredentialHashes, setSelectedVerifiableCredentialHashes, error, setError], ); - const onConfirmVerifiableCredentialSelection = useCallback(() => { + function createVerifiablePresentationFromSelectedCredentials(): IVerifiablePresentation | undefined { if (selectedVerifiableCredentialHashes.length === 0) { setError("Please select at least one credential."); - return; + return undefined; } const verifiableCredentials = cryptkeeperVerifiableCredentials @@ -95,24 +102,31 @@ export const usePresentVerifiableCredential = (): IUsePresentVerifiableCredentia selectedVerifiableCredentialHashes.includes(cryptkeeperVerifiableCredential.metadata.hash), ) .map((cryptkeeperVerifiableCredential) => cryptkeeperVerifiableCredential.verifiableCredential); - const newVerifiablePresentation = generateVerifiablePresentationFromVerifiableCredentials(verifiableCredentials); - setVerifiablePresentation(newVerifiablePresentation); - }, [cryptkeeperVerifiableCredentials, selectedVerifiableCredentialHashes, setVerifiablePresentation, setError]); + return generateVerifiablePresentationFromVerifiableCredentials(verifiableCredentials); + } - const onReturnToVerifiableCredentialSelection = useCallback(() => { - setVerifiablePresentation(undefined); - }, [setVerifiablePresentation]); + const onToggleMenu = () => { + setIsMenuOpen((prevOpen) => !prevOpen); + }; + + const onMenuItemClick = (index: number) => { + setMenuSelectedIndex(index); + setIsMenuOpen(false); + }; const onConnectWallet = useCallback(async () => { - await ethWallet.onConnect().catch(() => { + try { + await ethWallet.onConnect(); + } catch (e) { setError("Wallet connection error"); - }); + } }, [setError, ethWallet.onConnect]); - const onSubmitVerifiablePresentationWithSignature = useCallback(async () => { + const onSubmitVerifiablePresentationWithMetamask = useCallback(async () => { + const verifiablePresentation = createVerifiablePresentationFromSelectedCredentials(); + if (!verifiablePresentation) { - setError("Failed to generate Verifiable Presentation."); return; } @@ -145,33 +159,95 @@ export const usePresentVerifiableCredential = (): IUsePresentVerifiableCredentia } catch (e) { setError("Failed to sign Verifiable Presentation."); } - }, [verifiablePresentation, setError, dispatch, onCloseModal, generateVerifiablePresentation, ethWallet]); + }, [ + ethWallet, + setError, + dispatch, + onCloseModal, + generateVerifiablePresentation, + createVerifiablePresentationFromSelectedCredentials, + ]); + + const onSubmitVerifiablePresentationWithCryptkeeper = useCallback(async () => { + const verifiablePresentation = createVerifiablePresentationFromSelectedCredentials(); + + if (!verifiablePresentation) { + return; + } + + const address = cryptKeeperWallet.address?.toLowerCase(); + + if (!address) { + setError("Could not connect to Cryptkeeper account."); + return; + } + + await dispatch(generateVerifiablePresentationWithCryptkeeper({ verifiablePresentation, address })); + onCloseModal(); + }, [ + cryptKeeperWallet, + setError, + dispatch, + onCloseModal, + generateVerifiablePresentationWithCryptkeeper, + createVerifiablePresentationFromSelectedCredentials, + ]); const onSubmitVerifiablePresentationWithoutSignature = useCallback(async () => { + const verifiablePresentation = createVerifiablePresentationFromSelectedCredentials(); + if (!verifiablePresentation) { - setError("Failed to generate Verifiable Presentation."); return; } await dispatch(generateVerifiablePresentation(verifiablePresentation)); onCloseModal(); - }, [verifiablePresentation, setError, dispatch, onCloseModal, generateVerifiablePresentation]); + }, [ + setError, + dispatch, + onCloseModal, + generateVerifiablePresentation, + createVerifiablePresentationFromSelectedCredentials, + ]); + + // eslint-disable-next-line consistent-return + const onSubmitVerifiablePresentation = useCallback(async () => { + switch (true) { + case menuSelectedIndex === 0 && !isWalletConnected: + return onConnectWallet(); + case menuSelectedIndex === 0 && isWalletConnected: + return onSubmitVerifiablePresentationWithMetamask(); + case menuSelectedIndex === 1: + return onSubmitVerifiablePresentationWithCryptkeeper(); + case menuSelectedIndex === 2: + return onSubmitVerifiablePresentationWithoutSignature(); + default: + setError("Invalid menu index."); + } + }, [ + menuSelectedIndex, + isWalletConnected, + onConnectWallet, + onSubmitVerifiablePresentationWithMetamask, + onSubmitVerifiablePresentationWithCryptkeeper, + onSubmitVerifiablePresentationWithoutSignature, + ]); return { isWalletInstalled: ethWallet.isInjectedWallet, - isWalletConnected: ethWallet.isActive, + isWalletConnected, verifiablePresentationRequest, cryptkeeperVerifiableCredentials, selectedVerifiableCredentialHashes, - verifiablePresentation, error, + isMenuOpen, + menuSelectedIndex, + menuRef, onCloseModal, onRejectRequest: onRejectVerifiablePresentationRequest, onToggleSelection: onToggleSelectVerifiableCredential, - onConfirmSelection: onConfirmVerifiableCredentialSelection, - onReturnToSelection: onReturnToVerifiableCredentialSelection, - onConnectWallet, - onSubmitWithSignature: onSubmitVerifiablePresentationWithSignature, - onSubmitWithoutSignature: onSubmitVerifiablePresentationWithoutSignature, + onToggleMenu, + onMenuItemClick, + onSubmitVerifiablePresentation, }; }; diff --git a/packages/providers/src/constants/rpcAction.ts b/packages/providers/src/constants/rpcAction.ts index 9f0ab137d..b2b8b607e 100644 --- a/packages/providers/src/constants/rpcAction.ts +++ b/packages/providers/src/constants/rpcAction.ts @@ -61,6 +61,7 @@ export enum RPCAction { DELETE_VERIFIABLE_CREDENTIAL = "rpc/credentials/deleteVerifiableCredential", DELETE_ALL_VERIFIABLE_CREDENTIALS = "rpc/credentials/deleteAllVerifiableCredentials", GENERATE_VERIFIABLE_PRESENTATION = "rpc/credentials/generateVerifiablePresentation", + GENERATE_VERIFIABLE_PRESENTATION_WITH_CRYPTKEEPER = "rpc/credentials/generateVerifiablePresentationWithCryptkeeper", GENERATE_VERIFIABLE_PRESENTATION_REQUEST = "rpc/credentials/generateVerifiablePresentationRequest", REJECT_VERIFIABLE_PRESENTATION_REQUEST = "rpc/credentials/rejectVerifiablePresentationRequest", GENERATE_RLN_PROOF = "rpc/proofs/generate-rln-proof",