From db061521c01927300f9b8e15aaf4781d61cf6067 Mon Sep 17 00:00:00 2001 From: Anton <14254374+0xmad@users.noreply.github.com> Date: Wed, 13 Sep 2023 08:36:42 -0500 Subject: [PATCH] feat: join group ui (#859) - [x] Add join group page - [x] Add bandada site url - [x] Support history events and notifications for group join - [x] Emit event when joining the group - [x] Check group membership before joining - [x] Support user reject event - [x] Set max failures to zero for e2e --------- Co-authored-by: 0xmad <0xmad@users.noreply.github.com> --- .env.example | 1 + .github/workflows/build.yml | 1 + .github/workflows/check.yml | 1 + .github/workflows/deployDocs.yml | 16 +- .github/workflows/e2e.yml | 1 + .github/workflows/healthCheck.yml | 1 + .github/workflows/publishChrome.yml | 1 + .github/workflows/publishFirefox.yml | 1 + .github/workflows/publishPackages.yml | 1 + .github/workflows/release.yml | 1 + .github/workflows/webExtLint.yml | 1 + packages/app/src/background/contentScript.ts | 30 ++- .../__tests__/browserUtils.test.ts | 17 +- .../background/controllers/browserUtils.ts | 12 + packages/app/src/background/cryptKeeper.ts | 8 +- .../services/bandada/BandadaService.ts | 26 ++- .../bandada/__tests__/BandadaService.test.ts | 44 +++- .../background/services/credentials/index.ts | 10 +- .../background/services/group/GroupService.ts | 73 ++++++- .../group/__tests__/GroupService.test.ts | 56 ++++- .../history/__tests__/history.test.ts | 56 ++--- .../src/background/services/history/index.ts | 3 +- .../src/background/services/history/types.ts | 5 +- .../background/services/zkIdentity/index.ts | 11 +- packages/app/src/config/__tests__/env.test.ts | 8 +- packages/app/src/config/env.ts | 4 + packages/app/src/config/mock/zk.ts | 24 +- packages/app/src/constants/paths.ts | 1 + packages/app/src/setupTests.ts | 6 +- packages/app/src/types/history/index.ts | 4 +- .../app/src/ui/ducks/__tests__/groups.test.ts | 40 ++++ .../src/ui/ducks/__tests__/requests.test.tsx | 28 ++- packages/app/src/ui/ducks/groups.ts | 22 ++ packages/app/src/ui/ducks/requests.ts | 13 +- .../src/ui/hooks/url/__tests__/url.test.ts | 51 +++++ .../hooks/url/__tests__/useUrlParam.test.ts | 34 --- packages/app/src/ui/hooks/url/index.ts | 8 +- .../__tests__/CreateIdentity.test.tsx | 5 - .../ActivityList/Item/ActivityListItem.tsx | 19 ++ .../__tests__/ActivityList.test.tsx | 25 +-- .../__tests__/ActivityListItem.test.tsx | 28 +-- .../__tests__/useActivityList.test.ts | 25 +-- .../app/src/ui/pages/JoinGroup/JoinGroup.tsx | 181 ++++++++++++++++ .../JoinGroup/__tests__/JoinGroup.test.tsx | 138 ++++++++++++ .../JoinGroup/__tests__/useJoinGroup.test.ts | 205 ++++++++++++++++++ packages/app/src/ui/pages/JoinGroup/index.ts | 3 + .../src/ui/pages/JoinGroup/useJoinGroup.ts | 126 +++++++++++ packages/app/src/ui/pages/Popup/Popup.tsx | 2 + packages/app/src/ui/pages/Popup/usePopup.ts | 1 + .../RevealIdentityCommitment.test.tsx | 4 - .../useRevealIdentityCommitment.test.ts | 8 +- .../useRevealIdentityCommitment.ts | 11 +- packages/app/src/ui/popup.tsx | 4 +- .../app/src/util/__tests__/groups.test.ts | 10 + packages/app/src/util/groups.ts | 4 + packages/demo/useCryptKeeper.ts | 16 +- packages/e2e/playwright.config.ts | 1 + packages/providers/src/constants/rpcAction.ts | 6 +- packages/providers/src/event/index.ts | 2 +- packages/providers/src/event/types.ts | 22 +- packages/providers/src/index.ts | 2 +- .../src/sdk/CryptKeeperInjectedProvider.ts | 9 +- packages/types/src/group/index.ts | 14 +- packages/types/src/index.ts | 4 +- packages/types/src/request/index.ts | 5 + 65 files changed, 1256 insertions(+), 244 deletions(-) create mode 100644 packages/app/src/ui/ducks/__tests__/groups.test.ts create mode 100644 packages/app/src/ui/ducks/groups.ts create mode 100644 packages/app/src/ui/hooks/url/__tests__/url.test.ts delete mode 100644 packages/app/src/ui/hooks/url/__tests__/useUrlParam.test.ts create mode 100644 packages/app/src/ui/pages/JoinGroup/JoinGroup.tsx create mode 100644 packages/app/src/ui/pages/JoinGroup/__tests__/JoinGroup.test.tsx create mode 100644 packages/app/src/ui/pages/JoinGroup/__tests__/useJoinGroup.test.ts create mode 100644 packages/app/src/ui/pages/JoinGroup/index.ts create mode 100644 packages/app/src/ui/pages/JoinGroup/useJoinGroup.ts create mode 100644 packages/app/src/util/__tests__/groups.test.ts create mode 100644 packages/app/src/util/groups.ts diff --git a/.env.example b/.env.example index ec1baa78e..992f99cfc 100644 --- a/.env.example +++ b/.env.example @@ -10,6 +10,7 @@ ALCHEMY_API_KEY= FREIGHT_TRUST_NETWORK= PULSECHAIN_API_KEY= BANDADA_API_URL=https://api.bandada.pse.dev +BANDADA_URL=https://bandada.pse.dev TARGET=chrome DEMO_URL=https://ckdemo.appliedzkp.org MANIFEST_VERSION=3 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a65c89532..6f8730bed 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,6 +19,7 @@ env: FREIGHT_TRUST_NETWORK: ${{ secrets.FREIGHT_TRUST_NETWORK }} PULSECHAIN_API_KEY: ${{ secrets.PULSECHAIN_API_KEY }} BANDADA_API_URL: ${{ vars.BANDADA_API_URL }} + BANDADA_URL: ${{ vars.BANDADA_URL }} TARGET: "chrome" DEMO_URL: ${{ vars.DEMO_URL }} MERKLE_MOCK_SERVER: ${{ vars.MERKLE_MOCK_SERVER }} diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index c90ac1a62..fbf6a5a06 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -19,6 +19,7 @@ env: FREIGHT_TRUST_NETWORK: ${{ secrets.FREIGHT_TRUST_NETWORK }} PULSECHAIN_API_KEY: ${{ secrets.PULSECHAIN_API_KEY }} BANDADA_API_URL: ${{ vars.BANDADA_API_URL }} + BANDADA_URL: ${{ vars.BANDADA_URL }} TARGET: "chrome" DEMO_URL: ${{ vars.DEMO_URL }} MERKLE_MOCK_SERVER: ${{ vars.MERKLE_MOCK_SERVER }} diff --git a/.github/workflows/deployDocs.yml b/.github/workflows/deployDocs.yml index d9a01caca..07f8f13d7 100644 --- a/.github/workflows/deployDocs.yml +++ b/.github/workflows/deployDocs.yml @@ -4,7 +4,7 @@ on: branches: [main] paths: - "docs/**" - + workflow_dispatch: inputs: build: @@ -13,8 +13,8 @@ on: default: "enable" type: choice options: - - enable - - disable + - enable + - disable permissions: contents: read @@ -26,20 +26,20 @@ concurrency: cancel-in-progress: true defaults: - run: - working-directory: ./docs + run: + working-directory: ./docs jobs: build: runs-on: ubuntu-22.04 - + steps: - uses: actions/checkout@v4 - name: Setup mdBook uses: peaceiris/actions-mdbook@v1 with: - mdbook-version: 'latest' + mdbook-version: "latest" - name: Setup Pages id: pages @@ -57,7 +57,7 @@ jobs: environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} - + runs-on: ubuntu-22.04 needs: build steps: diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 0e572021a..a38c0723d 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -20,6 +20,7 @@ env: FREIGHT_TRUST_NETWORK: ${{ secrets.FREIGHT_TRUST_NETWORK }} PULSECHAIN_API_KEY: ${{ secrets.PULSECHAIN_API_KEY }} BANDADA_API_URL: ${{ vars.BANDADA_API_URL }} + BANDADA_URL: ${{ vars.BANDADA_URL }} TARGET: "chrome" DEMO_URL: ${{ vars.DEMO_URL }} MERKLE_MOCK_SERVER: ${{ vars.MERKLE_MOCK_SERVER }} diff --git a/.github/workflows/healthCheck.yml b/.github/workflows/healthCheck.yml index 589a0eea6..8115228d1 100644 --- a/.github/workflows/healthCheck.yml +++ b/.github/workflows/healthCheck.yml @@ -17,6 +17,7 @@ env: FREIGHT_TRUST_NETWORK: ${{ secrets.FREIGHT_TRUST_NETWORK }} PULSECHAIN_API_KEY: ${{ secrets.PULSECHAIN_API_KEY }} BANDADA_API_URL: ${{ vars.BANDADA_API_URL }} + BANDADA_URL: ${{ vars.BANDADA_URL }} TARGET: "chrome" DEMO_URL: ${{ vars.DEMO_URL }} MERKLE_MOCK_SERVER: ${{ vars.MERKLE_MOCK_SERVER }} diff --git a/.github/workflows/publishChrome.yml b/.github/workflows/publishChrome.yml index c13d65926..1896176a0 100644 --- a/.github/workflows/publishChrome.yml +++ b/.github/workflows/publishChrome.yml @@ -17,6 +17,7 @@ env: FREIGHT_TRUST_NETWORK: ${{ secrets.FREIGHT_TRUST_NETWORK }} PULSECHAIN_API_KEY: ${{ secrets.PULSECHAIN_API_KEY }} BANDADA_API_URL: ${{ vars.BANDADA_API_URL }} + BANDADA_URL: ${{ vars.BANDADA_URL }} TARGET: "chrome" DEMO_URL: ${{ vars.DEMO_URL }} MERKLE_MOCK_SERVER: ${{ vars.MERKLE_MOCK_SERVER }} diff --git a/.github/workflows/publishFirefox.yml b/.github/workflows/publishFirefox.yml index c3bd9829e..c610c0d79 100644 --- a/.github/workflows/publishFirefox.yml +++ b/.github/workflows/publishFirefox.yml @@ -17,6 +17,7 @@ env: FREIGHT_TRUST_NETWORK: ${{ secrets.FREIGHT_TRUST_NETWORK }} PULSECHAIN_API_KEY: ${{ secrets.PULSECHAIN_API_KEY }} BANDADA_API_URL: ${{ vars.BANDADA_API_URL }} + BANDADA_URL: ${{ vars.BANDADA_URL }} TARGET: "firefox" DEMO_URL: ${{ vars.DEMO_URL }} MERKLE_MOCK_SERVER: ${{ vars.MERKLE_MOCK_SERVER }} diff --git a/.github/workflows/publishPackages.yml b/.github/workflows/publishPackages.yml index d42cd4f0f..753c2b174 100644 --- a/.github/workflows/publishPackages.yml +++ b/.github/workflows/publishPackages.yml @@ -17,6 +17,7 @@ env: FREIGHT_TRUST_NETWORK: ${{ secrets.FREIGHT_TRUST_NETWORK }} PULSECHAIN_API_KEY: ${{ secrets.PULSECHAIN_API_KEY }} BANDADA_API_URL: ${{ vars.BANDADA_API_URL }} + BANDADA_URL: ${{ vars.BANDADA_URL }} TARGET: "chrome" DEMO_URL: ${{ vars.DEMO_URL }} MERKLE_MOCK_SERVER: ${{ vars.MERKLE_MOCK_SERVER }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dd2113502..693983292 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,6 +18,7 @@ env: FREIGHT_TRUST_NETWORK: ${{ secrets.FREIGHT_TRUST_NETWORK }} PULSECHAIN_API_KEY: ${{ secrets.PULSECHAIN_API_KEY }} BANDADA_API_URL: ${{ vars.BANDADA_API_URL }} + BANDADA_URL: ${{ vars.BANDADA_URL }} TARGET: "chrome" DEMO_URL: ${{ vars.DEMO_URL }} MERKLE_MOCK_SERVER: ${{ vars.MERKLE_MOCK_SERVER }} diff --git a/.github/workflows/webExtLint.yml b/.github/workflows/webExtLint.yml index 3e807168b..ba3705429 100644 --- a/.github/workflows/webExtLint.yml +++ b/.github/workflows/webExtLint.yml @@ -19,6 +19,7 @@ env: FREIGHT_TRUST_NETWORK: ${{ secrets.FREIGHT_TRUST_NETWORK }} PULSECHAIN_API_KEY: ${{ secrets.PULSECHAIN_API_KEY }} BANDADA_API_URL: ${{ vars.BANDADA_API_URL }} + BANDADA_URL: ${{ vars.BANDADA_URL }} TARGET: "firefox" DEMO_URL: ${{ vars.DEMO_URL }} MERKLE_MOCK_SERVER: ${{ vars.MERKLE_MOCK_SERVER }} diff --git a/packages/app/src/background/contentScript.ts b/packages/app/src/background/contentScript.ts index 7030d0d3e..26c7b7220 100644 --- a/packages/app/src/background/contentScript.ts +++ b/packages/app/src/background/contentScript.ts @@ -5,7 +5,12 @@ import browser from "webextension-polyfill"; import { setStatus } from "@src/ui/ducks/app"; import { setConnectedIdentity } from "@src/ui/ducks/identities"; -import type { IInjectedMessageData, IReduxAction, ConnectedIdentityMetadata } from "@cryptkeeperzk/types"; +import type { + IInjectedMessageData, + IReduxAction, + ConnectedIdentityMetadata, + IRejectedRequest, +} from "@cryptkeeperzk/types"; function injectScript() { const url = browser.runtime.getURL("js/injected.js"); @@ -69,23 +74,34 @@ function injectScript() { ); break; } - case EventName.REJECT_VERIFIABLE_CREDENTIAL: { + case EventName.REVEAL_COMMITMENT: { window.postMessage( { target: "injected-injectedscript", - payload: [null], - nonce: EventName.REJECT_VERIFIABLE_CREDENTIAL, + payload: [null, action.payload as { commitment: string }], + nonce: EventName.REVEAL_COMMITMENT, }, "*", ); break; } - case EventName.REVEAL_COMMITMENT: { + case EventName.JOIN_GROUP: { window.postMessage( { target: "injected-injectedscript", - payload: [null, action.payload as { commitment: string }], - nonce: EventName.REVEAL_COMMITMENT, + payload: [null, action.payload as { groupId: string }], + nonce: EventName.JOIN_GROUP, + }, + "*", + ); + break; + } + case EventName.USER_REJECT: { + window.postMessage( + { + target: "injected-injectedscript", + payload: [null, action.payload as IRejectedRequest], + nonce: EventName.USER_REJECT, }, "*", ); diff --git a/packages/app/src/background/controllers/__tests__/browserUtils.test.ts b/packages/app/src/background/controllers/__tests__/browserUtils.test.ts index cce9a8471..b8285ee4f 100644 --- a/packages/app/src/background/controllers/__tests__/browserUtils.test.ts +++ b/packages/app/src/background/controllers/__tests__/browserUtils.test.ts @@ -1,11 +1,13 @@ +/* eslint-disable @typescript-eslint/unbound-method */ import browser from "webextension-polyfill"; import BrowserUtils from "../browserUtils"; describe("background/controllers/browserUtils", () => { const defaultTabs = [ - { id: 1, active: true, highlighted: true }, - { id: 2, active: true, highlighted: false }, + { id: 1, active: true, highlighted: true, url: "http://localhost:3000" }, + { id: 2, active: true, highlighted: false, url: "http://localhost:3000" }, + { id: 3, active: true, highlighted: false }, ]; const defaultPopupTab = { id: 3, active: true, highlighted: true }; @@ -50,9 +52,7 @@ describe("background/controllers/browserUtils", () => { browserUtils.addRemoveWindowListener(callback); browserUtils.removeRemoveWindowListener(callback); - // eslint-disable-next-line @typescript-eslint/unbound-method expect(browser.windows.onRemoved.addListener).toBeCalledTimes(2); - // eslint-disable-next-line @typescript-eslint/unbound-method expect(browser.windows.onRemoved.removeListener).toBeCalledTimes(1); }); @@ -61,7 +61,14 @@ describe("background/controllers/browserUtils", () => { await browserUtils.clearStorage(); - // eslint-disable-next-line @typescript-eslint/unbound-method expect(browser.storage.sync.clear).toBeCalledTimes(1); }); + + test("should push event properly", async () => { + const browserUtils = BrowserUtils.getInstance(); + + await browserUtils.pushEvent({ type: "type" }, { urlOrigin: "http://localhost:3000" }); + + expect(browser.tabs.sendMessage).toBeCalledTimes(2); + }); }); diff --git a/packages/app/src/background/controllers/browserUtils.ts b/packages/app/src/background/controllers/browserUtils.ts index 6f9348961..d33870ce6 100644 --- a/packages/app/src/background/controllers/browserUtils.ts +++ b/packages/app/src/background/controllers/browserUtils.ts @@ -1,5 +1,7 @@ import browser, { Windows } from "webextension-polyfill"; +import type { IReduxAction, IZkMetadata } from "@cryptkeeperzk/types"; + interface CreateWindowArgs { type: "popup"; focused: boolean; @@ -86,6 +88,16 @@ export default class BrowserUtils { browser.windows.onRemoved.removeListener(callback); }; + pushEvent = async (action: IReduxAction, meta?: IZkMetadata): Promise => { + const tabs = await browser.tabs + .query({}) + .then((browserTabs) => + browserTabs.filter(({ url }) => (url && meta?.urlOrigin ? new URL(url).origin === meta.urlOrigin : false)), + ); + + await Promise.all(tabs.map((tab) => browser.tabs.sendMessage(tab.id!, action).catch(() => undefined))); + }; + private createTab = async (options: CreateTabArgs) => browser.tabs.create(options); private createWindow = async (options: CreateWindowArgs) => browser.windows.create(options); diff --git a/packages/app/src/background/cryptKeeper.ts b/packages/app/src/background/cryptKeeper.ts index c00f988af..6ee4dbdec 100644 --- a/packages/app/src/background/cryptKeeper.ts +++ b/packages/app/src/background/cryptKeeper.ts @@ -41,7 +41,7 @@ const RPC_METHOD_ACCESS: Record = { [RPCAction.ADD_VERIFIABLE_CREDENTIAL_REQUEST]: true, [RPCAction.REVEAL_CONNECTED_IDENTITY_COMMITMENT_REQUEST]: true, [RPCAction.JOIN_GROUP_REQUEST]: true, - [RPCAction.GENERATE_GROUP_MEMBERSHIP_PROOF_REQUEST]: true, + [RPCAction.GENERATE_GROUP_MERKLE_PROOF]: true, }; Object.freeze(RPC_METHOD_ACCESS); @@ -173,10 +173,11 @@ export default class CryptKeeperController { // Groups this.handler.add(RPCAction.JOIN_GROUP, this.lockService.ensure, this.groupService.joinGroup); this.handler.add( - RPCAction.GENERATE_GROUP_MEMBERSHIP_PROOF, + RPCAction.GENERATE_GROUP_MERKLE_PROOF, this.lockService.ensure, - this.groupService.generateGroupMembershipProof, + this.groupService.generateGroupMerkleProof, ); + this.handler.add(RPCAction.CHECK_GROUP_MEMBERSHIP, this.lockService.ensure, this.groupService.checkGroupMembership); // History this.handler.add(RPCAction.GET_IDENTITY_HISTORY, this.lockService.ensure, this.historyService.getOperations); @@ -281,6 +282,7 @@ export default class CryptKeeperController { // Browser this.handler.add(RPCAction.CLOSE_POPUP, this.browserService.closePopup); this.handler.add(RPCAction.CLEAR_STORAGE, this.lockService.ensure, this.browserService.clearStorage); + this.handler.add(RPCAction.PUSH_EVENT, this.lockService.ensure, this.browserService.pushEvent); return this; }; diff --git a/packages/app/src/background/services/bandada/BandadaService.ts b/packages/app/src/background/services/bandada/BandadaService.ts index 232ceb85f..bec49f775 100644 --- a/packages/app/src/background/services/bandada/BandadaService.ts +++ b/packages/app/src/background/services/bandada/BandadaService.ts @@ -1,6 +1,13 @@ +import { hexToBigint } from "bigint-conversion"; + import { getBandadaApiUrl } from "@src/config/env"; -import type { IMerkleProof, IGenerateBandadaMerkleProofArgs, IAddBandadaGroupMemberArgs } from "@cryptkeeperzk/types"; +import type { + IMerkleProof, + IGenerateBandadaMerkleProofArgs, + IAddBandadaGroupMemberArgs, + ICheckBandadaGroupMembershipArgs, +} from "@cryptkeeperzk/types"; const API_URL = getBandadaApiUrl(); @@ -23,7 +30,7 @@ export class BandadaService { return BandadaService.INSTANCE; }; - async addMember({ groupId, commitment, apiKey, inviteCode }: IAddBandadaGroupMemberArgs): Promise { + async addMember({ groupId, identity, apiKey, inviteCode }: IAddBandadaGroupMemberArgs): Promise { if (!apiKey && !inviteCode) { throw new Error("Provide api key or invide code"); } @@ -32,7 +39,7 @@ export class BandadaService { throw new Error("Don't provide both api key and invide code"); } - const response = await fetch(`${API_URL}/groups/${groupId}/members/${commitment}`, { + const response = await fetch(`${API_URL}/groups/${groupId}/members/${hexToBigint(identity.commitment)}`, { method: "POST", headers: apiKey ? { ...DEFAULT_HEADERS, "x-api-key": apiKey } : DEFAULT_HEADERS, body: inviteCode ? JSON.stringify({ inviteCode }) : undefined, @@ -47,8 +54,17 @@ export class BandadaService { throw new Error(result.message.toString()); } - async generateMerkleProof({ groupId, commitment }: IGenerateBandadaMerkleProofArgs): Promise { - const response = await fetch(`${API_URL}/groups/${groupId}/members/${commitment}/proof`, { + async checkGroupMembership({ identity, groupId }: ICheckBandadaGroupMembershipArgs): Promise { + const response = await fetch(`${API_URL}/groups/${groupId}/members/${hexToBigint(identity.commitment)}`, { + method: "GET", + headers: DEFAULT_HEADERS, + }).then((res) => res.json() as Promise); + + return JSON.parse(response) as boolean; + } + + async generateMerkleProof({ groupId, identity }: IGenerateBandadaMerkleProofArgs): Promise { + const response = await fetch(`${API_URL}/groups/${groupId}/members/${hexToBigint(identity.commitment)}/proof`, { method: "GET", headers: DEFAULT_HEADERS, }); diff --git a/packages/app/src/background/services/bandada/__tests__/BandadaService.test.ts b/packages/app/src/background/services/bandada/__tests__/BandadaService.test.ts index 1c75209b7..c74174885 100644 --- a/packages/app/src/background/services/bandada/__tests__/BandadaService.test.ts +++ b/packages/app/src/background/services/bandada/__tests__/BandadaService.test.ts @@ -1,17 +1,29 @@ -import type { IMerkleProof, IGenerateBandadaMerkleProofArgs, IAddBandadaGroupMemberArgs } from "@cryptkeeperzk/types"; +import { mockDefaultIdentity } from "@src/config/mock/zk"; + +import type { + IMerkleProof, + IGenerateBandadaMerkleProofArgs, + IAddBandadaGroupMemberArgs, + ICheckBandadaGroupMembershipArgs, +} from "@cryptkeeperzk/types"; import { BandadaService } from ".."; describe("background/services/bandada/BandadaService", () => { const defaultGenerateProofArgs: IGenerateBandadaMerkleProofArgs = { groupId: "90694543209366256629502773954857", - commitment: "1234", + identity: mockDefaultIdentity, }; const defaultAddMemberArgs: IAddBandadaGroupMemberArgs = { groupId: "90694543209366256629502773954857", - commitment: "1234", apiKey: "key", + identity: mockDefaultIdentity, + }; + + const defaultCheckMembershipArgs: ICheckBandadaGroupMembershipArgs = { + groupId: "90694543209366256629502773954857", + identity: mockDefaultIdentity, }; const defaultMerkleProof: IMerkleProof = { @@ -116,4 +128,30 @@ describe("background/services/bandada/BandadaService", () => { await expect(service.generateMerkleProof(defaultGenerateProofArgs)).rejects.toThrowError("Error"); expect(fetchSpy).toBeCalledTimes(1); }); + + test("should check membership properly", async () => { + const fetchSpy = jest.spyOn(global, "fetch").mockResolvedValue({ + ok: true, + json: () => Promise.resolve("true"), + } as Response); + const service = BandadaService.getInstance(); + + const result = await service.checkGroupMembership(defaultCheckMembershipArgs); + + expect(fetchSpy).toBeCalledTimes(1); + expect(result).toBe(true); + }); + + test("should check membership properly if user is not a member", async () => { + const fetchSpy = jest.spyOn(global, "fetch").mockResolvedValue({ + ok: true, + json: () => Promise.resolve("false"), + } as Response); + const service = BandadaService.getInstance(); + + const result = await service.checkGroupMembership(defaultCheckMembershipArgs); + + expect(fetchSpy).toBeCalledTimes(1); + expect(result).toBe(false); + }); }); diff --git a/packages/app/src/background/services/credentials/index.ts b/packages/app/src/background/services/credentials/index.ts index 4a9dcf1c7..931303f95 100644 --- a/packages/app/src/background/services/credentials/index.ts +++ b/packages/app/src/background/services/credentials/index.ts @@ -1,4 +1,5 @@ import { EventName } from "@cryptkeeperzk/providers"; +import { RejectRequests } from "@cryptkeeperzk/providers/dist/src/event"; import browser from "webextension-polyfill"; import BrowserUtils from "@src/background/controllers/browserUtils"; @@ -59,21 +60,22 @@ export default class VerifiableCredentialsService implements IBackupable { }; rejectVerifiableCredentialRequest = async (): Promise => { - await this.historyService.trackOperation(OperationType.REJECT_VERIFIABLE_CREDENTIAL_REQUEST, {}); await this.notificationService.create({ options: { - title: "Request to add Verifiable Credential rejected", - message: `Rejected a request to add 1 Verifiable Credential.`, + title: "Request rejected", + message: `Rejected a request to add Verifiable Credential.`, 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.REJECT_VERIFIABLE_CREDENTIAL, + type: EventName.USER_REJECT, + payload: { type: RejectRequests.ADD_VERIFIABLE_CREDENTIAL }, }) .catch(() => undefined), ), diff --git a/packages/app/src/background/services/group/GroupService.ts b/packages/app/src/background/services/group/GroupService.ts index ff3af36a9..057d2fb13 100644 --- a/packages/app/src/background/services/group/GroupService.ts +++ b/packages/app/src/background/services/group/GroupService.ts @@ -1,7 +1,20 @@ +import { EventName } from "@cryptkeeperzk/providers"; +import browser from "webextension-polyfill"; + +import BrowserUtils from "@src/background/controllers/browserUtils"; import { BandadaService } from "@src/background/services/bandada"; +import HistoryService from "@src/background/services/history"; +import NotificationService from "@src/background/services/notification"; import ZkIdentityService from "@src/background/services/zkIdentity"; +import { OperationType } from "@src/types"; -import type { IGenerateGroupMerkleProofArgs, IJoinGroupMemberArgs, IMerkleProof } from "@cryptkeeperzk/types"; +import type { + ICheckGroupMembershipArgs, + IGenerateGroupMerkleProofArgs, + IIdentityData, + IJoinGroupMemberArgs, + IMerkleProof, +} from "@cryptkeeperzk/types"; export class GroupService { private static INSTANCE?: GroupService; @@ -10,9 +23,18 @@ export class GroupService { private zkIdentityService: ZkIdentityService; + private historyService: HistoryService; + + private notificationService: NotificationService; + + private browserController: BrowserUtils; + private constructor() { this.bandadaSevice = BandadaService.getInstance(); this.zkIdentityService = ZkIdentityService.getInstance(); + this.historyService = HistoryService.getInstance(); + this.notificationService = NotificationService.getInstance(); + this.browserController = BrowserUtils.getInstance(); } static getInstance(): GroupService { @@ -24,22 +46,53 @@ export class GroupService { } joinGroup = async ({ groupId, apiKey, inviteCode }: IJoinGroupMemberArgs): Promise => { - const commitment = await this.zkIdentityService.getConnectedIdentityCommitment(); + const identity = await this.getConnectedIdentity(); - if (!commitment) { - throw new Error("No connected identity found"); - } + const result = await this.bandadaSevice.addMember({ groupId, apiKey, inviteCode, identity }); + + await this.historyService.trackOperation(OperationType.JOIN_GROUP, { identity, group: { id: groupId } }); + await this.notificationService.create({ + options: { + title: "Joined group", + message: "You've been successfully joined the group", + iconUrl: browser.runtime.getURL("/icons/logo.png"), + type: "basic", + }, + }); + + await this.browserController.pushEvent( + { type: EventName.JOIN_GROUP, payload: { groupId } }, + { urlOrigin: identity.metadata.host! }, + ); + + return result; + }; + + generateGroupMerkleProof = async ({ groupId }: IGenerateGroupMerkleProofArgs): Promise => { + const identity = await this.getConnectedIdentity(); + + return this.bandadaSevice.generateMerkleProof({ groupId, identity }); + }; + + checkGroupMembership = async ({ groupId }: ICheckGroupMembershipArgs): Promise => { + const identity = await this.getConnectedIdentity(); - return this.bandadaSevice.addMember({ groupId, apiKey, inviteCode, commitment }); + return this.bandadaSevice.checkGroupMembership({ groupId, identity }); }; - generateGroupMembershipProof = async ({ groupId }: IGenerateGroupMerkleProofArgs): Promise => { - const commitment = await this.zkIdentityService.getConnectedIdentityCommitment(); + private getConnectedIdentity = async (): Promise => { + const [commitment, identity] = await Promise.all([ + this.zkIdentityService.getConnectedIdentityCommitment(), + this.zkIdentityService.getConnectedIdentity(), + ]); - if (!commitment) { + if (!commitment || !identity) { throw new Error("No connected identity found"); } - return this.bandadaSevice.generateMerkleProof({ groupId, commitment }); + return { + commitment, + metadata: identity.metadata, + }; }; } diff --git a/packages/app/src/background/services/group/__tests__/GroupService.test.ts b/packages/app/src/background/services/group/__tests__/GroupService.test.ts index 07e1d8a6e..b4e1ce81c 100644 --- a/packages/app/src/background/services/group/__tests__/GroupService.test.ts +++ b/packages/app/src/background/services/group/__tests__/GroupService.test.ts @@ -1,16 +1,23 @@ -import { defaultMerkleProof, mockDefaultIdentityCommitment } from "@src/config/mock/zk"; +import { defaultMerkleProof, mockDefaultIdentity, mockDefaultIdentityCommitment } from "@src/config/mock/zk"; -import type { IGenerateGroupMerkleProofArgs, IJoinGroupMemberArgs } from "@cryptkeeperzk/types"; +import type { + ICheckGroupMembershipArgs, + IGenerateGroupMerkleProofArgs, + IIdentityData, + IJoinGroupMemberArgs, +} from "@cryptkeeperzk/types"; import { GroupService } from ".."; const mockGetConnectedIdentityCommitment = jest.fn(() => Promise.resolve(mockDefaultIdentityCommitment)); +const mockGetConnectedIdentity = jest.fn(() => Promise.resolve(mockDefaultIdentity)); jest.mock("@src/background/services/bandada", (): unknown => ({ BandadaService: { getInstance: jest.fn(() => ({ addMember: jest.fn(() => Promise.resolve(true)), generateMerkleProof: jest.fn(() => Promise.resolve(defaultMerkleProof)), + checkGroupMembership: jest.fn(() => Promise.resolve(true)), })), }, })); @@ -18,12 +25,28 @@ jest.mock("@src/background/services/bandada", (): unknown => ({ jest.mock("@src/background/services/zkIdentity", (): unknown => ({ getInstance: jest.fn(() => ({ getConnectedIdentityCommitment: mockGetConnectedIdentityCommitment, + getConnectedIdentity: mockGetConnectedIdentity, + })), +})); + +jest.mock("@src/background/services/history", (): unknown => ({ + getInstance: jest.fn(() => ({ + loadSettings: jest.fn(), + trackOperation: jest.fn(), + })), +})); + +jest.mock("@src/background/services/notification", (): unknown => ({ + getInstance: jest.fn(() => ({ + create: jest.fn(), })), })); describe("background/services/group/GroupService", () => { beforeEach(() => { mockGetConnectedIdentityCommitment.mockResolvedValue(mockDefaultIdentityCommitment); + + mockGetConnectedIdentity.mockResolvedValue(mockDefaultIdentity); }); afterEach(() => { @@ -60,18 +83,39 @@ describe("background/services/group/GroupService", () => { test("should generate proof properly ", async () => { const service = GroupService.getInstance(); - const result = await service.generateGroupMembershipProof(defaultArgs); + const result = await service.generateGroupMerkleProof(defaultArgs); expect(result).toStrictEqual(defaultMerkleProof); }); test("should throw error if there is no connected identity", async () => { mockGetConnectedIdentityCommitment.mockResolvedValue(""); + mockGetConnectedIdentity.mockResolvedValue(undefined as unknown as IIdentityData); + const service = GroupService.getInstance(); + + await expect(service.generateGroupMerkleProof(defaultArgs)).rejects.toThrowError("No connected identity found"); + }); + }); + + describe("check group membership", () => { + const defaultArgs: ICheckGroupMembershipArgs = { + groupId: "90694543209366256629502773954857", + }; + + test("should check membership properly ", async () => { + const service = GroupService.getInstance(); + + const result = await service.checkGroupMembership(defaultArgs); + + expect(result).toBe(true); + }); + + test("should throw error if there is no connected identity", async () => { + mockGetConnectedIdentityCommitment.mockResolvedValue(""); + mockGetConnectedIdentity.mockResolvedValue(undefined as unknown as IIdentityData); const service = GroupService.getInstance(); - await expect(service.generateGroupMembershipProof(defaultArgs)).rejects.toThrowError( - "No connected identity found", - ); + await expect(service.checkGroupMembership(defaultArgs)).rejects.toThrowError("No connected identity found"); }); }); }); diff --git a/packages/app/src/background/services/history/__tests__/history.test.ts b/packages/app/src/background/services/history/__tests__/history.test.ts index 47a45e0c8..7e023b3df 100644 --- a/packages/app/src/background/services/history/__tests__/history.test.ts +++ b/packages/app/src/background/services/history/__tests__/history.test.ts @@ -1,5 +1,5 @@ -import { ZERO_ADDRESS } from "@src/config/const"; import { getEnabledFeatures } from "@src/config/features"; +import { mockDefaultGroup, mockDefaultIdentity } from "@src/config/mock/zk"; import { Operation, OperationType } from "@src/types"; import HistoryService from ".."; @@ -9,46 +9,26 @@ const mockDefaultOperations: Operation[] = [ { id: "1", type: OperationType.CREATE_IDENTITY, - identity: { - commitment: "1234", - metadata: { - identityStrategy: "random", - account: ZERO_ADDRESS, - name: "Account #1", - groups: [], - host: "http://localhost:3000", - }, - }, + identity: mockDefaultIdentity, createdAt: new Date().toISOString(), }, { id: "2", type: OperationType.CREATE_IDENTITY, - identity: { - commitment: "1234", - metadata: { - identityStrategy: "interep", - account: ZERO_ADDRESS, - name: "Account #2", - groups: [], - host: "http://localhost:3000", - }, - }, + identity: mockDefaultIdentity, createdAt: new Date().toISOString(), }, { id: "3", type: OperationType.DELETE_IDENTITY, - identity: { - commitment: "1234", - metadata: { - identityStrategy: "interep", - account: ZERO_ADDRESS, - name: "Account #3", - groups: [], - host: "http://localhost:3000", - }, - }, + identity: mockDefaultIdentity, + createdAt: new Date().toISOString(), + }, + { + id: "4", + type: OperationType.JOIN_GROUP, + identity: { ...mockDefaultIdentity, metadata: { ...mockDefaultIdentity.metadata, identityStrategy: "random" } }, + group: mockDefaultGroup, createdAt: new Date().toISOString(), }, ]; @@ -100,7 +80,7 @@ describe("background/services/history", () => { const { operations, settings } = await service.loadOperations(); - expect(operations).toHaveLength(3); + expect(operations).toHaveLength(4); expect(settings).toStrictEqual(mockDefaultSettings); expect(service.getOperations()).toStrictEqual(operations); }); @@ -112,7 +92,7 @@ describe("background/services/history", () => { const { operations, settings } = await service.loadOperations(); - expect(operations).toHaveLength(3); + expect(operations).toHaveLength(4); expect(settings).toStrictEqual(mockDefaultSettings); expect(service.getOperations()).toStrictEqual(operations); expect(service.getSettings()).toStrictEqual(mockDefaultSettings); @@ -138,9 +118,9 @@ describe("background/services/history", () => { const { operations, settings } = await service.loadOperations(); - expect(operations).toHaveLength(3); + expect(operations).toHaveLength(4); expect(settings).toStrictEqual({ isEnabled: false }); - expect(service.getOperations()).toHaveLength(3); + expect(service.getOperations()).toHaveLength(4); }); test("should load history operations properly if the store is empty", async () => { @@ -183,7 +163,7 @@ describe("background/services/history", () => { }); const cachedOperations = service.getOperations(); - expect(cachedOperations).toHaveLength(4); + expect(cachedOperations).toHaveLength(5); }); test("should not track operation if history is disabled", async () => { @@ -197,7 +177,7 @@ describe("background/services/history", () => { }); const cachedOperations = service.getOperations(); - expect(cachedOperations).toHaveLength(3); + expect(cachedOperations).toHaveLength(4); }); test("should remove operation properly", async () => { @@ -209,7 +189,7 @@ describe("background/services/history", () => { await service.removeOperation(mockDefaultOperations[0].id); const cachedOperations = service.getOperations(); - expect(cachedOperations).toHaveLength(2); + expect(cachedOperations).toHaveLength(3); }); test("should enable/disable history properly", async () => { diff --git a/packages/app/src/background/services/history/index.ts b/packages/app/src/background/services/history/index.ts index 034e8564f..0f117fdff 100644 --- a/packages/app/src/background/services/history/index.ts +++ b/packages/app/src/background/services/history/index.ts @@ -87,7 +87,7 @@ export default class HistoryService { return this.settings; }; - trackOperation = async (type: OperationType, { identity }: OperationOptions): Promise => { + trackOperation = async (type: OperationType, { identity, group }: OperationOptions): Promise => { if (!this.settings?.isEnabled) { return; } @@ -96,6 +96,7 @@ export default class HistoryService { id: nanoid(), type, identity, + group, createdAt: new Date().toISOString(), }); diff --git a/packages/app/src/background/services/history/types.ts b/packages/app/src/background/services/history/types.ts index 7d5ce43a8..a1804b023 100644 --- a/packages/app/src/background/services/history/types.ts +++ b/packages/app/src/background/services/history/types.ts @@ -1,9 +1,10 @@ -import { IIdentityData } from "@cryptkeeperzk/types"; - import { OperationType, Operation, HistorySettings } from "@src/types"; +import type { IGroupData, IIdentityData } from "@cryptkeeperzk/types"; + export interface OperationOptions { identity?: IIdentityData; + group?: Partial; } export interface OperationFilter { diff --git a/packages/app/src/background/services/zkIdentity/index.ts b/packages/app/src/background/services/zkIdentity/index.ts index 52075e3d1..62d80d9ec 100644 --- a/packages/app/src/background/services/zkIdentity/index.ts +++ b/packages/app/src/background/services/zkIdentity/index.ts @@ -301,16 +301,11 @@ export default class ZkIdentityService implements IBackupable { throw new Error("No connected identity found"); } - const tabs = await browser.tabs.query({}); - const hostTabs = tabs.filter(({ url }) => (url ? new URL(url).origin === connectedIdentity.metadata.host : false)); const commitment = bigintToHex(connectedIdentity.genIdentityCommitment()); - await Promise.all( - hostTabs.map((tab) => - browser.tabs - .sendMessage(tab.id!, { type: EventName.REVEAL_COMMITMENT, payload: { commitment } }) - .catch(() => undefined), - ), + await this.browserController.pushEvent( + { type: EventName.REVEAL_COMMITMENT, payload: { commitment } }, + { urlOrigin: connectedIdentity.metadata.host! }, ); await this.historyService.trackOperation(OperationType.REVEAL_IDENTITY_COMMITMENT, { diff --git a/packages/app/src/config/__tests__/env.test.ts b/packages/app/src/config/__tests__/env.test.ts index f4256f754..98caeb4a9 100644 --- a/packages/app/src/config/__tests__/env.test.ts +++ b/packages/app/src/config/__tests__/env.test.ts @@ -1,4 +1,4 @@ -import { getApiKeys, getBandadaApiUrl, isDebugMode, isE2E } from "../env"; +import { getApiKeys, getBandadaApiUrl, getBandadaUrl, isDebugMode, isE2E } from "../env"; jest.unmock("@src/config/env"); @@ -10,6 +10,7 @@ describe("config/env", () => { process.env.PULSECHAIN_API_KEY = "pulseChain"; process.env.CRYPTKEEPER_DEBUG = "false"; process.env.BANDADA_API_URL = "https://api.bandada.pse.dev"; + process.env.BANDADA_URL = "https://bandada.pse.dev"; }); afterAll(() => { @@ -19,6 +20,7 @@ describe("config/env", () => { delete process.env.PULSECHAIN_API_KEY; delete process.env.CRYPTKEEPER_DEBUG; delete process.env.BANDADA_API_URL; + delete process.env.BANDADA_URL; }); test("should return env api config", () => { @@ -43,4 +45,8 @@ describe("config/env", () => { test("should return bandada api url", () => { expect(getBandadaApiUrl()).toBeDefined(); }); + + test("should return bandada url", () => { + expect(getBandadaUrl()).toBeDefined(); + }); }); diff --git a/packages/app/src/config/env.ts b/packages/app/src/config/env.ts index d047e399b..742703d75 100644 --- a/packages/app/src/config/env.ts +++ b/packages/app/src/config/env.ts @@ -10,6 +10,10 @@ export function getBandadaApiUrl(): string { return process.env.BANDADA_API_URL!; } +export function getBandadaUrl(): string { + return process.env.BANDADA_URL!; +} + export type Providers = "infura" | "alchemy" | "freightTrustNetwork" | "pulseChain"; export function getApiKeys(): Record { diff --git a/packages/app/src/config/mock/zk.ts b/packages/app/src/config/mock/zk.ts index c8b5790fe..9bd7e116d 100644 --- a/packages/app/src/config/mock/zk.ts +++ b/packages/app/src/config/mock/zk.ts @@ -1,6 +1,6 @@ -import { bigintToHex } from "bigint-conversion"; +import type { IGroupData, IIdentityData, IMerkleProof } from "@cryptkeeperzk/types"; -import type { IMerkleProof } from "@cryptkeeperzk/types"; +import { ZERO_ADDRESS } from "../const"; export const defaultMerkleProof: IMerkleProof = { root: "11390644220109896790698822461687897006579295248439520803064795506754669709244", @@ -26,5 +26,21 @@ export const defaultMerkleProof: IMerkleProof = { ], }; -export const mockDefaultIdentityCommitment = - bigintToHex(15206603389158210388485662342360617949291660595274505642693885456541816400294n); +export const mockDefaultIdentityCommitment = "219ea1ec38a6fffb63e2a591fec619fe9dc850d345f6d4d8823a4f72a6f729a6"; + +export const mockDefaultIdentity: IIdentityData = { + commitment: mockDefaultIdentityCommitment, + metadata: { + account: ZERO_ADDRESS, + name: "Account #1", + identityStrategy: "interep", + web2Provider: "twitter", + groups: [], + host: "http://localhost:3000", + }, +}; + +export const mockDefaultGroup: IGroupData = { + id: "90694543209366256629502773954857", + name: "Group #1", +}; diff --git a/packages/app/src/constants/paths.ts b/packages/app/src/constants/paths.ts index 12ccdc859..c075aa1ad 100644 --- a/packages/app/src/constants/paths.ts +++ b/packages/app/src/constants/paths.ts @@ -17,4 +17,5 @@ export enum Paths { RECOVER = "/recover", RESET_PASSWORD = "/reset-password", ADD_VERIFIABLE_CREDENTIAL = "/add-verifiable-credential", + JOIN_GROUP = "/groups/:id/join", } diff --git a/packages/app/src/setupTests.ts b/packages/app/src/setupTests.ts index 5cb75e2cb..97953f672 100644 --- a/packages/app/src/setupTests.ts +++ b/packages/app/src/setupTests.ts @@ -1,6 +1,6 @@ import { library } from "@fortawesome/fontawesome-svg-core"; import { faTwitter, faGithub, faReddit } from "@fortawesome/free-brands-svg-icons"; -import { faLink } from "@fortawesome/free-solid-svg-icons"; +import { faLink, faUsersRays } from "@fortawesome/free-solid-svg-icons"; import "@testing-library/jest-dom"; import "isomorphic-fetch"; @@ -8,7 +8,7 @@ import type { ReactElement } from "react"; jest.retryTimes(1, { logErrorsBeforeRetry: true }); -library.add(faTwitter, faGithub, faReddit, faLink); +library.add(faTwitter, faGithub, faReddit, faLink, faUsersRays); jest.mock("loglevel", () => ({ info: jest.fn(), @@ -129,6 +129,8 @@ jest.mock("@cryptkeeperzk/providers", (): unknown => ({ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment EventName: jest.requireActual("@cryptkeeperzk/providers/dist/src/event/types"), // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + RejectRequests: jest.requireActual("@cryptkeeperzk/providers/dist/src/event/types"), + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment RPCAction: jest.requireActual("@cryptkeeperzk/providers/dist/src/constants/rpcAction"), initializeCryptKeeperProvider: jest.fn(), })); diff --git a/packages/app/src/types/history/index.ts b/packages/app/src/types/history/index.ts index 7f8f4e65f..ff4489127 100644 --- a/packages/app/src/types/history/index.ts +++ b/packages/app/src/types/history/index.ts @@ -1,4 +1,4 @@ -import type { IIdentityData } from "@cryptkeeperzk/types"; +import type { IGroupData, IIdentityData } from "@cryptkeeperzk/types"; export enum OperationType { CREATE_IDENTITY = "CREATE_IDENTITY", @@ -13,12 +13,14 @@ export enum OperationType { DELETE_ALL_VERIFIABLE_CREDENTIALS = "DELETE_ALL_VERIFIABLE_CREDENTIALS", REJECT_VERIFIABLE_CREDENTIAL_REQUEST = "REJECT_VERIFIABLE_CREDENTIAL_REQUEST", REVEAL_IDENTITY_COMMITMENT = "REVEAL_IDENTITY_COMMITMENT", + JOIN_GROUP = "JOIN_GROUP", } export interface Operation { id: string; type: OperationType; identity?: IIdentityData; + group?: Partial; createdAt: string; } diff --git a/packages/app/src/ui/ducks/__tests__/groups.test.ts b/packages/app/src/ui/ducks/__tests__/groups.test.ts new file mode 100644 index 000000000..ef1c76fec --- /dev/null +++ b/packages/app/src/ui/ducks/__tests__/groups.test.ts @@ -0,0 +1,40 @@ +/** + * @jest-environment jsdom + */ + +import { RPCAction } from "@cryptkeeperzk/providers"; + +import { store } from "@src/ui/store/configureAppStore"; +import postMessage from "@src/util/postMessage"; + +import { checkGroupMembership, joinGroup } from "../groups"; + +jest.mock("@src/util/postMessage"); + +describe("ui/ducks/groups", () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + test("should join group properly", async () => { + const args = { groupId: "groupId", apiKey: "apiKey", inviteCode: "inviteCode" }; + (postMessage as jest.Mock).mockResolvedValue(true); + + const result = await Promise.resolve(store.dispatch(joinGroup(args))); + + expect(postMessage).toBeCalledTimes(1); + expect(postMessage).toBeCalledWith({ method: RPCAction.JOIN_GROUP, payload: args }); + expect(result).toBe(true); + }); + + test("should check group membership properly", async () => { + const args = { groupId: "groupId", apiKey: "apiKey", inviteCode: "inviteCode" }; + (postMessage as jest.Mock).mockResolvedValue(true); + + const result = await Promise.resolve(store.dispatch(checkGroupMembership(args))); + + expect(postMessage).toBeCalledTimes(1); + expect(postMessage).toBeCalledWith({ method: RPCAction.CHECK_GROUP_MEMBERSHIP, payload: args }); + expect(result).toBe(true); + }); +}); diff --git a/packages/app/src/ui/ducks/__tests__/requests.test.tsx b/packages/app/src/ui/ducks/__tests__/requests.test.tsx index f88940776..a1dc3103f 100644 --- a/packages/app/src/ui/ducks/__tests__/requests.test.tsx +++ b/packages/app/src/ui/ducks/__tests__/requests.test.tsx @@ -2,7 +2,7 @@ * @jest-environment jsdom */ -import { RPCAction } from "@cryptkeeperzk/providers"; +import { EventName, RPCAction } from "@cryptkeeperzk/providers"; import { IPendingRequest, PendingRequestType, RequestResolutionStatus } from "@cryptkeeperzk/types"; import { renderHook } from "@testing-library/react"; import { Provider } from "react-redux"; @@ -10,7 +10,13 @@ import { Provider } from "react-redux"; import { store } from "@src/ui/store/configureAppStore"; import postMessage from "@src/util/postMessage"; -import { fetchPendingRequests, finalizeRequest, setPendingRequests, usePendingRequests } from "../requests"; +import { + fetchPendingRequests, + finalizeRequest, + rejectUserRequest, + setPendingRequests, + usePendingRequests, +} from "../requests"; jest.unmock("@src/ui/ducks/hooks"); @@ -54,6 +60,24 @@ describe("ui/ducks/requests", () => { }); }); + test("should reject user request properly", async () => { + await Promise.resolve(store.dispatch(rejectUserRequest({ type: "request" }, "urlOrigin"))); + + expect(postMessage).toBeCalledTimes(1); + expect(postMessage).toBeCalledWith({ + method: RPCAction.PUSH_EVENT, + payload: { + type: EventName.USER_REJECT, + payload: { + type: "request", + }, + }, + meta: { + urlOrigin: "urlOrigin", + }, + }); + }); + test("should set pending requests properly", async () => { await Promise.resolve(store.dispatch(setPendingRequests(defaultPendingRequests))); const { requests } = store.getState(); diff --git a/packages/app/src/ui/ducks/groups.ts b/packages/app/src/ui/ducks/groups.ts new file mode 100644 index 000000000..27bf2bdb0 --- /dev/null +++ b/packages/app/src/ui/ducks/groups.ts @@ -0,0 +1,22 @@ +import { RPCAction } from "@cryptkeeperzk/providers"; + +import postMessage from "@src/util/postMessage"; + +import type { ICheckGroupMembershipArgs, IJoinGroupMemberArgs } from "@cryptkeeperzk/types"; +import type { TypedThunk } from "@src/ui/store/configureAppStore"; + +export const joinGroup = + (payload: IJoinGroupMemberArgs): TypedThunk> => + async () => + postMessage({ + method: RPCAction.JOIN_GROUP, + payload, + }); + +export const checkGroupMembership = + (payload: ICheckGroupMembershipArgs): TypedThunk> => + async () => + postMessage({ + method: RPCAction.CHECK_GROUP_MEMBERSHIP, + payload, + }); diff --git a/packages/app/src/ui/ducks/requests.ts b/packages/app/src/ui/ducks/requests.ts index 100e488d1..d4651233e 100644 --- a/packages/app/src/ui/ducks/requests.ts +++ b/packages/app/src/ui/ducks/requests.ts @@ -1,6 +1,6 @@ /* eslint-disable no-param-reassign */ -import { RPCAction } from "@cryptkeeperzk/providers"; -import { IPendingRequest, IRequestResolutionAction } from "@cryptkeeperzk/types"; +import { EventName, RPCAction } from "@cryptkeeperzk/providers"; +import { IPendingRequest, IRejectedRequest, IRequestResolutionAction } from "@cryptkeeperzk/types"; import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import deepEqual from "fast-deep-equal"; @@ -43,6 +43,15 @@ export const finalizeRequest = payload: request, }); +export const rejectUserRequest = + (payload: IRejectedRequest, urlOrigin?: string): TypedThunk> => + async () => + postMessage({ + method: RPCAction.PUSH_EVENT, + payload: { type: EventName.USER_REJECT, payload }, + meta: { urlOrigin }, + }); + export const usePendingRequests = (): IPendingRequest[] => useAppSelector((state) => state.requests.pendingRequests, deepEqual); diff --git a/packages/app/src/ui/hooks/url/__tests__/url.test.ts b/packages/app/src/ui/hooks/url/__tests__/url.test.ts new file mode 100644 index 000000000..807ab0221 --- /dev/null +++ b/packages/app/src/ui/hooks/url/__tests__/url.test.ts @@ -0,0 +1,51 @@ +/** + * @jest-environment jsdom + */ + +import { renderHook } from "@testing-library/react"; +import { useParams, useSearchParams } from "react-router-dom"; + +import { useSearchParam, useUrlParam } from ".."; + +jest.mock("react-router-dom", (): unknown => ({ + useParams: jest.fn(), + useSearchParams: jest.fn(), +})); + +describe("ui/hooks/url", () => { + const defaultUrlParams = { + param: "value", + }; + + const defaultSearchParams = new URLSearchParams([["param", "value"]]); + + beforeEach(() => { + (useParams as jest.Mock).mockReturnValue(defaultUrlParams); + + (useSearchParams as jest.Mock).mockReturnValue([defaultSearchParams]); + }); + + test("should return param from url", () => { + const { result } = renderHook(() => useUrlParam("param")); + + expect(result.current).toBe(defaultUrlParams.param); + }); + + test("should return undefined if there is no such url param", () => { + const { result } = renderHook(() => useUrlParam("unknown")); + + expect(result.current).toBeUndefined(); + }); + + test("should return param from search", () => { + const { result } = renderHook(() => useSearchParam("param")); + + expect(result.current).toBe(defaultUrlParams.param); + }); + + test("should return undefined if there is no such search param", () => { + const { result } = renderHook(() => useSearchParam("unknown")); + + expect(result.current).toBeUndefined(); + }); +}); diff --git a/packages/app/src/ui/hooks/url/__tests__/useUrlParam.test.ts b/packages/app/src/ui/hooks/url/__tests__/useUrlParam.test.ts deleted file mode 100644 index 2e69a78f7..000000000 --- a/packages/app/src/ui/hooks/url/__tests__/useUrlParam.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * @jest-environment jsdom - */ - -import { renderHook } from "@testing-library/react"; -import { useParams } from "react-router-dom"; - -import { useUrlParam } from ".."; - -jest.mock("react-router-dom", (): unknown => ({ - useParams: jest.fn(), -})); - -describe("ui/hooks/url", () => { - const defaultUrlParams = { - param: "value", - }; - - beforeEach(() => { - (useParams as jest.Mock).mockReturnValue(defaultUrlParams); - }); - - test("should return param from url", () => { - const { result } = renderHook(() => useUrlParam("param")); - - expect(result.current).toBe(defaultUrlParams.param); - }); - - test("should return undefined if there is no such param", () => { - const { result } = renderHook(() => useUrlParam("unknown")); - - expect(result.current).toBeUndefined(); - }); -}); diff --git a/packages/app/src/ui/hooks/url/index.ts b/packages/app/src/ui/hooks/url/index.ts index 1679bef2e..d2a705d81 100644 --- a/packages/app/src/ui/hooks/url/index.ts +++ b/packages/app/src/ui/hooks/url/index.ts @@ -1,7 +1,13 @@ -import { useParams } from "react-router-dom"; +import { useParams, useSearchParams } from "react-router-dom"; export const useUrlParam = (param: string): string | undefined => { const params = useParams(); return params[param]; }; + +export const useSearchParam = (param: string): string | undefined => { + const [params] = useSearchParams(); + + return params.get(param) ?? undefined; +}; diff --git a/packages/app/src/ui/pages/CreateIdentity/__tests__/CreateIdentity.test.tsx b/packages/app/src/ui/pages/CreateIdentity/__tests__/CreateIdentity.test.tsx index 9c4da3f61..b0cf857e6 100644 --- a/packages/app/src/ui/pages/CreateIdentity/__tests__/CreateIdentity.test.tsx +++ b/packages/app/src/ui/pages/CreateIdentity/__tests__/CreateIdentity.test.tsx @@ -9,7 +9,6 @@ import selectEvent from "react-select-event"; import { ZERO_ADDRESS } from "@src/config/const"; import { getEnabledFeatures } from "@src/config/features"; -import { createModalRoot, deleteModalRoot } from "@src/config/mock/modal"; import { defaultWalletHookData } from "@src/config/mock/wallet"; import { IDENTITY_TYPES, Paths, WEB2_PROVIDER_OPTIONS } from "@src/constants"; import { closePopup } from "@src/ui/ducks/app"; @@ -77,15 +76,11 @@ describe("ui/pages/CreateIdentity", () => { (createIdentity as jest.Mock).mockResolvedValue(true); (getEnabledFeatures as jest.Mock).mockReturnValue({ INTEREP_IDENTITY: true }); - - createModalRoot(); }); afterEach(() => { jest.clearAllMocks(); window.location.href = oldHref; - - deleteModalRoot(); }); test("should render properly with random", async () => { diff --git a/packages/app/src/ui/pages/Home/components/ActivityList/Item/ActivityListItem.tsx b/packages/app/src/ui/pages/Home/components/ActivityList/Item/ActivityListItem.tsx index 762d488df..db5b69650 100644 --- a/packages/app/src/ui/pages/Home/components/ActivityList/Item/ActivityListItem.tsx +++ b/packages/app/src/ui/pages/Home/components/ActivityList/Item/ActivityListItem.tsx @@ -10,6 +10,7 @@ import { Menu } from "@src/ui/components/Menu"; import { ellipsify } from "@src/util/account"; import { redirectToNewTab } from "@src/util/browser"; import { formatDate } from "@src/util/date"; +import { getBandadaGroupUrl } from "@src/util/groups"; import "./activityListItemStyles.scss"; @@ -33,6 +34,7 @@ const OPERATIONS: Record = { [OperationType.DELETE_ALL_VERIFIABLE_CREDENTIALS]: "All verifiable credentials deleted", [OperationType.REJECT_VERIFIABLE_CREDENTIAL_REQUEST]: "Verifiable credential request rejected", [OperationType.REVEAL_IDENTITY_COMMITMENT]: "Identity revealed", + [OperationType.JOIN_GROUP]: "Joined group", }; const web2ProvidersIcons: IconWeb2Providers = { @@ -52,6 +54,10 @@ export const ActivityItem = ({ operation, onDelete }: IActivityItemProps): JSX.E redirectToNewTab(metadata!.host!); }, [metadata?.host]); + const onGoToGroup = useCallback(() => { + redirectToNewTab(getBandadaGroupUrl(operation.group!.id!)); + }, [operation.group?.id]); + return (
@@ -75,6 +81,19 @@ export const ActivityItem = ({ operation, onDelete }: IActivityItemProps): JSX.E )} + + {operation.group?.id && ( + + + + + + )}
{operation.identity && ( diff --git a/packages/app/src/ui/pages/Home/components/ActivityList/__tests__/ActivityList.test.tsx b/packages/app/src/ui/pages/Home/components/ActivityList/__tests__/ActivityList.test.tsx index 869800f7a..5eaeb7766 100644 --- a/packages/app/src/ui/pages/Home/components/ActivityList/__tests__/ActivityList.test.tsx +++ b/packages/app/src/ui/pages/Home/components/ActivityList/__tests__/ActivityList.test.tsx @@ -4,7 +4,7 @@ import { act, render, screen } from "@testing-library/react"; -import { ZERO_ADDRESS } from "@src/config/const"; +import { mockDefaultIdentity } from "@src/config/mock/zk"; import { Operation, OperationType } from "@src/types"; import { ActivityList } from ".."; @@ -19,32 +19,13 @@ describe("ui/pages/Home/components/ActivityList", () => { { id: "1", type: OperationType.CREATE_IDENTITY, - identity: { - commitment: "1", - metadata: { - account: ZERO_ADDRESS, - name: "Account #1", - identityStrategy: "interep", - web2Provider: "twitter", - groups: [], - host: "http://localhost:3000", - }, - }, + identity: mockDefaultIdentity, createdAt: new Date().toISOString(), }, { id: "2", type: OperationType.DELETE_IDENTITY, - identity: { - commitment: "1", - metadata: { - account: ZERO_ADDRESS, - name: "Account #2", - identityStrategy: "random", - groups: [], - host: "http://localhost:3000", - }, - }, + identity: mockDefaultIdentity, createdAt: new Date().toISOString(), }, ]; diff --git a/packages/app/src/ui/pages/Home/components/ActivityList/__tests__/ActivityListItem.test.tsx b/packages/app/src/ui/pages/Home/components/ActivityList/__tests__/ActivityListItem.test.tsx index d848232bc..f1470949f 100644 --- a/packages/app/src/ui/pages/Home/components/ActivityList/__tests__/ActivityListItem.test.tsx +++ b/packages/app/src/ui/pages/Home/components/ActivityList/__tests__/ActivityListItem.test.tsx @@ -4,9 +4,10 @@ import { act, fireEvent, render, screen } from "@testing-library/react"; -import { ZERO_ADDRESS } from "@src/config/const"; +import { mockDefaultGroup, mockDefaultIdentity } from "@src/config/mock/zk"; import { Operation, OperationType } from "@src/types"; import { redirectToNewTab } from "@src/util/browser"; +import { getBandadaGroupUrl } from "@src/util/groups"; import { ActivityItem, IActivityItemProps } from "../Item"; @@ -18,18 +19,9 @@ describe("ui/pages/Home/components/ActivityList/Item", () => { const defaultProps: IActivityItemProps = { operation: { id: "1", - type: OperationType.CREATE_IDENTITY, - identity: { - commitment: "1", - metadata: { - account: ZERO_ADDRESS, - name: "Account #1", - identityStrategy: "interep", - web2Provider: "twitter", - groups: [], - host: "http://localhost:3000", - }, - }, + type: OperationType.JOIN_GROUP, + identity: mockDefaultIdentity, + group: mockDefaultGroup, createdAt: new Date().toISOString(), }, onDelete: jest.fn(), @@ -92,4 +84,14 @@ describe("ui/pages/Home/components/ActivityList/Item", () => { expect(redirectToNewTab).toBeCalledTimes(1); expect(redirectToNewTab).toBeCalledWith(defaultProps.operation.identity?.metadata.host); }); + + test("should go to group properly", async () => { + render(); + + const group = await screen.findByTestId("group"); + await act(() => fireEvent.click(group)); + + expect(redirectToNewTab).toBeCalledTimes(1); + expect(redirectToNewTab).toBeCalledWith(getBandadaGroupUrl(defaultProps.operation.group!.id!)); + }); }); diff --git a/packages/app/src/ui/pages/Home/components/ActivityList/__tests__/useActivityList.test.ts b/packages/app/src/ui/pages/Home/components/ActivityList/__tests__/useActivityList.test.ts index 20b13038d..32e19551f 100644 --- a/packages/app/src/ui/pages/Home/components/ActivityList/__tests__/useActivityList.test.ts +++ b/packages/app/src/ui/pages/Home/components/ActivityList/__tests__/useActivityList.test.ts @@ -4,7 +4,7 @@ import { act, renderHook, waitFor } from "@testing-library/react"; -import { ZERO_ADDRESS } from "@src/config/const"; +import { mockDefaultIdentity } from "@src/config/mock/zk"; import { HistorySettings, Operation, OperationType } from "@src/types"; import { useAppDispatch } from "@src/ui/ducks/hooks"; import { @@ -34,32 +34,13 @@ describe("ui/pages/Home/components/ActivityList/useActivityList", () => { { id: "1", type: OperationType.CREATE_IDENTITY, - identity: { - commitment: "1", - metadata: { - account: ZERO_ADDRESS, - name: "Account #1", - identityStrategy: "interep", - web2Provider: "twitter", - groups: [], - host: "http://localhost:3000", - }, - }, + identity: mockDefaultIdentity, createdAt: new Date().toISOString(), }, { id: "2", type: OperationType.DELETE_IDENTITY, - identity: { - commitment: "1", - metadata: { - account: ZERO_ADDRESS, - name: "Account #2", - identityStrategy: "random", - groups: [], - host: "http://localhost:3000", - }, - }, + identity: mockDefaultIdentity, createdAt: new Date().toISOString(), }, ]; diff --git a/packages/app/src/ui/pages/JoinGroup/JoinGroup.tsx b/packages/app/src/ui/pages/JoinGroup/JoinGroup.tsx new file mode 100644 index 000000000..8e1fcbfe9 --- /dev/null +++ b/packages/app/src/ui/pages/JoinGroup/JoinGroup.tsx @@ -0,0 +1,181 @@ +import Box from "@mui/material/Box"; +import Button from "@mui/material/Button"; +import Typography from "@mui/material/Typography"; + +import logoSVG from "@src/static/icons/logo.svg"; +import { FullModalContent, FullModalFooter, FullModalHeader } from "@src/ui/components/FullModal"; +import { Icon } from "@src/ui/components/Icon"; + +import { useJoinGroup } from "./useJoinGroup"; + +const JoinGroup = (): JSX.Element => { + const { + isLoading, + isJoined, + isSubmitting, + error, + faviconUrl, + apiKey, + inviteCode, + connectedIdentity, + groupId, + onGoBack, + onGoToHost, + onGoToGroup, + onJoin, + } = useJoinGroup(); + + const isShowContent = !isJoined && Boolean(connectedIdentity && groupId); + const isShowInviteInfo = Boolean(apiKey || inviteCode); + + if (isLoading) { + return ( + + Loading... + + ); + } + + return ( + + Join group + + + + + + + {!connectedIdentity && ( + + + No connected identity found + + + )} + + {!groupId && ( + + + No group found + + + )} + + + {isJoined && ( + + + You have already joined this + + + + Group + + + )} + + {isShowContent && ( + + + + {connectedIdentity!.metadata.host} + + + + requests to join + + + + Group + + + + using your connected identity + + + + {isShowInviteInfo && ( + + {apiKey && API key: {apiKey}} + + {inviteCode && Invite code: {inviteCode}} + + )} + + )} + + + {error && ( + + {error} + + )} + + + + + + + + + ); +}; + +export default JoinGroup; diff --git a/packages/app/src/ui/pages/JoinGroup/__tests__/JoinGroup.test.tsx b/packages/app/src/ui/pages/JoinGroup/__tests__/JoinGroup.test.tsx new file mode 100644 index 000000000..16917fd2d --- /dev/null +++ b/packages/app/src/ui/pages/JoinGroup/__tests__/JoinGroup.test.tsx @@ -0,0 +1,138 @@ +/** + * @jest-environment jsdom + */ + +import { render, waitFor } from "@testing-library/react"; +import { Suspense } from "react"; + +import { ZERO_ADDRESS } from "@src/config/const"; + +import JoinGroup from ".."; +import { IUseJoinGroupData, useJoinGroup } from "../useJoinGroup"; + +jest.mock("../useJoinGroup", (): unknown => ({ + useJoinGroup: jest.fn(), +})); + +describe("ui/pages/JoinGroup", () => { + const defaultHookData: IUseJoinGroupData = { + isLoading: false, + isJoined: false, + isSubmitting: false, + error: "", + faviconUrl: "favicon", + groupId: "groupId", + apiKey: "apiKey", + inviteCode: "inviteCode", + connectedIdentity: { + commitment: "commitment", + metadata: { + account: ZERO_ADDRESS, + name: "Account #1", + identityStrategy: "interep", + groups: [{ id: "1", name: "Group #1", description: "Description #1" }], + web2Provider: "twitter", + host: "http://localhost:3000", + }, + }, + onGoBack: jest.fn(), + onGoToHost: jest.fn(), + onGoToGroup: jest.fn(), + onJoin: jest.fn(), + }; + + beforeEach(() => { + (useJoinGroup as jest.Mock).mockReturnValue(defaultHookData); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test("should render properly", async () => { + const { container, findByTestId } = render( + + + , + ); + + await waitFor(() => container.firstChild !== null); + + const page = await findByTestId("join-group-page"); + + expect(page).toBeInTheDocument(); + }); + + test("should render loading state properly", async () => { + (useJoinGroup as jest.Mock).mockReturnValue({ ...defaultHookData, isLoading: true }); + + const { container, findByText } = render( + + + , + ); + + await waitFor(() => container.firstChild !== null); + + const loading = await findByText("Loading..."); + + expect(loading).toBeInTheDocument(); + }); + + test("should render error state properly", async () => { + (useJoinGroup as jest.Mock).mockReturnValue({ ...defaultHookData, error: "Error" }); + + const { container, findByText } = render( + + + , + ); + + await waitFor(() => container.firstChild !== null); + + const error = await findByText("Error"); + + expect(error).toBeInTheDocument(); + }); + + test("should render empty state properly", async () => { + (useJoinGroup as jest.Mock).mockReturnValue({ + ...defaultHookData, + connectedIdentity: undefined, + groupId: undefined, + inviteCode: undefined, + apiKey: undefined, + faviconUrl: "", + }); + + const { container, findByText } = render( + + + , + ); + + await waitFor(() => container.firstChild !== null); + + const emptyIdentity = await findByText("No connected identity found"); + const emptyGroup = await findByText("No group found"); + + expect(emptyIdentity).toBeInTheDocument(); + expect(emptyGroup).toBeInTheDocument(); + }); + + test("should render joined state properly", async () => { + (useJoinGroup as jest.Mock).mockReturnValue({ ...defaultHookData, isJoined: true }); + + const { container, findByTestId } = render( + + + , + ); + + await waitFor(() => container.firstChild !== null); + + const text = await findByTestId("joined-text"); + + expect(text).toBeInTheDocument(); + }); +}); diff --git a/packages/app/src/ui/pages/JoinGroup/__tests__/useJoinGroup.test.ts b/packages/app/src/ui/pages/JoinGroup/__tests__/useJoinGroup.test.ts new file mode 100644 index 000000000..fda249280 --- /dev/null +++ b/packages/app/src/ui/pages/JoinGroup/__tests__/useJoinGroup.test.ts @@ -0,0 +1,205 @@ +/** + * @jest-environment jsdom + */ + +import { act, renderHook, waitFor } from "@testing-library/react"; +import { getLinkPreview } from "link-preview-js"; +import { useNavigate } from "react-router-dom"; + +import { ZERO_ADDRESS } from "@src/config/const"; +import { getBandadaUrl } from "@src/config/env"; +import { Paths } from "@src/constants"; +import { closePopup } from "@src/ui/ducks/app"; +import { checkGroupMembership, joinGroup } from "@src/ui/ducks/groups"; +import { useAppDispatch } from "@src/ui/ducks/hooks"; +import { fetchIdentities, useConnectedIdentity } from "@src/ui/ducks/identities"; +import { rejectUserRequest } from "@src/ui/ducks/requests"; +import { useSearchParam, useUrlParam } from "@src/ui/hooks/url"; +import { redirectToNewTab } from "@src/util/browser"; + +import type { IIdentityData } from "@cryptkeeperzk/types"; + +import { IUseJoinGroupData, useJoinGroup } from "../useJoinGroup"; + +jest.mock("react-router-dom", (): unknown => ({ + useNavigate: jest.fn(), +})); + +jest.mock("link-preview-js", (): unknown => ({ + getLinkPreview: jest.fn(), +})); + +jest.mock("@src/ui/hooks/url", (): unknown => ({ + useSearchParam: jest.fn(), + useUrlParam: jest.fn(), +})); + +jest.mock("@src/util/browser", (): unknown => ({ + redirectToNewTab: jest.fn(), +})); + +jest.mock("@src/ui/ducks/app", (): unknown => ({ + closePopup: jest.fn(), +})); + +jest.mock("@src/ui/ducks/groups", (): unknown => ({ + joinGroup: jest.fn(), + checkGroupMembership: jest.fn(), +})); + +jest.mock("@src/ui/ducks/requests", (): unknown => ({ + rejectUserRequest: jest.fn(), +})); + +jest.mock("@src/ui/ducks/hooks", (): unknown => ({ + useAppDispatch: jest.fn(), +})); + +jest.mock("@src/ui/ducks/identities", (): unknown => ({ + fetchIdentities: jest.fn(), + useConnectedIdentity: jest.fn(), +})); + +describe("ui/pages/JoinGroup/useJoinGroup", () => { + const defaultIdentity: IIdentityData = { + commitment: "commitment", + metadata: { + account: ZERO_ADDRESS, + name: "Account #1", + identityStrategy: "interep", + groups: [], + web2Provider: "twitter", + host: "http://localhost:3000", + }, + }; + + const defaultFaviconsData = { favicons: [`${defaultIdentity.metadata.host}/favicon.ico`] }; + + const mockNavigate = jest.fn(); + const mockDispatch = jest.fn(() => Promise.resolve(false)); + + beforeEach(() => { + (getLinkPreview as jest.Mock).mockResolvedValue(defaultFaviconsData); + + (useConnectedIdentity as jest.Mock).mockReturnValue(defaultIdentity); + + (useNavigate as jest.Mock).mockReturnValue(mockNavigate); + + (useAppDispatch as jest.Mock).mockReturnValue(mockDispatch); + + (useUrlParam as jest.Mock).mockReturnValue("groupId"); + + (useSearchParam as jest.Mock).mockImplementation((arg: string) => arg); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + const waitForData = async (data: IUseJoinGroupData): Promise => { + await waitFor(() => !data.isLoading); + await waitFor(() => expect(fetchIdentities).toBeCalledTimes(1)); + }; + + test("should return initial data", async () => { + const { result } = renderHook(() => useJoinGroup()); + await waitForData(result.current); + + expect(result.current.isLoading).toBe(false); + expect(result.current.isJoined).toBe(false); + expect(result.current.isSubmitting).toBe(false); + expect(result.current.error).toBe(""); + expect(result.current.apiKey).toBe("apiKey"); + expect(result.current.inviteCode).toBe("inviteCode"); + expect(result.current.faviconUrl).toBe(defaultFaviconsData.favicons[0]); + expect(result.current.groupId).toBe("groupId"); + expect(result.current.connectedIdentity).toStrictEqual(defaultIdentity); + }); + + test("should go back properly", async () => { + const { result } = renderHook(() => useJoinGroup()); + await waitForData(result.current); + + await act(() => Promise.resolve(result.current.onGoBack())); + + expect(mockNavigate).toBeCalledTimes(1); + expect(mockNavigate).toBeCalledWith(Paths.HOME); + expect(mockDispatch).toBeCalledTimes(4); + expect(fetchIdentities).toBeCalledTimes(1); + expect(checkGroupMembership).toBeCalledTimes(1); + expect(rejectUserRequest).toBeCalledTimes(1); + expect(closePopup).toBeCalledTimes(1); + }); + + test("should handle error properly", async () => { + const error = new Error("error"); + (useAppDispatch as jest.Mock).mockReturnValue(jest.fn(() => Promise.reject(error))); + (getLinkPreview as jest.Mock).mockRejectedValue(error); + + const { result } = renderHook(() => useJoinGroup()); + await waitForData(result.current); + + expect(result.current.isLoading).toBe(false); + expect(result.current.error).toBe(error.message); + expect(result.current.faviconUrl).toBe(""); + }); + + test("should handle empty connected identity properly", async () => { + (useConnectedIdentity as jest.Mock).mockReturnValue(undefined); + + const { result } = renderHook(() => useJoinGroup()); + await waitForData(result.current); + + expect(result.current.isLoading).toBe(false); + expect(result.current.connectedIdentity).toBeUndefined(); + }); + + test("should go to host properly", async () => { + const { result } = renderHook(() => useJoinGroup()); + await waitForData(result.current); + + await act(() => Promise.resolve(result.current.onGoToHost())); + + expect(redirectToNewTab).toBeCalledTimes(1); + expect(redirectToNewTab).toBeCalledWith(defaultIdentity.metadata.host); + }); + + test("should go to group properly", async () => { + const { result } = renderHook(() => useJoinGroup()); + await waitForData(result.current); + + await act(() => Promise.resolve(result.current.onGoToGroup())); + + expect(redirectToNewTab).toBeCalledTimes(1); + expect(redirectToNewTab).toBeCalledWith(`${getBandadaUrl()}/groups/off-chain/groupId`); + }); + + test("should join group properly", async () => { + const { result } = renderHook(() => useJoinGroup()); + await waitForData(result.current); + + await act(() => Promise.resolve(result.current.onJoin())); + await waitFor(() => !result.current.isSubmitting); + + expect(mockDispatch).toBeCalledTimes(4); + expect(fetchIdentities).toBeCalledTimes(1); + expect(checkGroupMembership).toBeCalledTimes(1); + expect(joinGroup).toBeCalledTimes(1); + expect(closePopup).toBeCalledTimes(1); + expect(mockNavigate).toBeCalledTimes(1); + expect(mockNavigate).toBeCalledWith(Paths.HOME); + }); + + test("should handle reveal connected identity commitment error properly", async () => { + const error = new Error("error"); + (useAppDispatch as jest.Mock) + .mockReturnValueOnce(mockDispatch) + .mockReturnValue(jest.fn(() => Promise.reject(error))); + + const { result } = renderHook(() => useJoinGroup()); + + await act(() => Promise.resolve(result.current.onJoin())); + + expect(result.current.error).toBe(error.message); + }); +}); diff --git a/packages/app/src/ui/pages/JoinGroup/index.ts b/packages/app/src/ui/pages/JoinGroup/index.ts new file mode 100644 index 000000000..6218ad566 --- /dev/null +++ b/packages/app/src/ui/pages/JoinGroup/index.ts @@ -0,0 +1,3 @@ +import { lazy } from "react"; + +export default lazy(() => import("./JoinGroup")); diff --git a/packages/app/src/ui/pages/JoinGroup/useJoinGroup.ts b/packages/app/src/ui/pages/JoinGroup/useJoinGroup.ts new file mode 100644 index 000000000..624d39e02 --- /dev/null +++ b/packages/app/src/ui/pages/JoinGroup/useJoinGroup.ts @@ -0,0 +1,126 @@ +import { RejectRequests } from "@cryptkeeperzk/providers"; +import { getLinkPreview } from "link-preview-js"; +import { useCallback, useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; + +import { Paths } from "@src/constants"; +import { closePopup } from "@src/ui/ducks/app"; +import { checkGroupMembership, joinGroup } from "@src/ui/ducks/groups"; +import { useAppDispatch } from "@src/ui/ducks/hooks"; +import { fetchIdentities, useConnectedIdentity } from "@src/ui/ducks/identities"; +import { rejectUserRequest } from "@src/ui/ducks/requests"; +import { useSearchParam, useUrlParam } from "@src/ui/hooks/url"; +import { redirectToNewTab } from "@src/util/browser"; +import { getBandadaGroupUrl } from "@src/util/groups"; + +import type { IIdentityData } from "@cryptkeeperzk/types"; + +export interface IUseJoinGroupData { + isLoading: boolean; + isJoined: boolean; + isSubmitting: boolean; + error: string; + faviconUrl: string; + groupId?: string; + apiKey?: string; + inviteCode?: string; + connectedIdentity?: IIdentityData; + onGoBack: () => void; + onGoToHost: () => void; + onGoToGroup: () => void; + onJoin: () => void; +} + +export const useJoinGroup = (): IUseJoinGroupData => { + const [isLoading, setLoading] = useState(false); + const [isJoined, setJoined] = useState(false); + const [isSubmitting, setSubmitting] = useState(false); + const [error, setError] = useState(""); + const [faviconUrl, setFaviconUrl] = useState(""); + + const dispatch = useAppDispatch(); + const navigate = useNavigate(); + + const groupId = useUrlParam("id"); + const apiKey = useSearchParam("apiKey"); + const inviteCode = useSearchParam("inviteCode"); + + const connectedIdentity = useConnectedIdentity(); + + useEffect(() => { + setLoading(true); + Promise.all([dispatch(fetchIdentities()), dispatch(checkGroupMembership({ groupId: groupId! }))]) + .then(([, isJoinedToGroup]) => { + setJoined(isJoinedToGroup); + }) + .catch((err: Error) => { + setError(err.message); + }) + .finally(() => { + setLoading(false); + }); + }, [groupId, dispatch, setLoading, setError]); + + useEffect(() => { + if (!connectedIdentity?.metadata.host) { + return; + } + + getLinkPreview(connectedIdentity.metadata.host) + .then((data) => { + setFaviconUrl(data.favicons[0]); + }) + .catch(() => { + setFaviconUrl(""); + }); + }, [connectedIdentity?.metadata.host, setFaviconUrl]); + + const onGoBack = useCallback(() => { + dispatch( + rejectUserRequest({ type: RejectRequests.JOIN_GROUP, payload: { groupId } }, connectedIdentity?.metadata.host), + ) + .then(() => dispatch(closePopup())) + .then(() => { + navigate(Paths.HOME); + }); + }, [groupId, connectedIdentity?.metadata.host, dispatch, navigate]); + + const onGoToHost = useCallback(() => { + redirectToNewTab(connectedIdentity!.metadata.host!); + }, [connectedIdentity?.metadata.host]); + + const onGoToGroup = useCallback(() => { + redirectToNewTab(getBandadaGroupUrl(groupId!)); + }, [groupId]); + + const onJoin = useCallback(() => { + setSubmitting(true); + dispatch(joinGroup({ groupId: groupId!, apiKey, inviteCode })) + .then(() => dispatch(closePopup())) + .then(() => { + navigate(Paths.HOME); + }) + .catch((err: Error) => { + setError(err.message); + }) + .finally(() => { + setSubmitting(false); + }); + }, [groupId, apiKey, inviteCode, dispatch, navigate, setError, setSubmitting]); + + return { + isLoading, + isSubmitting, + isJoined, + error, + faviconUrl, + connectedIdentity, + groupId, + apiKey, + inviteCode, + onGoBack, + onGoToHost, + onGoToGroup, + onJoin, + }; +}; diff --git a/packages/app/src/ui/pages/Popup/Popup.tsx b/packages/app/src/ui/pages/Popup/Popup.tsx index fad32521b..a1682d478 100644 --- a/packages/app/src/ui/pages/Popup/Popup.tsx +++ b/packages/app/src/ui/pages/Popup/Popup.tsx @@ -9,6 +9,7 @@ import DownloadBackup from "@src/ui/pages/DownloadBackup"; import GenerateMnemonic from "@src/ui/pages/GenerateMnemonic"; import Home from "@src/ui/pages/Home"; import Identity from "@src/ui/pages/Identity"; +import JoinGroup from "@src/ui/pages/JoinGroup"; import Login from "@src/ui/pages/Login"; import Onboarding from "@src/ui/pages/Onboarding"; import OnboardingBackup from "@src/ui/pages/OnboardingBackup"; @@ -41,6 +42,7 @@ const routeConfig: RouteObject[] = [ { path: Paths.RECOVER, element: }, { path: Paths.RESET_PASSWORD, element: }, { path: Paths.ADD_VERIFIABLE_CREDENTIAL, element: }, + { path: Paths.JOIN_GROUP, element: }, { path: "*", element: , diff --git a/packages/app/src/ui/pages/Popup/usePopup.ts b/packages/app/src/ui/pages/Popup/usePopup.ts index b2c0dba1d..513f3c96a 100644 --- a/packages/app/src/ui/pages/Popup/usePopup.ts +++ b/packages/app/src/ui/pages/Popup/usePopup.ts @@ -21,6 +21,7 @@ const REDIRECT_PATHS: Record = { [Paths.UPLOAD_BACKUP]: Paths.UPLOAD_BACKUP, [Paths.ONBOARDING_BACKUP]: Paths.ONBOARDING_BACKUP, [Paths.ADD_VERIFIABLE_CREDENTIAL]: Paths.ADD_VERIFIABLE_CREDENTIAL, + [Paths.JOIN_GROUP]: Paths.JOIN_GROUP, }; const COMMON_PATHS = [Paths.RECOVER, Paths.RESET_PASSWORD, Paths.ONBOARDING_BACKUP]; diff --git a/packages/app/src/ui/pages/RevealIdentityCommitment/__tests__/RevealIdentityCommitment.test.tsx b/packages/app/src/ui/pages/RevealIdentityCommitment/__tests__/RevealIdentityCommitment.test.tsx index 1c2c80585..9de7fdea0 100644 --- a/packages/app/src/ui/pages/RevealIdentityCommitment/__tests__/RevealIdentityCommitment.test.tsx +++ b/packages/app/src/ui/pages/RevealIdentityCommitment/__tests__/RevealIdentityCommitment.test.tsx @@ -10,10 +10,6 @@ import { ZERO_ADDRESS } from "@src/config/const"; import RevealIdentityCommitment from ".."; import { IUseRevealIdentityCommitmentData, useRevealIdentityCommitment } from "../useRevealIdentityCommitment"; -jest.mock("react-router-dom", (): unknown => ({ - useNavigate: jest.fn(), -})); - jest.mock("../useRevealIdentityCommitment", (): unknown => ({ useRevealIdentityCommitment: jest.fn(), })); diff --git a/packages/app/src/ui/pages/RevealIdentityCommitment/__tests__/useRevealIdentityCommitment.test.ts b/packages/app/src/ui/pages/RevealIdentityCommitment/__tests__/useRevealIdentityCommitment.test.ts index 8ce208846..e5a940e15 100644 --- a/packages/app/src/ui/pages/RevealIdentityCommitment/__tests__/useRevealIdentityCommitment.test.ts +++ b/packages/app/src/ui/pages/RevealIdentityCommitment/__tests__/useRevealIdentityCommitment.test.ts @@ -10,6 +10,7 @@ import { Paths } from "@src/constants"; import { closePopup } from "@src/ui/ducks/app"; import { useAppDispatch } from "@src/ui/ducks/hooks"; import { fetchIdentities, revealConnectedIdentityCommitment, useConnectedIdentity } from "@src/ui/ducks/identities"; +import { rejectUserRequest } from "@src/ui/ducks/requests"; import { redirectToNewTab } from "@src/util/browser"; import type { IIdentityData } from "@cryptkeeperzk/types"; @@ -38,6 +39,10 @@ jest.mock("@src/ui/ducks/identities", (): unknown => ({ useConnectedIdentity: jest.fn(), })); +jest.mock("@src/ui/ducks/requests", (): unknown => ({ + rejectUserRequest: jest.fn(), +})); + describe("ui/pages/RevealIdentityCommitment/useRevealIdentityCommitment", () => { const defaultIdentity: IIdentityData = { commitment: "commitment", @@ -88,8 +93,9 @@ describe("ui/pages/RevealIdentityCommitment/useRevealIdentityCommitment", () => expect(mockNavigate).toBeCalledTimes(1); expect(mockNavigate).toBeCalledWith(Paths.HOME); - expect(mockDispatch).toBeCalledTimes(2); + expect(mockDispatch).toBeCalledTimes(3); expect(fetchIdentities).toBeCalledTimes(1); + expect(rejectUserRequest).toBeCalledTimes(1); expect(closePopup).toBeCalledTimes(1); }); diff --git a/packages/app/src/ui/pages/RevealIdentityCommitment/useRevealIdentityCommitment.ts b/packages/app/src/ui/pages/RevealIdentityCommitment/useRevealIdentityCommitment.ts index c71020776..8cedf822d 100644 --- a/packages/app/src/ui/pages/RevealIdentityCommitment/useRevealIdentityCommitment.ts +++ b/packages/app/src/ui/pages/RevealIdentityCommitment/useRevealIdentityCommitment.ts @@ -1,3 +1,4 @@ +import { RejectRequests } from "@cryptkeeperzk/providers"; import { useCallback, useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; @@ -5,6 +6,7 @@ import { Paths } from "@src/constants"; import { closePopup } from "@src/ui/ducks/app"; import { useAppDispatch } from "@src/ui/ducks/hooks"; import { fetchIdentities, revealConnectedIdentityCommitment, useConnectedIdentity } from "@src/ui/ducks/identities"; +import { rejectUserRequest } from "@src/ui/ducks/requests"; import { redirectToNewTab } from "@src/util/browser"; import type { IIdentityData } from "@cryptkeeperzk/types"; @@ -38,9 +40,12 @@ export const useRevealIdentityCommitment = (): IUseRevealIdentityCommitmentData }, [dispatch, setLoading, setError]); const onGoBack = useCallback(() => { - dispatch(closePopup()); - navigate(Paths.HOME); - }, [dispatch, navigate]); + dispatch(rejectUserRequest({ type: RejectRequests.REVEAL_COMMITMENT }, connectedIdentity?.metadata.host)) + .then(() => dispatch(closePopup())) + .then(() => { + navigate(Paths.HOME); + }); + }, [connectedIdentity?.metadata.host, dispatch, navigate]); const onGoToHost = useCallback(() => { redirectToNewTab(connectedIdentity!.metadata.host!); diff --git a/packages/app/src/ui/popup.tsx b/packages/app/src/ui/popup.tsx index cddab82ae..811ee64c6 100644 --- a/packages/app/src/ui/popup.tsx +++ b/packages/app/src/ui/popup.tsx @@ -1,6 +1,6 @@ import { library } from "@fortawesome/fontawesome-svg-core"; import { faTwitter, faGithub, faReddit } from "@fortawesome/free-brands-svg-icons"; -import { faLink } from "@fortawesome/free-solid-svg-icons"; +import { faLink, faUsersRays } from "@fortawesome/free-solid-svg-icons"; import { ThemeProvider } from "@mui/material/styles"; import { AnyAction } from "@reduxjs/toolkit"; import { Web3ReactProvider } from "@web3-react/core"; @@ -41,7 +41,7 @@ browser.tabs.query({ active: true, currentWindow: true }).then(() => { const root = ReactDOM.createRoot(document.getElementById("popup")!); - library.add(faTwitter, faGithub, faReddit, faLink); + library.add(faTwitter, faGithub, faReddit, faLink, faUsersRays); root.render( diff --git a/packages/app/src/util/__tests__/groups.test.ts b/packages/app/src/util/__tests__/groups.test.ts new file mode 100644 index 000000000..7166ef1c4 --- /dev/null +++ b/packages/app/src/util/__tests__/groups.test.ts @@ -0,0 +1,10 @@ +import { getBandadaUrl } from "@src/config/env"; + +import { getBandadaGroupUrl } from "../groups"; + +describe("util/groups", () => { + test("should get bandada group url", () => { + expect(getBandadaGroupUrl("id")).toBe(`${getBandadaUrl()}/groups/off-chain/id`); + expect(getBandadaGroupUrl("id", "on-chain")).toBe(`${getBandadaUrl()}/groups/on-chain/id`); + }); +}); diff --git a/packages/app/src/util/groups.ts b/packages/app/src/util/groups.ts new file mode 100644 index 000000000..d6ab8ee57 --- /dev/null +++ b/packages/app/src/util/groups.ts @@ -0,0 +1,4 @@ +import { getBandadaUrl } from "@src/config/env"; + +export const getBandadaGroupUrl = (groupId: string, type: "off-chain" | "on-chain" = "off-chain"): string => + `${getBandadaUrl()}/groups/${type}/${groupId}`; diff --git a/packages/demo/useCryptKeeper.ts b/packages/demo/useCryptKeeper.ts index 0e562528e..c417b8f9b 100644 --- a/packages/demo/useCryptKeeper.ts +++ b/packages/demo/useCryptKeeper.ts @@ -216,8 +216,8 @@ export const useCryptKeeper = (): IUseCryptKeeperData => { toast(`Added a Verifiable Credential! ${verifiableCredentialHash as string}`, { type: "success" }); }, []); - const onRejectVerifiableCredential = useCallback(() => { - toast(`Rejected request to add a Verifiable Credential.`, { type: "error" }); + const onReject = useCallback(() => { + toast(`User rejected request`, { type: "error" }); }, []); const onRevealCommitment = useCallback( @@ -236,7 +236,7 @@ export const useCryptKeeper = (): IUseCryptKeeperData => { client.on(EventName.IDENTITY_CHANGED, onIdentityChanged); client.on(EventName.LOGOUT, onLogout); client.on(EventName.ADD_VERIFIABLE_CREDENTIAL, onAddVerifiableCredential); - client.on(EventName.REJECT_VERIFIABLE_CREDENTIAL, onRejectVerifiableCredential); + client.on(EventName.USER_REJECT, onReject); client.on(EventName.REVEAL_COMMITMENT, onRevealCommitment); getConnectedIdentity(); @@ -244,15 +244,7 @@ export const useCryptKeeper = (): IUseCryptKeeperData => { return () => { client.cleanListeners(); }; - }, [ - client, - onLogout, - onIdentityChanged, - onLogin, - onAddVerifiableCredential, - onRejectVerifiableCredential, - onRevealCommitment, - ]); + }, [client, onLogout, onIdentityChanged, onLogin, onAddVerifiableCredential, onReject, onRevealCommitment]); return { client, diff --git a/packages/e2e/playwright.config.ts b/packages/e2e/playwright.config.ts index 8d1ad4aa5..6490be319 100644 --- a/packages/e2e/playwright.config.ts +++ b/packages/e2e/playwright.config.ts @@ -14,6 +14,7 @@ export default defineConfig({ expect: { timeout: 15_000, }, + maxFailures: 0, fullyParallel: true, forbidOnly: !!process.env.CI, retries: process.env.CI ? 1 : 0, diff --git a/packages/providers/src/constants/rpcAction.ts b/packages/providers/src/constants/rpcAction.ts index 70c98c167..a784e2777 100644 --- a/packages/providers/src/constants/rpcAction.ts +++ b/packages/providers/src/constants/rpcAction.ts @@ -21,8 +21,9 @@ export enum RPCAction { GET_IDENTITIES = "rpc/identity/getIdentities", JOIN_GROUP_REQUEST = "rpc/group/joinRequest", JOIN_GROUP = "rpc/group/join", - GENERATE_GROUP_MEMBERSHIP_PROOF_REQUEST = "rpc/group/generateMembershipProofRequest", - GENERATE_GROUP_MEMBERSHIP_PROOF = "rpc/group/generateMembershipProof", + GENERATE_GROUP_MERKLE_PROOF_REQUEST = "rpc/groups/generateGroupMerkleProofRequest", + GENERATE_GROUP_MERKLE_PROOF = "rpc/groups/generateMerkleProof", + CHECK_GROUP_MEMBERSHIP = "rpc/groups/checkMembership", GET_REQUEST_PENDING_STATUS = "rpc/identity/getRequestPendingStatus", FINALIZE_REQUEST = "rpc/requests/finalize", GET_PENDING_REQUESTS = "rpc/requests/get", @@ -67,6 +68,7 @@ export enum RPCAction { GENERATE_RLN_PROOF = "rpc/proofs/generate-rln-proof", GENERATE_RLN_PROOF_OFFSCREEN = "rpc/proofs/generate-rln-proof-offscreen", RLN_PROOF_RESULT = "rpc/proofs/rln-proof-result", + PUSH_EVENT = "rpc/browser/tabs/pushEvent", // DEV RPCS CLEAR_APPROVED_HOSTS = "rpc/hosts/clear", CLEAR_STORAGE = "rpc/browser/clear", diff --git a/packages/providers/src/event/index.ts b/packages/providers/src/event/index.ts index e2fa08b15..7b2dceef9 100644 --- a/packages/providers/src/event/index.ts +++ b/packages/providers/src/event/index.ts @@ -1,3 +1,3 @@ export { EventEmitter } from "./EventEmitter"; export type { EventHandler, Events } from "./types"; -export { EventName } from "./types"; +export { EventName, RejectRequests } from "./types"; diff --git a/packages/providers/src/event/types.ts b/packages/providers/src/event/types.ts index 9e8d4efe9..d4925473a 100644 --- a/packages/providers/src/event/types.ts +++ b/packages/providers/src/event/types.ts @@ -16,15 +16,18 @@ export type EventHandler = (data: unknown) => void; * @property {string} IDENTITY_CHANGED - "identityChanged" * @property {string} LOGOUT - "logout" * @property {string} ADD_VERIFIABLE_CREDENTIAL - "addVerifiableCredential" - * @property {string} REJECT_VERIFIABLE_CREDENTIAL - "rejectVerifiableCredential" + * @property {string} REVEAL_COMMITMENT - "revealCommitment" + * @property {string} JOIN_GROUP - "joinGroup" + * @property {string} USER_REJECT - "userReject" */ export enum EventName { LOGIN = "login", IDENTITY_CHANGED = "identityChanged", LOGOUT = "logout", ADD_VERIFIABLE_CREDENTIAL = "addVerifiableCredential", - REJECT_VERIFIABLE_CREDENTIAL = "rejectVerifiableCredential", REVEAL_COMMITMENT = "revealCommitment", + JOIN_GROUP = "joinGroup", + USER_REJECT = "userReject", } /** @@ -34,3 +37,18 @@ export enum EventName { * @typedef {Record} Events */ export type Events = Record; + +/** + * Enumeration representing possible rejected requests. + * + * @enum {string} + * @readonly + * @property {string} ADD_VERIFIABLE_CREDENTIAL - "addVerifiableCredential" + * @property {string} REVEAL_COMMITMENT - "revealCommitment" + * @property {string} JOIN_GROUP - "joinGroup" + */ +export enum RejectRequests { + ADD_VERIFIABLE_CREDENTIAL = "addVerifiableCredential", + JOIN_GROUP = "joinGroup", + REVEAL_COMMITMENT = "revealCommitment", +} diff --git a/packages/providers/src/index.ts b/packages/providers/src/index.ts index b3125977a..70024fb3e 100644 --- a/packages/providers/src/index.ts +++ b/packages/providers/src/index.ts @@ -1,3 +1,3 @@ export { CryptKeeperInjectedProvider, initializeCryptKeeperProvider, cryptkeeperConnect } from "./sdk"; export { RPCAction } from "./constants"; -export { EventName } from "./event"; +export { EventName, RejectRequests } from "./event"; diff --git a/packages/providers/src/sdk/CryptKeeperInjectedProvider.ts b/packages/providers/src/sdk/CryptKeeperInjectedProvider.ts index 452bb09e1..41d691c22 100644 --- a/packages/providers/src/sdk/CryptKeeperInjectedProvider.ts +++ b/packages/providers/src/sdk/CryptKeeperInjectedProvider.ts @@ -27,8 +27,9 @@ const EVENTS = [ EventName.LOGIN, EventName.LOGOUT, EventName.ADD_VERIFIABLE_CREDENTIAL, - EventName.REJECT_VERIFIABLE_CREDENTIAL, EventName.REVEAL_COMMITMENT, + EventName.JOIN_GROUP, + EventName.USER_REJECT, ]; interface Handlers { @@ -402,13 +403,13 @@ export class CryptKeeperInjectedProvider { } /** - * Requests user to generate a group membership proof with current connected identity. + * Requests user to generate a group Merkle proof with current connected identity. * * @returns {Promise} */ - async generateGroupMembershipProof(payload: IGenerateGroupMerkleProofArgs): Promise { + async generateGroupMerkleProof(payload: IGenerateGroupMerkleProofArgs): Promise { await this.post({ - method: RPCAction.GENERATE_GROUP_MEMBERSHIP_PROOF_REQUEST, + method: RPCAction.GENERATE_GROUP_MERKLE_PROOF_REQUEST, payload, }); } diff --git a/packages/types/src/group/index.ts b/packages/types/src/group/index.ts index 3bbe1ccd7..bacdeecdd 100644 --- a/packages/types/src/group/index.ts +++ b/packages/types/src/group/index.ts @@ -1,9 +1,11 @@ +import type { IIdentityData } from "../identity"; + export interface IGenerateGroupMerkleProofArgs { groupId: string; } export interface IGenerateBandadaMerkleProofArgs extends IGenerateGroupMerkleProofArgs { - commitment: string; + identity: IIdentityData; } export interface IJoinGroupMemberArgs { @@ -13,5 +15,13 @@ export interface IJoinGroupMemberArgs { } export interface IAddBandadaGroupMemberArgs extends IJoinGroupMemberArgs { - commitment: string; + identity: IIdentityData; +} + +export interface ICheckGroupMembershipArgs { + groupId: string; +} + +export interface ICheckBandadaGroupMembershipArgs extends ICheckGroupMembershipArgs { + identity: IIdentityData; } diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 397b9101f..58bd5b2f3 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -45,7 +45,7 @@ export type { ISemaphoreProofRequiredArgs, } from "./proof"; export { ZkProofType } from "./proof"; -export type { IRequestHandler, IPendingRequest } from "./request"; +export type { IRequestHandler, IPendingRequest, IRejectedRequest } from "./request"; export { RequestResolutionStatus, PendingRequestType } from "./request"; export type { IVerifiableCredential, @@ -61,4 +61,6 @@ export type { IAddBandadaGroupMemberArgs, IGenerateGroupMerkleProofArgs, IGenerateBandadaMerkleProofArgs, + ICheckGroupMembershipArgs, + ICheckBandadaGroupMembershipArgs, } from "./group"; diff --git a/packages/types/src/request/index.ts b/packages/types/src/request/index.ts index a23514cc9..e94c1143c 100644 --- a/packages/types/src/request/index.ts +++ b/packages/types/src/request/index.ts @@ -36,3 +36,8 @@ export interface IPendingRequest

{ type: PendingRequestType; payload?: P; } + +export interface IRejectedRequest

{ + type: string; + payload?: P; +}