Skip to content

Commit

Permalink
feat(cip-1694): integrate fetch receipt
Browse files Browse the repository at this point in the history
  • Loading branch information
vetalcore committed Jul 26, 2023
1 parent 0f66c2c commit 2e696e3
Show file tree
Hide file tree
Showing 11 changed files with 214 additions and 95 deletions.
1 change: 1 addition & 0 deletions ui/cip-1694/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ REACT_APP_CATEGORY_ID=CIP-1694_Pre_Ratification_4619

REACT_APP_EVENT_BY_ID_REFERENCE_URL=${REACT_APP_SERVER_URL}/api/reference/event
REACT_APP_CAST_VOTE_URL=${REACT_APP_SERVER_URL}/api/vote/cast
REACT_APP_VOTE_RECEIPT_URL=${REACT_APP_SERVER_URL}/api/vote/receipt
REACT_APP_BLOCKCHAIN_TIP_URL=${REACT_APP_SERVER_URL}/api/blockchain/tip
REACT_APP_VOTING_POWER_URL=${REACT_APP_SERVER_URL}/api/account

Expand Down
45 changes: 29 additions & 16 deletions ui/cip-1694/src/common/api/voteService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,51 @@ import { DEFAULT_CONTENT_TYPE_HEADERS, doRequest, HttpMethods } from '../handler
import {
EVENT_BY_ID_REFERENCE_URL,
CAST_VOTE_URL,
VOTE_RECEIPT_URL,
BLOCKCHAIN_TIP_URL,
VOTING_POWER_URL,
} from '../constants/appConstants';
import { Problem, SignedWeb3Request, Vote, Event, ChainTip, Account } from '../../types/backend-services-types';
import {
Problem,
SignedWeb3Request,
Vote,
Event,
ChainTip,
Account,
VoteReceipt,
} from '../../types/backend-services-types';

const getEventById = async (eventId: Event['id']) =>
export const getEventById = async (eventId: Event['id']) =>
await doRequest<Vote>(HttpMethods.GET, `${EVENT_BY_ID_REFERENCE_URL}/${eventId}`, {
...DEFAULT_CONTENT_TYPE_HEADERS,
});

const castAVoteWithDigitalSignature = async (jsonRequest: SignedWeb3Request) =>
export const castAVoteWithDigitalSignature = async (jsonRequest: SignedWeb3Request) =>
await doRequest<Problem | Vote>(
HttpMethods.POST,
`${CAST_VOTE_URL}`,
{ ...DEFAULT_CONTENT_TYPE_HEADERS },
CAST_VOTE_URL,
DEFAULT_CONTENT_TYPE_HEADERS,
JSON.stringify(jsonRequest),
false
);

const getSlotNumber = async () => {
return await doRequest<ChainTip>(HttpMethods.GET, `${BLOCKCHAIN_TIP_URL}`, { ...DEFAULT_CONTENT_TYPE_HEADERS });
export const getSlotNumber = async () => {
return await doRequest<ChainTip>(HttpMethods.GET, BLOCKCHAIN_TIP_URL, DEFAULT_CONTENT_TYPE_HEADERS);
};

const getVotingPower = async (eventId: Event['id'], stakeAddress: string) => {
return await doRequest<Account>(HttpMethods.GET, `${VOTING_POWER_URL}/${eventId}/${stakeAddress}`, {
...DEFAULT_CONTENT_TYPE_HEADERS,
});
export const getVoteReceipt = async (jsonRequest: SignedWeb3Request) => {
return await doRequest<Problem | VoteReceipt>(
HttpMethods.POST,
VOTE_RECEIPT_URL,
DEFAULT_CONTENT_TYPE_HEADERS,
JSON.stringify(jsonRequest)
);
};

export const voteService = {
getEventById: getEventById,
castAVoteWithDigitalSignature: castAVoteWithDigitalSignature,
getSlotNumber: getSlotNumber,
getVotingPower: getVotingPower,
export const getVotingPower = async (eventId: Event['id'], stakeAddress: string) => {
return await doRequest<Account>(
HttpMethods.GET,
`${VOTING_POWER_URL}/${eventId}/${stakeAddress}`,
DEFAULT_CONTENT_TYPE_HEADERS
);
};
1 change: 1 addition & 0 deletions ui/cip-1694/src/common/constants/appConstants.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Services URLs
export const EVENT_BY_ID_REFERENCE_URL = process.env.EVENT_BY_ID_REFERENCE_URL;
export const CAST_VOTE_URL = process.env.REACT_APP_CAST_VOTE_URL;
export const VOTE_RECEIPT_URL = process.env.REACT_APP_VOTE_RECEIPT_URL;
export const BLOCKCHAIN_TIP_URL = process.env.REACT_APP_BLOCKCHAIN_TIP_URL;
export const VOTING_POWER_URL = process.env.REACT_APP_VOTING_POWER_URL;

Expand Down
25 changes: 16 additions & 9 deletions ui/cip-1694/src/common/handlers/httpHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,10 @@ type AnuthorizedResponse = {
fault?: { faultstring?: string };
message?: string;
Error?: string | Error;
title?: string;
} & Response;
async function getErrorMessage(response: AnuthorizedResponse): Promise<string> {

const getErrorMessage = (response: AnuthorizedResponse): string => {
if (response.error) {
return response.error_description ? response.error_description : response.error;
} else if (response.errors && response.errors.length > 0) {
Expand All @@ -68,6 +70,8 @@ async function getErrorMessage(response: AnuthorizedResponse): Promise<string> {
return messages.toString();
} else if (response.fault && response.fault.faultstring) {
return response.fault.faultstring;
} else if (response.title) {
return response.title;
} else if (response.message) {
try {
const errors = JSON.parse(response.message);
Expand All @@ -84,7 +88,7 @@ async function getErrorMessage(response: AnuthorizedResponse): Promise<string> {
} else {
return '' + response;
}
}
};

export function responseErrorsHandler() {
return {
Expand All @@ -108,20 +112,23 @@ export function responseHandlerDelegate<T>() {

return {
async parse(response: Response | AnuthorizedResponse): Promise<T | never> {
let json!: T & Errors;
let parsedResponse!: T & Errors;

const contentType = response.headers.get(Headers.CONTENT_TYPE.toLowerCase());
const isJson = contentType && contentType.indexOf(MediaTypes.APPLICATION_JSON) !== -1;
try {
json = await response.json();
} catch (err) {
parsedResponse = await response[isJson ? 'json' : 'text']();
if (response.status !== 200) {
throw new HttpError(401, response.url, await getErrorMessage(response));
throw new HttpError(401, response.url, getErrorMessage(parsedResponse));
}
} catch (error) {
throw new Error(error?.message);
}

if (typeof json === 'object' && 'errors' in json && json.errors.length >= 1) {
throw new HttpError(400, response.url, errorsHandler.parse(json.errors));
if (parsedResponse?.errors?.length >= 1) {
throw new HttpError(400, response.url, errorsHandler.parse(parsedResponse.errors));
} else {
return json;
return parsedResponse;
}
},
};
Expand Down
4 changes: 4 additions & 0 deletions ui/cip-1694/src/common/store/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { VoteReceipt } from 'types/backend-services-types';

export interface UserState {
isConnectWalletModalVisible: boolean;
isVoteSubmittedModalVisible: boolean;
connectedWallet: string;
isReceiptFetched: boolean;
receipt: VoteReceipt | null;
}

export interface State {
Expand Down
17 changes: 16 additions & 1 deletion ui/cip-1694/src/common/store/userSlice.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { createSlice } from '@reduxjs/toolkit';
import type { PayloadAction } from '@reduxjs/toolkit';
import { VoteReceipt } from 'types/backend-services-types';
import { UserState } from './types';

const initialState: UserState = {
isConnectWalletModalVisible: false,
isVoteSubmittedModalVisible: false,
connectedWallet: '',
isReceiptFetched: false,
receipt: null,
};

export const userSlice = createSlice({
Expand All @@ -21,8 +24,20 @@ export const userSlice = createSlice({
setConnectedWallet: (state, action: PayloadAction<{ wallet: string }>) => {
state.connectedWallet = action.payload.wallet;
},
setVoteReceipt: (state, action: PayloadAction<{ receipt: VoteReceipt }>) => {
state.receipt = action.payload.receipt;
},
setIsReceiptFetched: (state, action: PayloadAction<{ isFetched: boolean }>) => {
state.isReceiptFetched = action.payload.isFetched;
},
},
});

export const { setIsConnectWalletModalVisible, setIsVoteSubmittedModalVisible, setConnectedWallet } = userSlice.actions;
export const {
setIsConnectWalletModalVisible,
setIsVoteSubmittedModalVisible,
setConnectedWallet,
setVoteReceipt,
setIsReceiptFetched,
} = userSlice.actions;
export default userSlice.reducer;
30 changes: 29 additions & 1 deletion ui/cip-1694/src/common/utils/voteUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,13 @@ type voteInput = {
votePower: string;
};

export const buildCanonicalVoteInputJson = ({ option, voteId, voter, slotNumber, votePower }: voteInput) => {
export const buildCanonicalVoteInputJson = ({
option,
voteId,
voter,
slotNumber,
votePower,
}: voteInput): ReturnType<typeof canonicalize> => {
const startOfCurrentDay = new Date();
startOfCurrentDay.setUTCMinutes(0, 0, 0);
return canonicalize({
Expand All @@ -30,3 +36,25 @@ export const buildCanonicalVoteInputJson = ({ option, voteId, voter, slotNumber,
},
});
};

type votereceiptInput = {
voter: string;
slotNumber: string;
};

export const buildCanonicalVoteReceiptInputJson = ({
voter,
slotNumber,
}: votereceiptInput): ReturnType<typeof canonicalize> =>
canonicalize({
uri: 'https://evoting.cardano.org/voltaire',
action: 'VIEW_VOTE_RECEIPT',
actionText: 'View Vote Receipt',
slot: slotNumber,
data: {
address: voter,
event: EVENT_ID,
category: CATEGORY_ID,
network: TARGET_NETWORK,
},
});
10 changes: 6 additions & 4 deletions ui/cip-1694/src/components/OptionCard/OptionCard.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,19 @@ import DoneIcon from '@mui/icons-material/Done';
import CloseIcon from '@mui/icons-material/Close';
import DoDisturbIcon from '@mui/icons-material/DoDisturb';
import { OptionCard } from './OptionCard';
import { OptionItem } from './OptionCard.types';

const items = [
const items: OptionItem[] = [
{
label: 'Yes',
label: 'yes',
icon: <DoneIcon />,
},
{
label: 'No',
label: 'no',
icon: <CloseIcon />,
},
{
label: 'Abstain',
label: 'abstain',
icon: <DoDisturbIcon />,
},
];
Expand All @@ -30,6 +31,7 @@ describe('<OptionCard />', () => {
cy.fixture('items.json').as('items');
cy.mount(
<OptionCard
selectedOption="Yes"
items={items}
onChangeOption={onChangeOption}
/>
Expand Down
15 changes: 11 additions & 4 deletions ui/cip-1694/src/components/OptionCard/OptionCard.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
import React, { useState, MouseEvent } from 'react';
import React, { useState, MouseEvent, useEffect } from 'react';
import cn from 'classnames';
import { Grid, Typography } from '@mui/material';
import ToggleButton from '@mui/material/ToggleButton';
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
import { OptionProps } from './OptionCard.types';
import styles from './OptionCard.module.scss';

export const OptionCard = ({ items, onChangeOption, disabled }: OptionProps) => {
const [active, setActive] = useState('');
export const OptionCard = ({ items, onChangeOption, disabled, selectedOption }: OptionProps) => {
const [active, setActive] = useState(selectedOption || '');

const handleChange = (_event: MouseEvent<HTMLElement>, _active: string | null) => {
if (disabled) return;
setActive(_active);
onChangeOption(_active);
};

useEffect(() => {
if (selectedOption) {
setActive(selectedOption);
}
}, [selectedOption]);

return (
<Grid
container
Expand All @@ -23,6 +29,7 @@ export const OptionCard = ({ items, onChangeOption, disabled }: OptionProps) =>
width={'flex'}
>
<ToggleButtonGroup
disabled={disabled}
sx={{ width: '100%' }}
color="primary"
value={active}
Expand All @@ -48,7 +55,7 @@ export const OptionCard = ({ items, onChangeOption, disabled }: OptionProps) =>
component="div"
variant="h5"
>
{option.label}
{(option.label[0].toUpperCase() + option.label.slice(1))}
</Typography>
</Grid>
</ToggleButton>
Expand Down
3 changes: 2 additions & 1 deletion ui/cip-1694/src/components/OptionCard/OptionCard.types.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import React from 'react';

interface OptionItem {
label: string;
label: 'yes' | 'no' | 'abstain';
icon: React.ReactElement | null;
}

interface OptionProps {
items: OptionItem[];
onChangeOption: (option: string) => void;
disabled?: boolean;
selectedOption: string;
}

export type { OptionItem, OptionProps };
Loading

0 comments on commit 2e696e3

Please sign in to comment.