feat: add the ability to create and sign while user is offline
This commit is contained in:
parent
a3c421d34a
commit
c3c9bf772d
27
src/components/copyModal.tsx
Normal file
27
src/components/copyModal.tsx
Normal 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
|
51
src/components/copyToClipboard.tsx
Normal file
51
src/components/copyToClipboard.tsx
Normal 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>
|
||||
)
|
||||
}
|
@ -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,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 = () => {
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
<CopyModal
|
||||
open={openCopyModal}
|
||||
handleClose={() => {
|
||||
setOpenCopyModel(false)
|
||||
navigate(appPrivateRoutes.sign)
|
||||
}}
|
||||
title="Decryption key for Sigit file"
|
||||
textToCopy={textToCopy}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -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,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 = () => {
|
||||
<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}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user