diff --git a/package-lock.json b/package-lock.json index 605626972..49c015de2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10353,9 +10353,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001570", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001570.tgz", - "integrity": "sha512-+3e0ASu4sw1SWaoCtvPeyXp+5PsjigkSt8OXZbF9StH5pQWbxEjLAZE3n8Aup5udop1uRiKA7a4utUk/uoSpUw==", + "version": "1.0.30001696", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001696.tgz", + "integrity": "sha512-pDCPkvzfa39ehJtJ+OwGT/2yvT2SbjfHhiIW2LWOAcMQ7BzwxT/XuyUp4OTOd0XFWA6BKw0JalnBHgSi5DGJBQ==", "dev": true, "funding": [ { @@ -24218,6 +24218,21 @@ "react": "^0.14.9 || ^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/react-intersection-observer": { + "version": "9.15.1", + "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.15.1.tgz", + "integrity": "sha512-vGrqYEVWXfH+AGu241uzfUpNK4HAdhCkSAyFdkMb9VWWXs6mxzBLpWCxEy9YcnDNY2g9eO6z7qUtTBdA9hc8pA==", + "license": "MIT", + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", @@ -30131,7 +30146,7 @@ }, "packages/shared": { "name": "@hats.finance/shared", - "version": "1.1.122", + "version": "1.1.125", "license": "ISC", "dependencies": { "@safe-global/protocol-kit": "^5.0.4", @@ -30142,7 +30157,7 @@ }, "devDependencies": { "@types/uuid": "^9.0.0", - "typescript": "^4.9.4" + "typescript": "^5.7.2" } }, "packages/shared/node_modules/@types/uuid": { @@ -30153,16 +30168,17 @@ "license": "MIT" }, "packages/shared/node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" } }, "packages/shared/node_modules/uuid": { @@ -30194,6 +30210,7 @@ "react-identicons": "^1.2.5", "react-image-file-resizer": "^0.4.8", "react-indexed-db-hook": "^1.0.14", + "react-intersection-observer": "^9.15.1", "sanitize-markdown": "^2.6.7", "siwe": "^1.1.6", "uuid-by-string": "^4.0.0" @@ -30268,7 +30285,7 @@ "stream-http": "^3.2.0", "styled-components": "^5.3.6", "swiper": "^8.3.0", - "typescript": "^4.9.4", + "typescript": "^5.7.2", "url": "^0.11.0", "util": "^0.12.4", "uuid": "^9.0.0", @@ -30332,9 +30349,9 @@ } }, "packages/web/node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -30342,7 +30359,7 @@ "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" } }, "packages/web/node_modules/uuid": { diff --git a/packages/shared/src/config/chains.ts b/packages/shared/src/config/chains.ts index 532ae906b..ee6f8ed07 100644 --- a/packages/shared/src/config/chains.ts +++ b/packages/shared/src/config/chains.ts @@ -78,7 +78,7 @@ export const ChainsConfig: { [index: number]: IChainConfiguration } = { uniswapSubgraph: undefined, paymentSplitterFactory: "0x8343D06cDFDe42cA0864029D5fE6138433A68a24", infuraKey: "sepolia", - provider: "https://eth-goerli.g.alchemy.com/v2/HMtXCk0FyIfbiNAVm4Xcgr8Eqlc5_DKd", + provider: "", }, [wagmiChains.optimism.id]: { // vaultsCreatorContract: "0xa80d0a371f4d37AFCc55188233BB4Ad463aF9E48", v2 diff --git a/packages/web/package.json b/packages/web/package.json index 90768435a..d76dbb681 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -120,8 +120,9 @@ "react-identicons": "^1.2.5", "react-image-file-resizer": "^0.4.8", "react-indexed-db-hook": "^1.0.14", + "react-intersection-observer": "^9.15.1", "sanitize-markdown": "^2.6.7", "siwe": "^1.1.6", "uuid-by-string": "^4.0.0" } -} \ No newline at end of file +} diff --git a/packages/web/src/hooks/auditFrameGame/auditFrameGameService.ts b/packages/web/src/hooks/auditFrameGame/auditFrameGameService.ts index 9100764ec..91c945f0c 100644 --- a/packages/web/src/hooks/auditFrameGame/auditFrameGameService.ts +++ b/packages/web/src/hooks/auditFrameGame/auditFrameGameService.ts @@ -11,7 +11,8 @@ export async function optInToAuditCompetition(editSessionIdOrAddress: string): P }); return response.data.ok; } catch (error) { - console.log(error); + console.error("Error opting in to audit competition:", error); + // Don't throw, just return false to indicate failure return false; } } @@ -26,7 +27,8 @@ export async function optOutToAuditCompetition(editSessionIdOrAddress: string): }); return response.data.ok; } catch (error) { - console.log(error); + console.error("Error opting out from audit competition:", error); + // Don't throw, just return false to indicate failure return false; } } @@ -41,7 +43,8 @@ export async function getAllOptedInOnAuditCompetition(editSessionIdOrAddress?: s const response = await axiosClient.get(`${BASE_SERVICE_URL}/edit-session/${editSessionIdOrAddress}/list-opted-in-users`); return response.data.optedInUsers; } catch (error) { - console.log(error); + console.error("Error getting opted in users:", error); + // Don't throw, just return empty array to indicate no users return []; } } diff --git a/packages/web/src/pages/Submissions/SubmissionFormPage/FormSteps/SubmissionDescriptions/SubmissionDescriptions.tsx b/packages/web/src/pages/Submissions/SubmissionFormPage/FormSteps/SubmissionDescriptions/SubmissionDescriptions.tsx index 71a1d680e..7656fcfa3 100644 --- a/packages/web/src/pages/Submissions/SubmissionFormPage/FormSteps/SubmissionDescriptions/SubmissionDescriptions.tsx +++ b/packages/web/src/pages/Submissions/SubmissionFormPage/FormSteps/SubmissionDescriptions/SubmissionDescriptions.tsx @@ -1,52 +1,63 @@ import { GithubIssue, ISubmissionMessageObject, IVulnerabilitySeverity } from "@hats.finance/shared"; +import { FormSupportFilesInput } from "components/FormControls"; import { yupResolver } from "@hookform/resolvers/yup"; import AddIcon from "@mui/icons-material/AddOutlined"; import CloseIcon from "@mui/icons-material/CloseOutlined"; import RemoveIcon from "@mui/icons-material/DeleteOutlined"; import FlagIcon from "@mui/icons-material/OutlinedFlagOutlined"; -import { - Alert, - Button, - FormInput, - FormMDEditor, - FormSelectInput, - FormSelectInputOption, - FormSupportFilesInput, - Loading, - Pill, - WithTooltip, -} from "components"; -import download from "downloadjs"; -import { getCustomIsDirty, useEnhancedForm } from "hooks/form"; -import moment from "moment"; -import { getGithubIssuesFromVault } from "pages/CommitteeTools/SubmissionsTool/submissionsService.api"; +import { useInView } from 'react-intersection-observer'; +import { useState, useEffect, useMemo, useContext } from 'react'; +import { Loading, Alert, Button, FormInput, FormMDEditor, FormSelectInput, FormSelectInputOption, Pill, WithTooltip } from 'components'; +import { Controller, useFieldArray, useWatch } from 'react-hook-form'; +import { ISubmissionsDescriptionsData } from "../../types"; +import { getCreateDescriptionSchema } from "./formSchema"; +import { StyledSubmissionDescription, StyledSubmissionDescriptionsList as BaseStyledSubmissionDescriptionsList } from "./styles"; +import { getAuditSubmissionTexts, getBountySubmissionTexts } from "./utils"; +import styled from 'styled-components'; +import { useSubmissionDebounce } from "../../hooks/useSubmissionDebounce"; +import { useTranslation } from "react-i18next"; +import { useAccount } from "wagmi"; +import { useNavigate } from "react-router-dom"; import { useProfileByAddress } from "pages/HackerProfile/hooks"; import { useClaimedIssuesByVaultAndClaimedBy } from "pages/Honeypots/VaultDetailsPage/Sections/VaultSubmissionsSection/PublicSubmissionCard/hooks"; import { getVaultRepoName } from "pages/Honeypots/VaultDetailsPage/savedSubmissionsService"; import { HoneypotsRoutePaths } from "pages/Honeypots/router"; -import { useContext, useEffect, useState } from "react"; -import { Controller, useFieldArray, useWatch } from "react-hook-form"; -import { useTranslation } from "react-i18next"; -import { useNavigate } from "react-router-dom"; import { searchFileInHatsRepo } from "utils/github.utils"; import { slugify } from "utils/slug.utils"; -import { useAccount } from "wagmi"; import { encryptWithHatsKey, encryptWithKeys } from "../../encrypt"; import { SUBMISSION_INIT_DATA, SubmissionFormContext } from "../../store"; -import { ISubmissionsDescriptionsData } from "../../types"; -import { getCreateDescriptionSchema } from "./formSchema"; -import { StyledSubmissionDescription, StyledSubmissionDescriptionsList } from "./styles"; -import { getAuditSubmissionTexts, getBountySubmissionTexts } from "./utils"; +import { getCustomIsDirty, useEnhancedForm } from "hooks/form"; +import download from "downloadjs"; +import moment from "moment"; +import { getGithubIssuesFromVault } from "pages/CommitteeTools/SubmissionsTool/submissionsService.api"; + +// Extend the base styled component +const StyledSubmissionDescriptionsList = styled(BaseStyledSubmissionDescriptionsList)` + .load-more-trigger { + display: flex; + justify-content: center; + align-items: center; + padding: 1rem; + margin-top: 1rem; + opacity: 0.7; + min-height: 60px; + } +`; export function SubmissionDescriptions() { const { t } = useTranslation(); const { address } = useAccount(); const navigate = useNavigate(); - const { data: hackerProfile } = useProfileByAddress(address); - const { submissionData, setSubmissionData, vault, allFormDisabled } = useContext(SubmissionFormContext); + + // State hooks const [severitiesOptions, setSeveritiesOptions] = useState(); + const [vaultGithubIssuesOpts, setVaultGithubIssuesOpts] = useState(); + const [vaultGithubIssues, setVaultGithubIssues] = useState(undefined); + const [isLoadingGH, setIsLoadingGH] = useState(false); + const [visibleSubmissions, setVisibleSubmissions] = useState(5); // ITEMS_PER_PAGE = 5 + const { ref: loadMoreRef, inView } = useInView(); const isAuditSubmission = vault?.description?.["project-metadata"].type === "audit"; const isPrivateAudit = vault?.description?.["project-metadata"].isPrivateAudit; @@ -54,10 +65,6 @@ export function SubmissionDescriptions() { const { data: claimedIssues, isLoading: isLoadingClaimedIssues } = useClaimedIssuesByVaultAndClaimedBy(vault, address); - const [vaultGithubIssuesOpts, setVaultGithubIssuesOpts] = useState(); - const [vaultGithubIssues, setVaultGithubIssues] = useState(undefined); - const [isLoadingGH, setIsLoadingGH] = useState(false); - const { register, handleSubmit, @@ -70,43 +77,56 @@ export function SubmissionDescriptions() { resolver: yupResolver(getCreateDescriptionSchema(t)), mode: "onChange", }); + const { fields, append: appendSubmissionDescription, remove: removeSubmissionDescription, } = useFieldArray({ control, name: `descriptions` }); + const watchDescriptions = useWatch({ control, name: `descriptions` }); - const controlledDescriptions = fields.map((field, index) => { - return { + + const { + debouncedUpdateDescription, + debouncedUpdateTitle, + debouncedHandleFixFiles, + debouncedHandleTestFiles + } = useSubmissionDebounce(setValue); + + // Memoized values + const memoizedControlledDescriptions = useMemo(() => + fields.map((field, index) => ({ ...field, ...watchDescriptions[index], - }; - }); + })), + [fields, watchDescriptions] + ); - // Reset form with saved data + const visibleDescriptions = useMemo(() => + memoizedControlledDescriptions.slice(0, visibleSubmissions), + [memoizedControlledDescriptions, visibleSubmissions] + ); + + // Effects useEffect(() => { - if (submissionData?.submissionsDescriptions) reset(submissionData.submissionsDescriptions); + if (submissionData?.submissionsDescriptions) { + reset(submissionData.submissionsDescriptions); + } }, [submissionData, reset]); - // Get severities information useEffect(() => { - if (!vault || !vault.description) return; - - if (vault.description) { - const severities = vault.description.severities.map((severity: IVulnerabilitySeverity) => ({ - label: severity.name.toLowerCase().replace("severity", "").trim(), - value: severity.name.toLowerCase(), - })); - - setSeveritiesOptions(severities); - } + if (!vault?.description) return; + const severities = vault.description.severities.map((severity: IVulnerabilitySeverity) => ({ + label: severity.name.toLowerCase().replace("severity", "").trim(), + value: severity.name.toLowerCase(), + })); + setSeveritiesOptions(severities); }, [vault, t]); - // Update isEncrypted field on descriptions useEffect(() => { if (!vault || !vault.description || !vault.description.severities) return; - for (const [idx, description] of controlledDescriptions.entries()) { + for (const [idx, description] of memoizedControlledDescriptions.entries()) { if (description.type === "complement") { if (description.isEncrypted === true) setValue(`descriptions.${idx}.isEncrypted`, false); continue; @@ -132,107 +152,59 @@ export function SubmissionDescriptions() { } } } - }, [controlledDescriptions, vault, setValue, isAuditSubmission]); + }, [memoizedControlledDescriptions, vault, setValue, isAuditSubmission]); - // Get information from github - const someComplementSubmission = controlledDescriptions.some((desc) => desc.type === "complement"); useEffect(() => { - if (!someComplementSubmission) return; - if (!vault) return; - if (!claimedIssues) return; - if (vaultGithubIssues !== undefined || isLoadingGH) return; - const loadGhIssues = async () => { - setIsLoadingGH(true); - const ghIssues = await getGithubIssuesFromVault(vault); - const ghIssuesOpts = ghIssues - .filter((ghIssue) => - claimedIssues?.some((ci) => +ci.issueNumber === +ghIssue.number && !moment(ci.expiresAt).isBefore(moment())) - ) - .filter((ghIssue) => ghIssue.bonusPointsLabels.needsFix || ghIssue.bonusPointsLabels.needsTest) - .map((ghIssue) => ({ + if (inView && visibleSubmissions < memoizedControlledDescriptions.length) { + setVisibleSubmissions(prev => Math.min(prev + 5, memoizedControlledDescriptions.length)); + } + }, [inView, memoizedControlledDescriptions.length, visibleSubmissions]); + + useEffect(() => { + setVisibleSubmissions(5); + }, [fields.length]); + + // Add GitHub issues loading effect + useEffect(() => { + const loadGithubIssues = async () => { + if (!vault || !claimedIssues || isLoadingGH) return; + + try { + setIsLoadingGH(true); + const ghIssues = await getGithubIssuesFromVault(vault); + const filteredIssues = ghIssues.filter((ghIssue) => + claimedIssues?.some( + (ci) => +ci.issueNumber === +ghIssue.number && !moment(ci.expiresAt).isBefore(moment()) + ) && (ghIssue.bonusPointsLabels.needsFix || ghIssue.bonusPointsLabels.needsTest) + ); + + const issueOptions = filteredIssues.map((ghIssue) => ({ label: `[#${ghIssue.number}] ${ghIssue.title}`, value: `${ghIssue.number}`, })); - setVaultGithubIssuesOpts(ghIssuesOpts); - setVaultGithubIssues(ghIssues); - setIsLoadingGH(false); + setVaultGithubIssues(ghIssues); + setVaultGithubIssuesOpts(issueOptions); + } catch (error) { + console.error('Failed to load GitHub issues:', error); + } finally { + setIsLoadingGH(false); + } }; - loadGhIssues(); - }, [vault, vaultGithubIssues, isLoadingGH, someComplementSubmission, claimedIssues, address]); + loadGithubIssues(); + }, [vault, claimedIssues, isLoadingGH]); + + // Reset GitHub issues when address changes useEffect(() => { setVaultGithubIssuesOpts(undefined); setVaultGithubIssues(undefined); }, [address]); - const handleSaveAndDownloadDescription = async (formData: ISubmissionsDescriptionsData) => { - if (!vault) return; - if (!submissionData) return alert("Please fill previous steps first."); - - let keyOrKeys: string | string[]; - - // Get public keys from vault description - if (vault.version === "v1") { - keyOrKeys = vault.description?.["communication-channel"]?.["pgp-pk"] ?? []; - } else { - keyOrKeys = - vault.description?.committee.members.reduce( - (prev: string[], curr) => [...prev, ...curr["pgp-keys"].map((key) => key.publicKey)], - [] - ) ?? []; - } - - keyOrKeys = typeof keyOrKeys === "string" ? keyOrKeys : keyOrKeys.filter((key) => !!key); - if (keyOrKeys.length === 0) return alert("This project has no keys to encrypt the description. Please contact HATS team."); - - const getSubmissionTextsFunction = isAuditSubmission ? getAuditSubmissionTexts : getBountySubmissionTexts; - const submissionTexts = getSubmissionTextsFunction(submissionData, formData.descriptions, hackerProfile); - - const toEncrypt = submissionTexts.toEncrypt; - const decrypted = submissionTexts.decrypted; - const submissionMessage = submissionTexts.submissionMessage; - - const encryptionResult = await encryptWithKeys(keyOrKeys, toEncrypt); - if (!encryptionResult) return alert("This vault doesn't have any valid key, please contact hats team"); - - const { encryptedData, sessionKey } = encryptionResult; - - let submissionInfo: ISubmissionMessageObject | undefined; - - try { - submissionInfo = { - ref: submissionData.ref, - isEncryptedByHats: isPrivateAudit, - decrypted: isPrivateAudit ? await encryptWithHatsKey(decrypted ?? "--Nothing decrypted--") : decrypted, - encrypted: encryptedData as string, - }; - } catch (error) { - console.log(error); - return alert("There was a problem encrypting the submission with Hats key. Please contact HATS team."); - } - - download( - JSON.stringify({ submission: submissionInfo, sessionKey }), - `${submissionData.project?.projectName}-${new Date().getTime()}.json` - ); - - setSubmissionData((prev) => { - if (!prev) return prev; - return { - ...prev, - submissionsDescriptions: { - verified: true, - submission: JSON.stringify(submissionInfo), - submissionMessage: submissionMessage, - descriptions: formData.descriptions, - }, - submissionResult: undefined, - }; - }); - }; - - if (!vault) return {t("Submissions.firstYouNeedToSelectAProject")}; + // Early return with error message + if (!vault) { + return {t("Submissions.firstYouNeedToSelectAProject")}; + } const getSubmissionsRoute = () => { const mainRoute = `/${isAuditSubmission ? HoneypotsRoutePaths.audits : HoneypotsRoutePaths.bugBounties}`; @@ -241,7 +213,7 @@ export function SubmissionDescriptions() { return `${mainRoute}/${vaultSlug}-${vault.id}/submissions`; }; - const getNewIssueForm = (submissionDescription: (typeof controlledDescriptions)[number], index: number) => { + const getNewIssueForm = (submissionDescription: (typeof memoizedControlledDescriptions)[number], index: number) => { return ( <>

{t("Submissions.provideExplanation")}

@@ -249,9 +221,10 @@ export function SubmissionDescriptions() { debouncedUpdateTitle(index, e.target.value)} /> (field.name, dirtyFields, defaultValues)} error={error} colorable - {...field} + value={field.value} + onChange={(value) => debouncedUpdateDescription(index, value)} /> )} /> - {/* {!submissionDescription.isEncrypted && !allFormDisabled && ( - ( - - )} - /> - )} */} -

Does this issue needs a fix?

{ + const getComplementIssueForm = (submissionDescription: (typeof memoizedControlledDescriptions)[number], index: number) => { const { complementGhIssue, needsFix, needsTest } = submissionDescription; return ( @@ -521,7 +485,6 @@ export function SubmissionDescriptions() { }} /> - {/*

{t("Submissions.filesAttached")}:

*/}
{(submissionDescription.complementTestFiles ?? []).map((item, idx) => ( @@ -584,71 +547,159 @@ export function SubmissionDescriptions() { ); }; + const handleSaveAndDownloadDescription = async (formData: ISubmissionsDescriptionsData) => { + if (!vault) return; + if (!submissionData) return alert("Please fill previous steps first."); + + try { + setIsLoadingGH(true); // Show loading while processing + + let keyOrKeys: string | string[]; + + // Get public keys from vault description + if (vault.version === "v1") { + keyOrKeys = vault.description?.["communication-channel"]?.["pgp-pk"] ?? []; + } else { + keyOrKeys = vault.description?.committee.members.flatMap( + (member) => member["pgp-keys"].map((key) => key.publicKey) + ) ?? []; + } + + keyOrKeys = typeof keyOrKeys === "string" ? keyOrKeys : keyOrKeys.filter((key) => !!key); + if (keyOrKeys.length === 0) { + throw new Error("This project has no keys to encrypt the description. Please contact HATS team."); + } + + const getSubmissionTextsFunction = isAuditSubmission ? getAuditSubmissionTexts : getBountySubmissionTexts; + const submissionTexts = getSubmissionTextsFunction(submissionData, formData.descriptions, hackerProfile); + + const toEncrypt = submissionTexts.toEncrypt; + const decrypted = submissionTexts.decrypted; + const submissionMessage = submissionTexts.submissionMessage; + + const encryptionResult = await encryptWithKeys(keyOrKeys, toEncrypt); + if (!encryptionResult) { + throw new Error("This vault doesn't have any valid key, please contact hats team"); + } + + let parsed; + try { + parsed = JSON.parse(encryptionResult); + if (!parsed || typeof parsed !== 'object' || !('encryptedData' in parsed) || !('sessionKey' in parsed)) { + throw new Error('Invalid encryption result format'); + } + } catch (error) { + console.error('Encryption result parsing failed:', error); + throw new Error('Failed to parse encryption result. Please try again or contact support.'); + } + + const { encryptedData, sessionKey } = parsed; + + let submissionInfo: ISubmissionMessageObject = { + ref: submissionData.ref, + isEncryptedByHats: isPrivateAudit, + decrypted: isPrivateAudit ? await encryptWithHatsKey(decrypted ?? "--Nothing decrypted--") : decrypted, + encrypted: encryptedData, + }; + + download( + JSON.stringify({ submission: submissionInfo, sessionKey }), + `${submissionData.project?.projectName}-${new Date().getTime()}.json` + ); + + setSubmissionData((prev) => { + if (!prev) return prev; + return { + ...prev, + submissionsDescriptions: { + verified: true, + submission: JSON.stringify(submissionInfo), + submissionMessage: submissionMessage, + descriptions: formData.descriptions, + }, + submissionResult: undefined, + }; + }); + } catch (error) { + console.error('Submission processing failed:', error); + alert(error instanceof Error ? error.message : "There was a problem processing your submission. Please try again."); + } finally { + setIsLoadingGH(false); // Hide loading when done + } + }; + return ( - {controlledDescriptions.map((submissionDescription, index) => { - return ( - -

- - {t("submission")} #{index + 1} - - {((submissionDescription.type === "new" && submissionDescription.severity) || - submissionDescription.type === "complement") && ( - - - {submissionDescription.isEncrypted - ? t("Submissions.encryptedSubmission") - : t("Submissions.decryptedSubmission")} - - - )} -

- - {bonusPointsEnabled && ( -
-
setValue(`descriptions.${index}.type`, "new")}> -
-
-

{t("newSubmission")}

-
+ {visibleDescriptions.map((submissionDescription, index) => ( + +

+ + {t("submission")} #{index + 1} + + {((submissionDescription.type === "new" && submissionDescription.severity) || + submissionDescription.type === "complement") && ( + + + {submissionDescription.isEncrypted + ? t("Submissions.encryptedSubmission") + : t("Submissions.decryptedSubmission")} + + + )} +

+ + {bonusPointsEnabled && ( +
+
setValue(`descriptions.${index}.type`, "new")}> +
+
+

{t("newSubmission")}

+
-
setValue(`descriptions.${index}.type`, "complement")}> -
-
-

{t("complementSubmission")}

-
+
setValue(`descriptions.${index}.type`, "complement")}> +
+
+

{t("complementSubmission")}

- )} +
+ )} - {submissionDescription.type === "new" - ? getNewIssueForm(submissionDescription, index) - : getComplementIssueForm(submissionDescription, index)} + {submissionDescription.type === "new" + ? getNewIssueForm(submissionDescription, index) + : getComplementIssueForm(submissionDescription, index)} - {controlledDescriptions.length > 1 && !allFormDisabled && ( -
- -
- )} - - ); - })} + {visibleDescriptions.length > 1 && !allFormDisabled && ( +
+ +
+ )} + + ))} + + {visibleSubmissions < memoizedControlledDescriptions.length && ( +
+ +
+ )}
{!allFormDisabled && (