Skip to content

Commit

Permalink
feat: import json identity
Browse files Browse the repository at this point in the history
- [x] Support file upload for import external identity
- [x] Add upload button component
- [x] Add e2e tests
  • Loading branch information
0xmad committed Oct 17, 2023
1 parent 780a374 commit cc30cb6
Show file tree
Hide file tree
Showing 26 changed files with 485 additions and 67 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,14 @@ describe("background/services/approval", () => {
"CryptKeeper: origin is not approved",
);
});

test("should throw error if origin is not provided", async () => {
await approvalService.unlock();

expect(() => approvalService.isOriginApproved({}, { urlOrigin: "" })).toThrowError(
"CryptKeeper: origin is not set",
);
});
});

describe("backup", () => {
Expand Down
16 changes: 16 additions & 0 deletions packages/app/src/config/mock/file.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
export const mockJsonFile = new File([JSON.stringify({ ping: true })], "ping.json", { type: "application/json" });

export const mockIdenityJsonFile = new File([JSON.stringify({ trapdoor: "1", nullifier: "1" })], "identity.json", {
type: "application/json",
});

export const mockArrayIdenityJsonFile = new File([JSON.stringify(["2", "2"])], "identity.json", {
type: "application/json",
});

export const mockIdenityPrivateJsonFile = new File(
[JSON.stringify({ _trapdoor: "3", _nullifier: "3" })],
"identity.json",
{ type: "application/json" },
);

export const mockEmptyJsonFile = new File([""], "empty.json", { type: "application/json" });

interface IDataTransfer {
dataTransfer: {
files: File[];
Expand Down
47 changes: 47 additions & 0 deletions packages/app/src/ui/components/UploadButton/UploadButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import Typography from "@mui/material/Typography";
import { forwardRef, Ref, type HTMLAttributes } from "react";

import type { Accept } from "react-dropzone";

import { type onDropCallback, useUploadButton } from "./useUploadButton";

export interface IUploadButtonProps extends Omit<HTMLAttributes<HTMLInputElement>, "onDrop"> {
isLoading?: boolean;
errorMessage?: string;
multiple?: boolean;
accept: Accept;
name: string;
onDrop: onDropCallback;
}

const UploadButtonUI = (
{ isLoading = false, multiple = true, errorMessage = "", accept, name, onDrop, ...rest }: IUploadButtonProps,
ref: Ref<HTMLInputElement>,
): JSX.Element => {
const { isDragActive, getRootProps, getInputProps } = useUploadButton({
isLoading,
accept,
multiple,
onDrop,
});

const fileTitle = multiple ? "files" : "file";

return (
<Box {...rest} {...getRootProps({ className: "dropzone" })} sx={{ width: "100%" }}>
<input ref={ref} name={name} {...getInputProps()} />

<Button sx={{ width: "100%" }} variant="outlined">
{isDragActive ? `Drop the ${fileTitle} here...` : `Upload ${fileTitle}`}
</Button>

<Typography color="error" sx={{ mt: 1, mx: 1, fontSize: "0.8125rem" }} variant="body2">
{errorMessage}
</Typography>
</Box>
);
};

export const UploadButton = forwardRef<HTMLInputElement, IUploadButtonProps>(UploadButtonUI);
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* @jest-environment jsdom
*/

import { render } from "@testing-library/react";

import { IUploadButtonProps, UploadButton } from "..";
import { IUseUploadButtonData, useUploadButton } from "../useUploadButton";

jest.mock("../useUploadButton", (): unknown => ({
...jest.requireActual("../useUploadButton"),
useUploadButton: jest.fn(),
}));

describe("ui/components/UploadButton", () => {
const defaultHookData: IUseUploadButtonData = {
isDragActive: false,
acceptedFiles: [],
getInputProps: jest.fn(),
getRootProps: jest.fn(),
};

const defaultProps: IUploadButtonProps = {
accept: { "application/json": [".json"] },
name: "file",
onDrop: jest.fn(),
};

beforeEach(() => {
(useUploadButton as jest.Mock).mockReturnValue(defaultHookData);
});

afterEach(() => {
jest.clearAllMocks();
});

test("should render properly", async () => {
const { findByText } = render(<UploadButton {...defaultProps} />);

const dragText = await findByText("Upload files");

expect(dragText).toBeInTheDocument();
});

test("should render properly while drag is active", async () => {
(useUploadButton as jest.Mock).mockReturnValue({ ...defaultHookData, isDragActive: true });

const { findByText } = render(<UploadButton {...defaultProps} />);

const dragText = await findByText("Drop the files here...");

expect(dragText).toBeInTheDocument();
});

test("should render error properly", async () => {
const { findByText } = render(<UploadButton {...defaultProps} errorMessage="error" />);

const error = await findByText("error");

expect(error).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* @jest-environment jsdom
*/

import { act, fireEvent, render, renderHook } from "@testing-library/react";

import { createDataTransfer, mockJsonFile } from "@src/config/mock/file";

import { IUseUploadButtonArgs, useUploadButton } from "../useUploadButton";

describe("ui/components/UploadButton/useUploadButton", () => {
const defaultHookArgs: IUseUploadButtonArgs = {
isLoading: false,
accept: { "application/json": [".json"] },
onDrop: jest.fn(),
};

afterEach(() => {
jest.clearAllMocks();
});

test("should upload file properly", async () => {
const data = createDataTransfer([mockJsonFile]);

const { result } = renderHook(() => useUploadButton(defaultHookArgs));

const { container } = render(
<div {...result.current.getRootProps()}>
<input {...result.current.getInputProps()} />
</div>,
);

await act(() => fireEvent.drop(container.querySelector("div")!, data));

expect(defaultHookArgs.onDrop).toBeCalledTimes(1);
});
});
2 changes: 2 additions & 0 deletions packages/app/src/ui/components/UploadButton/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { type IUploadButtonProps, UploadButton } from "./UploadButton";
export { type onDropCallback, type IUseUploadButtonArgs, type IUseUploadButtonData } from "./useUploadButton";
45 changes: 45 additions & 0 deletions packages/app/src/ui/components/UploadButton/useUploadButton.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { type HTMLAttributes } from "react";
import {
type DropzoneInputProps,
type DropzoneRootProps,
type FileRejection,
type Accept,
useDropzone,
} from "react-dropzone";

export interface IUseUploadButtonArgs {
isLoading: boolean;
accept: Accept;
multiple?: boolean;
onDrop: onDropCallback;
}

export interface IUseUploadButtonData {
isDragActive: boolean;
acceptedFiles: File[];
getInputProps: (props?: DropzoneInputProps) => HTMLAttributes<HTMLInputElement>;
getRootProps: (props?: DropzoneRootProps) => HTMLAttributes<HTMLDivElement>;
}

export type onDropCallback = (acceptedFiles: File[], fileRejections: FileRejection[]) => Promise<void>;

export const useUploadButton = ({
isLoading,
accept,
multiple = true,
onDrop,
}: IUseUploadButtonArgs): IUseUploadButtonData => {
const { isDragActive, acceptedFiles, getRootProps, getInputProps } = useDropzone({
accept,
disabled: isLoading,
multiple,
onDrop,
});

return {
isDragActive,
acceptedFiles,
getRootProps,
getInputProps,
};
};
2 changes: 1 addition & 1 deletion packages/app/src/ui/components/UploadInput/UploadInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export interface IUploadInputProps extends Omit<HTMLAttributes<HTMLInputElement>
onDrop: onDropCallback;
}

export const UploadInputUI = (
const UploadInputUI = (
{ isLoading = false, multiple = true, errorMessage = "", accept, onDrop, ...rest }: IUploadInputProps,
ref: Ref<HTMLInputElement>,
): JSX.Element => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ describe("ui/components/UploadInput", () => {
};

const defaultProps: IUploadInputProps = {
accept: { "application/json": [] },
accept: { "application/json": [".json"] },
onDrop: jest.fn(),
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { IUseUploadInputArgs, useUploadInput } from "../useUploadInput";
describe("ui/components/UploadInput/useUploadInput", () => {
const defaultHookArgs: IUseUploadInputArgs = {
isLoading: false,
accept: { "application/json": [] },
accept: { "application/json": [".json"] },
onDrop: jest.fn(),
};

Expand Down
62 changes: 16 additions & 46 deletions packages/app/src/ui/pages/ImportIdentity/ImportIdentity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,13 @@ import Typography from "@mui/material/Typography";
import { BigNumberInput } from "@src/ui/components/BigNumberInput";
import { FullModalContent, FullModalFooter, FullModalHeader } from "@src/ui/components/FullModal";
import { Input } from "@src/ui/components/Input";
import { UploadButton } from "@src/ui/components/UploadButton";

import { useImportIdentity } from "./useImportIdentity";

const ImportIdentity = (): JSX.Element => {
const {
isLoading,
urlOrigin,
errors,
secret,
commitment,
trapdoor,
nullifier,
register,
onGoBack,
onGoToHost,
onSubmit,
} = useImportIdentity();
const { isLoading, urlOrigin, errors, trapdoor, nullifier, register, onGoBack, onGoToHost, onDrop, onSubmit } =
useImportIdentity();

return (
<Box
Expand All @@ -35,7 +25,7 @@ const ImportIdentity = (): JSX.Element => {
<Box>
<Box>
{urlOrigin && (
<Typography component="div" fontWeight="bold" sx={{ textAlign: "left", mb: 3 }} variant="h6">
<Typography component="div" fontWeight="bold" sx={{ textAlign: "left", mb: 1 }} variant="h6">
<Typography
component="strong"
fontWeight="bold"
Expand All @@ -53,13 +43,13 @@ const ImportIdentity = (): JSX.Element => {
)}

{!urlOrigin && (
<Typography component="div" fontWeight="bold" sx={{ textAlign: "left", mb: 3 }} variant="h6">
<Typography component="div" fontWeight="bold" sx={{ textAlign: "left", mb: 1 }} variant="h6">
Import identity with trapdoor and nullifier
</Typography>
)}
</Box>

<Box sx={{ mt: 2 }}>
<Box>
<Box sx={{ mb: 2 }}>
<Input
autoFocus
Expand All @@ -74,6 +64,16 @@ const ImportIdentity = (): JSX.Element => {
/>
</Box>

<UploadButton
accept={{ "application/json": [".json"] }}
isLoading={isLoading}
multiple={false}
name="file"
onDrop={onDrop}
/>

<Typography sx={{ textAlign: "center", my: 1 }}>Or enter data manually</Typography>

<Box sx={{ mb: 2 }}>
<BigNumberInput
errorMessage={errors.trapdoor}
Expand Down Expand Up @@ -105,36 +105,6 @@ const ImportIdentity = (): JSX.Element => {
})}
/>
</Box>

<Box sx={{ mb: 2 }}>
<BigNumberInput
id="commitment"
InputProps={{
readOnly: true,
}}
label="Commitment"
placeholder="Enter identity commitment"
size="small"
type="text"
value={commitment}
variant="filled"
/>
</Box>

<Box sx={{ mb: 2 }}>
<BigNumberInput
id="secret"
InputProps={{
readOnly: true,
}}
label="Secret"
placeholder="Enter identity nullifier"
size="small"
type="text"
value={secret}
variant="filled"
/>
</Box>
</Box>
</Box>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ describe("ui/pages/ImportIdentity", () => {
register: jest.fn(),
onGoBack: jest.fn(),
onGoToHost: jest.fn(),
onDrop: jest.fn(),
onSubmit: jest.fn(),
};

Expand Down
Loading

0 comments on commit cc30cb6

Please sign in to comment.