diff --git a/package-lock.json b/package-lock.json index a03e759..ebeec88 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,7 @@ "idb": "8.0.0", "jszip": "3.10.1", "lodash": "4.17.21", + "material-ui-popup-state": "^5.3.1", "mui-file-input": "4.0.4", "nostr-login": "^1.6.6", "nostr-tools": "2.7.0", @@ -3330,6 +3331,12 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" + }, "node_modules/cli-cursor": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", @@ -5976,6 +5983,26 @@ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "dev": true }, + "node_modules/material-ui-popup-state": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/material-ui-popup-state/-/material-ui-popup-state-5.3.1.tgz", + "integrity": "sha512-mmx1DsQwF/2cmcpHvS/QkUwOQG2oAM+cDEQU0DaZVYnvwKyTB3AFgu8l1/E+LQFausmzpSJoljwQSZXkNvt7eA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.6", + "@types/prop-types": "^15.7.3", + "@types/react": "^18.0.26", + "classnames": "^2.2.6", + "prop-types": "^15.7.2" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "@mui/material": "^5.0.0 || ^6.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", diff --git a/package.json b/package.json index ab49da0..562aadf 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "idb": "8.0.0", "jszip": "3.10.1", "lodash": "4.17.21", + "material-ui-popup-state": "^5.3.1", "mui-file-input": "4.0.4", "nostr-login": "^1.6.6", "nostr-tools": "2.7.0", diff --git a/src/components/FileList/index.tsx b/src/components/FileList/index.tsx index a47ace7..a073ca4 100644 --- a/src/components/FileList/index.tsx +++ b/src/components/FileList/index.tsx @@ -1,23 +1,25 @@ import { CurrentUserFile } from '../../types/file.ts' import styles from './style.module.scss' -import { Button } from '@mui/material' +import { Button, Menu, MenuItem } from '@mui/material' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faCheck } from '@fortawesome/free-solid-svg-icons' +import PopupState, { bindTrigger, bindMenu } from 'material-ui-popup-state' +import React from 'react' interface FileListProps { files: CurrentUserFile[] currentFile: CurrentUserFile setCurrentFile: (file: CurrentUserFile) => void - handleDownload: () => void - downloadLabel?: string + handleExport: () => void + handleEncryptedExport?: () => void } const FileList = ({ files, currentFile, setCurrentFile, - handleDownload, - downloadLabel + handleExport, + handleEncryptedExport }: FileListProps) => { const isActive = (file: CurrentUserFile) => file.id === currentFile.id return ( @@ -42,9 +44,35 @@ const FileList = ({ ))} - + + + {(popupState) => ( + + + + { + popupState.close + handleExport() + }} + > + Export Files + + { + popupState.close + typeof handleEncryptedExport === 'function' && + handleEncryptedExport() + }} + > + Export Encrypted Files + + + + )} + ) } diff --git a/src/components/MarkFormField/index.tsx b/src/components/MarkFormField/index.tsx index 18bbf31..718a119 100644 --- a/src/components/MarkFormField/index.tsx +++ b/src/components/MarkFormField/index.tsx @@ -8,12 +8,14 @@ import { } from '../../utils' import React, { useState } from 'react' import { MarkInput } from '../MarkTypeStrategy/MarkInput.tsx' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faCheck } from '@fortawesome/free-solid-svg-icons' interface MarkFormFieldProps { currentUserMarks: CurrentUserMark[] handleCurrentUserMarkChange: (mark: CurrentUserMark) => void handleSelectedMarkValueChange: (value: string) => void - handleSubmit: (event: React.FormEvent) => void + handleSubmit: (event: React.MouseEvent) => void selectedMark: CurrentUserMark selectedMarkValue: string } @@ -30,11 +32,13 @@ const MarkFormField = ({ handleCurrentUserMarkChange }: MarkFormFieldProps) => { const [displayActions, setDisplayActions] = useState(true) + const [complete, setComplete] = useState(false) + const isReadyToSign = () => isCurrentUserMarksComplete(currentUserMarks) || isCurrentValueLast(currentUserMarks, selectedMark, selectedMarkValue) const isCurrent = (currentMark: CurrentUserMark) => - currentMark.id === selectedMark.id + currentMark.id === selectedMark.id && !complete const isDone = (currentMark: CurrentUserMark) => isCurrent(currentMark) ? !!selectedMarkValue : currentMark.isCompleted const findNext = () => { @@ -46,13 +50,36 @@ const MarkFormField = ({ const handleFormSubmit = (event: React.FormEvent) => { event.preventDefault() console.log('handle form submit runs...') - return isReadyToSign() - ? handleSubmit(event) - : handleCurrentUserMarkChange(findNext()!) + + // Without this line, we lose mark values when switching + handleCurrentUserMarkChange(selectedMark) + + if (!complete) { + isReadyToSign() + ? setComplete(true) + : handleCurrentUserMarkChange(findNext()!) + } } + const toggleActions = () => setDisplayActions(!displayActions) const markLabel = getToolboxLabelByMarkType(selectedMark.mark.type) + const handleCurrentUserMarkClick = (mark: CurrentUserMark) => { + setComplete(false) + handleCurrentUserMarkChange(mark) + } + + const handleSelectCompleteMark = () => { + handleCurrentUserMarkChange(selectedMark) + setComplete(true) + } + + const handleSignAndComplete = ( + event: React.MouseEvent + ) => { + handleSubmit(event) + } + return (
@@ -78,25 +105,43 @@ const MarkFormField = ({
-

Add {markLabel}

+ {!complete && ( +

Add {markLabel}

+ )} + {complete &&

Finish

}
-
handleFormSubmit(e)}> - + {!complete && ( + handleFormSubmit(e)}> + +
+ +
+ + )} + + {complete && (
-
- + )} +
{currentUserMarks.map((mark, index) => { @@ -104,7 +149,7 @@ const MarkFormField = ({
@@ -114,6 +159,20 @@ const MarkFormField = ({
) })} +
+ + {complete && ( +
+ )} +
diff --git a/src/components/MarkFormField/style.module.scss b/src/components/MarkFormField/style.module.scss index 686595f..3825146 100644 --- a/src/components/MarkFormField/style.module.scss +++ b/src/components/MarkFormField/style.module.scss @@ -216,3 +216,7 @@ flex-direction: column; grid-gap: 5px; } + +.finishPage { + padding: 1px 0; +} diff --git a/src/components/PDFView/PdfMarking.tsx b/src/components/PDFView/PdfMarking.tsx index 222c393..f0d4f60 100644 --- a/src/components/PDFView/PdfMarking.tsx +++ b/src/components/PDFView/PdfMarking.tsx @@ -24,11 +24,12 @@ import { interface PdfMarkingProps { currentUserMarks: CurrentUserMark[] files: CurrentUserFile[] - handleDownload: () => void + handleExport: () => void + handleEncryptedExport: () => void + handleSign: () => void meta: Meta | null otherUserMarks: Mark[] setCurrentUserMarks: (currentUserMarks: CurrentUserMark[]) => void - setIsMarksCompleted: (isMarksCompleted: boolean) => void setUpdatedMarks: (markToUpdate: Mark) => void } @@ -42,10 +43,11 @@ const PdfMarking = (props: PdfMarkingProps) => { const { files, currentUserMarks, - setIsMarksCompleted, setCurrentUserMarks, setUpdatedMarks, - handleDownload, + handleExport, + handleEncryptedExport, + handleSign, meta, otherUserMarks } = props @@ -86,11 +88,18 @@ const PdfMarking = (props: PdfMarkingProps) => { updatedSelectedMark ) setCurrentUserMarks(updatedCurrentUserMarks) - setSelectedMarkValue(mark.currentValue ?? EMPTY) - setSelectedMark(mark) + + // If clicking on the same mark, don't update the value, otherwise do update + if (mark.id !== selectedMark.id) { + setSelectedMarkValue(mark.currentValue ?? EMPTY) + setSelectedMark(mark) + } } - const handleSubmit = (event: React.FormEvent) => { + /** + * Sign and Complete + */ + const handleSubmit = (event: React.MouseEvent) => { event.preventDefault() if (!selectedMarkValue || !selectedMark) return @@ -106,8 +115,8 @@ const PdfMarking = (props: PdfMarkingProps) => { ) setCurrentUserMarks(updatedCurrentUserMarks) setSelectedMark(null) - setIsMarksCompleted(true) setUpdatedMarks(updatedMark.mark) + handleSign() } // const updateCurrentUserMarkValues = () => { @@ -132,7 +141,8 @@ const PdfMarking = (props: PdfMarkingProps) => { files={files} currentFile={currentFile} setCurrentFile={setCurrentFile} - handleDownload={handleDownload} + handleExport={handleExport} + handleEncryptedExport={handleEncryptedExport} /> )}
diff --git a/src/pages/sign/index.tsx b/src/pages/sign/index.tsx index f30ecdd..2c69adf 100644 --- a/src/pages/sign/index.tsx +++ b/src/pages/sign/index.tsx @@ -1,67 +1,54 @@ -import { Box, Button, Typography } from '@mui/material' import axios from 'axios' import saveAs from 'file-saver' import JSZip from 'jszip' import _ from 'lodash' -import { MuiFileInput } from 'mui-file-input' import { Event, verifyEvent } from 'nostr-tools' import { useCallback, useEffect, useState } from 'react' -import { useAppSelector } from '../../hooks/store' +import { useAppSelector } from '../../hooks' import { useLocation, useNavigate, useParams } from 'react-router-dom' import { toast } from 'react-toastify' import { LoadingSpinner } from '../../components/LoadingSpinner' import { NostrController } from '../../controllers' -import { appPublicRoutes } from '../../routes' +import { appPrivateRoutes, appPublicRoutes } from '../../routes' import { CreateSignatureEventContent, Meta, SignedEvent } from '../../types' import { + ARRAY_BUFFER, decryptArrayBuffer, + DEFLATE, encryptArrayBuffer, extractMarksFromSignedMeta, extractZipUrlAndEncryptionKey, + filterMarksByPubkey, + findOtherUserMarks, generateEncryptionKey, generateKeysFile, getCurrentUserFiles, + getCurrentUserMarks, getHash, hexToNpub, isOnline, loadZip, - unixNow, npubToHex, parseJson, + processMarks, readContentOfZipEntry, sendNotification, signEventForMetaFile, - updateUsersAppData, - findOtherUserMarks, timeout, - processMarks + unixNow, + updateMarks, + updateUsersAppData } from '../../utils' -import { Container } from '../../components/Container' -import { DisplayMeta } from './internal/displayMeta' -import styles from './style.module.scss' import { CurrentUserMark, Mark } from '../../types/mark.ts' -import { getLastSignersSig, isFullySigned } from '../../utils/sign.ts' -import { - filterMarksByPubkey, - getCurrentUserMarks, - isCurrentUserMarksComplete, - updateMarks -} from '../../utils' import PdfMarking from '../../components/PDFView/PdfMarking.tsx' import { convertToSigitFile, getZipWithFiles, SigitFile } from '../../utils/file.ts' -import { ARRAY_BUFFER, DEFLATE } from '../../utils/const.ts' import { generateTimestamp } from '../../utils/opentimestamps.ts' import { MARK_TYPE_CONFIG } from '../../components/MarkTypeStrategy/MarkStrategy.tsx' - -enum SignedStatus { - Fully_Signed, - User_Is_Next_Signer, - User_Is_Not_Next_Signer -} +import { getLastSignersSig } from '../../utils/sign.ts' export const SignPage = () => { const navigate = useNavigate() @@ -100,17 +87,12 @@ export const SignPage = () => { } } - const [displayInput, setDisplayInput] = useState(false) - - const [selectedFile, setSelectedFile] = useState(null) - const [files, setFiles] = useState<{ [filename: string]: SigitFile }>({}) const [isLoading, setIsLoading] = useState(true) const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') const [meta, setMeta] = useState(null) - const [signedStatus, setSignedStatus] = useState() const [submittedBy, setSubmittedBy] = useState() @@ -124,66 +106,14 @@ export const SignPage = () => { [key: string]: string | null }>({}) - const [signedBy, setSignedBy] = useState<`npub1${string}`[]>([]) - - const [nextSinger, setNextSinger] = useState() - - // This state variable indicates whether the logged-in user is a signer, a creator, or neither. - const [isSignerOrCreator, setIsSignerOrCreator] = useState(false) - const usersPubkey = useAppSelector((state) => state.auth.usersPubkey) const nostrController = NostrController.getInstance() const [currentUserMarks, setCurrentUserMarks] = useState( [] ) - const [isMarksCompleted, setIsMarksCompleted] = useState(false) const [otherUserMarks, setOtherUserMarks] = useState([]) - useEffect(() => { - if (signers.length > 0) { - // check if all signers have signed then its fully signed - if (isFullySigned(signers, signedBy)) { - setSignedStatus(SignedStatus.Fully_Signed) - } else { - for (const signer of signers) { - if (!signedBy.includes(signer)) { - // signers in meta.json are in npub1 format - // so, convert it to hex before setting to nextSigner - setNextSinger(npubToHex(signer)!) - - const usersNpub = hexToNpub(usersPubkey!) - - if (signer === usersNpub) { - // logged in user is the next signer - setSignedStatus(SignedStatus.User_Is_Next_Signer) - } else { - setSignedStatus(SignedStatus.User_Is_Not_Next_Signer) - } - - break - } - } - } - } else { - // there's no signer just viewers. So its fully signed - setSignedStatus(SignedStatus.Fully_Signed) - } - - // Determine and set the status of the user - if (submittedBy && usersPubkey && submittedBy === usersPubkey) { - // If the submission was made by the user, set the status to true - setIsSignerOrCreator(true) - } else if (usersPubkey) { - // Convert the user's public key from hex to npub format - const usersNpub = hexToNpub(usersPubkey) - if (signers.includes(usersNpub)) { - // If the user's npub is in the list of signers, set the status to true - setIsSignerOrCreator(true) - } - } - }, [signers, signedBy, usersPubkey, submittedBy]) - useEffect(() => { const handleUpdatedMeta = async (meta: Meta) => { const createSignatureEvent = await parseJson( @@ -263,11 +193,10 @@ export const SignPage = () => { m.value && encryptionKey ) { - const decrypted = await fetchAndDecrypt( + otherUserMarks[i].value = await fetchAndDecrypt( m.value, encryptionKey ) - otherUserMarks[i].value = decrypted } } catch (error) { console.error(`Error during mark fetchAndDecrypt phase`, error) @@ -278,10 +207,7 @@ export const SignPage = () => { setOtherUserMarks(otherUserMarks) setCurrentUserMarks(currentUserMarks) - setIsMarksCompleted(isCurrentUserMarksComplete(currentUserMarks)) } - - setSignedBy(Object.keys(meta.docSignatures) as `npub1${string}`[]) } if (meta) { @@ -290,29 +216,6 @@ export const SignPage = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [meta, usersPubkey]) - const handleDownload = async () => { - if (Object.entries(files).length === 0 || !meta || !usersPubkey) return - setLoadingSpinnerDesc('Generating file') - try { - const zip = await getZipWithFiles(meta, files) - const arrayBuffer = await zip.generateAsync({ - type: ARRAY_BUFFER, - compression: DEFLATE, - compressionOptions: { - level: 6 - } - }) - if (!arrayBuffer) return - const blob = new Blob([arrayBuffer]) - saveAs(blob, `exported-${unixNow()}.sigit.zip`) - } catch (error) { - console.log('error in zip:>> ', error) - if (error instanceof Error) { - toast.error(error.message || 'Error occurred in generating zip file') - } - } - } - const decrypt = useCallback( async (file: File) => { setLoadingSpinnerDesc('Decrypting file') @@ -424,7 +327,6 @@ export const SignPage = () => { }) } else { setIsLoading(false) - setDisplayInput(true) } }, [decryptedArrayBuffer, uploadedZip, metaInNavState, decrypt]) @@ -541,9 +443,6 @@ export const SignPage = () => { setFiles(files) setCurrentFileHashes(fileHashes) - - setDisplayInput(false) - setLoadingSpinnerDesc('Parsing meta.json') const metaFileContent = await readContentOfZipEntry( @@ -571,21 +470,6 @@ export const SignPage = () => { setMeta(parsedMetaJson) } - const handleDecrypt = async () => { - if (!selectedFile) return - - setIsLoading(true) - const arrayBuffer = await decrypt(selectedFile) - - if (!arrayBuffer) { - setIsLoading(false) - toast.error('Error decrypting file') - return - } - - handleDecryptedArrayBuffer(arrayBuffer) - } - const handleSign = async () => { if (Object.entries(files).length === 0 || !meta) return @@ -640,6 +524,13 @@ export const SignPage = () => { setMeta(updatedMeta) setIsLoading(false) } + + if (metaInNavState) { + const createSignature = JSON.parse(metaInNavState.createSignature) + navigate(`${appPublicRoutes.verify}/${createSignature.id}`) + } else { + navigate(appPrivateRoutes.homePage) + } } // Sign the event for the meta file @@ -730,6 +621,14 @@ export const SignPage = () => { }) } + // Check if the current user is the last signer + const checkIsLastSigner = (signers: string[]): boolean => { + const usersNpub = hexToNpub(usersPubkey!) + const lastSignerIndex = signers.length - 1 + const signerIndex = signers.indexOf(usersNpub) + return signerIndex === lastSignerIndex + } + // Handle errors during zip file generation const handleZipError = (err: unknown) => { console.log('Error in zip:>> ', err) @@ -740,7 +639,7 @@ export const SignPage = () => { return null } - // Handle the online flow: update users app data and send notifications + // Handle the online flow: update users' app data and send notifications const handleOnlineFlow = async (meta: Meta) => { setLoadingSpinnerDesc('Updating users app data') const updatedEvent = await updateUsersAppData(meta) @@ -795,16 +694,38 @@ export const SignPage = () => { setIsLoading(false) } - // Check if the current user is the last signer - const checkIsLastSigner = (signers: string[]): boolean => { - const usersNpub = hexToNpub(usersPubkey!) - const lastSignerIndex = signers.length - 1 - const signerIndex = signers.indexOf(usersNpub) - return signerIndex === lastSignerIndex + const handleExport = async () => { + const arrayBuffer = await prepareZipExport() + if (!arrayBuffer) return + + const blob = new Blob([arrayBuffer]) + saveAs(blob, `exported-${unixNow()}.sigit.zip`) + + setIsLoading(false) + + navigate(appPublicRoutes.verify) } - const handleExport = async () => { - if (Object.entries(files).length === 0 || !meta || !usersPubkey) return + const handleEncryptedExport = async () => { + const arrayBuffer = await prepareZipExport() + if (!arrayBuffer) return + + const key = await generateEncryptionKey() + + setLoadingSpinnerDesc('Encrypting zip file') + const encryptedArrayBuffer = await encryptArrayBuffer(arrayBuffer, key) + + const finalZipFile = await createFinalZipFile(encryptedArrayBuffer, key) + + if (!finalZipFile) return + saveAs(finalZipFile, `exported-${unixNow()}.sigit.zip`) + + setIsLoading(false) + } + + const prepareZipExport = async (): Promise => { + if (Object.entries(files).length === 0 || !meta || !usersPubkey) + return Promise.resolve(null) const usersNpub = hexToNpub(usersPubkey) if ( @@ -812,15 +733,15 @@ export const SignPage = () => { !viewers.includes(usersNpub) && submittedBy !== usersNpub ) - return + return Promise.resolve(null) setIsLoading(true) setLoadingSpinnerDesc('Signing nostr event') - if (!meta) return + if (!meta) return Promise.resolve(null) const prevSig = getLastSignersSig(meta, signers) - if (!prevSig) return + if (!prevSig) return Promise.resolve(null) const signedEvent = await signEventForMetaFile( JSON.stringify({ @@ -830,7 +751,7 @@ export const SignPage = () => { setIsLoading ) - if (!signedEvent) return + if (!signedEvent) return Promise.resolve(null) const exportSignature = JSON.stringify(signedEvent, null, 2) @@ -848,8 +769,8 @@ export const SignPage = () => { const arrayBuffer = await zip .generateAsync({ - type: 'arraybuffer', - compression: 'DEFLATE', + type: ARRAY_BUFFER, + compression: DEFLATE, compressionOptions: { level: 6 } @@ -861,50 +782,9 @@ export const SignPage = () => { return null }) - if (!arrayBuffer) return + if (!arrayBuffer) return Promise.resolve(null) - const blob = new Blob([arrayBuffer]) - saveAs(blob, `exported-${unixNow()}.sigit.zip`) - - setIsLoading(false) - - navigate(appPublicRoutes.verify) - } - - const handleEncryptedExport = async () => { - if (Object.entries(files).length === 0 || !meta) return - - const stringifiedMeta = JSON.stringify(meta, null, 2) - const zip = await getZipWithFiles(meta, files) - - zip.file('meta.json', stringifiedMeta) - - const arrayBuffer = await zip - .generateAsync({ - type: 'arraybuffer', - compression: 'DEFLATE', - compressionOptions: { - level: 6 - } - }) - .catch((err) => { - console.log('err in zip:>> ', err) - setIsLoading(false) - toast.error(err.message || 'Error occurred in generating zip file') - return null - }) - - if (!arrayBuffer) return - - const key = await generateEncryptionKey() - - setLoadingSpinnerDesc('Encrypting zip file') - const encryptedArrayBuffer = await encryptArrayBuffer(arrayBuffer, key) - - const finalZipFile = await createFinalZipFile(encryptedArrayBuffer, key) - - if (!finalZipFile) return - saveAs(finalZipFile, `exported-${unixNow()}.sigit.zip`) + return Promise.resolve(arrayBuffer) } /** @@ -944,90 +824,17 @@ export const SignPage = () => { return } - if (!isMarksCompleted && signedStatus === SignedStatus.User_Is_Next_Signer) { - return ( - - ) - } - return ( - <> - - {displayInput && ( - <> - - Select sigit file - - - - setSelectedFile(value)} - /> - - - {selectedFile && ( - - - - )} - - )} - - {submittedBy && Object.entries(files).length > 0 && meta && ( - <> - - - {signedStatus === SignedStatus.Fully_Signed && ( - - - - )} - - {signedStatus === SignedStatus.User_Is_Next_Signer && ( - - - - )} - - {isSignerOrCreator && ( - - - - )} - - )} - - + ) } diff --git a/src/pages/verify/index.tsx b/src/pages/verify/index.tsx index 515a257..1aedc4d 100644 --- a/src/pages/verify/index.tsx +++ b/src/pages/verify/index.tsx @@ -23,7 +23,12 @@ import { getCurrentUserFiles, updateUsersAppData, npubToHex, - sendNotification + sendNotification, + generateEncryptionKey, + encryptArrayBuffer, + generateKeysFile, + ARRAY_BUFFER, + DEFLATE } from '../../utils' import styles from './style.module.scss' import { useLocation, useParams } from 'react-router-dom' @@ -541,8 +546,114 @@ export const VerifyPage = () => { setIsLoading(false) } - const handleMarkedExport = async () => { - if (Object.entries(files).length === 0 || !meta || !usersPubkey) return + // Handle errors during zip file generation + const handleZipError = (err: unknown) => { + console.log('Error in zip:>> ', err) + setIsLoading(false) + if (err instanceof Error) { + toast.error(err.message || 'Error occurred in generating zip file') + } + return null + } + + // Check if the current user is the last signer + const checkIsLastSigner = (signers: string[]): boolean => { + const usersNpub = hexToNpub(usersPubkey!) + const lastSignerIndex = signers.length - 1 + const signerIndex = signers.indexOf(usersNpub) + return signerIndex === lastSignerIndex + } + + // create final zip file + const createFinalZipFile = async ( + encryptedArrayBuffer: ArrayBuffer, + encryptionKey: string + ): Promise => { + // Get the current timestamp in seconds + const blob = new Blob([encryptedArrayBuffer]) + // Create a File object with the Blob data + const file = new File([blob], `compressed.sigit`, { + type: 'application/sigit' + }) + + const isLastSigner = checkIsLastSigner(signers) + + const userSet = new Set() + + if (isLastSigner) { + if (submittedBy) { + userSet.add(submittedBy) + } + + signers.forEach((signer) => { + userSet.add(npubToHex(signer)!) + }) + + viewers.forEach((viewer) => { + userSet.add(npubToHex(viewer)!) + }) + } else { + const usersNpub = hexToNpub(usersPubkey!) + const signerIndex = signers.indexOf(usersNpub) + const nextSigner = signers[signerIndex + 1] + userSet.add(npubToHex(nextSigner)!) + } + + const keysFileContent = await generateKeysFile( + Array.from(userSet), + encryptionKey + ) + if (!keysFileContent) return null + + const zip = new JSZip() + zip.file(`compressed.sigit`, file) + zip.file('keys.json', keysFileContent) + + const arraybuffer = await zip + .generateAsync({ + type: 'arraybuffer', + compression: 'DEFLATE', + compressionOptions: { level: 6 } + }) + .catch(handleZipError) + + if (!arraybuffer) return null + + return new File([new Blob([arraybuffer])], `${unixNow()}.sigit.zip`, { + type: 'application/zip' + }) + } + + const handleExport = async () => { + const arrayBuffer = await prepareZipExport() + if (!arrayBuffer) return + + const blob = new Blob([arrayBuffer]) + saveAs(blob, `exported-${unixNow()}.sigit.zip`) + + setIsLoading(false) + } + + const handleEncryptedExport = async () => { + const arrayBuffer = await prepareZipExport() + if (!arrayBuffer) return + + const key = await generateEncryptionKey() + + setLoadingSpinnerDesc('Encrypting zip file') + const encryptedArrayBuffer = await encryptArrayBuffer(arrayBuffer, key) + + const finalZipFile = await createFinalZipFile(encryptedArrayBuffer, key) + + if (!finalZipFile) return + saveAs(finalZipFile, `exported-${unixNow()}.sigit.zip`) + + setIsLoading(false) + } + + const prepareZipExport = async (): Promise => { + if (Object.entries(files).length === 0 || !meta || !usersPubkey) + return Promise.resolve(null) const usersNpub = hexToNpub(usersPubkey) if ( @@ -550,14 +661,14 @@ export const VerifyPage = () => { !viewers.includes(usersNpub) && submittedBy !== usersNpub ) { - return + return Promise.resolve(null) } setIsLoading(true) setLoadingSpinnerDesc('Signing nostr event') const prevSig = getLastSignersSig(meta, signers) - if (!prevSig) return + if (!prevSig) return Promise.resolve(null) const signedEvent = await signEventForMetaFile( JSON.stringify({ prevSig }), @@ -565,7 +676,7 @@ export const VerifyPage = () => { setIsLoading ) - if (!signedEvent) return + if (!signedEvent) return Promise.resolve(null) const exportSignature = JSON.stringify(signedEvent, null, 2) const updatedMeta = { ...meta, exportSignature } @@ -576,8 +687,8 @@ export const VerifyPage = () => { const arrayBuffer = await zip .generateAsync({ - type: 'arraybuffer', - compression: 'DEFLATE', + type: ARRAY_BUFFER, + compression: DEFLATE, compressionOptions: { level: 6 } @@ -589,12 +700,9 @@ export const VerifyPage = () => { return null }) - if (!arrayBuffer) return + if (!arrayBuffer) return Promise.resolve(null) - const blob = new Blob([arrayBuffer]) - saveAs(blob, `exported-${unixNow()}.sigit.zip`) - - setIsLoading(false) + return Promise.resolve(arrayBuffer) } return ( @@ -640,8 +748,8 @@ export const VerifyPage = () => { )} currentFile={currentFile} setCurrentFile={setCurrentFile} - handleDownload={handleMarkedExport} - downloadLabel="Download Sigit" + handleExport={handleExport} + handleEncryptedExport={handleEncryptedExport} /> ) } diff --git a/src/utils/const.ts b/src/utils/const.ts index 522f242..2b8e822 100644 --- a/src/utils/const.ts +++ b/src/utils/const.ts @@ -120,5 +120,5 @@ export const SIGNATURE_PAD_OPTIONS = { export const SIGNATURE_PAD_SIZE = { width: 300, - height: 300 + height: 150 }