diff --git a/src/components/tx/ErrorMessage/index.tsx b/src/components/tx/ErrorMessage/index.tsx index ccf7d7de11..5f6cc07932 100644 --- a/src/components/tx/ErrorMessage/index.tsx +++ b/src/components/tx/ErrorMessage/index.tsx @@ -14,7 +14,7 @@ const ErrorMessage = ({ children: ReactNode error?: Error & { reason?: string } className?: string - level?: 'error' | 'info' + level?: 'error' | 'warning' | 'info' }): ReactElement => { const [showDetails, setShowDetails] = useState(false) diff --git a/src/components/tx/ErrorMessage/styles.module.css b/src/components/tx/ErrorMessage/styles.module.css index 5fe129aa3c..1fda0fddc0 100644 --- a/src/components/tx/ErrorMessage/styles.module.css +++ b/src/components/tx/ErrorMessage/styles.module.css @@ -9,6 +9,11 @@ color: var(--color-error-dark); } +.container.warning { + background-color: var(--color-warning-background); + color: var(--color-warning-dark); +} + .container.info { background-color: var(--color-info-background); color: var(--color-primary-main); diff --git a/src/components/tx/SignOrExecuteForm/ExecuteForm.tsx b/src/components/tx/SignOrExecuteForm/ExecuteForm.tsx index 857d275004..58a93c502f 100644 --- a/src/components/tx/SignOrExecuteForm/ExecuteForm.tsx +++ b/src/components/tx/SignOrExecuteForm/ExecuteForm.tsx @@ -9,7 +9,6 @@ import { getTxOptions } from '@/utils/transactions' import useIsValidExecution from '@/hooks/useIsValidExecution' import CheckWallet from '@/components/common/CheckWallet' import { useImmediatelyExecutable, useIsExecutionLoop, useTxActions } from './hooks' -import UnknownContractError from './UnknownContractError' import { useRelaysBySafe } from '@/hooks/useRemainingRelays' import useWalletCanRelay from '@/hooks/useWalletCanRelay' import { ExecutionMethod, ExecutionMethodSelector } from '../ExecutionMethodSelector' @@ -24,6 +23,7 @@ import { asError } from '@/services/exceptions/utils' import css from './styles.module.css' import commonCss from '@/components/tx-flow/common/styles.module.css' +import { TxSecurityContext } from '../security/shared/TxSecurityContext' const ExecuteForm = ({ safeTx, @@ -43,6 +43,7 @@ const ExecuteForm = ({ const { executeTx } = useTxActions() const [relays] = useRelaysBySafe() const { setTxFlow } = useContext(TxModalContext) + const { needsRiskConfirmation, isRiskConfirmed, setIsRiskIgnored } = useContext(TxSecurityContext) // Check that the transaction is executable const isCreation = !txId @@ -69,6 +70,12 @@ const ExecuteForm = ({ // On modal submit const handleSubmit = async (e: SyntheticEvent) => { e.preventDefault() + + if (needsRiskConfirmation && !isRiskConfirmed) { + setIsRiskIgnored(true) + return + } + setIsSubmittable(false) setSubmitError(undefined) @@ -126,10 +133,10 @@ const ExecuteForm = ({ ? 'To save gas costs, avoid creating the transaction.' : 'To save gas costs, reject this transaction.'} - ) : submitError ? ( - Error submitting the transaction. Please try again. ) : ( - + submitError && ( + Error submitting the transaction. Please try again. + ) )} diff --git a/src/components/tx/SignOrExecuteForm/RiskConfirmationError.tsx b/src/components/tx/SignOrExecuteForm/RiskConfirmationError.tsx new file mode 100644 index 0000000000..56dd38eb44 --- /dev/null +++ b/src/components/tx/SignOrExecuteForm/RiskConfirmationError.tsx @@ -0,0 +1,15 @@ +import { useContext } from 'react' +import ErrorMessage from '../ErrorMessage' +import { TxSecurityContext } from '../security/shared/TxSecurityContext' + +const RiskConfirmationError = () => { + const { isRiskConfirmed, isRiskIgnored } = useContext(TxSecurityContext) + + if (isRiskConfirmed || !isRiskIgnored) { + return null + } + + return Please acknowledge the risk before proceeding. +} + +export default RiskConfirmationError diff --git a/src/components/tx/SignOrExecuteForm/SignForm.tsx b/src/components/tx/SignOrExecuteForm/SignForm.tsx index 4bca8280c5..959577600b 100644 --- a/src/components/tx/SignOrExecuteForm/SignForm.tsx +++ b/src/components/tx/SignOrExecuteForm/SignForm.tsx @@ -11,6 +11,7 @@ import type { SafeTransaction } from '@safe-global/safe-core-sdk-types' import { TxModalContext } from '@/components/tx-flow' import { asError } from '@/services/exceptions/utils' import commonCss from '@/components/tx-flow/common/styles.module.css' +import { TxSecurityContext } from '../security/shared/TxSecurityContext' const SignForm = ({ safeTx, @@ -29,10 +30,17 @@ const SignForm = ({ const isOwner = useIsSafeOwner() const { signTx } = useTxActions() const { setTxFlow } = useContext(TxModalContext) + const { needsRiskConfirmation, isRiskConfirmed, setIsRiskIgnored } = useContext(TxSecurityContext) // On modal submit const handleSubmit = async (e: SyntheticEvent) => { e.preventDefault() + + if (needsRiskConfirmation && !isRiskConfirmed) { + setIsRiskIgnored(true) + return + } + setIsSubmittable(false) setSubmitError(undefined) diff --git a/src/components/tx/SignOrExecuteForm/index.tsx b/src/components/tx/SignOrExecuteForm/index.tsx index 4908d62c8e..37d224920f 100644 --- a/src/components/tx/SignOrExecuteForm/index.tsx +++ b/src/components/tx/SignOrExecuteForm/index.tsx @@ -13,6 +13,8 @@ import ConfirmationTitle, { ConfirmationTitleTypes } from '@/components/tx/SignO import { useAppSelector } from '@/store' import { selectSettings } from '@/store/settingsSlice' import { RedefineBalanceChanges } from '../security/redefine/RedefineBalanceChange' +import UnknownContractError from './UnknownContractError' +import RiskConfirmationError from './RiskConfirmationError' export type SignOrExecuteProps = { txId?: string @@ -67,6 +69,10 @@ const SignOrExecuteForm = (props: SignOrExecuteProps): ReactElement => { + + + + {willExecute ? : } diff --git a/src/components/tx/security/redefine/index.tsx b/src/components/tx/security/redefine/index.tsx index 7f3538eff3..32b06462d8 100644 --- a/src/components/tx/security/redefine/index.tsx +++ b/src/components/tx/security/redefine/index.tsx @@ -1,4 +1,4 @@ -import { useContext } from 'react' +import { useContext, useEffect, useRef } from 'react' import { mapRedefineSeverity } from '@/components/tx/security/redefine/useRedefine' import { TxSecurityContext } from '@/components/tx/security/shared/TxSecurityContext' import { SecuritySeverity } from '@/services/security/modules/types' @@ -22,8 +22,9 @@ import { RedefineHint } from '@/components/tx/security/redefine/RedefineHint' const MAX_SHOWN_WARNINGS = 3 const RedefineBlock = () => { - const { severity, isLoading, error, needsRiskConfirmation, isRiskConfirmed, setIsRiskConfirmed } = + const { severity, isLoading, error, needsRiskConfirmation, isRiskConfirmed, setIsRiskConfirmed, isRiskIgnored } = useContext(TxSecurityContext) + const checkboxRef = useRef(null) const isDarkMode = useDarkMode() const severityProps = severity !== undefined ? mapRedefineSeverity[severity] : undefined @@ -32,6 +33,13 @@ const RedefineBlock = () => { setIsRiskConfirmed((prev) => !prev) } + // Highlight checkbox if user tries to submit transaction without confirming risks + useEffect(() => { + if (isRiskIgnored) { + checkboxRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' }) + } + }, [isRiskIgnored, checkboxRef]) + return (
{
{needsRiskConfirmation && ( - + } + className={isRiskIgnored ? css.checkboxError : ''} /> diff --git a/src/components/tx/security/redefine/styles.module.css b/src/components/tx/security/redefine/styles.module.css index 48f7809ff9..ac5e3fbcdb 100644 --- a/src/components/tx/security/redefine/styles.module.css +++ b/src/components/tx/security/redefine/styles.module.css @@ -68,3 +68,24 @@ grid-template-columns: 35% auto; padding: var(--space-2) 12px; } + +@keyframes popup { + 0% { + transform: scale(1); + } + 50% { + transform: scale(1.05); + } + 100% { + transform: scale(1); + } +} + +.checkboxError { + color: var(--color-error-main); + animation: popup 0.5s ease-in-out; +} + +.checkboxError svg { + color: var(--color-error-main) !important; +} diff --git a/src/components/tx/security/shared/TxSecurityContext.tsx b/src/components/tx/security/shared/TxSecurityContext.tsx index bd77ee5344..f1840302fc 100644 --- a/src/components/tx/security/shared/TxSecurityContext.tsx +++ b/src/components/tx/security/shared/TxSecurityContext.tsx @@ -14,6 +14,8 @@ export const TxSecurityContext = createContext<{ needsRiskConfirmation: boolean isRiskConfirmed: boolean setIsRiskConfirmed: Dispatch> + isRiskIgnored: boolean + setIsRiskIgnored: Dispatch> }>({ warnings: [], simulationUuid: undefined, @@ -24,12 +26,15 @@ export const TxSecurityContext = createContext<{ needsRiskConfirmation: false, isRiskConfirmed: false, setIsRiskConfirmed: () => {}, + isRiskIgnored: false, + setIsRiskIgnored: () => {}, }) export const TxSecurityProvider = ({ children }: { children: JSX.Element }) => { const { safeTx } = useContext(SafeTxContext) const [redefineResponse, redefineError, redefineLoading] = useRedefine(safeTx) const [isRiskConfirmed, setIsRiskConfirmed] = useState(false) + const [isRiskIgnored, setIsRiskIgnored] = useState(false) const providedValue = useMemo( () => ({ @@ -42,8 +47,10 @@ export const TxSecurityProvider = ({ children }: { children: JSX.Element }) => { needsRiskConfirmation: !!redefineResponse && redefineResponse.severity >= SecuritySeverity.HIGH, isRiskConfirmed, setIsRiskConfirmed, + isRiskIgnored: isRiskIgnored && !isRiskConfirmed, + setIsRiskIgnored, }), - [isRiskConfirmed, redefineError, redefineLoading, redefineResponse], + [isRiskConfirmed, isRiskIgnored, redefineError, redefineLoading, redefineResponse], ) return {children}