diff --git a/src/containers/delegationGatingMenu/delegationGatingMenu.tsx b/src/containers/delegationGatingMenu/delegationGatingMenu.tsx index fec5cac46..c914f8b2b 100644 --- a/src/containers/delegationGatingMenu/delegationGatingMenu.tsx +++ b/src/containers/delegationGatingMenu/delegationGatingMenu.tsx @@ -8,41 +8,48 @@ import { ButtonText, IllustrationHuman, IlluObject, - shortenAddress, Link, IconLinkExternal, } from '@aragon/ods-old'; import {useDaoDetailsQuery} from 'hooks/useDaoDetails'; import {useDaoToken} from 'hooks/useDaoToken'; -import {Address, useBalance, useEnsName} from 'wagmi'; +import {Address, useBalance} from 'wagmi'; import {CHAIN_METADATA, SupportedNetworks} from 'utils/constants'; import {useDelegatee} from 'services/aragon-sdk/queries/use-delegatee'; import {abbreviateTokenAmount} from 'utils/tokens'; import {useWallet} from 'hooks/useWallet'; +import {usePastVotingPower} from 'services/aragon-sdk/queries/use-past-voting-power'; +import {TokenVotingProposal} from '@aragon/sdk-client'; + +export interface IDelegationGatingMenuState { + proposal?: TokenVotingProposal; +} const getDelegationLabels = (params: { needsSelfDelegation: boolean; - noVotingPower: boolean; + hasPastVotingPower: boolean; + hasBalance: boolean; + hasPastBalance: boolean; }) => { - const {needsSelfDelegation, noVotingPower} = params; - - let bodyLabel = 'delegationActive'; - let ctaLabel = 'delegationActive.CtaLabel'; + const {needsSelfDelegation, hasPastVotingPower, hasBalance, hasPastBalance} = + params; - if (needsSelfDelegation) { - bodyLabel = 'delegationInactive'; - ctaLabel = 'delegation.ctaLabelDelegateNow'; - } else if (noVotingPower) { - bodyLabel = 'delegation.NoVotingPower'; - ctaLabel = 'delegation.NoVotingPower.ctaLabel'; - } + const firstKey = hasPastBalance ? 'sentence1b' : 'sentence1a'; + const secondKey = hasPastBalance + ? 'sentence2a' + : hasBalance + ? 'sentence2b' + : 'sentence2c'; - return {bodyLabel, ctaLabel}; + return {firstKey, secondKey}; }; export const DelegationGatingMenu: React.FC = () => { const {t} = useTranslation(); - const {isOpen, close, open} = useGlobalModalContext('delegationGating'); + const {isOpen, modalState, close, open} = + useGlobalModalContext('delegationGating'); + + const {proposal} = modalState ?? {}; const {network, address} = useWallet(); @@ -60,6 +67,23 @@ export const DelegationGatingMenu: React.FC = () => { const tokenAmount = abbreviateTokenAmount(tokenBalance?.formatted ?? '0'); + const {data: pastVotingPower} = usePastVotingPower( + { + tokenAddress: daoToken?.address as string, + address: address as string, + blockNumber: proposal?.creationBlockNumber as number, + }, + {enabled: daoToken != null && address != null && proposal != null} + ); + + const hasBalance = tokenBalance != null && tokenBalance.value > 0; + + // TODO: to be fetched from the SDK + const hasPastBalance = true; + + const hasPastVotingPower = + pastVotingPower != null && pastVotingPower.gt(constants.Zero); + const {data: delegateData} = useDelegatee( {tokenAddress: daoToken?.address as string}, {enabled: daoToken != null} @@ -70,33 +94,32 @@ export const DelegationGatingMenu: React.FC = () => { delegateData === null ? (address as string) : delegateData; // For imported ERC-20 tokens, there's no self-delegation and the delegation data is set to address-zero. - const needsSelfDelegation = delegateData === constants.AddressZero; - - // Defines the case when the user is not delegating the tokens to someone else but had no - // voting power when the proposal has been created. - const noVotingPower = - !needsSelfDelegation && - currentDelegate?.toLowerCase() === address?.toLowerCase(); - - const {data: delegateEns} = useEnsName({ - address: currentDelegate as Address, - enabled: currentDelegate != null, - }); - - const delegateName = delegateEns ?? shortenAddress(currentDelegate ?? ''); + const needsSelfDelegation = + hasBalance && currentDelegate?.toLowerCase() !== address?.toLowerCase(); const handleCtaClick = () => { - if (noVotingPower) { + if (!hasPastVotingPower) { close(); } else { open('delegateVoting', {reclaimMode: true}); } }; - const {bodyLabel, ctaLabel} = getDelegationLabels({ - noVotingPower, + const {firstKey, secondKey} = getDelegationLabels({ + hasPastVotingPower, needsSelfDelegation, + hasBalance, + hasPastBalance, }); + const firstSentence = t(`modal.delegation.CanVote.${firstKey}`, { + tokenSymbol: daoToken?.symbol, + balance: tokenAmount, + }); + const secondSentence = t(`modal.delegation.CanVote.${secondKey}`, { + tokenSymbol: daoToken?.symbol, + }); + + const ctaLabel = return ( { )}

- {t(`modal.${bodyLabel}.title`)} + {t(`modal.delegation.CantVote.title`)}

- {t(`modal.${bodyLabel}.desc`, { - balance: tokenAmount, - tokenSymbol: daoToken?.symbol, - walletAddressDelegation: delegateName, + {t('modal.delegation.CanVote.description', { + sentence1: firstSentence, + sentence2: secondSentence, })}

diff --git a/src/locales/en/common.json b/src/locales/en/common.json index 334102889..6030f09e2 100644 --- a/src/locales/en/common.json +++ b/src/locales/en/common.json @@ -1268,6 +1268,15 @@ "ctaLabel": "Ok, understood", "Link": "Learn more", "LinkURL": "https://aragon.org/how-to/delegate-your-voting-power" + }, + "CantVote": { + "title": "You can't vote", + "description": "{{sentence1}} {{sentence2}}", + "sentence1a": "You weren't holding {{tokenSymbol}} and had no voting power when this proposal was created.", + "sentence1b": "You held {{balance}} {{tokenSymbol}} when this proposal was created but had no voting power.", + "sentence2a": "", + "sentence2b": "If you want to participate in future proposals you have to claim your voting power. ", + "sentence2c": "If you want to participate in future proposals you have to obtain {{tokenSymbol}}." } }, "delegationInactive": { @@ -1651,4 +1660,4 @@ "membersDesc": "These addresses form the governing body that approves and executes proposals passed by the community. Add the addresses who will be members of the execution multisig." } } -} \ No newline at end of file +} diff --git a/src/pages/proposal.tsx b/src/pages/proposal.tsx index f8e1149e1..65e9f2f24 100644 --- a/src/pages/proposal.tsx +++ b/src/pages/proposal.tsx @@ -18,14 +18,11 @@ import {DaoAction, ProposalStatus} from '@aragon/sdk-client-common'; import TipTapLink from '@tiptap/extension-link'; import {useEditor} from '@tiptap/react'; import StarterKit from '@tiptap/starter-kit'; -import {BigNumber, constants} from 'ethers'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {useTranslation} from 'react-i18next'; import {generatePath, useNavigate, useParams} from 'react-router-dom'; import sanitizeHtml from 'sanitize-html'; import styled from 'styled-components'; -import {Address} from 'viem'; -import {useBalance} from 'wagmi'; import {ExecutionWidget} from 'components/executionWidget'; import ResourceList from 'components/resourceList'; @@ -47,7 +44,6 @@ import {PluginTypes, usePluginClient} from 'hooks/usePluginClient'; import useScreen from 'hooks/useScreen'; import {useWallet} from 'hooks/useWallet'; import {useWalletCanVote} from 'hooks/useWalletCanVote'; -import {usePastVotingPower} from 'services/aragon-sdk/queries/use-past-voting-power'; import {useProposal} from 'services/aragon-sdk/queries/use-proposal'; import { isMultisigVotingSettings, @@ -55,7 +51,7 @@ import { useVotingSettings, } from 'services/aragon-sdk/queries/use-voting-settings'; import {useTokenAsync} from 'services/token/queries/use-token'; -import {CHAIN_METADATA, SupportedNetworks} from 'utils/constants'; +import {CHAIN_METADATA} from 'utils/constants'; import {featureFlags} from 'utils/featureFlags'; import { decodeAddMembersToAction, @@ -172,23 +168,6 @@ export const Proposal: React.FC = () => { proposalStatus as string ); - const {data: tokenBalanceData} = useBalance({ - address: address as Address, - token: daoToken?.address as Address, - chainId: CHAIN_METADATA[network as SupportedNetworks].id, - enabled: address != null && daoToken != null, - }); - const tokenBalance = BigNumber.from(tokenBalanceData?.value ?? 0); - - const {data: pastVotingPower = constants.Zero} = usePastVotingPower( - { - address: address as string, - tokenAddress: daoToken?.address as string, - blockNumber: proposal?.creationBlockNumber as number, - }, - {enabled: address != null && daoToken != null && proposal != null} - ); - const pluginClient = usePluginClient(pluginType); // ref used to hold "memories" of previous "state" @@ -201,12 +180,8 @@ export const Proposal: React.FC = () => { const [votingInProcess, setVotingInProcess] = useState(false); const [expandedProposal, setExpandedProposal] = useState(false); - // Display the voting-power gating dialog when user has balance but delegated - // his token to someone else - const shouldDisplayDelegationVoteGating = - !isMultisigPlugin && - tokenBalance.gt(constants.Zero) && - pastVotingPower.lte(constants.Zero); + // Display the delegation gating dialog when user cannot vote on token-based proposal + const displayDelegationVoteGating = !isMultisigPlugin && !canVote; const editor = useEditor({ editable: false, @@ -516,13 +491,10 @@ export const Proposal: React.FC = () => { isTokenVotingSettings(votingSettings) && votingSettings.votingMode === VotingMode.VOTE_REPLACEMENT; - let votingDisabled = - (proposal && proposal.status !== ProposalStatus.ACTIVE) || + const votingDisabled = + (address != null && proposal?.status !== ProposalStatus.ACTIVE) || (isMultisigPlugin && voted) || - (isTokenVotingPlugin && voted && canRevote === false) || - (canVote === false && shouldDisplayDelegationVoteGating === false); - - if (!address) votingDisabled = false; + (isTokenVotingPlugin && voted && !canRevote); const handleApprovalClick = useCallback( (tryExecution: boolean) => { @@ -553,18 +525,12 @@ export const Proposal: React.FC = () => { } else if (isOnWrongNetwork) { open('network'); statusRef.current.wasOnWrongNetwork = true; - } else if (shouldDisplayDelegationVoteGating) { + } else if (displayDelegationVoteGating) { return open('delegationGating'); } else if (canVote) { setVotingInProcess(true); } - }, [ - address, - canVote, - shouldDisplayDelegationVoteGating, - isOnWrongNetwork, - open, - ]); + }, [address, canVote, displayDelegationVoteGating, isOnWrongNetwork, open]); // handler for execution const handleExecuteNowClicked = () => { @@ -579,24 +545,17 @@ export const Proposal: React.FC = () => { } }; - // alert message, only shown when not eligible to vote - let alertMessage = ''; - if ( - proposal && - proposal.status === 'Active' && // active proposal + const displayAlertMessage = + proposal?.status === 'Active' && // active proposal address && // logged in - isOnWrongNetwork === false && // on proper network - voted === false && // haven't voted - canVote === false && // cannot vote - shouldDisplayDelegationVoteGating === false // user delegated tokens - ) { - // presence of token delineates token voting proposal - alertMessage = isErc20VotingProposal(proposal) - ? t('votingTerminal.status.ineligibleTokenBased', { - token: proposal.token.name, - }) - : t('votingTerminal.status.ineligibleWhitelist'); - } + !isOnWrongNetwork && // on proper network + !voted && // haven't voted + !canVote && // cannot vote + isMultisigPlugin; // is multisig plugin + + const alertMessage = displayAlertMessage + ? t('votingTerminal.status.ineligibleWhitelist') + : undefined; // status steps for proposal const proposalSteps = proposal @@ -770,10 +729,6 @@ const ContentWrapper = styled.div.attrs({ className: 'flex flex-col md:flex-row gap-x-6 gap-y-3', })``; -// const BadgeContainer = styled.div.attrs({ -// className: 'flex flex-wrap gap-x-3', -// })``; - const ProposerLink = styled.p.attrs({ className: 'text-neutral-500', })``;