Feat/1513: Vote buttons using subscription and dialog for tx (#1943)
* Feat/1513: Vote buttons using subscription and dialog for tx * Feat/1513: Staking journey using dialog * Feat/1513: Cleaning up unused imports * Feat/1513: Format tweak
This commit is contained in:
parent
e31422ae82
commit
d026c9bdd6
@ -302,7 +302,7 @@
|
||||
"stakingHasAssociated": "You have {{tokens}} $VEGA tokens associated.",
|
||||
"stakingAssociateMoreButton": "Associate more $VEGA tokens with wallet",
|
||||
"stakingDisassociateButton": "Disassociate $VEGA tokens from wallet",
|
||||
"stakingConfirm": "Open you wallet app to confirm",
|
||||
"stakingConfirm": "Open your wallet app to confirm",
|
||||
"associateButton": "Associate $VEGA tokens with wallet",
|
||||
"nodeQueryFailed": "Could not get data for validator {{node}}",
|
||||
"stakeAddPendingTitle": "Adding {{amount}} $VEGA to validator {{node}}",
|
||||
|
@ -1,17 +1,9 @@
|
||||
import { captureException, captureMessage } from '@sentry/minimal';
|
||||
import { captureMessage } from '@sentry/minimal';
|
||||
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { VoteValue } from '@vegaprotocol/types';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
export type Vote = {
|
||||
value: VoteValue;
|
||||
datetime: string;
|
||||
party: { id: string };
|
||||
};
|
||||
|
||||
export type Votes = Array<Vote | null>;
|
||||
|
||||
export enum VoteState {
|
||||
NotCast = 'NotCast',
|
||||
Yes = 'Yes',
|
||||
@ -21,6 +13,14 @@ export enum VoteState {
|
||||
Failed = 'Failed',
|
||||
}
|
||||
|
||||
export type Vote = {
|
||||
value: VoteValue;
|
||||
datetime: string;
|
||||
party: { id: string };
|
||||
};
|
||||
|
||||
export type Votes = Array<Vote | null>;
|
||||
|
||||
export function getUserVote(pubkey: string, yesVotes?: Votes, noVotes?: Votes) {
|
||||
const yesVote = yesVotes?.find((v) => v && v.party.id === pubkey);
|
||||
const noVote = noVotes?.find((v) => v && v.party.id === pubkey);
|
||||
@ -42,7 +42,7 @@ export function useUserVote(
|
||||
yesVotes: Votes | null,
|
||||
noVotes: Votes | null
|
||||
) {
|
||||
const { pubKey, sendTx } = useVegaWallet();
|
||||
const { pubKey } = useVegaWallet();
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const [timeout, setTimeoutValue] = useState<any>(null);
|
||||
const yes = useMemo(() => yesVotes || [], [yesVotes]);
|
||||
@ -87,37 +87,8 @@ export function useUserVote(
|
||||
return () => clearTimeout(timeout);
|
||||
}, [timeout, voteState]);
|
||||
|
||||
/**
|
||||
* Casts a vote using the users connected wallet
|
||||
*/
|
||||
async function castVote(value: VoteValue) {
|
||||
if (!proposalId || !pubKey) return;
|
||||
const previousVoteState = voteState;
|
||||
setVoteState(VoteState.Requested);
|
||||
|
||||
try {
|
||||
const res = await sendTx(pubKey, {
|
||||
voteSubmission: {
|
||||
value: value,
|
||||
proposalId,
|
||||
},
|
||||
});
|
||||
if (res === null) {
|
||||
setVoteState(previousVoteState);
|
||||
} else {
|
||||
setVoteState(VoteState.Pending);
|
||||
}
|
||||
|
||||
// Now await vote via poll in parent component
|
||||
} catch (err) {
|
||||
setVoteState(VoteState.Failed);
|
||||
captureException(err);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
voteState,
|
||||
castVote,
|
||||
userVote,
|
||||
voteDatetime: userVote ? new Date(userVote.datetime) : null,
|
||||
};
|
||||
|
@ -2,26 +2,29 @@ import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { VoteButtons } from './vote-buttons';
|
||||
import { VoteState } from './use-user-vote';
|
||||
import { ProposalState, VoteValue } from '@vegaprotocol/types';
|
||||
import { ProposalState } from '@vegaprotocol/types';
|
||||
import { VegaWalletContext } from '@vegaprotocol/wallet';
|
||||
import { mockWalletContext } from '../../test-helpers/mocks';
|
||||
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
|
||||
import { MockedProvider } from '@apollo/react-testing';
|
||||
|
||||
describe('Vote buttons', () => {
|
||||
it('should render successfully', () => {
|
||||
const { baseElement } = render(
|
||||
<AppStateProvider>
|
||||
<VegaWalletContext.Provider value={mockWalletContext}>
|
||||
<VoteButtons
|
||||
voteState={VoteState.NotCast}
|
||||
castVote={jest.fn()}
|
||||
voteDatetime={null}
|
||||
proposalState={ProposalState.STATE_OPEN}
|
||||
minVoterBalance={null}
|
||||
spamProtectionMinTokens={null}
|
||||
currentStakeAvailable={new BigNumber(1)}
|
||||
/>
|
||||
</VegaWalletContext.Provider>
|
||||
<MockedProvider>
|
||||
<VegaWalletContext.Provider value={mockWalletContext}>
|
||||
<VoteButtons
|
||||
voteState={VoteState.NotCast}
|
||||
voteDatetime={null}
|
||||
proposalState={ProposalState.STATE_OPEN}
|
||||
proposalId={null}
|
||||
minVoterBalance={null}
|
||||
spamProtectionMinTokens={null}
|
||||
currentStakeAvailable={new BigNumber(1)}
|
||||
/>
|
||||
</VegaWalletContext.Provider>
|
||||
</MockedProvider>
|
||||
</AppStateProvider>
|
||||
);
|
||||
expect(baseElement).toBeTruthy();
|
||||
@ -30,17 +33,19 @@ describe('Vote buttons', () => {
|
||||
it('should explain that voting is closed if the proposal is not open', () => {
|
||||
render(
|
||||
<AppStateProvider>
|
||||
<VegaWalletContext.Provider value={mockWalletContext}>
|
||||
<VoteButtons
|
||||
voteState={VoteState.NotCast}
|
||||
castVote={jest.fn()}
|
||||
voteDatetime={null}
|
||||
proposalState={ProposalState.STATE_PASSED}
|
||||
minVoterBalance={null}
|
||||
spamProtectionMinTokens={null}
|
||||
currentStakeAvailable={new BigNumber(1)}
|
||||
/>
|
||||
</VegaWalletContext.Provider>
|
||||
<MockedProvider>
|
||||
<VegaWalletContext.Provider value={mockWalletContext}>
|
||||
<VoteButtons
|
||||
voteState={VoteState.NotCast}
|
||||
voteDatetime={null}
|
||||
proposalState={ProposalState.STATE_PASSED}
|
||||
proposalId={null}
|
||||
minVoterBalance={null}
|
||||
spamProtectionMinTokens={null}
|
||||
currentStakeAvailable={new BigNumber(1)}
|
||||
/>
|
||||
</VegaWalletContext.Provider>
|
||||
</MockedProvider>
|
||||
</AppStateProvider>
|
||||
);
|
||||
expect(screen.getByText('Voting has ended.')).toBeTruthy();
|
||||
@ -59,17 +64,19 @@ describe('Vote buttons', () => {
|
||||
|
||||
render(
|
||||
<AppStateProvider>
|
||||
<VegaWalletContext.Provider value={mockWalletNoPubKeyContext}>
|
||||
<VoteButtons
|
||||
voteState={VoteState.NotCast}
|
||||
castVote={jest.fn()}
|
||||
voteDatetime={null}
|
||||
proposalState={ProposalState.STATE_OPEN}
|
||||
minVoterBalance={null}
|
||||
spamProtectionMinTokens={null}
|
||||
currentStakeAvailable={new BigNumber(1)}
|
||||
/>
|
||||
</VegaWalletContext.Provider>
|
||||
<MockedProvider>
|
||||
<VegaWalletContext.Provider value={mockWalletNoPubKeyContext}>
|
||||
<VoteButtons
|
||||
voteState={VoteState.NotCast}
|
||||
voteDatetime={null}
|
||||
proposalState={ProposalState.STATE_OPEN}
|
||||
proposalId={null}
|
||||
minVoterBalance={null}
|
||||
spamProtectionMinTokens={null}
|
||||
currentStakeAvailable={new BigNumber(1)}
|
||||
/>
|
||||
</VegaWalletContext.Provider>
|
||||
</MockedProvider>
|
||||
</AppStateProvider>
|
||||
);
|
||||
|
||||
@ -79,17 +86,19 @@ describe('Vote buttons', () => {
|
||||
it('should tell the user they need tokens if their current stake is 0', () => {
|
||||
render(
|
||||
<AppStateProvider>
|
||||
<VegaWalletContext.Provider value={mockWalletContext}>
|
||||
<VoteButtons
|
||||
voteState={VoteState.NotCast}
|
||||
castVote={jest.fn()}
|
||||
voteDatetime={null}
|
||||
proposalState={ProposalState.STATE_OPEN}
|
||||
minVoterBalance={null}
|
||||
spamProtectionMinTokens={null}
|
||||
currentStakeAvailable={new BigNumber(0)}
|
||||
/>
|
||||
</VegaWalletContext.Provider>
|
||||
<MockedProvider>
|
||||
<VegaWalletContext.Provider value={mockWalletContext}>
|
||||
<VoteButtons
|
||||
voteState={VoteState.NotCast}
|
||||
voteDatetime={null}
|
||||
proposalState={ProposalState.STATE_OPEN}
|
||||
proposalId={null}
|
||||
minVoterBalance={null}
|
||||
spamProtectionMinTokens={null}
|
||||
currentStakeAvailable={new BigNumber(0)}
|
||||
/>
|
||||
</VegaWalletContext.Provider>
|
||||
</MockedProvider>
|
||||
</AppStateProvider>
|
||||
);
|
||||
expect(
|
||||
@ -100,17 +109,19 @@ describe('Vote buttons', () => {
|
||||
it('should tell the user of the minimum requirements if they have some, but not enough tokens', () => {
|
||||
render(
|
||||
<AppStateProvider>
|
||||
<VegaWalletContext.Provider value={mockWalletContext}>
|
||||
<VoteButtons
|
||||
voteState={VoteState.NotCast}
|
||||
castVote={jest.fn()}
|
||||
voteDatetime={null}
|
||||
proposalState={ProposalState.STATE_OPEN}
|
||||
minVoterBalance="2000000000000000000"
|
||||
spamProtectionMinTokens="1000000000000000000"
|
||||
currentStakeAvailable={new BigNumber(1)}
|
||||
/>
|
||||
</VegaWalletContext.Provider>
|
||||
<MockedProvider>
|
||||
<VegaWalletContext.Provider value={mockWalletContext}>
|
||||
<VoteButtons
|
||||
voteState={VoteState.NotCast}
|
||||
voteDatetime={null}
|
||||
proposalState={ProposalState.STATE_OPEN}
|
||||
proposalId={null}
|
||||
minVoterBalance="2000000000000000000"
|
||||
spamProtectionMinTokens="1000000000000000000"
|
||||
currentStakeAvailable={new BigNumber(1)}
|
||||
/>
|
||||
</VegaWalletContext.Provider>
|
||||
</MockedProvider>
|
||||
</AppStateProvider>
|
||||
);
|
||||
expect(
|
||||
@ -120,148 +131,47 @@ describe('Vote buttons', () => {
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should display vote requested', () => {
|
||||
render(
|
||||
<AppStateProvider>
|
||||
<VegaWalletContext.Provider value={mockWalletContext}>
|
||||
<VoteButtons
|
||||
voteState={VoteState.Requested}
|
||||
castVote={jest.fn()}
|
||||
voteDatetime={null}
|
||||
proposalState={ProposalState.STATE_OPEN}
|
||||
minVoterBalance="2000000000000000000"
|
||||
spamProtectionMinTokens="1000000000000000000"
|
||||
currentStakeAvailable={new BigNumber(10)}
|
||||
/>
|
||||
</VegaWalletContext.Provider>
|
||||
</AppStateProvider>
|
||||
);
|
||||
expect(screen.getByTestId('vote-requested')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display vote pending', () => {
|
||||
render(
|
||||
<AppStateProvider>
|
||||
<VegaWalletContext.Provider value={mockWalletContext}>
|
||||
<VoteButtons
|
||||
voteState={VoteState.Pending}
|
||||
castVote={jest.fn()}
|
||||
voteDatetime={null}
|
||||
proposalState={ProposalState.STATE_OPEN}
|
||||
minVoterBalance="2000000000000000000"
|
||||
spamProtectionMinTokens="1000000000000000000"
|
||||
currentStakeAvailable={new BigNumber(10)}
|
||||
/>
|
||||
</VegaWalletContext.Provider>
|
||||
</AppStateProvider>
|
||||
);
|
||||
expect(screen.getByTestId('vote-pending')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show you voted if vote state is correct, and if the proposal is still open, it will display a change vote button', () => {
|
||||
render(
|
||||
<AppStateProvider>
|
||||
<VegaWalletContext.Provider value={mockWalletContext}>
|
||||
<VoteButtons
|
||||
voteState={VoteState.Yes}
|
||||
castVote={jest.fn()}
|
||||
voteDatetime={null}
|
||||
proposalState={ProposalState.STATE_OPEN}
|
||||
minVoterBalance="2000000000000000000"
|
||||
spamProtectionMinTokens="1000000000000000000"
|
||||
currentStakeAvailable={new BigNumber(10)}
|
||||
/>
|
||||
</VegaWalletContext.Provider>
|
||||
<MockedProvider>
|
||||
<VegaWalletContext.Provider value={mockWalletContext}>
|
||||
<VoteButtons
|
||||
voteState={VoteState.Yes}
|
||||
voteDatetime={null}
|
||||
proposalState={ProposalState.STATE_OPEN}
|
||||
proposalId={null}
|
||||
minVoterBalance="2000000000000000000"
|
||||
spamProtectionMinTokens="1000000000000000000"
|
||||
currentStakeAvailable={new BigNumber(10)}
|
||||
/>
|
||||
</VegaWalletContext.Provider>
|
||||
</MockedProvider>
|
||||
</AppStateProvider>
|
||||
);
|
||||
expect(screen.getByTestId('you-voted')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('change-vote-button')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display vote failure', () => {
|
||||
render(
|
||||
<AppStateProvider>
|
||||
<VegaWalletContext.Provider value={mockWalletContext}>
|
||||
<VoteButtons
|
||||
voteState={VoteState.Failed}
|
||||
castVote={jest.fn()}
|
||||
voteDatetime={null}
|
||||
proposalState={ProposalState.STATE_OPEN}
|
||||
minVoterBalance="2000000000000000000"
|
||||
spamProtectionMinTokens="1000000000000000000"
|
||||
currentStakeAvailable={new BigNumber(10)}
|
||||
/>
|
||||
</VegaWalletContext.Provider>
|
||||
</AppStateProvider>
|
||||
);
|
||||
expect(screen.getByTestId('vote-failure')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should cast yes vote when vote-for button is clicked', () => {
|
||||
const castVote = jest.fn();
|
||||
render(
|
||||
<AppStateProvider>
|
||||
<VegaWalletContext.Provider value={mockWalletContext}>
|
||||
<VoteButtons
|
||||
voteState={VoteState.NotCast}
|
||||
castVote={castVote}
|
||||
voteDatetime={null}
|
||||
proposalState={ProposalState.STATE_OPEN}
|
||||
minVoterBalance="2000000000000000000"
|
||||
spamProtectionMinTokens="1000000000000000000"
|
||||
currentStakeAvailable={new BigNumber(10)}
|
||||
/>
|
||||
</VegaWalletContext.Provider>
|
||||
</AppStateProvider>
|
||||
);
|
||||
expect(screen.getByTestId('vote-buttons')).toBeInTheDocument();
|
||||
const button = screen.getByTestId('vote-for');
|
||||
fireEvent.click(button);
|
||||
expect(castVote).toHaveBeenCalledWith(VoteValue.VALUE_YES);
|
||||
});
|
||||
|
||||
it('should cast no vote when vote-against button is clicked', () => {
|
||||
const castVote = jest.fn();
|
||||
render(
|
||||
<AppStateProvider>
|
||||
<VegaWalletContext.Provider value={mockWalletContext}>
|
||||
<VoteButtons
|
||||
voteState={VoteState.NotCast}
|
||||
castVote={castVote}
|
||||
voteDatetime={null}
|
||||
proposalState={ProposalState.STATE_OPEN}
|
||||
minVoterBalance="2000000000000000000"
|
||||
spamProtectionMinTokens="1000000000000000000"
|
||||
currentStakeAvailable={new BigNumber(10)}
|
||||
/>
|
||||
</VegaWalletContext.Provider>
|
||||
</AppStateProvider>
|
||||
);
|
||||
expect(screen.getByTestId('vote-buttons')).toBeInTheDocument();
|
||||
const button = screen.getByTestId('vote-against');
|
||||
fireEvent.click(button);
|
||||
expect(castVote).toHaveBeenCalledWith(VoteValue.VALUE_NO);
|
||||
});
|
||||
|
||||
it('should allow you to change your vote', () => {
|
||||
const castVote = jest.fn();
|
||||
render(
|
||||
<AppStateProvider>
|
||||
<VegaWalletContext.Provider value={mockWalletContext}>
|
||||
<VoteButtons
|
||||
voteState={VoteState.No}
|
||||
castVote={castVote}
|
||||
voteDatetime={null}
|
||||
proposalState={ProposalState.STATE_OPEN}
|
||||
minVoterBalance="2000000000000000000"
|
||||
spamProtectionMinTokens="1000000000000000000"
|
||||
currentStakeAvailable={new BigNumber(10)}
|
||||
/>
|
||||
</VegaWalletContext.Provider>
|
||||
<MockedProvider>
|
||||
<VegaWalletContext.Provider value={mockWalletContext}>
|
||||
<VoteButtons
|
||||
voteState={VoteState.No}
|
||||
voteDatetime={null}
|
||||
proposalState={ProposalState.STATE_OPEN}
|
||||
proposalId={null}
|
||||
minVoterBalance="2000000000000000000"
|
||||
spamProtectionMinTokens="1000000000000000000"
|
||||
currentStakeAvailable={new BigNumber(10)}
|
||||
/>
|
||||
</VegaWalletContext.Provider>
|
||||
</MockedProvider>
|
||||
</AppStateProvider>
|
||||
);
|
||||
fireEvent.click(screen.getByTestId('change-vote-button'));
|
||||
fireEvent.click(screen.getByTestId('vote-for'));
|
||||
expect(castVote).toHaveBeenCalledWith(VoteValue.VALUE_YES);
|
||||
expect(screen.getByTestId('vote-buttons')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
@ -2,29 +2,29 @@ import { gql, useQuery } from '@apollo/client';
|
||||
import { format } from 'date-fns';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
|
||||
import { AsyncRenderer, Button, ButtonLink } from '@vegaprotocol/ui-toolkit';
|
||||
import { addDecimal, toBigNum } from '@vegaprotocol/react-helpers';
|
||||
import { useVoteSubmit } from '@vegaprotocol/governance';
|
||||
import { ProposalState, VoteValue } from '@vegaprotocol/types';
|
||||
import {
|
||||
AppStateActionType,
|
||||
useAppState,
|
||||
} from '../../../../contexts/app-state/app-state-context';
|
||||
import { BigNumber } from '../../../../lib/bignumber';
|
||||
import { DATE_FORMAT_LONG } from '../../../../lib/date-formats';
|
||||
import { VoteState } from './use-user-vote';
|
||||
import { ProposalMinRequirements, ProposalUserAction } from '../shared';
|
||||
import { VoteTransactionDialog } from './vote-transaction-dialog';
|
||||
import type {
|
||||
VoteButtonsQuery as VoteButtonsQueryResult,
|
||||
VoteButtonsQueryVariables,
|
||||
} from './__generated__/VoteButtonsQuery';
|
||||
import { VoteState } from './use-user-vote';
|
||||
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
|
||||
import { ProposalState, VoteValue } from '@vegaprotocol/types';
|
||||
import { ProposalUserAction } from '../shared';
|
||||
import { AsyncRenderer, Button, ButtonLink } from '@vegaprotocol/ui-toolkit';
|
||||
import { ProposalMinRequirements } from '../shared';
|
||||
import { addDecimal, toBigNum } from '@vegaprotocol/react-helpers';
|
||||
|
||||
interface VoteButtonsContainerProps {
|
||||
voteState: VoteState | null;
|
||||
castVote: (vote: VoteValue) => void;
|
||||
voteDatetime: Date | null;
|
||||
proposalId: string | null;
|
||||
proposalState: ProposalState;
|
||||
minVoterBalance: string | null;
|
||||
spamProtectionMinTokens: string | null;
|
||||
@ -74,9 +74,9 @@ interface VoteButtonsProps extends VoteButtonsContainerProps {
|
||||
|
||||
export const VoteButtons = ({
|
||||
voteState,
|
||||
castVote,
|
||||
voteDatetime,
|
||||
proposalState,
|
||||
proposalId,
|
||||
currentStakeAvailable,
|
||||
minVoterBalance,
|
||||
spamProtectionMinTokens,
|
||||
@ -84,6 +84,7 @@ export const VoteButtons = ({
|
||||
const { t } = useTranslation();
|
||||
const { appDispatch } = useAppState();
|
||||
const { pubKey } = useVegaWallet();
|
||||
const { submit, Dialog } = useVoteSubmit();
|
||||
const { openVegaWalletDialog } = useVegaWalletDialogStore((store) => ({
|
||||
openVegaWalletDialog: store.openVegaWalletDialog,
|
||||
}));
|
||||
@ -161,7 +162,7 @@ export const VoteButtons = ({
|
||||
|
||||
function submitVote(vote: VoteValue) {
|
||||
setChangeVote(false);
|
||||
castVote(vote);
|
||||
submit(vote, proposalId);
|
||||
}
|
||||
|
||||
// Should only render null for a split second while initial vote state
|
||||
@ -174,64 +175,56 @@ export const VoteButtons = ({
|
||||
return <div>{cantVoteUI}</div>;
|
||||
}
|
||||
|
||||
if (voteState === VoteState.Requested) {
|
||||
return <p data-testid="vote-requested">{t('voteRequested')}...</p>;
|
||||
}
|
||||
|
||||
if (voteState === VoteState.Pending) {
|
||||
return <p data-testid="vote-pending">{t('votePending')}...</p>;
|
||||
}
|
||||
|
||||
// If voted show current vote info`
|
||||
if (
|
||||
!changeVote &&
|
||||
(voteState === VoteState.Yes || voteState === VoteState.No)
|
||||
) {
|
||||
const className =
|
||||
voteState === VoteState.Yes ? 'text-success' : 'text-danger';
|
||||
return (
|
||||
<p data-testid="you-voted">
|
||||
<span>{t('youVoted')}:</span>{' '}
|
||||
<span className={className}>{t(`voteState_${voteState}`)}</span>{' '}
|
||||
{voteDatetime ? (
|
||||
<span>{format(voteDatetime, DATE_FORMAT_LONG)}. </span>
|
||||
) : null}
|
||||
{proposalVotable ? (
|
||||
<ButtonLink
|
||||
data-testid="change-vote-button"
|
||||
onClick={() => {
|
||||
setChangeVote(true);
|
||||
}}
|
||||
>
|
||||
{t('changeVote')}
|
||||
</ButtonLink>
|
||||
) : null}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
if (!changeVote && voteState === VoteState.Failed) {
|
||||
return <p data-testid="vote-failure">{t('voteError')}</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex gap-4" data-testid="vote-buttons">
|
||||
<div className="flex-1">
|
||||
<Button
|
||||
data-testid="vote-for"
|
||||
onClick={() => submitVote(VoteValue.VALUE_YES)}
|
||||
>
|
||||
{t('voteFor')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<Button
|
||||
data-testid="vote-against"
|
||||
onClick={() => submitVote(VoteValue.VALUE_NO)}
|
||||
>
|
||||
{t('voteAgainst')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<>
|
||||
{changeVote || (voteState === VoteState.NotCast && proposalVotable) ? (
|
||||
<div className="flex gap-4" data-testid="vote-buttons">
|
||||
<div className="flex-1">
|
||||
<Button
|
||||
data-testid="vote-for"
|
||||
onClick={() => submitVote(VoteValue.VALUE_YES)}
|
||||
>
|
||||
{t('voteFor')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<Button
|
||||
data-testid="vote-against"
|
||||
onClick={() => submitVote(VoteValue.VALUE_NO)}
|
||||
>
|
||||
{t('voteAgainst')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
(voteState === VoteState.Yes || voteState === VoteState.No) && (
|
||||
<p data-testid="you-voted">
|
||||
<span>{t('youVoted')}:</span>{' '}
|
||||
<span
|
||||
className={
|
||||
voteState === VoteState.Yes ? 'text-success' : 'text-danger'
|
||||
}
|
||||
>
|
||||
{t(`voteState_${voteState}`)}
|
||||
</span>{' '}
|
||||
{voteDatetime ? (
|
||||
<span>{format(voteDatetime, DATE_FORMAT_LONG)}. </span>
|
||||
) : null}
|
||||
{proposalVotable ? (
|
||||
<ButtonLink
|
||||
data-testid="change-vote-button"
|
||||
onClick={() => {
|
||||
setChangeVote(true);
|
||||
voteState = VoteState.NotCast;
|
||||
}}
|
||||
>
|
||||
{t('changeVote')}
|
||||
</ButtonLink>
|
||||
) : null}
|
||||
</p>
|
||||
)
|
||||
)}
|
||||
<VoteTransactionDialog voteState={voteState} TransactionDialog={Dialog} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -39,12 +39,11 @@ export const VoteDetails = ({
|
||||
} = useVoteInformation({ proposal });
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { voteState, voteDatetime, castVote } = useUserVote(
|
||||
const { voteState, voteDatetime } = useUserVote(
|
||||
proposal.id,
|
||||
proposal.votes.yes.votes,
|
||||
proposal.votes.no.votes
|
||||
);
|
||||
|
||||
const defaultDecimals = 2;
|
||||
|
||||
return (
|
||||
@ -164,9 +163,9 @@ export const VoteDetails = ({
|
||||
<h3 className="text-xl mb-2">{t('yourVote')}</h3>
|
||||
<VoteButtonsContainer
|
||||
voteState={voteState}
|
||||
castVote={castVote}
|
||||
voteDatetime={voteDatetime}
|
||||
proposalState={proposal.state}
|
||||
proposalId={proposal.id}
|
||||
minVoterBalance={minVoterBalance}
|
||||
spamProtectionMinTokens={spamProtectionMinTokens}
|
||||
className="flex"
|
||||
|
@ -0,0 +1,40 @@
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { VoteState } from './use-user-vote';
|
||||
import type { DialogProps } from '@vegaprotocol/wallet';
|
||||
|
||||
interface VoteTransactionDialogProps {
|
||||
voteState: VoteState;
|
||||
TransactionDialog: (props: DialogProps) => JSX.Element;
|
||||
}
|
||||
|
||||
const dialogTitle = (voteState: VoteState): string | undefined => {
|
||||
switch (voteState) {
|
||||
case VoteState.Requested:
|
||||
return t('voteRequested');
|
||||
case VoteState.Pending:
|
||||
return t('votePending');
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
export const VoteTransactionDialog = ({
|
||||
voteState,
|
||||
TransactionDialog,
|
||||
}: VoteTransactionDialogProps) => {
|
||||
// Render a custom message if the voting fails otherwise
|
||||
// pass undefined so that the default vega transaction dialog UI gets used
|
||||
const customMessage =
|
||||
voteState === VoteState.Failed ? <p>{t('voteError')}</p> : undefined;
|
||||
|
||||
return (
|
||||
<div data-testid="vote-transaction-dialog">
|
||||
<TransactionDialog
|
||||
title={dialogTitle(voteState)}
|
||||
content={{
|
||||
Complete: customMessage,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,4 +1,4 @@
|
||||
import { Callout, Intent } from '@vegaprotocol/ui-toolkit';
|
||||
import { Dialog, Intent } from '@vegaprotocol/ui-toolkit';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface StakeFailureProps {
|
||||
@ -8,12 +8,16 @@ interface StakeFailureProps {
|
||||
export const StakeFailure = ({ nodeName }: StakeFailureProps) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Callout intent={Intent.Danger} title={t('Something went wrong')}>
|
||||
<Dialog
|
||||
intent={Intent.Danger}
|
||||
title={t('Something went wrong')}
|
||||
open={true}
|
||||
>
|
||||
<p>
|
||||
{t('stakeFailed', {
|
||||
node: nodeName,
|
||||
})}
|
||||
</p>
|
||||
</Callout>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Callout, Loader } from '@vegaprotocol/ui-toolkit';
|
||||
import { Dialog, Loader } from '@vegaprotocol/ui-toolkit';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Actions } from './staking-form';
|
||||
import type { StakeAction } from './staking-form';
|
||||
@ -22,8 +22,8 @@ export const StakePending = ({
|
||||
: t('stakeRemovePendingTitle', titleArgs);
|
||||
|
||||
return (
|
||||
<Callout icon={<Loader size="small" />} title={title}>
|
||||
<Dialog icon={<Loader size="small" />} title={title} open={true}>
|
||||
<p>{t('timeForConfirmation')}</p>
|
||||
</Callout>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Callout, Intent } from '@vegaprotocol/ui-toolkit';
|
||||
import { Dialog, Icon, Intent } from '@vegaprotocol/ui-toolkit';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
import Routes from '../../routes';
|
||||
@ -30,7 +30,12 @@ export const StakeSuccess = ({
|
||||
: t('stakeRemoveNowSuccessMessage');
|
||||
|
||||
return (
|
||||
<Callout iconName="tick" intent={Intent.Success} title={title}>
|
||||
<Dialog
|
||||
icon={<Icon name="tick" />}
|
||||
intent={Intent.Success}
|
||||
title={title}
|
||||
open={true}
|
||||
>
|
||||
<div>
|
||||
<p>{message}</p>
|
||||
<p>
|
||||
@ -39,6 +44,6 @@ export const StakeSuccess = ({
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</Callout>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
@ -17,7 +17,7 @@ import { StakePending } from './stake-pending';
|
||||
import { StakeSuccess } from './stake-success';
|
||||
import {
|
||||
ButtonLink,
|
||||
Callout,
|
||||
Dialog,
|
||||
FormGroup,
|
||||
Intent,
|
||||
Radio,
|
||||
@ -200,9 +200,13 @@ export const StakingForm = ({
|
||||
return <StakeFailure nodeName={nodeName} />;
|
||||
} else if (formState === FormState.Requested) {
|
||||
return (
|
||||
<Callout title="Confirm transaction in wallet" intent={Intent.Warning}>
|
||||
<Dialog
|
||||
title="Confirm transaction in wallet"
|
||||
intent={Intent.Warning}
|
||||
open={true}
|
||||
>
|
||||
<p>{t('stakingConfirm')}</p>
|
||||
</Callout>
|
||||
</Dialog>
|
||||
);
|
||||
} else if (formState === FormState.Pending) {
|
||||
return <StakePending action={action} amount={amount} nodeName={nodeName} />;
|
||||
|
@ -1,2 +1,3 @@
|
||||
export * from './proposals-hooks';
|
||||
export * from './proposals-queries';
|
||||
export * from './voting-hooks';
|
||||
|
18
libs/governance/src/lib/voting-hooks/VoteSubsciption.graphql
Normal file
18
libs/governance/src/lib/voting-hooks/VoteSubsciption.graphql
Normal file
@ -0,0 +1,18 @@
|
||||
fragment VoteEventFields on Vote {
|
||||
proposalId
|
||||
value
|
||||
datetime
|
||||
}
|
||||
|
||||
subscription VoteEvent($partyId: ID!) {
|
||||
busEvents(partyId: $partyId, batchSize: 0, types: [Vote]) {
|
||||
type
|
||||
event {
|
||||
... on Vote {
|
||||
proposalId
|
||||
value
|
||||
datetime
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
import { Schema as Types } from '@vegaprotocol/types';
|
||||
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type VoteEventFieldsFragment = { __typename?: 'Vote', proposalId: string, value: Types.VoteValue, datetime: string };
|
||||
|
||||
export type VoteEventSubscriptionVariables = Types.Exact<{
|
||||
partyId: Types.Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type VoteEventSubscription = { __typename?: 'Subscription', busEvents?: Array<{ __typename?: 'BusEvent', type: Types.BusEventType, event: { __typename?: 'AccountEvent' } | { __typename?: 'Asset' } | { __typename?: 'AuctionEvent' } | { __typename?: 'Deposit' } | { __typename?: 'LiquidityProvision' } | { __typename?: 'LossSocialization' } | { __typename?: 'MarginLevels' } | { __typename?: 'Market' } | { __typename?: 'MarketData' } | { __typename?: 'MarketEvent' } | { __typename?: 'MarketTick' } | { __typename?: 'NodeSignature' } | { __typename?: 'OracleSpec' } | { __typename?: 'Order' } | { __typename?: 'Party' } | { __typename?: 'PositionResolution' } | { __typename?: 'Proposal' } | { __typename?: 'RiskFactor' } | { __typename?: 'SettleDistressed' } | { __typename?: 'SettlePosition' } | { __typename?: 'TimeUpdate' } | { __typename?: 'Trade' } | { __typename?: 'TransactionResult' } | { __typename?: 'TransferResponses' } | { __typename?: 'Vote', proposalId: string, value: Types.VoteValue, datetime: string } | { __typename?: 'Withdrawal' } }> | null };
|
||||
|
||||
export const VoteEventFieldsFragmentDoc = gql`
|
||||
fragment VoteEventFields on Vote {
|
||||
proposalId
|
||||
value
|
||||
datetime
|
||||
}
|
||||
`;
|
||||
export const VoteEventDocument = gql`
|
||||
subscription VoteEvent($partyId: ID!) {
|
||||
busEvents(partyId: $partyId, batchSize: 0, types: [Vote]) {
|
||||
type
|
||||
event {
|
||||
... on Vote {
|
||||
proposalId
|
||||
value
|
||||
datetime
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useVoteEventSubscription__
|
||||
*
|
||||
* To run a query within a React component, call `useVoteEventSubscription` and pass it any options that fit your needs.
|
||||
* When your component renders, `useVoteEventSubscription` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the subscription, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useVoteEventSubscription({
|
||||
* variables: {
|
||||
* partyId: // value for 'partyId'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useVoteEventSubscription(baseOptions: Apollo.SubscriptionHookOptions<VoteEventSubscription, VoteEventSubscriptionVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useSubscription<VoteEventSubscription, VoteEventSubscriptionVariables>(VoteEventDocument, options);
|
||||
}
|
||||
export type VoteEventSubscriptionHookResult = ReturnType<typeof useVoteEventSubscription>;
|
||||
export type VoteEventSubscriptionResult = Apollo.SubscriptionResult<VoteEventSubscription>;
|
1
libs/governance/src/lib/voting-hooks/index.ts
Normal file
1
libs/governance/src/lib/voting-hooks/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './use-vote-submit';
|
62
libs/governance/src/lib/voting-hooks/use-vote-event.ts
Normal file
62
libs/governance/src/lib/voting-hooks/use-vote-event.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { VoteEventDocument } from './__generated___/VoteSubsciption';
|
||||
import type { Subscription } from 'zen-observable-ts';
|
||||
import type { VegaTxState } from '@vegaprotocol/wallet';
|
||||
import type {
|
||||
VoteEventFieldsFragment,
|
||||
VoteEventSubscription,
|
||||
VoteEventSubscriptionVariables,
|
||||
} from './__generated___/VoteSubsciption';
|
||||
|
||||
export const useVoteEvent = (transaction: VegaTxState) => {
|
||||
const client = useApolloClient();
|
||||
const subRef = useRef<Subscription | null>(null);
|
||||
|
||||
const waitForVoteEvent = useCallback(
|
||||
(
|
||||
proposalId: string,
|
||||
partyId: string,
|
||||
callback: (vote: VoteEventFieldsFragment) => void
|
||||
) => {
|
||||
subRef.current = client
|
||||
.subscribe<VoteEventSubscription, VoteEventSubscriptionVariables>({
|
||||
query: VoteEventDocument,
|
||||
variables: { partyId },
|
||||
})
|
||||
.subscribe(({ data }) => {
|
||||
if (!data?.busEvents?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const matchingVoteEvent = data.busEvents.find((e) => {
|
||||
if (e.event.__typename !== 'Vote') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return e.event.proposalId === proposalId;
|
||||
});
|
||||
|
||||
if (
|
||||
matchingVoteEvent &&
|
||||
matchingVoteEvent.event.__typename === 'Vote'
|
||||
) {
|
||||
callback(matchingVoteEvent.event);
|
||||
subRef.current?.unsubscribe();
|
||||
}
|
||||
});
|
||||
},
|
||||
[client]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!transaction.dialogOpen) {
|
||||
subRef.current?.unsubscribe();
|
||||
}
|
||||
return () => {
|
||||
subRef.current?.unsubscribe();
|
||||
};
|
||||
}, [transaction.dialogOpen]);
|
||||
|
||||
return waitForVoteEvent;
|
||||
};
|
51
libs/governance/src/lib/voting-hooks/use-vote-submit.ts
Normal file
51
libs/governance/src/lib/voting-hooks/use-vote-submit.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { useVegaTransaction, useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { useVoteEvent } from './use-vote-event';
|
||||
import type { VoteValue } from '@vegaprotocol/types';
|
||||
import type { VoteEventFieldsFragment } from './__generated___/VoteSubsciption';
|
||||
|
||||
export const useVoteSubmit = () => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
const { send, transaction, setComplete, Dialog } = useVegaTransaction();
|
||||
const waitForVoteEvent = useVoteEvent(transaction);
|
||||
|
||||
const [finalizedVote, setFinalizedVote] =
|
||||
useState<VoteEventFieldsFragment | null>(null);
|
||||
|
||||
const submit = useCallback(
|
||||
async (voteValue: VoteValue, proposalId: string | null) => {
|
||||
if (!pubKey || !voteValue || !proposalId) {
|
||||
return;
|
||||
}
|
||||
|
||||
setFinalizedVote(null);
|
||||
|
||||
try {
|
||||
const res = await send(pubKey, {
|
||||
voteSubmission: {
|
||||
value: voteValue,
|
||||
proposalId,
|
||||
},
|
||||
});
|
||||
|
||||
if (res) {
|
||||
waitForVoteEvent(proposalId, pubKey, (v) => {
|
||||
setFinalizedVote(v);
|
||||
setComplete();
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
Sentry.captureException(e);
|
||||
}
|
||||
},
|
||||
[pubKey, send, setComplete, waitForVoteEvent]
|
||||
);
|
||||
|
||||
return {
|
||||
transaction,
|
||||
finalizedVote,
|
||||
Dialog,
|
||||
submit,
|
||||
};
|
||||
};
|
Loading…
Reference in New Issue
Block a user