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

Merged
s merged 5 commits from issue-73 into staging 2024-05-28 10:27:40 +00:00
4 changed files with 261 additions and 92 deletions
Showing only changes of commit c3c9bf772d - Show all commits

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={{
s marked this conversation as resolved
Review

it would be nice to move css logic into separate file

it would be nice to move css logic into separate file
Review

it would be nice to move css logic into separate file

This is a simple utility component, I think it's fine to add inline style in this case. Otherwise, I'll create a folder and separate .scss file just for 1 class

> it would be nice to move css logic into separate file This is a simple utility component, I think it's fine to add inline style in this case. Otherwise, I'll create a folder and separate .scss file just for 1 class
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 { HTML5Backend } from 'react-dnd-html5-backend'
import type { Identifier, XYCoord } from 'dnd-core' import type { Identifier, XYCoord } from 'dnd-core'
import { useDrag, useDrop } from 'react-dnd' import { useDrag, useDrop } from 'react-dnd'
import saveAs from 'file-saver'
import CopyModal from '../../components/copyModal'
export const CreatePage = () => { export const CreatePage = () => {
const navigate = useNavigate() const navigate = useNavigate()
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
const [openCopyModal, setOpenCopyModel] = useState(false)
const [textToCopy, setTextToCopy] = useState('')
const [authUrl, setAuthUrl] = useState<string>() const [authUrl, setAuthUrl] = useState<string>()
@ -329,10 +333,12 @@ export const CreatePage = () => {
const encryptedArrayBuffer = await encryptArrayBuffer( const encryptedArrayBuffer = await encryptArrayBuffer(
arraybuffer, arraybuffer,
encryptionKey encryptionKey
) ).finally(() => setIsLoading(false))
const blob = new Blob([encryptedArrayBuffer]) const blob = new Blob([encryptedArrayBuffer])
if (navigator.onLine) {
setIsLoading(true)
setLoadingSpinnerDesc('Uploading zip file to file storage.') setLoadingSpinnerDesc('Uploading zip file to file storage.')
const fileUrl = await uploadToFileStorage(blob, nostrController) const fileUrl = await uploadToFileStorage(blob, nostrController)
.then((url) => { .then((url) => {
@ -381,6 +387,11 @@ export const CreatePage = () => {
fileUrl fileUrl
)}&key=${encodeURIComponent(encryptionKey)}` )}&key=${encodeURIComponent(encryptionKey)}`
) )
} else {
saveAs(blob, 'request.sigit')
setTextToCopy(encryptionKey)
setOpenCopyModel(true)
}
} }
if (authUrl) { if (authUrl) {
@ -471,6 +482,15 @@ export const CreatePage = () => {
</> </>
)} )}
</Box> </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, Download,
HourglassTop HourglassTop
} from '@mui/icons-material' } from '@mui/icons-material'
import CopyModal from '../../components/copyModal'
enum SignedStatus { enum SignedStatus {
Fully_Signed, Fully_Signed,
User_Is_Next_Signer, User_Is_Next_Signer,
@ -79,6 +79,8 @@ export const SignPage = () => {
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
const [openCopyModal, setOpenCopyModel] = useState(false)
const [textToCopy, setTextToCopy] = useState('')
const [meta, setMeta] = useState<Meta | null>(null) const [meta, setMeta] = useState<Meta | null>(null)
const [signedStatus, setSignedStatus] = useState<SignedStatus>() const [signedStatus, setSignedStatus] = useState<SignedStatus>()
@ -98,6 +100,9 @@ export const SignPage = () => {
const [nextSinger, setNextSinger] = useState<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 = useSelector((state: State) => state.auth.usersPubkey) const usersPubkey = useSelector((state: State) => state.auth.usersPubkey)
const [authUrl, setAuthUrl] = useState<string>() const [authUrl, setAuthUrl] = useState<string>()
@ -167,7 +172,20 @@ export const SignPage = () => {
// there's no signer just viewers. So its fully signed // there's no signer just viewers. So its fully signed
setSignedStatus(SignedStatus.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(() => { useEffect(() => {
const fileUrl = searchParams.get('file') const fileUrl = searchParams.get('file')
@ -236,6 +254,7 @@ export const SignPage = () => {
if (!zip) return if (!zip) return
setZip(zip) setZip(zip)
setDisplayInput(false)
setLoadingSpinnerDesc('Parsing meta.json') setLoadingSpinnerDesc('Parsing meta.json')
@ -414,6 +433,7 @@ export const SignPage = () => {
const blob = new Blob([encryptedArrayBuffer]) const blob = new Blob([encryptedArrayBuffer])
if (navigator.onLine) {
setLoadingSpinnerDesc('Uploading zip file to file storage.') setLoadingSpinnerDesc('Uploading zip file to file storage.')
const fileUrl = await uploadToFileStorage(blob, nostrController) const fileUrl = await uploadToFileStorage(blob, nostrController)
.then((url) => { .then((url) => {
@ -483,6 +503,9 @@ export const SignPage = () => {
file: fileUrl, file: fileUrl,
key: key key: key
}) })
} else {
handleDecryptedArrayBuffer(arrayBuffer).finally(() => setIsLoading(false))
}
} }
const handleExport = async () => { const handleExport = async () => {
@ -549,6 +572,37 @@ export const SignPage = () => {
navigate(appPrivateRoutes.verify) 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 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 * 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}> <Box className={styles.inputBlock}>
<MuiFileInput <MuiFileInput
placeholder="Select file" placeholder="Select file"
inputProps={{ accept: '.sigit' }}
value={selectedFile} value={selectedFile}
onChange={(value) => setSelectedFile(value)} onChange={(value) => setSelectedFile(value)}
/> />
@ -675,6 +730,7 @@ export const SignPage = () => {
nextSigner={nextSinger} nextSigner={nextSinger}
getPrevSignersSig={getPrevSignersSig} getPrevSignersSig={getPrevSignersSig}
/> />
{signedStatus === SignedStatus.Fully_Signed && ( {signedStatus === SignedStatus.Fully_Signed && (
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}> <Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
<Button onClick={handleExport} variant="contained"> <Button onClick={handleExport} variant="contained">
@ -690,9 +746,24 @@ export const SignPage = () => {
</Button> </Button>
</Box> </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> </Box>
<CopyModal
open={openCopyModal}
handleClose={() => setOpenCopyModel(false)}
title="Decryption key for Sigit file"
textToCopy={textToCopy}
/>
</> </>
) )
} }