From bb3437a5e6cbfd9366e0388fa4f043cbbae5015b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Bruus=20Zeppelin?= Date: Fri, 6 Dec 2024 10:11:42 +0100 Subject: [PATCH 1/6] Add import/export buttons --- .node-version | 1 + .../browser-wallet/src/assets/svgX/export.svg | 3 +++ .../browser-wallet/src/assets/svgX/import.svg | 3 +++ .../popupX/pages/Web3Id/Web3IdCredentials.tsx | 15 +++++++++++---- 4 files changed, 18 insertions(+), 4 deletions(-) create mode 100644 .node-version create mode 100644 packages/browser-wallet/src/assets/svgX/export.svg create mode 100644 packages/browser-wallet/src/assets/svgX/import.svg diff --git a/.node-version b/.node-version new file mode 100644 index 00000000..dc0bb0f4 --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +v22.12.0 diff --git a/packages/browser-wallet/src/assets/svgX/export.svg b/packages/browser-wallet/src/assets/svgX/export.svg new file mode 100644 index 00000000..edb696fe --- /dev/null +++ b/packages/browser-wallet/src/assets/svgX/export.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/browser-wallet/src/assets/svgX/import.svg b/packages/browser-wallet/src/assets/svgX/import.svg new file mode 100644 index 00000000..6c19aa49 --- /dev/null +++ b/packages/browser-wallet/src/assets/svgX/import.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/browser-wallet/src/popup/popupX/pages/Web3Id/Web3IdCredentials.tsx b/packages/browser-wallet/src/popup/popupX/pages/Web3Id/Web3IdCredentials.tsx index cfbdb374..a7ca420c 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/Web3Id/Web3IdCredentials.tsx +++ b/packages/browser-wallet/src/popup/popupX/pages/Web3Id/Web3IdCredentials.tsx @@ -3,7 +3,8 @@ import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import { useAtomValue } from 'jotai'; -import Plus from '@assets/svgX/plus.svg'; +import Export from '@assets/svgX/export.svg'; +import Import from '@assets/svgX/import.svg'; import Page from '@popup/popupX/shared/Page'; import Button from '@popup/popupX/shared/Button'; import Web3IdCard from '@popup/popupX/shared/Web3IdCard'; @@ -11,10 +12,12 @@ import { relativeRoutes, web3IdDetailsRoute } from '@popup/popupX/constants/rout import { storedVerifiableCredentialsAtom } from '@popup/store/verifiable-credential'; import { VerifiableCredential } from '@shared/storage/types'; import { parseCredentialDID } from '@shared/utils/verifiable-credential-helpers'; +import Text from '@popup/popupX/shared/Text'; +import { useVerifiableCredentialExport } from '@popup/pages/VerifiableCredentialBackup/utils'; export default function Web3IdCredentials() { const { t } = useTranslation('x', { keyPrefix: 'web3Id.credentials' }); - const verifiableCredentials = useAtomValue(storedVerifiableCredentialsAtom); + const { value: verifiableCredentials, loading } = useAtomValue(storedVerifiableCredentialsAtom); const nav = useNavigate(); const toDetails = (vc: VerifiableCredential) => { @@ -22,13 +25,17 @@ export default function Web3IdCredentials() { nav(web3IdDetailsRoute(contract, id)); }; + const exportCredentials = useVerifiableCredentialExport(); + return ( - } onClick={() => nav(relativeRoutes.settings.web3Id.import.path)} /> + } onClick={() => nav(relativeRoutes.settings.web3Id.import.path)} /> + } onClick={exportCredentials} /> - {verifiableCredentials.value.map((vc) => ( + {verifiableCredentials.length === 0 && !loading && {t('noCredentials')}} + {verifiableCredentials.map((vc) => ( toDetails(vc)}> From d4c3a704918fe3107e98d90a5923721f00f89a0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Bruus=20Zeppelin?= Date: Fri, 6 Dec 2024 14:53:48 +0100 Subject: [PATCH 2/6] Added file input component --- .../popupX/pages/Web3Id/Web3IdCredentials.tsx | 3 +- .../src/popup/popupX/pages/Web3Id/utils.ts | 70 ++++++++++++++++ .../Form/FileInput/FileInput.stories.tsx | 31 +++++++ .../shared/Form/FileInput/FileInput.tsx | 84 +++++++++++++++++++ .../popupX/shared/Form/FileInput/index.ts | 1 + 5 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 packages/browser-wallet/src/popup/popupX/pages/Web3Id/utils.ts create mode 100644 packages/browser-wallet/src/popup/popupX/shared/Form/FileInput/FileInput.stories.tsx create mode 100644 packages/browser-wallet/src/popup/popupX/shared/Form/FileInput/FileInput.tsx create mode 100644 packages/browser-wallet/src/popup/popupX/shared/Form/FileInput/index.ts diff --git a/packages/browser-wallet/src/popup/popupX/pages/Web3Id/Web3IdCredentials.tsx b/packages/browser-wallet/src/popup/popupX/pages/Web3Id/Web3IdCredentials.tsx index a7ca420c..3ebdae6a 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/Web3Id/Web3IdCredentials.tsx +++ b/packages/browser-wallet/src/popup/popupX/pages/Web3Id/Web3IdCredentials.tsx @@ -13,7 +13,8 @@ import { storedVerifiableCredentialsAtom } from '@popup/store/verifiable-credent import { VerifiableCredential } from '@shared/storage/types'; import { parseCredentialDID } from '@shared/utils/verifiable-credential-helpers'; import Text from '@popup/popupX/shared/Text'; -import { useVerifiableCredentialExport } from '@popup/pages/VerifiableCredentialBackup/utils'; + +import { useVerifiableCredentialExport } from './utils'; export default function Web3IdCredentials() { const { t } = useTranslation('x', { keyPrefix: 'web3Id.credentials' }); diff --git a/packages/browser-wallet/src/popup/popupX/pages/Web3Id/utils.ts b/packages/browser-wallet/src/popup/popupX/pages/Web3Id/utils.ts new file mode 100644 index 00000000..56cd74ed --- /dev/null +++ b/packages/browser-wallet/src/popup/popupX/pages/Web3Id/utils.ts @@ -0,0 +1,70 @@ +import { useHdWallet } from '@popup/shared/utils/account-helpers'; +import { NetworkConfiguration, VerifiableCredential, VerifiableCredentialSchema } from '@shared/storage/types'; +import { VerifiableCredentialMetadata } from '@shared/utils/verifiable-credential-helpers'; +import { encrypt } from '@shared/utils/crypto'; +import { getNet } from '@shared/utils/network-helpers'; +import { useAtomValue } from 'jotai'; +import { + storedVerifiableCredentialSchemasAtom, + storedVerifiableCredentialsAtom, + storedVerifiableCredentialMetadataAtom, +} from '@popup/store/verifiable-credential'; +import { networkConfigurationAtom } from '@popup/store/settings'; +import { saveData } from '@popup/shared/utils/file-helpers'; +import { stringify } from 'json-bigint'; + +export type VerifiableCredentialExport = { + verifiableCredentials: VerifiableCredential[]; + schemas: Record; + metadata: Record; +}; + +export type ExportFormat = { + type: 'concordium-browser-wallet-verifiable-credentials'; + v: number; + environment: string; // 'testnet' or 'mainnet' + value: VerifiableCredentialExport; +}; + +function createExport( + verifiableCredentials: VerifiableCredential[], + schemas: Record, + metadata: Record, + network: NetworkConfiguration, + encryptionKey: string +) { + const exportContent: ExportFormat = { + type: 'concordium-browser-wallet-verifiable-credentials', + v: 0, + environment: getNet(network).toLowerCase(), + value: { + verifiableCredentials, + schemas, + metadata, + }, + }; + + // Use json-bigint to serialize bigints as json numbers. + // Ensure that Dates are stored as timestamps to not lose typing (otherwise they are serialized as strings). + return encrypt(stringify(exportContent), encryptionKey); +} + +export function useVerifiableCredentialExport() { + const verifiableCredentials = useAtomValue(storedVerifiableCredentialsAtom); + const schemas = useAtomValue(storedVerifiableCredentialSchemasAtom); + const metadata = useAtomValue(storedVerifiableCredentialMetadataAtom); + const network = useAtomValue(networkConfigurationAtom); + const wallet = useHdWallet(); + + if (schemas.loading || verifiableCredentials.loading || metadata.loading || !wallet) { + return undefined; + } + + const handleExport = async () => { + const key = wallet.getVerifiableCredentialBackupEncryptionKey().toString('hex'); + const data = await createExport(verifiableCredentials.value, schemas.value, metadata.value, network, key); + saveData(data, `web3IdCredentials.export`); + }; + + return handleExport; +} diff --git a/packages/browser-wallet/src/popup/popupX/shared/Form/FileInput/FileInput.stories.tsx b/packages/browser-wallet/src/popup/popupX/shared/Form/FileInput/FileInput.stories.tsx new file mode 100644 index 00000000..bb81de40 --- /dev/null +++ b/packages/browser-wallet/src/popup/popupX/shared/Form/FileInput/FileInput.stories.tsx @@ -0,0 +1,31 @@ +/* eslint-disable react/function-component-definition */ +import React, { useState } from 'react'; +import { Meta, StoryObj } from '@storybook/react'; +import { FileInput, FileInputValue } from './FileInput'; + +const render: Story['render'] = (args) => { + const [value, setValue] = useState(null); + + return ; +}; + +export default { + title: 'X/Shared/Form/FileInput', + component: FileInput, + render, + beforeEach: () => { + const body = document.getElementsByTagName('body').item(0); + body?.classList.add('popup-x'); + + return () => { + body?.classList.remove('popup-x'); + }; + }, +} as Meta; + +type Story = StoryObj; +export const Main: Story = { + args: { + buttonTitle: 'This is the button title', + }, +}; diff --git a/packages/browser-wallet/src/popup/popupX/shared/Form/FileInput/FileInput.tsx b/packages/browser-wallet/src/popup/popupX/shared/Form/FileInput/FileInput.tsx new file mode 100644 index 00000000..4a800a26 --- /dev/null +++ b/packages/browser-wallet/src/popup/popupX/shared/Form/FileInput/FileInput.tsx @@ -0,0 +1,84 @@ +import clsx from 'clsx'; +import React, { forwardRef, InputHTMLAttributes, useImperativeHandle, useMemo, useRef, useState } from 'react'; +import ErrorMessage from '../ErrorMessage'; +import Button from '../../Button'; + +export interface FileInputRef { + reset(): void; +} + +export type FileInputValue = FileList | null; + +export type FileInputProps = Pick< + InputHTMLAttributes, + 'accept' | 'multiple' | 'placeholder' | 'disabled' | 'className' +> & { + buttonTitle: string; + value: FileInputValue; + disableFileNames?: boolean; + onChange(files: FileInputValue): void; + valid: boolean; + error?: string; +}; + +/** + * @description + * Component for handling file input. Parsing of file should be done externally. Supports drag and drop + click to browse. + * + * @example + * + */ +export const FileInput = forwardRef( + ( + { value, onChange, valid, error, placeholder, className, buttonTitle, disableFileNames = false, ...inputProps }, + ref + ): JSX.Element => { + const inputRef = useRef(null); + const [dragOver, setDragOver] = useState(false); + const files = useMemo(() => new Array(value?.length ?? 0).fill(0).map((_, i) => value?.item(i)), [value]); + + const { disabled } = inputProps; + + useImperativeHandle(ref, () => ({ + reset: () => { + if (inputRef.current) { + inputRef.current.value = ''; + } + }, + })); + + return ( + + ); + } +); diff --git a/packages/browser-wallet/src/popup/popupX/shared/Form/FileInput/index.ts b/packages/browser-wallet/src/popup/popupX/shared/Form/FileInput/index.ts new file mode 100644 index 00000000..8e0b9e8f --- /dev/null +++ b/packages/browser-wallet/src/popup/popupX/shared/Form/FileInput/index.ts @@ -0,0 +1 @@ +export { default, FileInput } from './FileInput'; From fd398e7d5c2ca6f0789f83cb444cc672a732d004 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Bruus=20Zeppelin?= Date: Fri, 6 Dec 2024 15:02:32 +0100 Subject: [PATCH 3/6] fix build not working with new node versions --- .node-version | 2 +- packages/browser-wallet/package.json | 3 +- yarn.lock | 47 +++++++++++++++++++++++++++- 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/.node-version b/.node-version index dc0bb0f4..3f430af8 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -v22.12.0 +v18 diff --git a/packages/browser-wallet/package.json b/packages/browser-wallet/package.json index c4724a27..39aa9f3f 100644 --- a/packages/browser-wallet/package.json +++ b/packages/browser-wallet/package.json @@ -6,7 +6,7 @@ "author": "Concordium Software", "license": "Apache-2.0", "scripts": { - "build-base": "node --loader ts-node/esm -r tsconfig-paths/register ./build/build.ts", + "build-base": "tsx ./build/build.ts", "build:dev": "cross-env NODE_ENV=development yarn build-base", "build:prod": "cross-env NODE_ENV=production yarn build-base", "build": "yarn build:prod", @@ -95,6 +95,7 @@ "style-loader": "^3.3.1", "ts-jest": "^29.0.5", "tsconfig-paths-webpack-plugin": "^3.5.2", + "tsx": "^4.19.2", "typescript": "^5.2.2" } } diff --git a/yarn.lock b/yarn.lock index 1b519e15..9c95a3a3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2241,6 +2241,7 @@ __metadata: style-loader: ^3.3.1 ts-jest: ^29.0.5 tsconfig-paths-webpack-plugin: ^3.5.2 + tsx: ^4.19.2 typescript: ^5.2.2 uuid: ^10.0.0 languageName: unknown @@ -10439,7 +10440,7 @@ __metadata: languageName: node linkType: hard -"esbuild@npm:^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0": +"esbuild@npm:^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0, esbuild@npm:~0.23.0": version: 0.23.1 resolution: "esbuild@npm:0.23.1" dependencies: @@ -11834,6 +11835,16 @@ __metadata: languageName: node linkType: hard +"fsevents@npm:~2.3.3": + version: 2.3.3 + resolution: "fsevents@npm:2.3.3" + dependencies: + node-gyp: latest + checksum: 11e6ea6fea15e42461fc55b4b0e4a0a3c654faa567f1877dbd353f39156f69def97a69936d1746619d656c4b93de2238bf731f6085a03a50cabf287c9d024317 + conditions: os=darwin + languageName: node + linkType: hard + "fsevents@patch:fsevents@^1.2.7#~builtin": version: 1.2.13 resolution: "fsevents@patch:fsevents@npm%3A1.2.13#~builtin::version=1.2.13&hash=18f3a7" @@ -11853,6 +11864,15 @@ __metadata: languageName: node linkType: hard +"fsevents@patch:fsevents@~2.3.3#~builtin": + version: 2.3.3 + resolution: "fsevents@patch:fsevents@npm%3A2.3.3#~builtin::version=2.3.3&hash=18f3a7" + dependencies: + node-gyp: latest + conditions: os=darwin + languageName: node + linkType: hard + "function-bind@npm:^1.1.1": version: 1.1.1 resolution: "function-bind@npm:1.1.1" @@ -11980,6 +12000,15 @@ __metadata: languageName: node linkType: hard +"get-tsconfig@npm:^4.7.5": + version: 4.8.1 + resolution: "get-tsconfig@npm:4.8.1" + dependencies: + resolve-pkg-maps: ^1.0.0 + checksum: 12df01672e691d2ff6db8cf7fed1ddfef90ed94a5f3d822c63c147a26742026d582acd86afcd6f65db67d809625d17dd7f9d34f4d3f38f69bc2f48e19b2bdd5b + languageName: node + linkType: hard + "get-value@npm:^2.0.3, get-value@npm:^2.0.6": version: 2.0.6 resolution: "get-value@npm:2.0.6" @@ -19412,6 +19441,22 @@ __metadata: languageName: node linkType: hard +"tsx@npm:^4.19.2": + version: 4.19.2 + resolution: "tsx@npm:4.19.2" + dependencies: + esbuild: ~0.23.0 + fsevents: ~2.3.3 + get-tsconfig: ^4.7.5 + dependenciesMeta: + fsevents: + optional: true + bin: + tsx: dist/cli.mjs + checksum: 7f9f1b338a73297725a9217cedaaad862f7c81d5264093c74b98a71491ad5413b11248d604c0e650f4f7da6f365249f1426fdb58a1325ab9e15448156b1edff6 + languageName: node + linkType: hard + "tty-browserify@npm:0.0.1": version: 0.0.1 resolution: "tty-browserify@npm:0.0.1" From 2b5412e0d2cc1d7d50582bc9cf22d851fe0c85ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Bruus=20Zeppelin?= Date: Mon, 9 Dec 2024 14:09:37 +0100 Subject: [PATCH 4/6] Add file input component --- .../shared/Form/FileInput/FileInput.scss | 79 +++++++++++++++++++ .../Form/FileInput/FileInput.stories.tsx | 2 +- .../shared/Form/FileInput/FileInput.tsx | 48 ++++++----- .../src/popup/popupX/shared/i18n/en.ts | 3 + .../src/popup/popupX/styles/_elements.scss | 1 + 5 files changed, 112 insertions(+), 21 deletions(-) create mode 100644 packages/browser-wallet/src/popup/popupX/shared/Form/FileInput/FileInput.scss diff --git a/packages/browser-wallet/src/popup/popupX/shared/Form/FileInput/FileInput.scss b/packages/browser-wallet/src/popup/popupX/shared/Form/FileInput/FileInput.scss new file mode 100644 index 00000000..10ddafcc --- /dev/null +++ b/packages/browser-wallet/src/popup/popupX/shared/Form/FileInput/FileInput.scss @@ -0,0 +1,79 @@ +.form-file-input-x { + position: relative; + z-index: 0; + font-size: rem(12px); + + &, + &__wrapper { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + border-radius: 10px; + } + + &__label { + margin-bottom: 5px; + } + + &--disabled { + cursor: default; + + .form-file-input-x__wrapper { + cursor: default; + } + } + + &--hovering { + .form-file-input-x__wrapper { + border-color: $color-mineral-3; + } + } + + &--invalid { + .form-file-input-x__wrapper { + border-color: $color-red-attention; + } + } + + &__wrapper { + flex: 1; + width: 100%; + padding: 20px 30px; + position: relative; + border: 1px dashed $color-mineral-1; + cursor: pointer; + } + + &__filename { + word-break: break-all; + text-align: center; + } + + &__empty { + color: $color-mineral-2; + } + + &__input { + z-index: -1; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + opacity: 0; + width: 100%; + } + + &__button { + cursor: pointer; + display: flex; + align-items: center; + font-size: 15px; + margin-top: rem(10px); + + svg { + margin-right: rem(5px); + } + } +} diff --git a/packages/browser-wallet/src/popup/popupX/shared/Form/FileInput/FileInput.stories.tsx b/packages/browser-wallet/src/popup/popupX/shared/Form/FileInput/FileInput.stories.tsx index bb81de40..e3109199 100644 --- a/packages/browser-wallet/src/popup/popupX/shared/Form/FileInput/FileInput.stories.tsx +++ b/packages/browser-wallet/src/popup/popupX/shared/Form/FileInput/FileInput.stories.tsx @@ -26,6 +26,6 @@ export default { type Story = StoryObj; export const Main: Story = { args: { - buttonTitle: 'This is the button title', + placeholder: 'This is the placeholder', }, }; diff --git a/packages/browser-wallet/src/popup/popupX/shared/Form/FileInput/FileInput.tsx b/packages/browser-wallet/src/popup/popupX/shared/Form/FileInput/FileInput.tsx index 4a800a26..c7646f19 100644 --- a/packages/browser-wallet/src/popup/popupX/shared/Form/FileInput/FileInput.tsx +++ b/packages/browser-wallet/src/popup/popupX/shared/Form/FileInput/FileInput.tsx @@ -1,5 +1,9 @@ import clsx from 'clsx'; import React, { forwardRef, InputHTMLAttributes, useImperativeHandle, useMemo, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +import FolderIcon from '@assets/svgX/folder-open.svg'; +import FileIcon from '@assets/svgX/file.svg'; import ErrorMessage from '../ErrorMessage'; import Button from '../../Button'; @@ -11,13 +15,13 @@ export type FileInputValue = FileList | null; export type FileInputProps = Pick< InputHTMLAttributes, - 'accept' | 'multiple' | 'placeholder' | 'disabled' | 'className' + 'accept' | 'multiple' | 'disabled' | 'className' > & { - buttonTitle: string; value: FileInputValue; disableFileNames?: boolean; onChange(files: FileInputValue): void; valid: boolean; + placeholder: string; error?: string; }; @@ -30,13 +34,13 @@ export type FileInputProps = Pick< */ export const FileInput = forwardRef( ( - { value, onChange, valid, error, placeholder, className, buttonTitle, disableFileNames = false, ...inputProps }, + { value, onChange, valid, error, placeholder, className, disableFileNames = false, ...inputProps }, ref ): JSX.Element => { + const { t } = useTranslation('x', { keyPrefix: 'sharedX.form.fileInput' }); const inputRef = useRef(null); const [dragOver, setDragOver] = useState(false); const files = useMemo(() => new Array(value?.length ?? 0).fill(0).map((_, i) => value?.item(i)), [value]); - const { disabled } = inputProps; useImperativeHandle(ref, () => ({ @@ -50,34 +54,38 @@ export const FileInput = forwardRef( return (