From c3c9bf772d5e10ba6bf55d39e8f21ba261828b60 Mon Sep 17 00:00:00 2001 From: SwiftHawk Date: Tue, 28 May 2024 15:10:06 +0500 Subject: [PATCH] feat: add the ability to create and sign while user is offline --- src/components/copyModal.tsx | 27 +++++ src/components/copyToClipboard.tsx | 51 ++++++++ src/pages/create/index.tsx | 96 ++++++++++------ src/pages/sign/index.tsx | 179 ++++++++++++++++++++--------- 4 files changed, 261 insertions(+), 92 deletions(-) create mode 100644 src/components/copyModal.tsx create mode 100644 src/components/copyToClipboard.tsx diff --git a/src/components/copyModal.tsx b/src/components/copyModal.tsx new file mode 100644 index 0000000..858b424 --- /dev/null +++ b/src/components/copyModal.tsx @@ -0,0 +1,27 @@ +import { Dialog, DialogContent, DialogTitle } from '@mui/material' +import { CopyToClipboard } from './copyToClipboard' + +interface CopyModalProps { + open: boolean + handleClose: () => void + title: string + textToCopy: string +} + +export const CopyModal = ({ + open, + handleClose, + title, + textToCopy +}: CopyModalProps) => { + return ( + + {title} + + + + + ) +} + +export default CopyModal diff --git a/src/components/copyToClipboard.tsx b/src/components/copyToClipboard.tsx new file mode 100644 index 0000000..79c01c4 --- /dev/null +++ b/src/components/copyToClipboard.tsx @@ -0,0 +1,51 @@ +import { ContentCopy } from '@mui/icons-material/' +import { Box, IconButton, Typography } from '@mui/material' +import { toast } from 'react-toastify' + +type Props = { + textToCopy: string +} + +export const CopyToClipboard = ({ textToCopy }: Props) => { + const handleCopyClick = () => { + navigator.clipboard.writeText(textToCopy) + toast.success('Copied to clipboard', { + autoClose: 1000, + hideProgressBar: true + }) + } + + return ( + + { + e.stopPropagation() + handleCopyClick() + }} + component="label" + sx={{ + flex: '1', + overflow: 'auto', + whiteSpace: 'nowrap', + cursor: 'pointer' + }} + > + {textToCopy} + + { + e.stopPropagation() + handleCopyClick() + }} + > + + + + ) +} diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx index 9acb138..4289525 100644 --- a/src/pages/create/index.tsx +++ b/src/pages/create/index.tsx @@ -47,11 +47,15 @@ import { DndProvider } from 'react-dnd' import { HTML5Backend } from 'react-dnd-html5-backend' import type { Identifier, XYCoord } from 'dnd-core' import { useDrag, useDrop } from 'react-dnd' +import saveAs from 'file-saver' +import CopyModal from '../../components/copyModal' export const CreatePage = () => { const navigate = useNavigate() const [isLoading, setIsLoading] = useState(false) const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') + const [openCopyModal, setOpenCopyModel] = useState(false) + const [textToCopy, setTextToCopy] = useState('') const [authUrl, setAuthUrl] = useState() @@ -329,58 +333,65 @@ export const CreatePage = () => { const encryptedArrayBuffer = await encryptArrayBuffer( arraybuffer, encryptionKey - ) + ).finally(() => setIsLoading(false)) const blob = new Blob([encryptedArrayBuffer]) - setLoadingSpinnerDesc('Uploading zip file to file storage.') - const fileUrl = await uploadToFileStorage(blob, nostrController) - .then((url) => { - toast.success('zip file uploaded to file storage') - return url - }) - .catch((err) => { - console.log('err in upload:>> ', err) - setIsLoading(false) - toast.error(err.message || 'Error occurred in uploading zip file') - return null - }) + if (navigator.onLine) { + setIsLoading(true) + setLoadingSpinnerDesc('Uploading zip file to file storage.') + const fileUrl = await uploadToFileStorage(blob, nostrController) + .then((url) => { + toast.success('zip file uploaded to file storage') + return url + }) + .catch((err) => { + console.log('err in upload:>> ', err) + setIsLoading(false) + toast.error(err.message || 'Error occurred in uploading zip file') + return null + }) - if (!fileUrl) return + if (!fileUrl) return - setLoadingSpinnerDesc('Sending DM to signers/viewers') + setLoadingSpinnerDesc('Sending DM to signers/viewers') - // send DM to first signer if exists - if (signers.length > 0) { - await sendDM( - fileUrl, - encryptionKey, - signers[0].pubkey, - nostrController, - true, - setAuthUrl - ) - } else { - // send DM to all viewers if no signer - for (const viewer of viewers) { - // todo: execute in parallel + // send DM to first signer if exists + if (signers.length > 0) { await sendDM( fileUrl, encryptionKey, - viewer.pubkey, + signers[0].pubkey, nostrController, - false, + true, setAuthUrl ) + } else { + // send DM to all viewers if no signer + for (const viewer of viewers) { + // todo: execute in parallel + await sendDM( + fileUrl, + encryptionKey, + viewer.pubkey, + nostrController, + false, + setAuthUrl + ) + } } - } - setIsLoading(false) + setIsLoading(false) - navigate( - `${appPrivateRoutes.sign}?file=${encodeURIComponent( - fileUrl - )}&key=${encodeURIComponent(encryptionKey)}` - ) + navigate( + `${appPrivateRoutes.sign}?file=${encodeURIComponent( + fileUrl + )}&key=${encodeURIComponent(encryptionKey)}` + ) + } else { + saveAs(blob, 'request.sigit') + setTextToCopy(encryptionKey) + setOpenCopyModel(true) + } } if (authUrl) { @@ -471,6 +482,15 @@ export const CreatePage = () => { )} + { + setOpenCopyModel(false) + navigate(appPrivateRoutes.sign) + }} + title="Decryption key for Sigit file" + textToCopy={textToCopy} + /> ) } diff --git a/src/pages/sign/index.tsx b/src/pages/sign/index.tsx index aa26270..269264f 100644 --- a/src/pages/sign/index.tsx +++ b/src/pages/sign/index.tsx @@ -59,7 +59,7 @@ import { Download, HourglassTop } from '@mui/icons-material' - +import CopyModal from '../../components/copyModal' enum SignedStatus { Fully_Signed, User_Is_Next_Signer, @@ -79,6 +79,8 @@ export const SignPage = () => { const [isLoading, setIsLoading] = useState(true) const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') + const [openCopyModal, setOpenCopyModel] = useState(false) + const [textToCopy, setTextToCopy] = useState('') const [meta, setMeta] = useState(null) const [signedStatus, setSignedStatus] = useState() @@ -98,6 +100,9 @@ export const SignPage = () => { 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 = useSelector((state: State) => state.auth.usersPubkey) const [authUrl, setAuthUrl] = useState() @@ -167,7 +172,20 @@ export const SignPage = () => { // there's no signer just viewers. So its fully signed setSignedStatus(SignedStatus.Fully_Signed) } - }, [signers, signedBy, usersPubkey]) + + // 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 fileUrl = searchParams.get('file') @@ -236,6 +254,7 @@ export const SignPage = () => { if (!zip) return setZip(zip) + setDisplayInput(false) setLoadingSpinnerDesc('Parsing meta.json') @@ -414,75 +433,79 @@ export const SignPage = () => { const blob = new Blob([encryptedArrayBuffer]) - setLoadingSpinnerDesc('Uploading zip file to file storage.') - const fileUrl = await uploadToFileStorage(blob, nostrController) - .then((url) => { - toast.success('zip file uploaded to file storage') - return url - }) - .catch((err) => { - console.log('err in upload:>> ', err) - setIsLoading(false) - toast.error(err.message || 'Error occurred in uploading zip file') - return null - }) + if (navigator.onLine) { + setLoadingSpinnerDesc('Uploading zip file to file storage.') + const fileUrl = await uploadToFileStorage(blob, nostrController) + .then((url) => { + toast.success('zip file uploaded to file storage') + return url + }) + .catch((err) => { + console.log('err in upload:>> ', err) + setIsLoading(false) + toast.error(err.message || 'Error occurred in uploading zip file') + return null + }) - if (!fileUrl) return + if (!fileUrl) return - // check if the current user is the last signer - const usersNpub = hexToNpub(usersPubkey!) - const lastSignerIndex = signers.length - 1 - const signerIndex = signers.indexOf(usersNpub) - const isLastSigner = signerIndex === lastSignerIndex + // check if the current user is the last signer + const usersNpub = hexToNpub(usersPubkey!) + const lastSignerIndex = signers.length - 1 + const signerIndex = signers.indexOf(usersNpub) + const isLastSigner = signerIndex === lastSignerIndex - // if current user is the last signer, then send DMs to all signers and viewers - if (isLastSigner) { - const userSet = new Set<`npub1${string}`>() + // if current user is the last signer, then send DMs to all signers and viewers + if (isLastSigner) { + const userSet = new Set<`npub1${string}`>() - if (submittedBy) { - userSet.add(hexToNpub(submittedBy)) - } + if (submittedBy) { + userSet.add(hexToNpub(submittedBy)) + } - signers.forEach((signer) => { - userSet.add(signer) - }) + signers.forEach((signer) => { + userSet.add(signer) + }) - viewers.forEach((viewer) => { - userSet.add(viewer) - }) + viewers.forEach((viewer) => { + userSet.add(viewer) + }) - const users = Array.from(userSet) + const users = Array.from(userSet) - for (const user of users) { - // todo: execute in parallel + for (const user of users) { + // todo: execute in parallel + await sendDM( + fileUrl, + key, + npubToHex(user)!, + nostrController, + false, + setAuthUrl + ) + } + } else { + const nextSigner = signers[signerIndex + 1] await sendDM( fileUrl, key, - npubToHex(user)!, + npubToHex(nextSigner)!, nostrController, - false, + true, setAuthUrl ) } + + setIsLoading(false) + + // update search params with updated file url and encryption key + setSearchParams({ + file: fileUrl, + key: key + }) } else { - const nextSigner = signers[signerIndex + 1] - await sendDM( - fileUrl, - key, - npubToHex(nextSigner)!, - nostrController, - true, - setAuthUrl - ) + handleDecryptedArrayBuffer(arrayBuffer).finally(() => setIsLoading(false)) } - - setIsLoading(false) - - // update search params with updated file url and encryption key - setSearchParams({ - file: fileUrl, - key: key - }) } const handleExport = async () => { @@ -549,6 +572,37 @@ export const SignPage = () => { navigate(appPrivateRoutes.verify) } + const handleExportSigit = async () => { + if (!zip) return + + 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 blob = new Blob([encryptedArrayBuffer]) + saveAs(blob, 'exported.sigit') + + setTextToCopy(key) + setOpenCopyModel(true) + } + /** * This function accepts an npub of a signer and return the signature of its previous signer. * This prevSig will be used in the content of the provided signer's signedEvent @@ -637,6 +691,7 @@ export const SignPage = () => { setSelectedFile(value)} /> @@ -675,6 +730,7 @@ export const SignPage = () => { nextSigner={nextSinger} getPrevSignersSig={getPrevSignersSig} /> + {signedStatus === SignedStatus.Fully_Signed && ( )} + + {isSignerOrCreator && + signedStatus === SignedStatus.User_Is_Not_Next_Signer && ( + + + + )} )} + setOpenCopyModel(false)} + title="Decryption key for Sigit file" + textToCopy={textToCopy} + /> ) }