Skip to content

Commit

Permalink
feat: refactor secret key modals to use same hook and common secret k…
Browse files Browse the repository at this point in the history
…ey form modal component for easier drag drop implementation
  • Loading branch information
kevin9foong committed Aug 7, 2024
1 parent c898a2c commit 6ec94cf
Show file tree
Hide file tree
Showing 4 changed files with 337 additions and 439 deletions.
Original file line number Diff line number Diff line change
@@ -1,20 +1,6 @@
import { useCallback, useRef } from 'react'
import { useForm } from 'react-hook-form'
import { BiRightArrowAlt, BiUpload } from 'react-icons/bi'
import { useCallback } from 'react'
import { useQueryClient } from 'react-query'
import {
Container,
FormControl,
FormLabel,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalHeader,
Stack,
useBreakpointValue,
UseDisclosureReturn,
} from '@chakra-ui/react'
import { UseDisclosureReturn } from '@chakra-ui/react'
import Papa from 'papaparse'

import {
Expand All @@ -23,15 +9,10 @@ import {
} from '~shared/utils/crypto'

import { useToast } from '~hooks/useToast'
import { isKeypairValid, SECRET_KEY_REGEX } from '~utils/secretKeyValidation'
import Button from '~components/Button'
import { downloadFile } from '~components/Field/Attachment/utils/downloadFile'
import { FormErrorMessage } from '~components/FormControl/FormErrorMessage/FormErrorMessage'
import IconButton from '~components/IconButton'
import Input from '~components/Input'

import { fetchAdminFormEncryptedWhitelistedSubmitterIds } from '../../queries'
import { FormActivationSvg } from '../FormActivationSvg'
import { SecretKeyFormModal } from '../SecretKeyFormModal'

export interface SecretKeyDownloadWhitelistFileModalProps
extends Pick<UseDisclosureReturn, 'onClose' | 'isOpen'> {
Expand All @@ -40,45 +21,36 @@ export interface SecretKeyDownloadWhitelistFileModalProps
downloadFileName: string
}

const SECRET_KEY_NAME = 'secretKey'

interface SecretKeyFormInputs {
[SECRET_KEY_NAME]: string
}

const useSecretKeyWhitelistFileModal = ({
publicKey,
export const SecretKeyDownloadWhitelistFileModal = ({
onClose,
formId,
isOpen,
publicKey,
downloadFileName,
}: Pick<
SecretKeyDownloadWhitelistFileModalProps,
'publicKey' | 'onClose' | 'formId' | 'downloadFileName'
>) => {
const queryClient = useQueryClient()
formId,
}: SecretKeyDownloadWhitelistFileModalProps) => {
const toast = useToast({ status: 'success', isClosable: true })
const errorToast = useToast({ status: 'danger', isClosable: true })
const {
formState: { errors },
setError,
setValue,
register,
handleSubmit,
reset,
watch,
} = useForm<SecretKeyFormInputs>()

const handleError = useCallback(
(error: Error) => {
const queryClient = useQueryClient()

const toastErrorMessage = useCallback(
(errorMessage: string) => {
errorToast.closeAll()
errorToast({
description: error.message,
description: errorMessage,
})
},
[errorToast],
)

const fileUploadRef = useRef<HTMLInputElement | null>(null)
const toastSuccessMessage = useCallback(
(message) => {
toast.closeAll()
toast({
description: message,
})
},
[toast],
)

const decryptSubmitterIds = useCallback(
(
Expand All @@ -89,23 +61,8 @@ const useSecretKeyWhitelistFileModal = ({
},
[],
)

const verifyKeyPairAndDownloadWhitelistFile = handleSubmit(
const handleWhitelistCsvDownload = useCallback(
({ secretKey }) => {
const isValid = isKeypairValid(publicKey, secretKey)

if (!isValid) {
return setError(
SECRET_KEY_NAME,
{
type: 'manual',
message: 'The secret key provided is invalid',
},
{
shouldFocus: true,
},
)
}
fetchAdminFormEncryptedWhitelistedSubmitterIds(formId, queryClient)
.then((data) => {
const { encryptedWhitelistedSubmitterIds } = data
Expand Down Expand Up @@ -143,161 +100,39 @@ const useSecretKeyWhitelistFileModal = ({
type: 'text/csv',
})
downloadFile(csvFile)

handleFormClose()
toast.closeAll()
toast({
description: 'Whitelist setting file downloaded successfully',
})
onClose()
toastSuccessMessage(
'Whitelist setting file downloaded successfully',
)
} else {
errorToast.closeAll()
errorToast({
description: 'Whitelist settings could not be decrypted',
})
toastErrorMessage('Whitelist settings could not be decrypted')
}
})
.catch(handleError)
},
)

const readValidateSetSecretKeyFormFieldFromFile = useCallback(
({ target }: React.ChangeEvent<HTMLInputElement>) => {
const file = target.files?.[0]
// Reset file input so the same file selected will trigger this onChange
// function.
if (fileUploadRef.current) {
fileUploadRef.current.value = ''
}

if (!file) return

const reader = new FileReader()
reader.onload = async (e) => {
if (!e.target) return
const text = e.target.result?.toString()

if (!text || !SECRET_KEY_REGEX.test(text)) {
return setError(
SECRET_KEY_NAME,
{
type: 'invalidFile',
message: 'Selected file seems to be invalid',
},
{ shouldFocus: true },
)
}

setValue(SECRET_KEY_NAME, text, { shouldValidate: true })
}
reader.readAsText(file)
.catch((error: Error) => {
toastErrorMessage(error.message)
})
},
[setError, setValue],
[
formId,
queryClient,
decryptSubmitterIds,
downloadFileName,
onClose,
toastSuccessMessage,
toastErrorMessage,
],
)

// Reset form before closing.
const handleFormClose = useCallback(() => {
reset()
return onClose()
}, [onClose, reset])

const isSecretKeyUploaded = !!watch(SECRET_KEY_NAME)

return {
register,
errors,
fileUploadRef,
handleSubmit: verifyKeyPairAndDownloadWhitelistFile,
handleFileSelect: readValidateSetSecretKeyFormFieldFromFile,
handleOnClose: handleFormClose,
isSecretKeyUploaded,
}
}

export const SecretKeyDownloadWhitelistFileModal = ({
onClose,
isOpen,
publicKey,
downloadFileName,
formId,
}: SecretKeyDownloadWhitelistFileModalProps): JSX.Element => {
const modalSize = useBreakpointValue({
base: 'mobile',
xs: 'mobile',
md: 'full',
})

const {
errors,
register,
fileUploadRef,
handleSubmit,
handleFileSelect,
handleOnClose,
isSecretKeyUploaded,
} = useSecretKeyWhitelistFileModal({
publicKey,
onClose,
formId,
downloadFileName,
})

return (
<Modal isOpen={isOpen} onClose={handleOnClose} size={modalSize}>
<ModalContent py={{ base: 'initial', md: '4.5rem' }}>
<ModalCloseButton />
{/* Hidden input field to trigger file selector, can be anywhere in the DOM */}
<Input
name="secretKeyFile"
ref={fileUploadRef}
type="file"
accept="text/plain"
onChange={handleFileSelect}
display="none"
/>
<ModalHeader color="secondary.500">
<Container maxW="42.5rem">
Download whitelisted NRIC/FIN/UEN .csv file
</Container>
</ModalHeader>
<ModalBody whiteSpace="pre-wrap">
<Container maxW="42.5rem">
<FormActivationSvg mb="2rem" />
<form onSubmit={handleSubmit} noValidate>
<FormControl isRequired isInvalid={!!errors.secretKey} mb="1rem">
<FormLabel>Enter or upload Secret Key</FormLabel>
<Stack direction="row" spacing="0.5rem">
<Input
type="password"
{...register(SECRET_KEY_NAME, {
required: "Please enter the form's secret key",
pattern: {
value: SECRET_KEY_REGEX,
message: 'The secret key provided is invalid',
},
})}
placeholder="Enter or upload your Secret Key to continue"
/>
<IconButton
variant="outline"
aria-label="Pass secret key from file"
icon={<BiUpload />}
onClick={() => fileUploadRef.current?.click()}
/>
</Stack>
<FormErrorMessage>{errors.secretKey?.message}</FormErrorMessage>
</FormControl>
<Button
rightIcon={<BiRightArrowAlt fontSize="1.5rem" />}
type="submit"
isFullWidth
isDisabled={!isSecretKeyUploaded}
>
Download
</Button>
</form>
</Container>
</ModalBody>
</ModalContent>
</Modal>
<SecretKeyFormModal
isLoading={false}
onSecretKeyFormSubmit={handleWhitelistCsvDownload}
onClose={onClose}
isOpen={isOpen}
publicKey={publicKey}
modalActionText=" Download CSV file of whitelisted NRIC/FIN/UEN(s)"
submitButtonText="Download file"
hasAck={false}
/>
)
}
Loading

0 comments on commit 6ec94cf

Please sign in to comment.