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
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:
commit
80dd597ada
27
package-lock.json
generated
27
package-lock.json
generated
@ -31,6 +31,7 @@
|
|||||||
"idb": "8.0.0",
|
"idb": "8.0.0",
|
||||||
"jszip": "3.10.1",
|
"jszip": "3.10.1",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
|
"material-ui-popup-state": "^5.3.1",
|
||||||
"mui-file-input": "4.0.4",
|
"mui-file-input": "4.0.4",
|
||||||
"nostr-login": "^1.6.6",
|
"nostr-login": "^1.6.6",
|
||||||
"nostr-tools": "2.7.0",
|
"nostr-tools": "2.7.0",
|
||||||
@ -3330,6 +3331,12 @@
|
|||||||
"safe-buffer": "^5.0.1"
|
"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": {
|
"node_modules/cli-cursor": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
|
||||||
@ -5976,6 +5983,26 @@
|
|||||||
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/md5.js": {
|
||||||
"version": "1.3.5",
|
"version": "1.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
|
||||||
|
@ -41,6 +41,7 @@
|
|||||||
"idb": "8.0.0",
|
"idb": "8.0.0",
|
||||||
"jszip": "3.10.1",
|
"jszip": "3.10.1",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
|
"material-ui-popup-state": "^5.3.1",
|
||||||
"mui-file-input": "4.0.4",
|
"mui-file-input": "4.0.4",
|
||||||
"nostr-login": "^1.6.6",
|
"nostr-login": "^1.6.6",
|
||||||
"nostr-tools": "2.7.0",
|
"nostr-tools": "2.7.0",
|
||||||
|
@ -1,23 +1,25 @@
|
|||||||
import { CurrentUserFile } from '../../types/file.ts'
|
import { CurrentUserFile } from '../../types/file.ts'
|
||||||
import styles from './style.module.scss'
|
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 { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
import { faCheck } from '@fortawesome/free-solid-svg-icons'
|
import { faCheck } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import PopupState, { bindTrigger, bindMenu } from 'material-ui-popup-state'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
interface FileListProps {
|
interface FileListProps {
|
||||||
files: CurrentUserFile[]
|
files: CurrentUserFile[]
|
||||||
currentFile: CurrentUserFile
|
currentFile: CurrentUserFile
|
||||||
setCurrentFile: (file: CurrentUserFile) => void
|
setCurrentFile: (file: CurrentUserFile) => void
|
||||||
handleDownload: () => void
|
handleExport: () => void
|
||||||
downloadLabel?: string
|
handleEncryptedExport?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const FileList = ({
|
const FileList = ({
|
||||||
files,
|
files,
|
||||||
currentFile,
|
currentFile,
|
||||||
setCurrentFile,
|
setCurrentFile,
|
||||||
handleDownload,
|
handleExport,
|
||||||
downloadLabel
|
handleEncryptedExport
|
||||||
}: FileListProps) => {
|
}: FileListProps) => {
|
||||||
const isActive = (file: CurrentUserFile) => file.id === currentFile.id
|
const isActive = (file: CurrentUserFile) => file.id === currentFile.id
|
||||||
return (
|
return (
|
||||||
@ -42,9 +44,35 @@ const FileList = ({
|
|||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
<Button variant="contained" fullWidth onClick={handleDownload}>
|
|
||||||
{downloadLabel || 'Download Files'}
|
<PopupState variant="popover" popupId="download-popup-menu">
|
||||||
</Button>
|
{(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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -8,12 +8,14 @@ import {
|
|||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { MarkInput } from '../MarkTypeStrategy/MarkInput.tsx'
|
import { MarkInput } from '../MarkTypeStrategy/MarkInput.tsx'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
|
import { faCheck } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
interface MarkFormFieldProps {
|
interface MarkFormFieldProps {
|
||||||
currentUserMarks: CurrentUserMark[]
|
currentUserMarks: CurrentUserMark[]
|
||||||
handleCurrentUserMarkChange: (mark: CurrentUserMark) => void
|
handleCurrentUserMarkChange: (mark: CurrentUserMark) => void
|
||||||
handleSelectedMarkValueChange: (value: string) => void
|
handleSelectedMarkValueChange: (value: string) => void
|
||||||
handleSubmit: (event: React.FormEvent<HTMLFormElement>) => void
|
handleSubmit: (event: React.MouseEvent<HTMLButtonElement>) => void
|
||||||
selectedMark: CurrentUserMark
|
selectedMark: CurrentUserMark
|
||||||
selectedMarkValue: string
|
selectedMarkValue: string
|
||||||
}
|
}
|
||||||
@ -30,11 +32,13 @@ const MarkFormField = ({
|
|||||||
handleCurrentUserMarkChange
|
handleCurrentUserMarkChange
|
||||||
}: MarkFormFieldProps) => {
|
}: MarkFormFieldProps) => {
|
||||||
const [displayActions, setDisplayActions] = useState(true)
|
const [displayActions, setDisplayActions] = useState(true)
|
||||||
|
const [complete, setComplete] = useState(false)
|
||||||
|
|
||||||
const isReadyToSign = () =>
|
const isReadyToSign = () =>
|
||||||
isCurrentUserMarksComplete(currentUserMarks) ||
|
isCurrentUserMarksComplete(currentUserMarks) ||
|
||||||
isCurrentValueLast(currentUserMarks, selectedMark, selectedMarkValue)
|
isCurrentValueLast(currentUserMarks, selectedMark, selectedMarkValue)
|
||||||
const isCurrent = (currentMark: CurrentUserMark) =>
|
const isCurrent = (currentMark: CurrentUserMark) =>
|
||||||
currentMark.id === selectedMark.id
|
currentMark.id === selectedMark.id && !complete
|
||||||
const isDone = (currentMark: CurrentUserMark) =>
|
const isDone = (currentMark: CurrentUserMark) =>
|
||||||
isCurrent(currentMark) ? !!selectedMarkValue : currentMark.isCompleted
|
isCurrent(currentMark) ? !!selectedMarkValue : currentMark.isCompleted
|
||||||
const findNext = () => {
|
const findNext = () => {
|
||||||
@ -46,13 +50,36 @@ const MarkFormField = ({
|
|||||||
const handleFormSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
const handleFormSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
console.log('handle form submit runs...')
|
console.log('handle form submit runs...')
|
||||||
return isReadyToSign()
|
|
||||||
? handleSubmit(event)
|
// Without this line, we lose mark values when switching
|
||||||
: handleCurrentUserMarkChange(findNext()!)
|
handleCurrentUserMarkChange(selectedMark)
|
||||||
|
|
||||||
|
if (!complete) {
|
||||||
|
isReadyToSign()
|
||||||
|
? setComplete(true)
|
||||||
|
: handleCurrentUserMarkChange(findNext()!)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleActions = () => setDisplayActions(!displayActions)
|
const toggleActions = () => setDisplayActions(!displayActions)
|
||||||
const markLabel = getToolboxLabelByMarkType(selectedMark.mark.type)
|
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 (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div className={styles.trigger}>
|
<div className={styles.trigger}>
|
||||||
@ -78,25 +105,43 @@ const MarkFormField = ({
|
|||||||
<div className={styles.actionsWrapper}>
|
<div className={styles.actionsWrapper}>
|
||||||
<div className={styles.actionsTop}>
|
<div className={styles.actionsTop}>
|
||||||
<div className={styles.actionsTopInfo}>
|
<div className={styles.actionsTopInfo}>
|
||||||
<p className={styles.actionsTopInfoText}>Add {markLabel}</p>
|
{!complete && (
|
||||||
|
<p className={styles.actionsTopInfoText}>Add {markLabel}</p>
|
||||||
|
)}
|
||||||
|
{complete && <p className={styles.actionsTopInfoText}>Finish</p>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.inputWrapper}>
|
<div className={styles.inputWrapper}>
|
||||||
<form onSubmit={(e) => handleFormSubmit(e)}>
|
{!complete && (
|
||||||
<MarkInput
|
<form onSubmit={(e) => handleFormSubmit(e)}>
|
||||||
markType={selectedMark.mark.type}
|
<MarkInput
|
||||||
key={selectedMark.id}
|
markType={selectedMark.mark.type}
|
||||||
value={selectedMarkValue}
|
key={selectedMark.id}
|
||||||
placeholder={markLabel}
|
value={selectedMarkValue}
|
||||||
handler={handleSelectedMarkValueChange}
|
placeholder={markLabel}
|
||||||
userMark={selectedMark}
|
handler={handleSelectedMarkValueChange}
|
||||||
/>
|
userMark={selectedMark}
|
||||||
|
/>
|
||||||
|
<div className={styles.actionsBottom}>
|
||||||
|
<button type="submit" className={styles.submitButton}>
|
||||||
|
NEXT
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{complete && (
|
||||||
<div className={styles.actionsBottom}>
|
<div className={styles.actionsBottom}>
|
||||||
<button type="submit" className={styles.submitButton}>
|
<button
|
||||||
NEXT
|
onClick={handleSignAndComplete}
|
||||||
|
className={styles.submitButton}
|
||||||
|
disabled={!isReadyToSign()}
|
||||||
|
>
|
||||||
|
SIGN AND COMPLETE
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
)}
|
||||||
|
|
||||||
<div className={styles.footerContainer}>
|
<div className={styles.footerContainer}>
|
||||||
<div className={styles.footer}>
|
<div className={styles.footer}>
|
||||||
{currentUserMarks.map((mark, index) => {
|
{currentUserMarks.map((mark, index) => {
|
||||||
@ -104,7 +149,7 @@ const MarkFormField = ({
|
|||||||
<div className={styles.pagination} key={index}>
|
<div className={styles.pagination} key={index}>
|
||||||
<button
|
<button
|
||||||
className={`${styles.paginationButton} ${isDone(mark) ? styles.paginationButtonDone : ''}`}
|
className={`${styles.paginationButton} ${isDone(mark) ? styles.paginationButtonDone : ''}`}
|
||||||
onClick={() => handleCurrentUserMarkChange(mark)}
|
onClick={() => handleCurrentUserMarkClick(mark)}
|
||||||
>
|
>
|
||||||
{mark.id}
|
{mark.id}
|
||||||
</button>
|
</button>
|
||||||
@ -114,6 +159,20 @@ const MarkFormField = ({
|
|||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -216,3 +216,7 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
grid-gap: 5px;
|
grid-gap: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.finishPage {
|
||||||
|
padding: 1px 0;
|
||||||
|
}
|
||||||
|
@ -24,11 +24,12 @@ import {
|
|||||||
interface PdfMarkingProps {
|
interface PdfMarkingProps {
|
||||||
currentUserMarks: CurrentUserMark[]
|
currentUserMarks: CurrentUserMark[]
|
||||||
files: CurrentUserFile[]
|
files: CurrentUserFile[]
|
||||||
handleDownload: () => void
|
handleExport: () => void
|
||||||
|
handleEncryptedExport: () => void
|
||||||
|
handleSign: () => void
|
||||||
meta: Meta | null
|
meta: Meta | null
|
||||||
otherUserMarks: Mark[]
|
otherUserMarks: Mark[]
|
||||||
setCurrentUserMarks: (currentUserMarks: CurrentUserMark[]) => void
|
setCurrentUserMarks: (currentUserMarks: CurrentUserMark[]) => void
|
||||||
setIsMarksCompleted: (isMarksCompleted: boolean) => void
|
|
||||||
setUpdatedMarks: (markToUpdate: Mark) => void
|
setUpdatedMarks: (markToUpdate: Mark) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,10 +43,11 @@ const PdfMarking = (props: PdfMarkingProps) => {
|
|||||||
const {
|
const {
|
||||||
files,
|
files,
|
||||||
currentUserMarks,
|
currentUserMarks,
|
||||||
setIsMarksCompleted,
|
|
||||||
setCurrentUserMarks,
|
setCurrentUserMarks,
|
||||||
setUpdatedMarks,
|
setUpdatedMarks,
|
||||||
handleDownload,
|
handleExport,
|
||||||
|
handleEncryptedExport,
|
||||||
|
handleSign,
|
||||||
meta,
|
meta,
|
||||||
otherUserMarks
|
otherUserMarks
|
||||||
} = props
|
} = props
|
||||||
@ -86,11 +88,18 @@ const PdfMarking = (props: PdfMarkingProps) => {
|
|||||||
updatedSelectedMark
|
updatedSelectedMark
|
||||||
)
|
)
|
||||||
setCurrentUserMarks(updatedCurrentUserMarks)
|
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<HTMLFormElement>) => {
|
/**
|
||||||
|
* Sign and Complete
|
||||||
|
*/
|
||||||
|
const handleSubmit = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
if (!selectedMarkValue || !selectedMark) return
|
if (!selectedMarkValue || !selectedMark) return
|
||||||
|
|
||||||
@ -106,8 +115,8 @@ const PdfMarking = (props: PdfMarkingProps) => {
|
|||||||
)
|
)
|
||||||
setCurrentUserMarks(updatedCurrentUserMarks)
|
setCurrentUserMarks(updatedCurrentUserMarks)
|
||||||
setSelectedMark(null)
|
setSelectedMark(null)
|
||||||
setIsMarksCompleted(true)
|
|
||||||
setUpdatedMarks(updatedMark.mark)
|
setUpdatedMarks(updatedMark.mark)
|
||||||
|
handleSign()
|
||||||
}
|
}
|
||||||
|
|
||||||
// const updateCurrentUserMarkValues = () => {
|
// const updateCurrentUserMarkValues = () => {
|
||||||
@ -132,7 +141,8 @@ const PdfMarking = (props: PdfMarkingProps) => {
|
|||||||
files={files}
|
files={files}
|
||||||
currentFile={currentFile}
|
currentFile={currentFile}
|
||||||
setCurrentFile={setCurrentFile}
|
setCurrentFile={setCurrentFile}
|
||||||
handleDownload={handleDownload}
|
handleExport={handleExport}
|
||||||
|
handleEncryptedExport={handleEncryptedExport}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,67 +1,54 @@
|
|||||||
import { Box, Button, Typography } from '@mui/material'
|
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import saveAs from 'file-saver'
|
import saveAs from 'file-saver'
|
||||||
import JSZip from 'jszip'
|
import JSZip from 'jszip'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import { MuiFileInput } from 'mui-file-input'
|
|
||||||
import { Event, verifyEvent } from 'nostr-tools'
|
import { Event, verifyEvent } from 'nostr-tools'
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import { useAppSelector } from '../../hooks/store'
|
import { useAppSelector } from '../../hooks'
|
||||||
import { useLocation, useNavigate, useParams } from 'react-router-dom'
|
import { useLocation, useNavigate, useParams } from 'react-router-dom'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
||||||
import { NostrController } from '../../controllers'
|
import { NostrController } from '../../controllers'
|
||||||
import { appPublicRoutes } from '../../routes'
|
import { appPrivateRoutes, appPublicRoutes } from '../../routes'
|
||||||
import { CreateSignatureEventContent, Meta, SignedEvent } from '../../types'
|
import { CreateSignatureEventContent, Meta, SignedEvent } from '../../types'
|
||||||
import {
|
import {
|
||||||
|
ARRAY_BUFFER,
|
||||||
decryptArrayBuffer,
|
decryptArrayBuffer,
|
||||||
|
DEFLATE,
|
||||||
encryptArrayBuffer,
|
encryptArrayBuffer,
|
||||||
extractMarksFromSignedMeta,
|
extractMarksFromSignedMeta,
|
||||||
extractZipUrlAndEncryptionKey,
|
extractZipUrlAndEncryptionKey,
|
||||||
|
filterMarksByPubkey,
|
||||||
|
findOtherUserMarks,
|
||||||
generateEncryptionKey,
|
generateEncryptionKey,
|
||||||
generateKeysFile,
|
generateKeysFile,
|
||||||
getCurrentUserFiles,
|
getCurrentUserFiles,
|
||||||
|
getCurrentUserMarks,
|
||||||
getHash,
|
getHash,
|
||||||
hexToNpub,
|
hexToNpub,
|
||||||
isOnline,
|
isOnline,
|
||||||
loadZip,
|
loadZip,
|
||||||
unixNow,
|
|
||||||
npubToHex,
|
npubToHex,
|
||||||
parseJson,
|
parseJson,
|
||||||
|
processMarks,
|
||||||
readContentOfZipEntry,
|
readContentOfZipEntry,
|
||||||
sendNotification,
|
sendNotification,
|
||||||
signEventForMetaFile,
|
signEventForMetaFile,
|
||||||
updateUsersAppData,
|
|
||||||
findOtherUserMarks,
|
|
||||||
timeout,
|
timeout,
|
||||||
processMarks
|
unixNow,
|
||||||
|
updateMarks,
|
||||||
|
updateUsersAppData
|
||||||
} from '../../utils'
|
} 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 { 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 PdfMarking from '../../components/PDFView/PdfMarking.tsx'
|
||||||
import {
|
import {
|
||||||
convertToSigitFile,
|
convertToSigitFile,
|
||||||
getZipWithFiles,
|
getZipWithFiles,
|
||||||
SigitFile
|
SigitFile
|
||||||
} from '../../utils/file.ts'
|
} from '../../utils/file.ts'
|
||||||
import { ARRAY_BUFFER, DEFLATE } from '../../utils/const.ts'
|
|
||||||
import { generateTimestamp } from '../../utils/opentimestamps.ts'
|
import { generateTimestamp } from '../../utils/opentimestamps.ts'
|
||||||
import { MARK_TYPE_CONFIG } from '../../components/MarkTypeStrategy/MarkStrategy.tsx'
|
import { MARK_TYPE_CONFIG } from '../../components/MarkTypeStrategy/MarkStrategy.tsx'
|
||||||
|
import { getLastSignersSig } from '../../utils/sign.ts'
|
||||||
enum SignedStatus {
|
|
||||||
Fully_Signed,
|
|
||||||
User_Is_Next_Signer,
|
|
||||||
User_Is_Not_Next_Signer
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SignPage = () => {
|
export const SignPage = () => {
|
||||||
const navigate = useNavigate()
|
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 [files, setFiles] = useState<{ [filename: string]: SigitFile }>({})
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
|
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
|
||||||
|
|
||||||
const [meta, setMeta] = useState<Meta | null>(null)
|
const [meta, setMeta] = useState<Meta | null>(null)
|
||||||
const [signedStatus, setSignedStatus] = useState<SignedStatus>()
|
|
||||||
|
|
||||||
const [submittedBy, setSubmittedBy] = useState<string>()
|
const [submittedBy, setSubmittedBy] = useState<string>()
|
||||||
|
|
||||||
@ -124,66 +106,14 @@ export const SignPage = () => {
|
|||||||
[key: string]: string | null
|
[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 usersPubkey = useAppSelector((state) => state.auth.usersPubkey)
|
||||||
|
|
||||||
const nostrController = NostrController.getInstance()
|
const nostrController = NostrController.getInstance()
|
||||||
const [currentUserMarks, setCurrentUserMarks] = useState<CurrentUserMark[]>(
|
const [currentUserMarks, setCurrentUserMarks] = useState<CurrentUserMark[]>(
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
const [isMarksCompleted, setIsMarksCompleted] = useState(false)
|
|
||||||
const [otherUserMarks, setOtherUserMarks] = useState<Mark[]>([])
|
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(() => {
|
useEffect(() => {
|
||||||
const handleUpdatedMeta = async (meta: Meta) => {
|
const handleUpdatedMeta = async (meta: Meta) => {
|
||||||
const createSignatureEvent = await parseJson<Event>(
|
const createSignatureEvent = await parseJson<Event>(
|
||||||
@ -263,11 +193,10 @@ export const SignPage = () => {
|
|||||||
m.value &&
|
m.value &&
|
||||||
encryptionKey
|
encryptionKey
|
||||||
) {
|
) {
|
||||||
const decrypted = await fetchAndDecrypt(
|
otherUserMarks[i].value = await fetchAndDecrypt(
|
||||||
m.value,
|
m.value,
|
||||||
encryptionKey
|
encryptionKey
|
||||||
)
|
)
|
||||||
otherUserMarks[i].value = decrypted
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error during mark fetchAndDecrypt phase`, error)
|
console.error(`Error during mark fetchAndDecrypt phase`, error)
|
||||||
@ -278,10 +207,7 @@ export const SignPage = () => {
|
|||||||
|
|
||||||
setOtherUserMarks(otherUserMarks)
|
setOtherUserMarks(otherUserMarks)
|
||||||
setCurrentUserMarks(currentUserMarks)
|
setCurrentUserMarks(currentUserMarks)
|
||||||
setIsMarksCompleted(isCurrentUserMarksComplete(currentUserMarks))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setSignedBy(Object.keys(meta.docSignatures) as `npub1${string}`[])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (meta) {
|
if (meta) {
|
||||||
@ -290,29 +216,6 @@ export const SignPage = () => {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [meta, usersPubkey])
|
}, [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(
|
const decrypt = useCallback(
|
||||||
async (file: File) => {
|
async (file: File) => {
|
||||||
setLoadingSpinnerDesc('Decrypting file')
|
setLoadingSpinnerDesc('Decrypting file')
|
||||||
@ -424,7 +327,6 @@ export const SignPage = () => {
|
|||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
setDisplayInput(true)
|
|
||||||
}
|
}
|
||||||
}, [decryptedArrayBuffer, uploadedZip, metaInNavState, decrypt])
|
}, [decryptedArrayBuffer, uploadedZip, metaInNavState, decrypt])
|
||||||
|
|
||||||
@ -541,9 +443,6 @@ export const SignPage = () => {
|
|||||||
|
|
||||||
setFiles(files)
|
setFiles(files)
|
||||||
setCurrentFileHashes(fileHashes)
|
setCurrentFileHashes(fileHashes)
|
||||||
|
|
||||||
setDisplayInput(false)
|
|
||||||
|
|
||||||
setLoadingSpinnerDesc('Parsing meta.json')
|
setLoadingSpinnerDesc('Parsing meta.json')
|
||||||
|
|
||||||
const metaFileContent = await readContentOfZipEntry(
|
const metaFileContent = await readContentOfZipEntry(
|
||||||
@ -571,21 +470,6 @@ export const SignPage = () => {
|
|||||||
setMeta(parsedMetaJson)
|
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 () => {
|
const handleSign = async () => {
|
||||||
if (Object.entries(files).length === 0 || !meta) return
|
if (Object.entries(files).length === 0 || !meta) return
|
||||||
|
|
||||||
@ -640,6 +524,13 @@ export const SignPage = () => {
|
|||||||
setMeta(updatedMeta)
|
setMeta(updatedMeta)
|
||||||
setIsLoading(false)
|
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
|
// 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
|
// Handle errors during zip file generation
|
||||||
const handleZipError = (err: unknown) => {
|
const handleZipError = (err: unknown) => {
|
||||||
console.log('Error in zip:>> ', err)
|
console.log('Error in zip:>> ', err)
|
||||||
@ -740,7 +639,7 @@ export const SignPage = () => {
|
|||||||
return null
|
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) => {
|
const handleOnlineFlow = async (meta: Meta) => {
|
||||||
setLoadingSpinnerDesc('Updating users app data')
|
setLoadingSpinnerDesc('Updating users app data')
|
||||||
const updatedEvent = await updateUsersAppData(meta)
|
const updatedEvent = await updateUsersAppData(meta)
|
||||||
@ -795,16 +694,38 @@ export const SignPage = () => {
|
|||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the current user is the last signer
|
const handleExport = async () => {
|
||||||
const checkIsLastSigner = (signers: string[]): boolean => {
|
const arrayBuffer = await prepareZipExport()
|
||||||
const usersNpub = hexToNpub(usersPubkey!)
|
if (!arrayBuffer) return
|
||||||
const lastSignerIndex = signers.length - 1
|
|
||||||
const signerIndex = signers.indexOf(usersNpub)
|
const blob = new Blob([arrayBuffer])
|
||||||
return signerIndex === lastSignerIndex
|
saveAs(blob, `exported-${unixNow()}.sigit.zip`)
|
||||||
|
|
||||||
|
setIsLoading(false)
|
||||||
|
|
||||||
|
navigate(appPublicRoutes.verify)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleExport = async () => {
|
const handleEncryptedExport = async () => {
|
||||||
if (Object.entries(files).length === 0 || !meta || !usersPubkey) return
|
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)
|
const usersNpub = hexToNpub(usersPubkey)
|
||||||
if (
|
if (
|
||||||
@ -812,15 +733,15 @@ export const SignPage = () => {
|
|||||||
!viewers.includes(usersNpub) &&
|
!viewers.includes(usersNpub) &&
|
||||||
submittedBy !== usersNpub
|
submittedBy !== usersNpub
|
||||||
)
|
)
|
||||||
return
|
return Promise.resolve(null)
|
||||||
|
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
setLoadingSpinnerDesc('Signing nostr event')
|
setLoadingSpinnerDesc('Signing nostr event')
|
||||||
|
|
||||||
if (!meta) return
|
if (!meta) return Promise.resolve(null)
|
||||||
|
|
||||||
const prevSig = getLastSignersSig(meta, signers)
|
const prevSig = getLastSignersSig(meta, signers)
|
||||||
if (!prevSig) return
|
if (!prevSig) return Promise.resolve(null)
|
||||||
|
|
||||||
const signedEvent = await signEventForMetaFile(
|
const signedEvent = await signEventForMetaFile(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
@ -830,7 +751,7 @@ export const SignPage = () => {
|
|||||||
setIsLoading
|
setIsLoading
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!signedEvent) return
|
if (!signedEvent) return Promise.resolve(null)
|
||||||
|
|
||||||
const exportSignature = JSON.stringify(signedEvent, null, 2)
|
const exportSignature = JSON.stringify(signedEvent, null, 2)
|
||||||
|
|
||||||
@ -848,8 +769,8 @@ export const SignPage = () => {
|
|||||||
|
|
||||||
const arrayBuffer = await zip
|
const arrayBuffer = await zip
|
||||||
.generateAsync({
|
.generateAsync({
|
||||||
type: 'arraybuffer',
|
type: ARRAY_BUFFER,
|
||||||
compression: 'DEFLATE',
|
compression: DEFLATE,
|
||||||
compressionOptions: {
|
compressionOptions: {
|
||||||
level: 6
|
level: 6
|
||||||
}
|
}
|
||||||
@ -861,50 +782,9 @@ export const SignPage = () => {
|
|||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!arrayBuffer) return
|
if (!arrayBuffer) return Promise.resolve(null)
|
||||||
|
|
||||||
const blob = new Blob([arrayBuffer])
|
return Promise.resolve(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`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -944,90 +824,17 @@ export const SignPage = () => {
|
|||||||
return <LoadingSpinner desc={loadingSpinnerDesc} />
|
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}
|
|
||||||
otherUserMarks={otherUserMarks}
|
|
||||||
meta={meta}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<PdfMarking
|
||||||
<Container className={styles.container}>
|
files={getCurrentUserFiles(files, currentFileHashes, creatorFileHashes)}
|
||||||
{displayInput && (
|
currentUserMarks={currentUserMarks}
|
||||||
<>
|
setCurrentUserMarks={setCurrentUserMarks}
|
||||||
<Typography component="label" variant="h6">
|
setUpdatedMarks={setUpdatedMarks}
|
||||||
Select sigit file
|
handleSign={handleSign}
|
||||||
</Typography>
|
handleExport={handleExport}
|
||||||
|
handleEncryptedExport={handleEncryptedExport}
|
||||||
<Box className={styles.inputBlock}>
|
otherUserMarks={otherUserMarks}
|
||||||
<MuiFileInput
|
meta={meta}
|
||||||
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>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,12 @@ import {
|
|||||||
getCurrentUserFiles,
|
getCurrentUserFiles,
|
||||||
updateUsersAppData,
|
updateUsersAppData,
|
||||||
npubToHex,
|
npubToHex,
|
||||||
sendNotification
|
sendNotification,
|
||||||
|
generateEncryptionKey,
|
||||||
|
encryptArrayBuffer,
|
||||||
|
generateKeysFile,
|
||||||
|
ARRAY_BUFFER,
|
||||||
|
DEFLATE
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
import styles from './style.module.scss'
|
import styles from './style.module.scss'
|
||||||
import { useLocation, useParams } from 'react-router-dom'
|
import { useLocation, useParams } from 'react-router-dom'
|
||||||
@ -541,8 +546,114 @@ export const VerifyPage = () => {
|
|||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleMarkedExport = async () => {
|
// Handle errors during zip file generation
|
||||||
if (Object.entries(files).length === 0 || !meta || !usersPubkey) return
|
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)
|
const usersNpub = hexToNpub(usersPubkey)
|
||||||
if (
|
if (
|
||||||
@ -550,14 +661,14 @@ export const VerifyPage = () => {
|
|||||||
!viewers.includes(usersNpub) &&
|
!viewers.includes(usersNpub) &&
|
||||||
submittedBy !== usersNpub
|
submittedBy !== usersNpub
|
||||||
) {
|
) {
|
||||||
return
|
return Promise.resolve(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
setLoadingSpinnerDesc('Signing nostr event')
|
setLoadingSpinnerDesc('Signing nostr event')
|
||||||
|
|
||||||
const prevSig = getLastSignersSig(meta, signers)
|
const prevSig = getLastSignersSig(meta, signers)
|
||||||
if (!prevSig) return
|
if (!prevSig) return Promise.resolve(null)
|
||||||
|
|
||||||
const signedEvent = await signEventForMetaFile(
|
const signedEvent = await signEventForMetaFile(
|
||||||
JSON.stringify({ prevSig }),
|
JSON.stringify({ prevSig }),
|
||||||
@ -565,7 +676,7 @@ export const VerifyPage = () => {
|
|||||||
setIsLoading
|
setIsLoading
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!signedEvent) return
|
if (!signedEvent) return Promise.resolve(null)
|
||||||
|
|
||||||
const exportSignature = JSON.stringify(signedEvent, null, 2)
|
const exportSignature = JSON.stringify(signedEvent, null, 2)
|
||||||
const updatedMeta = { ...meta, exportSignature }
|
const updatedMeta = { ...meta, exportSignature }
|
||||||
@ -576,8 +687,8 @@ export const VerifyPage = () => {
|
|||||||
|
|
||||||
const arrayBuffer = await zip
|
const arrayBuffer = await zip
|
||||||
.generateAsync({
|
.generateAsync({
|
||||||
type: 'arraybuffer',
|
type: ARRAY_BUFFER,
|
||||||
compression: 'DEFLATE',
|
compression: DEFLATE,
|
||||||
compressionOptions: {
|
compressionOptions: {
|
||||||
level: 6
|
level: 6
|
||||||
}
|
}
|
||||||
@ -589,12 +700,9 @@ export const VerifyPage = () => {
|
|||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!arrayBuffer) return
|
if (!arrayBuffer) return Promise.resolve(null)
|
||||||
|
|
||||||
const blob = new Blob([arrayBuffer])
|
return Promise.resolve(arrayBuffer)
|
||||||
saveAs(blob, `exported-${unixNow()}.sigit.zip`)
|
|
||||||
|
|
||||||
setIsLoading(false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -640,8 +748,8 @@ export const VerifyPage = () => {
|
|||||||
)}
|
)}
|
||||||
currentFile={currentFile}
|
currentFile={currentFile}
|
||||||
setCurrentFile={setCurrentFile}
|
setCurrentFile={setCurrentFile}
|
||||||
handleDownload={handleMarkedExport}
|
handleExport={handleExport}
|
||||||
downloadLabel="Download Sigit"
|
handleEncryptedExport={handleEncryptedExport}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -120,5 +120,5 @@ export const SIGNATURE_PAD_OPTIONS = {
|
|||||||
|
|
||||||
export const SIGNATURE_PAD_SIZE = {
|
export const SIGNATURE_PAD_SIZE = {
|
||||||
width: 300,
|
width: 300,
|
||||||
height: 300
|
height: 150
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user