Offline flow separation #304
@ -44,7 +44,6 @@ import {
|
|||||||
generateKeysFile,
|
generateKeysFile,
|
||||||
getHash,
|
getHash,
|
||||||
hexToNpub,
|
hexToNpub,
|
||||||
isOnline,
|
|
||||||
unixNow,
|
unixNow,
|
||||||
npubToHex,
|
npubToHex,
|
||||||
queryNip05,
|
queryNip05,
|
||||||
@ -65,6 +64,7 @@ import { Mark } from '../../types/mark.ts'
|
|||||||
import { StickySideColumns } from '../../layouts/StickySideColumns.tsx'
|
import { StickySideColumns } from '../../layouts/StickySideColumns.tsx'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
import {
|
import {
|
||||||
|
faDownload,
|
||||||
faEllipsis,
|
faEllipsis,
|
||||||
faEye,
|
faEye,
|
||||||
faFile,
|
faFile,
|
||||||
@ -84,6 +84,7 @@ import _, { truncate } from 'lodash'
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { AvatarIconButton } from '../../components/UserAvatarIconButton'
|
import { AvatarIconButton } from '../../components/UserAvatarIconButton'
|
||||||
import { useImmer } from 'use-immer'
|
import { useImmer } from 'use-immer'
|
||||||
|
import { ButtonUnderline } from '../../components/ButtonUnderline/index.tsx'
|
||||||
|
|
||||||
type FoundUser = Event & { npub: string }
|
type FoundUser = Event & { npub: string }
|
||||||
|
|
||||||
@ -774,30 +775,6 @@ export const CreatePage = () => {
|
|||||||
.catch(handleUploadError)
|
.catch(handleUploadError)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Manage offline scenarios for signing or viewing the file
|
|
||||||
const handleOfflineFlow = async (
|
|
||||||
encryptedArrayBuffer: ArrayBuffer,
|
|
||||||
encryptionKey: string
|
|
||||||
) => {
|
|
||||||
const finalZipFile = await createFinalZipFile(
|
|
||||||
encryptedArrayBuffer,
|
|
||||||
encryptionKey
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!finalZipFile) {
|
|
||||||
setIsLoading(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
saveAs(finalZipFile, `request-${unixNow()}.sigit.zip`)
|
|
||||||
|
|
||||||
// If user is the next signer, we can navigate directly to sign page
|
|
||||||
if (signers[0].pubkey === usersPubkey) {
|
|
||||||
navigate(appPrivateRoutes.sign, { state: { uploadedZip: finalZipFile } })
|
|
||||||
}
|
|
||||||
setIsLoading(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const generateFilesZip = async (): Promise<ArrayBuffer | null> => {
|
const generateFilesZip = async (): Promise<ArrayBuffer | null> => {
|
||||||
const zip = new JSZip()
|
const zip = new JSZip()
|
||||||
selectedFiles.forEach((file) => {
|
selectedFiles.forEach((file) => {
|
||||||
@ -863,7 +840,7 @@ export const CreatePage = () => {
|
|||||||
return e.id
|
return e.id
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCreate = async () => {
|
const initCreation = async () => {
|
||||||
try {
|
try {
|
||||||
if (!validateInputs()) return
|
if (!validateInputs()) return
|
||||||
|
|
||||||
@ -875,132 +852,183 @@ export const CreatePage = () => {
|
|||||||
setLoadingSpinnerDesc('Generating encryption key')
|
setLoadingSpinnerDesc('Generating encryption key')
|
||||||
const encryptionKey = await generateEncryptionKey()
|
const encryptionKey = await generateEncryptionKey()
|
||||||
|
|
||||||
if (await isOnline()) {
|
setLoadingSpinnerDesc('Creating marks')
|
||||||
setLoadingSpinnerDesc('generating files.zip')
|
const markConfig = createMarks(fileHashes)
|
||||||
const arrayBuffer = await generateFilesZip()
|
|
||||||
if (!arrayBuffer) return
|
|
||||||
|
|
||||||
setLoadingSpinnerDesc('Encrypting files.zip')
|
return {
|
||||||
const encryptedArrayBuffer = await encryptZipFile(
|
encryptionKey,
|
||||||
arrayBuffer,
|
markConfig,
|
||||||
encryptionKey
|
fileHashes
|
||||||
)
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
toast.error(error.message)
|
||||||
|
}
|
||||||
|
console.error(error)
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const markConfig = createMarks(fileHashes)
|
const handleCreate = async () => {
|
||||||
|
try {
|
||||||
|
const result = await initCreation()
|
||||||
|
if (!result) return
|
||||||
|
|
||||||
setLoadingSpinnerDesc('Uploading files.zip to file storage')
|
const { encryptionKey, markConfig, fileHashes } = result
|
||||||
const fileUrl = await uploadFile(encryptedArrayBuffer)
|
|
||||||
if (!fileUrl) return
|
|
||||||
|
|
||||||
setLoadingSpinnerDesc('Generating create signature')
|
setLoadingSpinnerDesc('generating files.zip')
|
||||||
const createSignature = await generateCreateSignature(
|
const arrayBuffer = await generateFilesZip()
|
||||||
markConfig,
|
if (!arrayBuffer) return
|
||||||
fileHashes,
|
|
||||||
fileUrl
|
|
||||||
)
|
|
||||||
if (!createSignature) return
|
|
||||||
|
|
||||||
setLoadingSpinnerDesc('Generating keys for decryption')
|
setLoadingSpinnerDesc('Encrypting files.zip')
|
||||||
|
const encryptedArrayBuffer = await encryptZipFile(
|
||||||
|
arrayBuffer,
|
||||||
|
encryptionKey
|
||||||
|
)
|
||||||
|
|
||||||
// generate key pairs for decryption
|
setLoadingSpinnerDesc('Uploading files.zip to file storage')
|
||||||
const pubkeys = users.map((user) => user.pubkey)
|
const fileUrl = await uploadFile(encryptedArrayBuffer)
|
||||||
// also add creator in the list
|
if (!fileUrl) return
|
||||||
if (pubkeys.includes(usersPubkey!)) {
|
|
||||||
pubkeys.push(usersPubkey!)
|
|
||||||
}
|
|
||||||
|
|
||||||
const keys = await generateKeys(pubkeys, encryptionKey)
|
setLoadingSpinnerDesc('Generating create signature')
|
||||||
if (!keys) return
|
const createSignature = await generateCreateSignature(
|
||||||
|
markConfig,
|
||||||
|
fileHashes,
|
||||||
|
fileUrl
|
||||||
|
)
|
||||||
|
if (!createSignature) return
|
||||||
|
|
||||||
setLoadingSpinnerDesc('Generating an open timestamp.')
|
setLoadingSpinnerDesc('Generating keys for decryption')
|
||||||
|
|
||||||
const timestamp = await generateTimestamp(
|
// generate key pairs for decryption
|
||||||
extractNostrId(createSignature)
|
const pubkeys = users.map((user) => user.pubkey)
|
||||||
)
|
// also add creator in the list
|
||||||
|
if (pubkeys.includes(usersPubkey!)) {
|
||||||
|
pubkeys.push(usersPubkey!)
|
||||||
|
}
|
||||||
|
|
||||||
const meta: Meta = {
|
const keys = await generateKeys(pubkeys, encryptionKey)
|
||||||
createSignature,
|
if (!keys) return
|
||||||
keys,
|
|
||||||
modifiedAt: unixNow(),
|
|
||||||
docSignatures: {}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (timestamp) {
|
setLoadingSpinnerDesc('Generating an open timestamp.')
|
||||||
meta.timestamps = [timestamp]
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoadingSpinnerDesc('Updating user app data')
|
const timestamp = await generateTimestamp(extractNostrId(createSignature))
|
||||||
|
|
||||||
const event = await updateUsersAppData(meta)
|
const meta: Meta = {
|
||||||
if (!event) return
|
createSignature,
|
||||||
|
keys,
|
||||||
|
modifiedAt: unixNow(),
|
||||||
|
docSignatures: {}
|
||||||
|
}
|
||||||
|
|
||||||
const metaUrl = await uploadMetaToFileStorage(meta, encryptionKey)
|
if (timestamp) {
|
||||||
|
meta.timestamps = [timestamp]
|
||||||
|
}
|
||||||
|
|
||||||
setLoadingSpinnerDesc('Sending notifications to counterparties')
|
setLoadingSpinnerDesc('Updating user app data')
|
||||||
const promises = sendNotifications({
|
|
||||||
metaUrl,
|
const event = await updateUsersAppData(meta)
|
||||||
keys: meta.keys
|
if (!event) return
|
||||||
|
|
||||||
|
const metaUrl = await uploadMetaToFileStorage(meta, encryptionKey)
|
||||||
|
|
||||||
|
setLoadingSpinnerDesc('Sending notifications to counterparties')
|
||||||
|
const promises = sendNotifications({
|
||||||
|
metaUrl,
|
||||||
|
keys: meta.keys
|
||||||
|
})
|
||||||
|
|
||||||
|
await Promise.all(promises)
|
||||||
|
.then(() => {
|
||||||
|
toast.success('Notifications sent successfully')
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error('Failed to publish notifications')
|
||||||
})
|
})
|
||||||
|
|
||||||
await Promise.all(promises)
|
const isFirstSigner = signers[0].pubkey === usersPubkey
|
||||||
.then(() => {
|
|
||||||
toast.success('Notifications sent successfully')
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error('Failed to publish notifications')
|
|
||||||
})
|
|
||||||
|
|
||||||
const isFirstSigner = signers[0].pubkey === usersPubkey
|
if (isFirstSigner) {
|
||||||
|
navigate(appPrivateRoutes.sign, { state: { meta } })
|
||||||
if (isFirstSigner) {
|
|
||||||
navigate(appPrivateRoutes.sign, { state: { meta } })
|
|
||||||
} else {
|
|
||||||
const createSignatureJson = JSON.parse(createSignature)
|
|
||||||
navigate(`${appPublicRoutes.verify}/${createSignatureJson.id}`)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
const zip = new JSZip()
|
const createSignatureJson = JSON.parse(createSignature)
|
||||||
|
navigate(`${appPublicRoutes.verify}/${createSignatureJson.id}`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
toast.error(error.message)
|
||||||
|
}
|
||||||
|
console.error(error)
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
selectedFiles.forEach((file) => {
|
const handleCreateOffline = async () => {
|
||||||
zip.file(`files/${file.name}`, file)
|
try {
|
||||||
|
const result = await initCreation()
|
||||||
|
if (!result) return
|
||||||
|
|
||||||
|
const { encryptionKey, markConfig, fileHashes } = result
|
||||||
|
|
||||||
|
const zip = new JSZip()
|
||||||
|
|
||||||
|
selectedFiles.forEach((file) => {
|
||||||
|
zip.file(`files/${file.name}`, file)
|
||||||
|
})
|
||||||
|
|
||||||
|
setLoadingSpinnerDesc('Generating create signature')
|
||||||
|
const createSignature = await generateCreateSignature(
|
||||||
|
markConfig,
|
||||||
|
fileHashes,
|
||||||
|
''
|
||||||
|
)
|
||||||
|
if (!createSignature) return
|
||||||
|
|
||||||
|
const meta: Meta = {
|
||||||
|
createSignature,
|
||||||
|
modifiedAt: unixNow(),
|
||||||
|
docSignatures: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add meta to zip
|
||||||
|
try {
|
||||||
|
const stringifiedMeta = JSON.stringify(meta, null, 2)
|
||||||
|
zip.file('meta.json', stringifiedMeta)
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
toast.error('An error occurred in converting meta json to string')
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const arrayBuffer = await generateZipFile(zip)
|
||||||
|
if (!arrayBuffer) return
|
||||||
|
|
||||||
|
setLoadingSpinnerDesc('Encrypting zip file')
|
||||||
|
const encryptedArrayBuffer = await encryptZipFile(
|
||||||
|
arrayBuffer,
|
||||||
|
encryptionKey
|
||||||
|
)
|
||||||
|
|
||||||
|
const finalZipFile = await createFinalZipFile(
|
||||||
|
encryptedArrayBuffer,
|
||||||
|
encryptionKey
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!finalZipFile) {
|
||||||
|
setIsLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
saveAs(finalZipFile, `request-${unixNow()}.sigit.zip`)
|
||||||
|
|
||||||
|
// If user is the next signer, we can navigate directly to sign page
|
||||||
|
if (signers[0].pubkey === usersPubkey) {
|
||||||
|
navigate(appPrivateRoutes.sign, {
|
||||||
|
state: { uploadedZip: finalZipFile }
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
const markConfig = createMarks(fileHashes)
|
navigate(appPrivateRoutes.homePage)
|
||||||
|
|
||||||
setLoadingSpinnerDesc('Generating create signature')
|
|
||||||
const createSignature = await generateCreateSignature(
|
|
||||||
markConfig,
|
|
||||||
fileHashes,
|
|
||||||
''
|
|
||||||
)
|
|
||||||
if (!createSignature) return
|
|
||||||
|
|
||||||
const meta: Meta = {
|
|
||||||
createSignature,
|
|
||||||
modifiedAt: unixNow(),
|
|
||||||
docSignatures: {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// add meta to zip
|
|
||||||
try {
|
|
||||||
const stringifiedMeta = JSON.stringify(meta, null, 2)
|
|
||||||
zip.file('meta.json', stringifiedMeta)
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err)
|
|
||||||
toast.error('An error occurred in converting meta json to string')
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const arrayBuffer = await generateZipFile(zip)
|
|
||||||
if (!arrayBuffer) return
|
|
||||||
|
|
||||||
setLoadingSpinnerDesc('Encrypting zip file')
|
|
||||||
const encryptedArrayBuffer = await encryptZipFile(
|
|
||||||
arrayBuffer,
|
|
||||||
encryptionKey
|
|
||||||
)
|
|
||||||
|
|
||||||
await handleOfflineFlow(encryptedArrayBuffer, encryptionKey)
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
@ -1258,6 +1286,11 @@ export const CreatePage = () => {
|
|||||||
Publish
|
Publish
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
<ButtonUnderline onClick={handleCreateOffline}>
|
||||||
|
<FontAwesomeIcon icon={faDownload} />
|
||||||
|
Create and export locally
|
||||||
|
</ButtonUnderline>
|
||||||
|
|
||||||
{!!error && (
|
{!!error && (
|
||||||
<FormHelperText error={!!error}>{error}</FormHelperText>
|
<FormHelperText error={!!error}>{error}</FormHelperText>
|
||||||
)}
|
)}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user