Merge pull request 'feat: Sign Directly From the Marking Screen fix: Marking inputs glitches, losing values' (#272) from issue-173 into staging
All checks were successful
Release to Staging / build_and_release (push) Successful in 1m38s

Reviewed-on: #272
Reviewed-by: enes <enes@noreply.git.nostrdev.com>
This commit is contained in:
b 2024-12-10 15:34:01 +00:00
commit 80dd597ada
9 changed files with 367 additions and 323 deletions

27
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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 = ({
</li>
))}
</ul>
<Button variant="contained" fullWidth onClick={handleDownload}>
{downloadLabel || 'Download Files'}
<PopupState variant="popover" popupId="download-popup-menu">
{(popupState) => (
<React.Fragment>
<Button variant="contained" {...bindTrigger(popupState)}>
Export files
</Button>
<Menu {...bindMenu(popupState)}>
<MenuItem
onClick={() => {
popupState.close
handleExport()
}}
>
Export Files
</MenuItem>
<MenuItem
onClick={() => {
popupState.close
typeof handleEncryptedExport === 'function' &&
handleEncryptedExport()
}}
>
Export Encrypted Files
</MenuItem>
</Menu>
</React.Fragment>
)}
</PopupState>
</div>
)
}

View File

@ -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<HTMLFormElement>) => void
handleSubmit: (event: React.MouseEvent<HTMLButtonElement>) => 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<HTMLFormElement>) => {
event.preventDefault()
console.log('handle form submit runs...')
return isReadyToSign()
? handleSubmit(event)
// 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<HTMLButtonElement>
) => {
handleSubmit(event)
}
return (
<div className={styles.container}>
<div className={styles.trigger}>
@ -78,10 +105,14 @@ const MarkFormField = ({
<div className={styles.actionsWrapper}>
<div className={styles.actionsTop}>
<div className={styles.actionsTopInfo}>
{!complete && (
<p className={styles.actionsTopInfoText}>Add {markLabel}</p>
)}
{complete && <p className={styles.actionsTopInfoText}>Finish</p>}
</div>
</div>
<div className={styles.inputWrapper}>
{!complete && (
<form onSubmit={(e) => handleFormSubmit(e)}>
<MarkInput
markType={selectedMark.mark.type}
@ -97,6 +128,20 @@ const MarkFormField = ({
</button>
</div>
</form>
)}
{complete && (
<div className={styles.actionsBottom}>
<button
onClick={handleSignAndComplete}
className={styles.submitButton}
disabled={!isReadyToSign()}
>
SIGN AND COMPLETE
</button>
</div>
)}
<div className={styles.footerContainer}>
<div className={styles.footer}>
{currentUserMarks.map((mark, index) => {
@ -104,7 +149,7 @@ const MarkFormField = ({
<div className={styles.pagination} key={index}>
<button
className={`${styles.paginationButton} ${isDone(mark) ? styles.paginationButtonDone : ''}`}
onClick={() => handleCurrentUserMarkChange(mark)}
onClick={() => handleCurrentUserMarkClick(mark)}
>
{mark.id}
</button>
@ -114,6 +159,20 @@ const MarkFormField = ({
</div>
)
})}
<div className={styles.pagination}>
<button
className={`${styles.paginationButton} ${isReadyToSign() ? styles.paginationButtonDone : ''}`}
onClick={handleSelectCompleteMark}
>
<FontAwesomeIcon
className={styles.finishPage}
icon={faCheck}
/>
</button>
{complete && (
<div className={styles.paginationButtonCurrent}></div>
)}
</div>
</div>
</div>
</div>

View File

@ -216,3 +216,7 @@
flex-direction: column;
grid-gap: 5px;
}
.finishPage {
padding: 1px 0;
}

View File

@ -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)
// 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<HTMLFormElement>) => {
/**
* Sign and Complete
*/
const handleSubmit = (event: React.MouseEvent<HTMLButtonElement>) => {
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}
/>
)}
</div>

View File

@ -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<File | null>(null)
const [files, setFiles] = useState<{ [filename: string]: SigitFile }>({})
const [isLoading, setIsLoading] = useState(true)
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
const [meta, setMeta] = useState<Meta | null>(null)
const [signedStatus, setSignedStatus] = useState<SignedStatus>()
const [submittedBy, setSubmittedBy] = useState<string>()
@ -124,66 +106,14 @@ export const SignPage = () => {
[key: string]: string | null
}>({})
const [signedBy, setSignedBy] = useState<`npub1${string}`[]>([])
const [nextSinger, setNextSinger] = useState<string>()
// 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<CurrentUserMark[]>(
[]
)
const [isMarksCompleted, setIsMarksCompleted] = useState(false)
const [otherUserMarks, setOtherUserMarks] = useState<Mark[]>([])
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<Event>(
@ -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<ArrayBuffer | null> => {
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 <LoadingSpinner desc={loadingSpinnerDesc} />
}
if (!isMarksCompleted && signedStatus === SignedStatus.User_Is_Next_Signer) {
return (
<PdfMarking
files={getCurrentUserFiles(files, currentFileHashes, creatorFileHashes)}
currentUserMarks={currentUserMarks}
setIsMarksCompleted={setIsMarksCompleted}
setCurrentUserMarks={setCurrentUserMarks}
setUpdatedMarks={setUpdatedMarks}
handleDownload={handleDownload}
handleSign={handleSign}
handleExport={handleExport}
handleEncryptedExport={handleEncryptedExport}
otherUserMarks={otherUserMarks}
meta={meta}
/>
)
}
return (
<>
<Container className={styles.container}>
{displayInput && (
<>
<Typography component="label" variant="h6">
Select sigit file
</Typography>
<Box className={styles.inputBlock}>
<MuiFileInput
placeholder="Select file"
inputProps={{ accept: '.sigit.zip' }}
value={selectedFile}
onChange={(value) => setSelectedFile(value)}
/>
</Box>
{selectedFile && (
<Box sx={{ mt: 2, display: 'flex', justifyContent: 'center' }}>
<Button onClick={handleDecrypt} variant="contained">
Decrypt
</Button>
</Box>
)}
</>
)}
{submittedBy && Object.entries(files).length > 0 && meta && (
<>
<DisplayMeta
meta={meta}
files={files}
submittedBy={submittedBy}
signers={signers}
viewers={viewers}
creatorFileHashes={creatorFileHashes}
currentFileHashes={currentFileHashes}
signedBy={signedBy}
nextSigner={nextSinger}
getPrevSignersSig={getPrevSignersSig}
/>
{signedStatus === SignedStatus.Fully_Signed && (
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
<Button onClick={handleExport} variant="contained">
Export Sigit
</Button>
</Box>
)}
{signedStatus === SignedStatus.User_Is_Next_Signer && (
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
<Button onClick={handleSign} variant="contained">
Sign
</Button>
</Box>
)}
{isSignerOrCreator && (
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
<Button onClick={handleEncryptedExport} variant="contained">
Export Encrypted Sigit
</Button>
</Box>
)}
</>
)}
</Container>
</>
)
}

View File

@ -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<File | null> => {
// 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<string>()
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<ArrayBuffer | null> => {
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}
/>
)
}

View File

@ -120,5 +120,5 @@ export const SIGNATURE_PAD_OPTIONS = {
export const SIGNATURE_PAD_SIZE = {
width: 300,
height: 300
height: 150
}