Skip to content

Commit f2c8401

Browse files
authored
Merge pull request #1720 from kleros/feat/disable-buttons-if-insufficient-balance
fix(web): disable buttons if insufficient balance
2 parents f84e4a9 + 0dc7b17 commit f2c8401

File tree

6 files changed

+137
-57
lines changed

6 files changed

+137
-57
lines changed
+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import styled from "styled-components";
2+
3+
export const ErrorButtonMessage = styled.div`
4+
display: flex;
5+
align-items: center;
6+
gap: 4px;
7+
justify-content: center;
8+
margin: 12px;
9+
color: ${({ theme }) => theme.error};
10+
font-size: 14px;
11+
`;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import React from "react";
2+
import styled from "styled-components";
3+
import ClosedCircle from "svgs/icons/close-circle.svg";
4+
5+
const StyledClosedCircle = styled(ClosedCircle)`
6+
path {
7+
fill: ${({ theme }) => theme.error};
8+
}
9+
`;
10+
11+
const ClosedCircleIcon: React.FC = () => {
12+
return <StyledClosedCircle />;
13+
};
14+
export default ClosedCircleIcon;

web/src/pages/Cases/CaseDetails/Appeal/Classic/Fund.tsx

+43-28
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import { isUndefined } from "utils/index";
1515
import { wrapWithToast } from "utils/wrapWithToast";
1616

1717
import { EnsureChain } from "components/EnsureChain";
18+
import { ErrorButtonMessage } from "components/ErrorButtonMessage";
19+
import ClosedCircleIcon from "components/StyledIcons/ClosedCircleIcon";
1820

1921
const Container = styled.div`
2022
display: flex;
@@ -46,6 +48,7 @@ const StyledButton = styled(Button)`
4648
const StyledLabel = styled.label`
4749
align-self: flex-start;
4850
`;
51+
4952
const useNeedFund = () => {
5053
const { loserSideCountdown } = useCountdownContext();
5154
const { fundedChoices, winningChoice } = useFundingContext();
@@ -59,20 +62,24 @@ const useNeedFund = () => {
5962
return needFund;
6063
};
6164

62-
const useFundAppeal = (parsedAmount) => {
65+
const useFundAppeal = (parsedAmount, insufficientBalance) => {
6366
const { id } = useParams();
6467
const { selectedOption } = useSelectedOptionContext();
65-
const { data: fundAppealConfig, isError } = useSimulateDisputeKitClassicFundAppeal({
68+
const {
69+
data: fundAppealConfig,
70+
isLoading,
71+
isError,
72+
} = useSimulateDisputeKitClassicFundAppeal({
6673
query: {
67-
enabled: !isUndefined(id) && !isUndefined(selectedOption),
74+
enabled: !isUndefined(id) && !isUndefined(selectedOption) && !insufficientBalance,
6875
},
6976
args: [BigInt(id ?? 0), BigInt(selectedOption ?? 0)],
7077
value: parsedAmount,
7178
});
7279

7380
const { writeContractAsync: fundAppeal } = useWriteDisputeKitClassicFundAppeal();
7481

75-
return { fundAppeal, fundAppealConfig, isError };
82+
return { fundAppeal, fundAppealConfig, isLoading, isError };
7683
};
7784

7885
interface IFund {
@@ -98,12 +105,15 @@ const Fund: React.FC<IFund> = ({ amount, setAmount, setIsOpen }) => {
98105

99106
const parsedAmount = useParsedAmount(debouncedAmount as `${number}`);
100107

101-
const { fundAppealConfig, fundAppeal, isError } = useFundAppeal(parsedAmount);
108+
const insufficientBalance = useMemo(() => {
109+
return balance && balance.value < parsedAmount;
110+
}, [balance, parsedAmount]);
111+
112+
const { fundAppealConfig, fundAppeal, isLoading, isError } = useFundAppeal(parsedAmount, insufficientBalance);
102113

103114
const isFundDisabled = useMemo(
104-
() =>
105-
isDisconnected || isSending || !balance || parsedAmount > balance.value || Number(parsedAmount) <= 0 || isError,
106-
[isDisconnected, isSending, balance, parsedAmount, isError]
115+
() => isDisconnected || isSending || !balance || insufficientBalance || Number(parsedAmount) <= 0 || isError,
116+
[isDisconnected, isSending, balance, insufficientBalance, parsedAmount, isError]
107117
);
108118

109119
return needFund ? (
@@ -118,28 +128,33 @@ const Fund: React.FC<IFund> = ({ amount, setAmount, setIsOpen }) => {
118128
placeholder="Amount to fund"
119129
/>
120130
<EnsureChain>
121-
<StyledButton
122-
disabled={isFundDisabled}
123-
isLoading={isSending}
124-
text={isDisconnected ? "Connect to Fund" : "Fund"}
125-
onClick={() => {
126-
if (fundAppeal && fundAppealConfig && publicClient) {
127-
setIsSending(true);
128-
wrapWithToast(async () => await fundAppeal(fundAppealConfig.request), publicClient)
129-
.then((res) => {
130-
res.status && setIsOpen(true);
131-
})
132-
.finally(() => {
133-
setIsSending(false);
134-
});
135-
}
136-
}}
137-
/>
131+
<div>
132+
<StyledButton
133+
disabled={isFundDisabled}
134+
isLoading={(isSending || isLoading) && !insufficientBalance}
135+
text={isDisconnected ? "Connect to Fund" : "Fund"}
136+
onClick={() => {
137+
if (fundAppeal && fundAppealConfig && publicClient) {
138+
setIsSending(true);
139+
wrapWithToast(async () => await fundAppeal(fundAppealConfig.request), publicClient)
140+
.then((res) => {
141+
res.status && setIsOpen(true);
142+
})
143+
.finally(() => {
144+
setIsSending(false);
145+
});
146+
}
147+
}}
148+
/>
149+
{insufficientBalance && (
150+
<ErrorButtonMessage>
151+
<ClosedCircleIcon /> Insufficient balance
152+
</ErrorButtonMessage>
153+
)}
154+
</div>
138155
</EnsureChain>
139156
</Container>
140-
) : (
141-
<></>
142-
);
157+
) : null;
143158
};
144159

145160
export default Fund;

web/src/pages/Cases/CaseDetails/MaintenanceButtons/DrawButton.tsx

+18-1
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@ import { isUndefined } from "src/utils";
1717
import { Phases } from "components/Phase";
1818

1919
import { IBaseMaintenanceButton } from ".";
20+
import { Link } from "react-router-dom";
2021

2122
const StyledButton = styled(Button)`
2223
width: 100%;
2324
`;
2425

26+
const StyledLabel = styled.label``;
2527
interface IDrawButton extends IBaseMaintenanceButton {
2628
numberOfVotes?: string;
2729
period?: string;
@@ -40,6 +42,11 @@ const DrawButton: React.FC<IDrawButton> = ({ id, numberOfVotes, setIsOpen, perio
4042
[maintenanceData, isDrawn, phase, period]
4143
);
4244

45+
const needToPassPhase = useMemo(
46+
() => !isUndefined(maintenanceData) && !isDrawn && period === Period.Evidence && phase !== Phases.drawing,
47+
[maintenanceData, isDrawn, phase, period]
48+
);
49+
4350
const {
4451
data: drawConfig,
4552
isLoading: isLoadingConfig,
@@ -68,7 +75,17 @@ const DrawButton: React.FC<IDrawButton> = ({ id, numberOfVotes, setIsOpen, perio
6875
setIsOpen(false);
6976
});
7077
};
71-
return <StyledButton text="Draw" small isLoading={isLoading} disabled={isDisabled} onClick={handleClick} />;
78+
return (
79+
<>
80+
{needToPassPhase ? (
81+
<StyledLabel>
82+
Jurors can be drawn in <small>drawing</small> phase.
83+
<br /> Pass phase <Link to="/courts/1/purpose/#maintenance">here</Link>.
84+
</StyledLabel>
85+
) : null}
86+
<StyledButton text="Draw" small isLoading={isLoading} disabled={isDisabled} onClick={handleClick} />
87+
</>
88+
);
7289
};
7390

7491
export default DrawButton;

web/src/pages/Courts/StakeMaintenanceButton/index.tsx

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState } from "react";
1+
import React, { useEffect, useState } from "react";
22
import styled from "styled-components";
33

44
import DottedMenuButton from "components/DottedMenuButton";
@@ -47,6 +47,11 @@ interface IStakeMaintenanceButtons {
4747
const StakeMaintenanceButtons: React.FC<IStakeMaintenanceButtons> = ({ className }) => {
4848
const [isOpen, setIsOpen] = useState(false);
4949

50+
useEffect(() => {
51+
const openDefault = location.hash.includes("#maintenance");
52+
if (openDefault) setIsOpen(true);
53+
}, []);
54+
5055
const toggle = () => setIsOpen((prevValue) => !prevValue);
5156
return (
5257
<Container {...{ className }}>

web/src/pages/Resolver/NavigationButtons/SubmitDisputeButton.tsx

+45-27
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React, { useMemo, useState } from "react";
22
import styled from "styled-components";
33

44
import { Log, decodeEventLog, parseAbi } from "viem";
5-
import { usePublicClient } from "wagmi";
5+
import { useAccount, useBalance, usePublicClient } from "wagmi";
66

77
import { Button } from "@kleros/ui-components-library";
88

@@ -20,6 +20,9 @@ import { wrapWithToast } from "utils/wrapWithToast";
2020
import { EnsureChain } from "components/EnsureChain";
2121
import Popup, { PopupType } from "components/Popup";
2222

23+
import { ErrorButtonMessage } from "components/ErrorButtonMessage";
24+
import ClosedCircleIcon from "components/StyledIcons/ClosedCircleIcon";
25+
2326
const StyledButton = styled(Button)``;
2427

2528
const SubmitDisputeButton: React.FC = () => {
@@ -31,10 +34,18 @@ const SubmitDisputeButton: React.FC = () => {
3134
const { disputeTemplate, disputeData, resetDisputeData, isSubmittingCase, setIsSubmittingCase } =
3235
useNewDisputeContext();
3336

37+
const { address } = useAccount();
38+
const { data: userBalance, isLoading: isBalanceLoading } = useBalance({ address });
39+
40+
const insufficientBalance = useMemo(() => {
41+
const arbitrationCost = disputeData.arbitrationCost ? BigInt(disputeData.arbitrationCost) : BigInt(0);
42+
return userBalance && userBalance.value < arbitrationCost;
43+
}, [userBalance, disputeData]);
44+
3445
// TODO: decide which dispute kit to use
3546
const { data: submitCaseConfig } = useSimulateDisputeResolverCreateDisputeForTemplate({
3647
query: {
37-
enabled: isTemplateValid(disputeTemplate),
48+
enabled: !insufficientBalance && isTemplateValid(disputeTemplate),
3849
},
3950
args: [
4051
prepareArbitratorExtradata(disputeData.courtId ?? "1", disputeData.numberOfJurors ?? "", 1),
@@ -48,37 +59,44 @@ const SubmitDisputeButton: React.FC = () => {
4859
const { writeContractAsync: submitCase } = useWriteDisputeResolverCreateDisputeForTemplate();
4960

5061
const isButtonDisabled = useMemo(
51-
() => isSubmittingCase || !isTemplateValid(disputeTemplate),
52-
[isSubmittingCase, disputeTemplate]
62+
() => isSubmittingCase || !isTemplateValid(disputeTemplate) || isBalanceLoading || insufficientBalance,
63+
[isSubmittingCase, insufficientBalance, isBalanceLoading, disputeTemplate]
5364
);
5465

5566
return (
5667
<>
5768
{" "}
5869
<EnsureChain>
59-
<StyledButton
60-
text="Submit the case"
61-
disabled={isButtonDisabled}
62-
isLoading={isSubmittingCase}
63-
onClick={() => {
64-
if (submitCaseConfig) {
65-
setIsSubmittingCase(true);
66-
wrapWithToast(async () => await submitCase(submitCaseConfig.request), publicClient)
67-
.then((res) => {
68-
if (res.status && !isUndefined(res.result)) {
69-
const id = retrieveDisputeId(res.result.logs[1]);
70-
setDisputeId(Number(id));
71-
setCourtId(disputeData.courtId ?? "1");
72-
setIsPopupOpen(true);
73-
resetDisputeData();
74-
}
75-
})
76-
.finally(() => {
77-
setIsSubmittingCase(false);
78-
});
79-
}
80-
}}
81-
/>
70+
<div>
71+
<StyledButton
72+
text="Submit the case"
73+
disabled={isButtonDisabled}
74+
isLoading={(isSubmittingCase || isBalanceLoading) && !insufficientBalance}
75+
onClick={() => {
76+
if (submitCaseConfig) {
77+
setIsSubmittingCase(true);
78+
wrapWithToast(async () => await submitCase(submitCaseConfig.request), publicClient)
79+
.then((res) => {
80+
if (res.status && !isUndefined(res.result)) {
81+
const id = retrieveDisputeId(res.result.logs[1]);
82+
setDisputeId(Number(id));
83+
setCourtId(disputeData.courtId ?? "1");
84+
setIsPopupOpen(true);
85+
resetDisputeData();
86+
}
87+
})
88+
.finally(() => {
89+
setIsSubmittingCase(false);
90+
});
91+
}
92+
}}
93+
/>
94+
{insufficientBalance && (
95+
<ErrorButtonMessage>
96+
<ClosedCircleIcon /> Insufficient balance
97+
</ErrorButtonMessage>
98+
)}
99+
</div>
82100
</EnsureChain>
83101
{isPopupOpen && disputeId && (
84102
<Popup

0 commit comments

Comments
 (0)