diff --git a/src/common/AccountInfo.test.tsx b/src/common/AccountInfo.test.tsx index 9e61a330..d156205e 100644 --- a/src/common/AccountInfo.test.tsx +++ b/src/common/AccountInfo.test.tsx @@ -3,7 +3,6 @@ import { UserIdentity as IdentityType, PostalAddress as PostalAddressType } from import { render } from "../tests"; import { DEFAULT_IDENTITY, COLOR_THEME, DEFAULT_ADDRESS } from "./TestData"; import AccountInfo from "./AccountInfo"; -import { TEST_WALLET_USER } from "../wallet-user/TestData"; const DIFFERENT_IDENTITY: IdentityType = { firstName: "John2", @@ -24,7 +23,6 @@ test("renders without comparison", () => { const tree = render( { const tree = render( { const tree = render( - props.address } - colors={ props.colors } - squeeze={ props.squeeze } - noComparison={ true } - /> { - const response = await axios.put("/api/protection-request", specification); - return response.data.requests; -} - export function isGrantedAccess(address: ValidAccountId | undefined, loc: LocData): boolean { return loc.ownerAccountId.equals(address) || (loc.requesterAccountId?.equals(address) || false); } diff --git a/src/common/Spacer.css b/src/common/Spacer.css index 495614f4..07952cca 100644 --- a/src/common/Spacer.css +++ b/src/common/Spacer.css @@ -1,20 +1,12 @@ .Spacer { - margin-left: -15px; position: absolute; - horiz-align: center; top: 50%; -ms-transform: translateY(-50%); transform: translateY(-50%); - width: 100%; justify-content: center; align-items: center; - display: -ms-flexbox; - display: -webkit-flex; display: flex; - - -webkit-align-items: center; - -webkit-box-align: center; } diff --git a/src/common/__snapshots__/AccountInfo.test.tsx.snap b/src/common/__snapshots__/AccountInfo.test.tsx.snap index a627b4a1..e20169b5 100644 --- a/src/common/__snapshots__/AccountInfo.test.tsx.snap +++ b/src/common/__snapshots__/AccountInfo.test.tsx.snap @@ -4,49 +4,6 @@ exports[`renders and compares with different data 1`] = `
-
-
- -
- - -
-
-
@@ -512,49 +469,6 @@ exports[`renders and compares with same data 1`] = `
-
-
- -
- - -
-
-
@@ -1020,49 +934,6 @@ exports[`renders without comparison 1`] = `
-
-
- -
- - -
-
-
diff --git a/src/legal-officer/LegalOfficerContext.tsx b/src/legal-officer/LegalOfficerContext.tsx index 2e04b42b..82e49df2 100644 --- a/src/legal-officer/LegalOfficerContext.tsx +++ b/src/legal-officer/LegalOfficerContext.tsx @@ -5,11 +5,9 @@ import { LegalOfficer, LocsState, Votes, - ProtectionRequest, } from '@logion/client'; import { LocType, LegalOfficerData, ValidAccountId } from "@logion/node-api"; -import { fetchProtectionRequests } from '../common/Model'; import { useCommonContext } from '../common/CommonContext'; import { LIGHT_MODE } from './Types'; import { useLogionChain } from '../logion-chain'; @@ -17,6 +15,7 @@ import { VaultApi } from '../vault/VaultApi'; import { DateTime } from "luxon"; import { fetchAllLocsParams } from 'src/loc/LegalOfficerLocContext'; import { Locs, getLocsMap } from 'src/loc/Locs'; +import { RecoveryRequest, fetchRecoveryRequests } from './Model'; export const SETTINGS_KEYS = [ 'oath' ]; @@ -36,13 +35,10 @@ export interface MissingSettings { } export interface LegalOfficerContext { - refreshRequests: ((clearBeforeRefresh: boolean) => void), - locsState: LocsState | null, - pendingProtectionRequests: ProtectionRequest[] | null, - activatedProtectionRequests: ProtectionRequest[] | null, - protectionRequestsHistory: ProtectionRequest[] | null, - pendingRecoveryRequests: ProtectionRequest[] | null, - recoveryRequestsHistory: ProtectionRequest[] | null, + refreshRequests: ((clearBeforeRefresh: boolean) => void); + locsState: LocsState | null; + pendingRecoveryRequests: RecoveryRequest[] | null; + recoveryRequestsHistory: RecoveryRequest[] | null; axios?: AxiosInstance; pendingVaultTransferRequests?: VaultTransferRequest[]; vaultTransferRequestsHistory?: VaultTransferRequest[]; @@ -76,9 +72,6 @@ function initialContextValue(): FullLegalOfficerContext { dataAddress: null, refreshRequests: DEFAULT_NOOP, locsState: null, - pendingProtectionRequests: null, - activatedProtectionRequests: null, - protectionRequestsHistory: null, pendingRecoveryRequests: null, recoveryRequestsHistory: null, updateSetting: () => Promise.reject(), @@ -134,11 +127,8 @@ interface Action { type: ActionType; dataAddress?: ValidAccountId; locsState?: LocsState; - pendingProtectionRequests?: ProtectionRequest[]; - protectionRequestsHistory?: ProtectionRequest[]; - activatedProtectionRequests?: ProtectionRequest[]; - pendingRecoveryRequests?: ProtectionRequest[], - recoveryRequestsHistory?: ProtectionRequest[], + pendingRecoveryRequests?: RecoveryRequest[], + recoveryRequestsHistory?: RecoveryRequest[], refreshRequests?: (clearBeforeRefresh: boolean) => void; clearBeforeRefresh?: boolean; axios?: AxiosInstance; @@ -203,9 +193,6 @@ const reducer: Reducer = (state: FullLegalOffic if(action.dataAddress === state.dataAddress) { return { ...state, - pendingProtectionRequests: action.pendingProtectionRequests!, - protectionRequestsHistory: action.protectionRequestsHistory!, - activatedProtectionRequests: action.activatedProtectionRequests!, pendingRecoveryRequests: action.pendingRecoveryRequests!, recoveryRequestsHistory: action.recoveryRequestsHistory!, pendingVaultTransferRequests: action.pendingVaultTransferRequests!, @@ -506,20 +493,15 @@ export function LegalOfficerContextProvider(props: Props) { (async function() { const axios = axiosFactory(currentAddress); - const allRequests = await fetchProtectionRequests(axios, { - legalOfficerAddress: currentAddress.address, - }); - const pendingProtectionRequests = allRequests.filter(request => ["PENDING"].includes(request.status) && !request.isRecovery); - const activatedProtectionRequests = allRequests.filter(request => ["ACTIVATED"].includes(request.status)); - const pendingRecoveryRequests = allRequests.filter(request => ["PENDING"].includes(request.status) && request.isRecovery); - const protectionRequestsHistory = allRequests.filter(request => - ["ACCEPTED", "REJECTED", "ACTIVATED", "CANCELLED", "REJECTED_CANCELLED", "ACCEPTED_CANCELLED"].includes(request.status) - && !request.isRecovery - ); - const recoveryRequestsHistory = allRequests.filter(request => - ["ACCEPTED", "REJECTED", "ACTIVATED", "CANCELLED", "REJECTED_CANCELLED", "ACCEPTED_CANCELLED"].includes(request.status) - && request.isRecovery - ); + const allAccountRecoveryRequests = await fetchRecoveryRequests(axios); + const pendingRecoveryRequests: RecoveryRequest[] = allAccountRecoveryRequests + .filter(request => ["PENDING"].includes(request.status)) + .sort((a, b) => a.createdOn.localeCompare(b.createdOn)); + const recoveryRequestsHistory: RecoveryRequest[] = allAccountRecoveryRequests + .filter(request => + ["ACCEPTED", "REJECTED", "ACTIVATED", "CANCELLED", "REJECTED_CANCELLED", "ACCEPTED_CANCELLED"].includes(request.status) + ) + .sort((a, b) => b.createdOn.localeCompare(a.createdOn)); const allVaultTransferRequestsResult = (await new VaultApi(axios, currentAddress).getVaultTransferRequests({ legalOfficerAddress: currentAddress.address, @@ -532,9 +514,6 @@ export function LegalOfficerContextProvider(props: Props) { dispatch({ type: "SET_REQUESTS_DATA", dataAddress: currentAddress, - pendingProtectionRequests, - activatedProtectionRequests, - protectionRequestsHistory, pendingRecoveryRequests, recoveryRequestsHistory, pendingVaultTransferRequests, diff --git a/src/legal-officer/LegalOfficerPaths.tsx b/src/legal-officer/LegalOfficerPaths.tsx index 829059b0..eb7bcaff 100644 --- a/src/legal-officer/LegalOfficerPaths.tsx +++ b/src/legal-officer/LegalOfficerPaths.tsx @@ -7,16 +7,19 @@ import { LOC_REQUESTS_RELATIVE_PATH, LOC_DETAILS_RELATIVE_PATH, } from '../RootPaths'; +import { RecoveryRequestType } from './Model'; export const HOME_PATH = LEGAL_OFFICER_PATH; export const RECOVERY_REQUESTS_RELATIVE_PATH = '/recovery'; export const RECOVERY_REQUESTS_PATH = LEGAL_OFFICER_PATH + RECOVERY_REQUESTS_RELATIVE_PATH; -export const RECOVERY_DETAILS_RELATIVE_PATH = '/recovery-details/:requestId'; +export const RECOVERY_DETAILS_RELATIVE_PATH = RECOVERY_REQUESTS_RELATIVE_PATH + '/:requestId/:type'; export const RECOVERY_DETAILS_PATH = LEGAL_OFFICER_PATH + RECOVERY_DETAILS_RELATIVE_PATH; -export function recoveryDetailsPath(requestId: string): string { - return RECOVERY_DETAILS_PATH.replace(":requestId", requestId); +export function recoveryDetailsPath(requestId: string, type: RecoveryRequestType): string { + return RECOVERY_DETAILS_PATH + .replace(":requestId", requestId) + .replace(":type", type); } export const SETTINGS_RELATIVE_PATH = '/settings'; diff --git a/src/legal-officer/Model.tsx b/src/legal-officer/Model.tsx index 3bb930c8..a4e5b7ba 100644 --- a/src/legal-officer/Model.tsx +++ b/src/legal-officer/Model.tsx @@ -1,10 +1,115 @@ import { AxiosInstance } from 'axios'; -import { RecoveryInfo } from './Types'; +import { PostalAddress, ProtectionRequest, ProtectionRequestStatus, UserIdentity } from '@logion/client'; + +export interface RecoveryRequest { + userIdentity: UserIdentity; + userPostalAddress: PostalAddress; + createdOn: string; + status: RecoveryRequestStatus; + type: RecoveryRequestType; + id: string; + rejectReason?: string; +} + +export type RecoveryRequestStatus = ProtectionRequestStatus; + +export type RecoveryRequestType = "ACCOUNT" | "SECRET"; + +export function toRecoveryRequestType(value: string | undefined | null): RecoveryRequestType { + if(value === "ACCOUNT" || value === "SECRET") { + return value; + } else { + throw new Error(`Unexpected value ${ value }`); + } +} + +export async function fetchRecoveryRequests( + axios: AxiosInstance, +): Promise { + const response = await axios.put(`/api/recovery-requests`); + return response.data.requests; +} + +export interface RecoveryInfo { + type: RecoveryRequestType; + identity1?: { + userIdentity: UserIdentity; + userPostalAddress: PostalAddress; + }, + identity2: { + userIdentity: UserIdentity; + userPostalAddress: PostalAddress; + }, + accountRecovery?: { + address1: string; + address2: string; + }, +} + +export interface BackendRecoveryInfo { + addressToRecover: string, + recoveryAccount: ProtectionRequest, + accountToRecover?: ProtectionRequest, +} export async function fetchRecoveryInfo( axios: AxiosInstance, - requestId: string + requestId: string, + recoveryType: RecoveryRequestType, ): Promise { - const response = await axios.put(`/api/protection-request/${requestId}/recovery-info`, {}) - return response.data; + if(recoveryType === "ACCOUNT") { + const response = await axios.put(`/api/protection-request/${requestId}/recovery-info`); + return response.data; + } else if(recoveryType === "SECRET") { + const response = await axios.put(`/api/secret-recovery/${requestId}/recovery-info`); + return response.data; + } else { + throw new Error(`Unsupported recovery type ${ recoveryType }`); + } +} + +export interface RejectProtectionRequestParameters { + requestId: string, + rejectReason: string, +} + +export async function rejectAccountRecoveryRequest( + axios: AxiosInstance, + parameters: RejectProtectionRequestParameters +): Promise { + await axios.post(`/api/protection-request/${parameters.requestId}/reject`, { + rejectReason: parameters.rejectReason, + }); +} + +export interface AcceptProtectionRequestParameters { + requestId: string, +} + +export async function acceptAccountRecoveryRequest( + axios: AxiosInstance, + parameters: AcceptProtectionRequestParameters +): Promise { + await axios.post(`/api/protection-request/${parameters.requestId}/accept`); +} + + +export async function rejectSecretRecoveryRequest( + axios: AxiosInstance, + parameters: RejectProtectionRequestParameters +): Promise { + await axios.post(`/api/secret-recovery/${parameters.requestId}/reject`, { + rejectReason: parameters.rejectReason, + }); +} + +export interface AcceptProtectionRequestParameters { + requestId: string, +} + +export async function acceptSecretRecoveryRequest( + axios: AxiosInstance, + parameters: AcceptProtectionRequestParameters +): Promise { + await axios.post(`/api/secret-recovery/${parameters.requestId}/accept`); } diff --git a/src/legal-officer/TestData.tsx b/src/legal-officer/TestData.tsx index b76eeab0..4fda1063 100644 --- a/src/legal-officer/TestData.tsx +++ b/src/legal-officer/TestData.tsx @@ -4,68 +4,6 @@ import { ProtectionRequest } from '@logion/client/dist/RecoveryClient.js'; import { DEFAULT_LEGAL_OFFICER, ANOTHER_LEGAL_OFFICER } from "../common/TestData"; -export const PENDING_PROTECTION_REQUESTS: ProtectionRequest[] = [ - { - id: "1", - requesterAddress: "5Ew3MyB15VprZrjQVkpQFj8okmc9xLDSEdNhqMMS5cXsqxoW", - legalOfficerAddress: DEFAULT_LEGAL_OFFICER.address, - otherLegalOfficerAddress: ANOTHER_LEGAL_OFFICER.address, - requesterIdentityLoc: "460acfb9-0bae-42d2-9a23-0545d964e633", - decision: { - rejectReason: null, - decisionOn: null, - }, - userIdentity: { - firstName: "John", - lastName: "Doe", - email: "john.doe@logion.network", - phoneNumber: "+1234", - }, - userPostalAddress: { - line1: "Place de le République Française, 10", - line2: "boite 15", - postalCode: "4000", - city: "Liège", - country: "Belgium", - }, - createdOn: toIsoString(DateTime.fromISO('2021-06-10T11:40:00.000', {zone: "utc"})), - isRecovery: false, - addressToRecover: null, - status: "PENDING" - } -]; - -export const PROTECTION_REQUESTS_HISTORY: ProtectionRequest[] = [ - { - id: "1", - requesterAddress: "5Ew3MyB15VprZrjQVkpQFj8okmc9xLDSEdNhqMMS5cXsqxoW", - legalOfficerAddress: DEFAULT_LEGAL_OFFICER.address, - otherLegalOfficerAddress: ANOTHER_LEGAL_OFFICER.address, - requesterIdentityLoc: "460acfb9-0bae-42d2-9a23-0545d964e633", - decision: { - rejectReason: null, - decisionOn: toIsoString(DateTime.fromISO('2021-06-10T11:40:00.000', {zone: "utc"})), - }, - userIdentity: { - firstName: "John", - lastName: "Doe", - email: "john.doe@logion.network", - phoneNumber: "+1234", - }, - userPostalAddress: { - line1: "Place de le République Française, 10", - line2: "boite 15", - postalCode: "4000", - city: "Liège", - country: "Belgium", - }, - createdOn: toIsoString(DateTime.fromISO('2021-06-10T11:40:00.000', {zone: "utc"})), - isRecovery: false, - addressToRecover: null, - status: "ACTIVATED" - } -]; - export const RECOVERY_REQUESTS_HISTORY: ProtectionRequest[] = [ { id: "1", @@ -92,7 +30,7 @@ export const RECOVERY_REQUESTS_HISTORY: ProtectionRequest[] = [ }, createdOn: toIsoString(DateTime.fromISO('2021-06-10T11:40:00.000', {zone: "utc"})), isRecovery: true, - addressToRecover: "an-address", + addressToRecover: "5EsuBEtGbx8DoKTcKYDceJudEuzzHSS6GBPhbaPh4rsYsuoL", status: "PENDING" } ]; diff --git a/src/legal-officer/Types.ts b/src/legal-officer/Types.ts index 515275f4..e6bb236e 100644 --- a/src/legal-officer/Types.ts +++ b/src/legal-officer/Types.ts @@ -1,13 +1,5 @@ -import { ProtectionRequest } from '@logion/client/dist/RecoveryClient.js'; - import { ColorTheme, rgbaToHex } from '../common/ColorTheme'; -export interface RecoveryInfo { - addressToRecover: string, - recoveryAccount: ProtectionRequest, - accountToRecover?: ProtectionRequest, -} - export const LIGHT_MODE: ColorTheme = { type: 'light', shadowColor: rgbaToHex('#3b6cf4', 0.1), diff --git a/src/legal-officer/__mocks__/Model.tsx b/src/legal-officer/__mocks__/Model.tsx index e7131acc..7e86130f 100644 --- a/src/legal-officer/__mocks__/Model.tsx +++ b/src/legal-officer/__mocks__/Model.tsx @@ -1,7 +1,15 @@ import { fetchRecoveryInfo, + acceptAccountRecoveryRequest, + rejectAccountRecoveryRequest, } from './ModelMock'; export { fetchRecoveryInfo, + acceptAccountRecoveryRequest, + rejectAccountRecoveryRequest, }; + +export function toRecoveryRequestType(value: string | undefined) { + return value; +} diff --git a/src/legal-officer/__mocks__/ModelMock.tsx b/src/legal-officer/__mocks__/ModelMock.tsx index 47026f00..86b5eb21 100644 --- a/src/legal-officer/__mocks__/ModelMock.tsx +++ b/src/legal-officer/__mocks__/ModelMock.tsx @@ -3,3 +3,15 @@ export let fetchRecoveryInfo = jest.fn(); export function setFetchRecoveryInfo(mockFn: any) { fetchRecoveryInfo = mockFn; } + +export let rejectAccountRecoveryRequest = jest.fn(); + +export function setRejectAccountRecoveryRequest(mockFn: any) { + rejectAccountRecoveryRequest = mockFn; +} + +export let acceptAccountRecoveryRequest = jest.fn(); + +export function setAcceptAccountRecoveryRequest(mockFn: any) { + acceptAccountRecoveryRequest = mockFn; +} diff --git a/src/legal-officer/__snapshots__/LegalOfficerRouter.test.tsx.snap b/src/legal-officer/__snapshots__/LegalOfficerRouter.test.tsx.snap index f7f69b0c..da12e76e 100644 --- a/src/legal-officer/__snapshots__/LegalOfficerRouter.test.tsx.snap +++ b/src/legal-officer/__snapshots__/LegalOfficerRouter.test.tsx.snap @@ -12,7 +12,7 @@ exports[`renders 1`] = ` /> } - path="/recovery-details/:requestId" + path="/recovery/:requestId/:type" /> (""); - const [ reviewState, setReviewState ] = useState(NO_REVIEW_STATE); + const { api } = useLogionChain(); + const { pendingRecoveryRequests } = useLegalOfficerContext(); const navigate = useNavigate(); - const handleClose = useCallback(() => { - setReviewState(NO_REVIEW_STATE); - }, [ setReviewState ]); - - const rejectAndCloseModal = useCallback(() => { - const currentAddress = accounts!.current!.accountId; - (async function () { - const requestId = reviewState.request!.id; - await rejectProtectionRequest(axiosFactory!(currentAddress)!, { - legalOfficerAddress: currentAddress.address, - requestId, - rejectReason, - }); - setReviewState(NO_REVIEW_STATE); - refreshRequests!(false); - })(); - }, [ axiosFactory, reviewState, accounts, rejectReason, setReviewState, refreshRequests ]); - - const acceptAndCloseModal = useCallback(async () => { - const currentAddress = accounts!.current!.accountId; - const requestId = reviewState.request!.id; - await acceptProtectionRequest(axiosFactory!(currentAddress)!, { requestId }); - setReviewState(NO_REVIEW_STATE); - refreshRequests!(false); - }, [ axiosFactory, reviewState, accounts, setReviewState, refreshRequests ]); - if (!api || pendingRecoveryRequests === null) { return null; } - let columns: Column[]; + let columns: Column[]; columns = [ { header: "First name", render: request => , - width: "200px", align: 'left', }, { header: "Last name", render: request => , - width: "200px", - renderDetails: request => , align: 'left', }, { header: "Status", - render: request => , + render: request => , width: "140px", - splitAfter: true, }, { header: "Submission date", @@ -96,16 +43,9 @@ export default function PendingRecoveryRequests() { width: "120px", }, { - header: "Account number", - render: request => , - align: 'left', - }, - { - header: "Account to recover", - render: request => , - align: 'left', + header: "Type", + render: request => , + width: "200px", }, { header: "Action", @@ -113,98 +53,21 @@ export default function PendingRecoveryRequests() { ), + width: "300px", } ]; return ( - <> - No request to display } - /> - - { - reviewState.status === ReviewStatus.PENDING && - setReviewState({ ...reviewState, status: ReviewStatus.REJECTING }), - mayProceed: true, - buttonVariant: "secondary", - buttonText: "No", - }, - { - id: "accept", - mayProceed: true, - buttonVariant: "primary", - buttonText: "Yes", - callback: acceptAndCloseModal, - } - ] } - > - -

I executed my due diligence and accept to be the Legal Officer of this user

-
- } - { - reviewState.status === ReviewStatus.REJECTING && - - - Reason - setRejectReason(e.target.value) } - value={ rejectReason } - data-testid="reason" - /> - - - } - +
No request to display } + /> ); } diff --git a/src/legal-officer/recovery/RecoveryDetails.css b/src/legal-officer/recovery/RecoveryDetails.css index 91f41a95..3935dea1 100644 --- a/src/legal-officer/recovery/RecoveryDetails.css +++ b/src/legal-officer/recovery/RecoveryDetails.css @@ -3,6 +3,10 @@ margin-bottom: 50px; } +.RecoveryDetails .main-frame > .row:first-child { + position: relative; +} + .RecoveryDetails h3 { text-align: center; font-weight: bold; diff --git a/src/legal-officer/recovery/RecoveryDetails.test.tsx b/src/legal-officer/recovery/RecoveryDetails.test.tsx index f0cd74f7..5f61833f 100644 --- a/src/legal-officer/recovery/RecoveryDetails.test.tsx +++ b/src/legal-officer/recovery/RecoveryDetails.test.tsx @@ -1,6 +1,5 @@ jest.mock('../../logion-chain/Signature'); jest.mock("../../common/CommonContext"); -jest.mock("../../loc/Model"); jest.mock("../Model"); jest.mock("../LegalOfficerContext"); jest.mock("../../logion-chain"); @@ -11,15 +10,14 @@ import { render, waitFor, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import RecoveryDetails from './RecoveryDetails'; -import { RecoveryInfo } from '../Types'; -import { acceptProtectionRequest, rejectProtectionRequest } from '../../loc/__mocks__/ModelMock'; -import { setFetchRecoveryInfo } from '../__mocks__/ModelMock'; -import { PROTECTION_REQUESTS_HISTORY } from '../TestData'; +import { acceptAccountRecoveryRequest, rejectAccountRecoveryRequest, setFetchRecoveryInfo } from '../__mocks__/ModelMock'; +import { RECOVERY_REQUESTS_HISTORY } from '../TestData'; import { axiosMock, setAddresses, DEFAULT_LEGAL_OFFICER_ACCOUNT } from '../../logion-chain/__mocks__/LogionChainMock'; import { setParams, navigate } from '../../__mocks__/ReactRouterMock'; import { refreshRequests } from '../__mocks__/LegalOfficerContextMock'; import { It, Mock } from 'moq.ts'; import { setupApiMock } from '../../__mocks__/LogionMock'; +import { RecoveryInfo } from "../Model"; describe("RecoveryDetails", () => { @@ -28,11 +26,21 @@ describe("RecoveryDetails", () => { current: DEFAULT_LEGAL_OFFICER_ACCOUNT, all: [ DEFAULT_LEGAL_OFFICER_ACCOUNT], }); - const protectionRequest = PROTECTION_REQUESTS_HISTORY[0]; + const protectionRequest = RECOVERY_REQUESTS_HISTORY[0]; const recoveryConfig: RecoveryInfo = { - addressToRecover: protectionRequest.addressToRecover!, - accountToRecover: protectionRequest, - recoveryAccount: protectionRequest, + type: "ACCOUNT", + identity1: { + userIdentity: protectionRequest.userIdentity, + userPostalAddress: protectionRequest.userPostalAddress, + }, + identity2: { + userIdentity: protectionRequest.userIdentity, + userPostalAddress: protectionRequest.userPostalAddress, + }, + accountRecovery: { + address1: protectionRequest.addressToRecover!, + address2: protectionRequest.requesterAddress, + }, }; setFetchRecoveryInfo(jest.fn().mockResolvedValue(recoveryConfig)); setParams({ requestId: protectionRequest.id }); @@ -54,7 +62,7 @@ describe("RecoveryDetails", () => { await waitFor(() => processButton = screen.getByRole("button", {name: "Proceed"})); await userEvent.click(processButton!); - await waitFor(() => expect(acceptProtectionRequest).toBeCalledWith( + await waitFor(() => expect(acceptAccountRecoveryRequest).toHaveBeenCalledWith( axiosMock.object(), expect.objectContaining({ requestId: protectionRequest.id, @@ -67,14 +75,24 @@ describe("RecoveryDetails", () => { current: DEFAULT_LEGAL_OFFICER_ACCOUNT, all: [ DEFAULT_LEGAL_OFFICER_ACCOUNT], }); - const protectionRequest = PROTECTION_REQUESTS_HISTORY[0]; + const protectionRequest = RECOVERY_REQUESTS_HISTORY[0]; const recoveryConfig: RecoveryInfo = { - addressToRecover: protectionRequest.addressToRecover!, - accountToRecover: protectionRequest, - recoveryAccount: protectionRequest, + type: "ACCOUNT", + identity1: { + userIdentity: protectionRequest.userIdentity, + userPostalAddress: protectionRequest.userPostalAddress, + }, + identity2: { + userIdentity: protectionRequest.userIdentity, + userPostalAddress: protectionRequest.userPostalAddress, + }, + accountRecovery: { + address1: protectionRequest.addressToRecover!, + address2: protectionRequest.requesterAddress, + }, }; setFetchRecoveryInfo(jest.fn().mockResolvedValue(recoveryConfig)); - setParams({ requestId: protectionRequest.id }); + setParams({ requestId: protectionRequest.id, type: "ACCOUNT" }); render(); @@ -86,11 +104,11 @@ describe("RecoveryDetails", () => { await waitFor(() => confirmButton = screen.getAllByRole("button", {name: "Refuse"})[1]); await userEvent.click(confirmButton!); - await waitFor(() => expect(rejectProtectionRequest).toBeCalledWith(axiosMock.object(), expect.objectContaining({ - legalOfficerAddress: DEFAULT_LEGAL_OFFICER_ACCOUNT.accountId.address, + await waitFor(() => expect(rejectAccountRecoveryRequest).toHaveBeenCalledWith(axiosMock.object(), expect.objectContaining({ requestId: protectionRequest.id, + rejectReason: "", }))); - await waitFor(() => expect(refreshRequests).toBeCalled()); - await waitFor(() => expect(navigate).toBeCalledWith("/legal-officer/recovery")); + await waitFor(() => expect(refreshRequests).toHaveBeenCalled()); + await waitFor(() => expect(navigate).toHaveBeenCalledWith("/legal-officer/recovery")); }); }); diff --git a/src/legal-officer/recovery/RecoveryDetails.tsx b/src/legal-officer/recovery/RecoveryDetails.tsx index bad23bff..6cfa50a3 100644 --- a/src/legal-officer/recovery/RecoveryDetails.tsx +++ b/src/legal-officer/recovery/RecoveryDetails.tsx @@ -9,9 +9,7 @@ import { FullWidthPane } from "../../common/Dashboard"; import { useParams, useNavigate } from 'react-router'; import { RECOVERY_REQUESTS_PATH } from "../LegalOfficerPaths"; import Button from "../../common/Button"; -import { acceptProtectionRequest, rejectProtectionRequest } from "../../loc/Model"; -import { fetchRecoveryInfo } from "../Model"; -import { RecoveryInfo } from "../Types"; +import { RecoveryInfo, acceptAccountRecoveryRequest, acceptSecretRecoveryRequest, fetchRecoveryInfo, rejectAccountRecoveryRequest, rejectSecretRecoveryRequest, toRecoveryRequestType } from "../Model"; import AccountInfo from "../../common/AccountInfo"; import Alert from "../../common/Alert"; import Frame from "../../common/Frame"; @@ -35,6 +33,7 @@ export default function RecoveryDetails() { const { colorTheme } = useCommonContext(); const { refreshRequests } = useLegalOfficerContext(); const { requestId } = useParams<"requestId">(); + const { type } = useParams<"type">(); const [ recoveryInfo, setRecoveryInfo ] = useState(null); const [ visible, setVisible ] = useState(Visible.NONE); const navigate = useNavigate(); @@ -43,28 +42,26 @@ export default function RecoveryDetails() { useEffect(() => { if (recoveryInfo === null && axiosFactory !== undefined) { const currentAccount = accounts!.current!.accountId; - fetchRecoveryInfo(axiosFactory(currentAccount)!, requestId!) + fetchRecoveryInfo(axiosFactory(currentAccount)!, requestId!, toRecoveryRequestType(type)) .then(recoveryInfo => setRecoveryInfo(recoveryInfo)); } - }, [ axiosFactory, accounts, recoveryInfo, setRecoveryInfo, requestId ]); + }, [ axiosFactory, accounts, recoveryInfo, setRecoveryInfo, requestId, type ]); const alreadyVouched = useCallback(async (lost: ValidAccountId, rescuer: ValidAccountId, currentAddress: ValidAccountId) => { const activeRecovery = await api?.queries.getActiveRecovery( lost, rescuer ); - return !!(activeRecovery && activeRecovery.legalOfficers.find(lo => lo.equals(currentAddress))); - }, [ api ]); - const accept = useCallback(async () => { + const acceptAccountRecovery = useCallback(async () => { const currentAddress = accounts!.current!.accountId; - const lost = ValidAccountId.polkadot(recoveryInfo!.accountToRecover!.requesterAddress); - const rescuer = ValidAccountId.polkadot(recoveryInfo!.recoveryAccount.requesterAddress); + const lost = ValidAccountId.polkadot(recoveryInfo!.accountRecovery!.address1); + const rescuer = ValidAccountId.polkadot(recoveryInfo!.accountRecovery!.address2); if (await alreadyVouched(lost, rescuer, currentAddress)) { - await acceptProtectionRequest(axiosFactory!(currentAddress)!, { + await acceptAccountRecoveryRequest(axiosFactory!(currentAddress)!, { requestId: requestId!, }); refreshRequests!(false); @@ -80,7 +77,7 @@ export default function RecoveryDetails() { submittable, callback, }); - await acceptProtectionRequest(axiosFactory!(currentAddress)!, { + await acceptAccountRecoveryRequest(axiosFactory!(currentAddress)!, { requestId: requestId!, }); }; @@ -96,18 +93,36 @@ export default function RecoveryDetails() { } }, [ axiosFactory, requestId, accounts, api, recoveryInfo, alreadyVouched, navigate, refreshRequests, submitCall, clearSubmissionState, signer ]); - const doReject = useCallback(() => { - (async function() { + const acceptSecretRecovery = useCallback(async () => { + const currentAddress = accounts!.current!.accountId; + await acceptSecretRecoveryRequest(axiosFactory!(currentAddress)!, { + requestId: requestId!, + }); + refreshRequests!(false); + navigate(RECOVERY_REQUESTS_PATH); + }, [ axiosFactory, requestId, accounts, navigate, refreshRequests ]); + + const doReject = useCallback(async () => { + if(type === "ACCOUNT") { const currentAccount = accounts!.current!.accountId; - await rejectProtectionRequest(axiosFactory!(currentAccount)!, { - legalOfficerAddress: currentAccount.address, + await rejectAccountRecoveryRequest(axiosFactory!(currentAccount)!, { requestId: requestId!, rejectReason, }); refreshRequests!(false); navigate(RECOVERY_REQUESTS_PATH); - })(); - }, [ axiosFactory, requestId, accounts, rejectReason, refreshRequests, navigate ]); + } else if(type === "SECRET") { + const currentAccount = accounts!.current!.accountId; + await rejectSecretRecoveryRequest(axiosFactory!(currentAccount)!, { + requestId: requestId!, + rejectReason, + }); + refreshRequests!(false); + navigate(RECOVERY_REQUESTS_PATH); + } else { + throw new Error(`Unsupported type ${ type }`); + } + }, [ axiosFactory, requestId, accounts, rejectReason, refreshRequests, navigate, type ]); if (!api || recoveryInfo === null) { return null; @@ -116,7 +131,7 @@ export default function RecoveryDetails() { return ( - - - I did my due diligence and authorize the transfer of all assets
- from the account address "From" to the account address "To" as detailed below : -
-
-

From

+

Identity 1

-

To

+

Identity 2

{ - (extrinsicSubmissionState.canSubmit() || extrinsicSubmissionState.callEnded) && + recoveryInfo.type === "ACCOUNT" && - - - + + I did my due diligence and authorize the transfer of all assets + from the account address
+ { recoveryInfo.accountRecovery?.address1 || "" }
+ to the account address
+ { recoveryInfo.accountRecovery?.address2 || "" } +
+
+ } + { + recoveryInfo.type === "SECRET" && + + + I did my due diligence and authorize the retrieval of a secret by the above person. + + + } + + + + + { + recoveryInfo.type === "ACCOUNT" && - - - } + } + { + recoveryInfo.type === "SECRET" && + + } + + - I did my due diligence and refuse to grant the - account { recoveryInfo.accountToRecover?.requesterAddress || "-" } the right to transfer all assets - to the account { recoveryInfo.recoveryAccount.requesterAddress }. + { + recoveryInfo.type === "ACCOUNT" && + <> + I did my due diligence and refuse to grant the{" "} + account { recoveryInfo.accountRecovery?.address1 || "-" } the right to transfer all assets{" "} + to the account { recoveryInfo.accountRecovery?.address2 || "-" }. + + } + { + recoveryInfo.type === "SECRET" && + <> + I did my due diligence and do not authorize the retrieval of a secret by the above person. + + } Reason -
- - - - - - - - - - - - - - - - - - + + + + + + + + + { + props.request.status === "REJECTED" && + + + + } + + ); } diff --git a/src/legal-officer/recovery/RecoveryRequestStatus.tsx b/src/legal-officer/recovery/RecoveryRequestStatus.tsx index c8457a3f..4430f4aa 100644 --- a/src/legal-officer/recovery/RecoveryRequestStatus.tsx +++ b/src/legal-officer/recovery/RecoveryRequestStatus.tsx @@ -4,9 +4,11 @@ import Icon from "../../common/Icon"; import { ORANGE, GREEN, RED, YELLOW } from "../../common/ColorTheme"; import './RecoveryRequestStatus.css'; +import { RecoveryRequestType } from '../Model'; export interface Props { - status: ProtectionRequestStatusType, + status: ProtectionRequestStatusType; + type: RecoveryRequestType; } export default function RecoveryRequestStatus(props: Props) { @@ -18,11 +20,11 @@ export default function RecoveryRequestStatus(props: Props) { statusColor = ORANGE; icon = (); status = Pending; - } else if(props.status === "ACCEPTED") { + } else if(props.status === "ACCEPTED" && props.type === "ACCOUNT") { statusColor = YELLOW; icon = (); status = Accepted; - } else if(props.status === "ACTIVATED") { + } else if(props.status === "ACTIVATED" || (props.status === "ACCEPTED" && props.type === "SECRET")) { statusColor = GREEN; icon = (); status = Accepted; diff --git a/src/legal-officer/recovery/RecoveryRequestsHistory.tsx b/src/legal-officer/recovery/RecoveryRequestsHistory.tsx index d1979637..8af1db54 100644 --- a/src/legal-officer/recovery/RecoveryRequestsHistory.tsx +++ b/src/legal-officer/recovery/RecoveryRequestsHistory.tsx @@ -1,9 +1,8 @@ -import Table, { Cell, EmptyTableMessage, DateTimeCell, CopyPasteCell } from '../../common/Table'; +import Table, { Cell, EmptyTableMessage, DateTimeCell } from '../../common/Table'; import { useLegalOfficerContext } from '../LegalOfficerContext'; -import RecoveryRequestStatus from './RecoveryRequestStatus'; import RecoveryRequestDetails from './RecoveryRequestDetails'; - +import RecoveryRequestStatus from './RecoveryRequestStatus'; export default function RecoveryRequestsHistory() { const { recoveryRequestsHistory } = useLegalOfficerContext(); @@ -19,38 +18,29 @@ export default function RecoveryRequestsHistory() { { header: "First name", render: request => , - width: "200px", align: 'left', }, { header: "Last name", render: request => , - width: "200px", - renderDetails: request => , align: 'left', + renderDetails: request => }, { header: "Status", - render: request => , + render: request => , width: "170px", - splitAfter: true, }, { header: "Submission date", render: request => , width: "120px", - smallerText: true, }, { - header: "Account number", - render: request => , - align: 'left', + header: "Type", + render: request => , + width: "120px", }, - { - header: "Account to recover", - render: request => , - align: 'left', - } ]} data={ recoveryRequestsHistory } renderEmpty={ () => No processed request} diff --git a/src/legal-officer/recovery/__snapshots__/RecoveryRequestsHistory.test.tsx.snap b/src/legal-officer/recovery/__snapshots__/RecoveryRequestsHistory.test.tsx.snap index d968da87..9f5ce462 100644 --- a/src/legal-officer/recovery/__snapshots__/RecoveryRequestsHistory.test.tsx.snap +++ b/src/legal-officer/recovery/__snapshots__/RecoveryRequestsHistory.test.tsx.snap @@ -9,36 +9,27 @@ exports[`Renders null with no data 1`] = ` "align": "left", "header": "First name", "render": [Function], - "width": "200px", }, Object { "align": "left", "header": "Last name", "render": [Function], "renderDetails": [Function], - "width": "200px", }, Object { "header": "Status", "render": [Function], - "splitAfter": true, "width": "170px", }, Object { "header": "Submission date", "render": [Function], - "smallerText": true, "width": "120px", }, Object { - "align": "left", - "header": "Account number", - "render": [Function], - }, - Object { - "align": "left", - "header": "Account to recover", + "header": "Type", "render": [Function], + "width": "120px", }, ] } @@ -56,36 +47,27 @@ exports[`Renders requests history 1`] = ` "align": "left", "header": "First name", "render": [Function], - "width": "200px", }, Object { "align": "left", "header": "Last name", "render": [Function], "renderDetails": [Function], - "width": "200px", }, Object { "header": "Status", "render": [Function], - "splitAfter": true, "width": "170px", }, Object { "header": "Submission date", "render": [Function], - "smallerText": true, "width": "120px", }, Object { - "align": "left", - "header": "Account number", - "render": [Function], - }, - Object { - "align": "left", - "header": "Account to recover", + "header": "Type", "render": [Function], + "width": "120px", }, ] } diff --git a/src/legal-officer/transaction-protection/LocRequestAcceptance.test.tsx b/src/legal-officer/transaction-protection/LocRequestAcceptance.test.tsx index cdc67f8f..8e108f76 100644 --- a/src/legal-officer/transaction-protection/LocRequestAcceptance.test.tsx +++ b/src/legal-officer/transaction-protection/LocRequestAcceptance.test.tsx @@ -2,7 +2,6 @@ jest.mock('../../common/CommonContext'); jest.mock('../LegalOfficerContext'); jest.mock('../../logion-chain'); jest.mock('../../logion-chain/Signature'); -jest.mock('../../loc/Model'); jest.mock("../../loc/LocContext"); import { render, screen } from '@testing-library/react'; diff --git a/src/legal-officer/transaction-protection/LocRequestAcceptanceAndCreation.test.tsx b/src/legal-officer/transaction-protection/LocRequestAcceptanceAndCreation.test.tsx index b31abee0..2777cf69 100644 --- a/src/legal-officer/transaction-protection/LocRequestAcceptanceAndCreation.test.tsx +++ b/src/legal-officer/transaction-protection/LocRequestAcceptanceAndCreation.test.tsx @@ -2,7 +2,6 @@ jest.mock('../../common/CommonContext'); jest.mock('../LegalOfficerContext'); jest.mock('../../logion-chain'); jest.mock('../../logion-chain/Signature'); -jest.mock('../../loc/Model'); jest.mock("../../loc/LocContext"); import { render, screen, waitFor } from '@testing-library/react'; diff --git a/src/loc/AcceptRejectLocRequest.test.tsx b/src/loc/AcceptRejectLocRequest.test.tsx index a3ceee4e..250d6bbd 100644 --- a/src/loc/AcceptRejectLocRequest.test.tsx +++ b/src/loc/AcceptRejectLocRequest.test.tsx @@ -8,7 +8,6 @@ import { shallowRender } from "../tests"; import AcceptRejectLocRequest from "./AcceptRejectLocRequest"; jest.mock("../logion-chain"); -jest.mock('./Model'); jest.mock("./LocContext"); describe("AcceptRejectLocRequest", () => { diff --git a/src/loc/LocCreationDialog.test.tsx b/src/loc/LocCreationDialog.test.tsx index a5cdfabf..e77fe5fc 100644 --- a/src/loc/LocCreationDialog.test.tsx +++ b/src/loc/LocCreationDialog.test.tsx @@ -15,7 +15,6 @@ import { SUCCESSFUL_SUBMISSION, setClientMock, setExtrinsicSubmissionState } fro jest.mock("../logion-chain/Signature"); jest.mock("../common/CommonContext"); jest.mock("../common/Model"); -jest.mock("./Model"); jest.mock("../legal-officer/LegalOfficerContext"); jest.mock("../logion-chain"); jest.mock("./LocContext"); diff --git a/src/loc/Model.test.tsx b/src/loc/Model.test.tsx deleted file mode 100644 index bf5cc33e..00000000 --- a/src/loc/Model.test.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { AxiosInstance } from "axios"; - -import { DEFAULT_LEGAL_OFFICER } from "src/common/TestData"; -import { acceptProtectionRequest, rejectProtectionRequest } from "./Model" - -describe("Model", () => { - - it("rejects protection request", async () => { - const axios = { - post: jest.fn().mockResolvedValue(undefined), - } as unknown as AxiosInstance; - - const requestId = "0e16421a-2550-4be5-a6a8-1ab2239b7dc4"; - await rejectProtectionRequest(axios, { - requestId, - legalOfficerAddress: DEFAULT_LEGAL_OFFICER.address, - rejectReason: "Because" - }); - - expect(axios.post).toBeCalledWith( - `/api/protection-request/${requestId}/reject`, - expect.objectContaining({ - legalOfficerAddress: DEFAULT_LEGAL_OFFICER.address, - rejectReason: "Because", - }) - ); - }); - - it("accepts protection request", async () => { - const axios = { - post: jest.fn().mockResolvedValue(undefined), - } as unknown as AxiosInstance; - - const requestId = "0e16421a-2550-4be5-a6a8-1ab2239b7dc4"; - await acceptProtectionRequest(axios, { - requestId, - }); - - expect(axios.post).toBeCalledWith( - `/api/protection-request/${requestId}/accept`, - ); - }); -}); diff --git a/src/loc/Model.tsx b/src/loc/Model.tsx deleted file mode 100644 index 889949f2..00000000 --- a/src/loc/Model.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { AxiosInstance } from 'axios'; - -export interface RejectProtectionRequestParameters { - requestId: string, - rejectReason: string, - legalOfficerAddress: string, -} - -export async function rejectProtectionRequest( - axios: AxiosInstance, - parameters: RejectProtectionRequestParameters -): Promise { - await axios.post(`/api/protection-request/${parameters.requestId}/reject`, { - legalOfficerAddress: parameters.legalOfficerAddress, - rejectReason: parameters.rejectReason, - }); -} - -export interface AcceptProtectionRequestParameters { - requestId: string, -} - -export async function acceptProtectionRequest( - axios: AxiosInstance, - parameters: AcceptProtectionRequestParameters -): Promise { - await axios.post(`/api/protection-request/${parameters.requestId}/accept`); -} diff --git a/src/loc/__mocks__/Model.tsx b/src/loc/__mocks__/Model.tsx deleted file mode 100644 index f94c86d4..00000000 --- a/src/loc/__mocks__/Model.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { - rejectProtectionRequest, - acceptProtectionRequest, -} from './ModelMock'; - -export { - rejectProtectionRequest, - acceptProtectionRequest, -}; diff --git a/src/loc/__mocks__/ModelMock.tsx b/src/loc/__mocks__/ModelMock.tsx deleted file mode 100644 index 069eecba..00000000 --- a/src/loc/__mocks__/ModelMock.tsx +++ /dev/null @@ -1,11 +0,0 @@ -export let rejectProtectionRequest = jest.fn(); - -export function setRejectProtectionRequest(mockFn: any) { - rejectProtectionRequest = mockFn; -} - -export let acceptProtectionRequest = jest.fn(); - -export function setAcceptProtectionRequest(mockFn: any) { - acceptProtectionRequest = mockFn; -}