feat: add the ability to create and sign while user is offline

This commit is contained in:
SwiftHawk 2024-05-28 15:10:06 +05:00
parent a3c421d34a
commit c3c9bf772d
4 changed files with 261 additions and 92 deletions

View File

@ -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 (
<Dialog open={open} onClose={handleClose} maxWidth="xs">
<DialogTitle>{title}</DialogTitle>
<DialogContent>
<CopyToClipboard textToCopy={textToCopy} />
</DialogContent>
</Dialog>
)
}
export default CopyModal

View File

@ -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 (
<Box
sx={{
display: 'flex',
alignItems: 'center',
width: '100%'
}}
>
<Typography
onClick={(e) => {
e.stopPropagation()
handleCopyClick()
}}
component="label"
sx={{
flex: '1',
overflow: 'auto',
whiteSpace: 'nowrap',
cursor: 'pointer'
}}
>
{textToCopy}
</Typography>
<IconButton
onClick={(e) => {
e.stopPropagation()
handleCopyClick()
}}
>
<ContentCopy />
</IconButton>
</Box>
)
}

View File

@ -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<string>()
@ -329,10 +333,12 @@ export const CreatePage = () => {
const encryptedArrayBuffer = await encryptArrayBuffer(
arraybuffer,
encryptionKey
)
).finally(() => setIsLoading(false))
const blob = new Blob([encryptedArrayBuffer])
if (navigator.onLine) {
setIsLoading(true)
setLoadingSpinnerDesc('Uploading zip file to file storage.')
const fileUrl = await uploadToFileStorage(blob, nostrController)
.then((url) => {
@ -381,6 +387,11 @@ export const CreatePage = () => {
fileUrl
)}&key=${encodeURIComponent(encryptionKey)}`
)
} else {
saveAs(blob, 'request.sigit')
setTextToCopy(encryptionKey)
setOpenCopyModel(true)
}
}
if (authUrl) {
@ -471,6 +482,15 @@ export const CreatePage = () => {
</>
)}
</Box>
<CopyModal
open={openCopyModal}
handleClose={() => {
setOpenCopyModel(false)
navigate(appPrivateRoutes.sign)
}}
title="Decryption key for Sigit file"
textToCopy={textToCopy}
/>
</>
)
}

View File

@ -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<Meta | null>(null)
const [signedStatus, setSignedStatus] = useState<SignedStatus>()
@ -98,6 +100,9 @@ export const SignPage = () => {
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 = useSelector((state: State) => state.auth.usersPubkey)
const [authUrl, setAuthUrl] = useState<string>()
@ -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,6 +433,7 @@ export const SignPage = () => {
const blob = new Blob([encryptedArrayBuffer])
if (navigator.onLine) {
setLoadingSpinnerDesc('Uploading zip file to file storage.')
const fileUrl = await uploadToFileStorage(blob, nostrController)
.then((url) => {
@ -483,6 +503,9 @@ export const SignPage = () => {
file: fileUrl,
key: key
})
} else {
handleDecryptedArrayBuffer(arrayBuffer).finally(() => setIsLoading(false))
}
}
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 = () => {
<Box className={styles.inputBlock}>
<MuiFileInput
placeholder="Select file"
inputProps={{ accept: '.sigit' }}
value={selectedFile}
onChange={(value) => setSelectedFile(value)}
/>
@ -675,6 +730,7 @@ export const SignPage = () => {
nextSigner={nextSinger}
getPrevSignersSig={getPrevSignersSig}
/>
{signedStatus === SignedStatus.Fully_Signed && (
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
<Button onClick={handleExport} variant="contained">
@ -690,9 +746,24 @@ export const SignPage = () => {
</Button>
</Box>
)}
{isSignerOrCreator &&
signedStatus === SignedStatus.User_Is_Not_Next_Signer && (
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
<Button onClick={handleExportSigit} variant="contained">
Export Sigit
</Button>
</Box>
)}
</>
)}
</Box>
<CopyModal
open={openCopyModal}
handleClose={() => setOpenCopyModel(false)}
title="Decryption key for Sigit file"
textToCopy={textToCopy}
/>
</>
)
}