diff --git a/packages/chakra-components/package.json b/packages/chakra-components/package.json index f76732cc..203b6885 100644 --- a/packages/chakra-components/package.json +++ b/packages/chakra-components/package.json @@ -38,12 +38,13 @@ "@chakra-ui/progress": "^2.1.6", "@chakra-ui/radio": "^2.0.22", "@chakra-ui/react-use-disclosure": "^2.1.0", + "@chakra-ui/spinner": "^2.1.0", "@chakra-ui/system": "2.5.5", "@chakra-ui/table": "^2.0.17", "@chakra-ui/tag": "^3.0.0", "@chakra-ui/theme": "^3.0.0", "@chakra-ui/toast": "^6.1.1", - "@vocdoni/sdk": "~0.5.1", + "@vocdoni/sdk": "~0.6.0", "react": ">= 16.8.0", "react-dom": ">= 16.8.0", "react-markdown": ">= 8.0.0", @@ -60,6 +61,7 @@ "@chakra-ui/progress": "^2.1.6", "@chakra-ui/radio": "^2.0.22", "@chakra-ui/react-use-disclosure": "^2.1.0", + "@chakra-ui/spinner": "^2.1.0", "@chakra-ui/system": "2.5.5", "@chakra-ui/table": "^2.0.17", "@chakra-ui/tag": "^3.0.0", @@ -69,7 +71,7 @@ "@ethersproject/wallet": "^5.7.0", "@types/react": "^18.0.30", "@types/react-dom": "^18.0.11", - "@vocdoni/sdk": "~0.5.1", + "@vocdoni/sdk": "~0.6.0", "clean-package": "^2.2.0", "date-fns": "^2.29.3", "eslint": "^8.42.0", diff --git a/packages/chakra-components/src/components/Election/Questions.tsx b/packages/chakra-components/src/components/Election/Questions.tsx index bdcc5d76..8890eb0e 100644 --- a/packages/chakra-components/src/components/Election/Questions.tsx +++ b/packages/chakra-components/src/components/Election/Questions.tsx @@ -1,6 +1,8 @@ import { Alert, AlertDescription, AlertIcon, AlertTitle } from '@chakra-ui/alert' +import { Button } from '@chakra-ui/button' import { FormControl, FormErrorMessage } from '@chakra-ui/form-control' import { Box, Link, Stack, Text } from '@chakra-ui/layout' +import { ModalBody, ModalCloseButton, ModalFooter, ModalHeader } from '@chakra-ui/modal' import { Radio, RadioGroup } from '@chakra-ui/radio' import { chakra, ChakraProps, omitThemingProps, useMultiStyleConfig } from '@chakra-ui/system' import { Wallet } from '@ethersproject/wallet' @@ -91,23 +93,39 @@ export type QuestionsConfirmationProps = { } export const QuestionsConfirmation = ({ answers, questions, ...rest }: QuestionsConfirmationProps) => { + const mstyles = useMultiStyleConfig('ConfirmModal') const styles = useMultiStyleConfig('QuestionsConfirmation', rest) + const { cancel, proceed } = useConfirm() const props = omitThemingProps(rest) const { localize } = useClient() return ( - - {localize('vote.confirm')} - {questions.map((q, k) => { - const choice = q.choices.find((v) => v.value === parseInt(answers[k.toString()], 10)) - return ( - - {q.title.default} - {choice?.title.default} - - ) - })} - + <> + {localize('confirm.title')} + + + + {localize('vote.confirm')} + {questions.map((q, k) => { + const choice = q.choices.find((v) => v.value === parseInt(answers[k.toString()], 10)) + return ( + + {q.title.default} + {choice?.title.default} + + ) + })} + + + + + + + ) } diff --git a/packages/chakra-components/src/components/Election/SpreadsheetAccess.tsx b/packages/chakra-components/src/components/Election/SpreadsheetAccess.tsx index 31135301..2d3c04e4 100644 --- a/packages/chakra-components/src/components/Election/SpreadsheetAccess.tsx +++ b/packages/chakra-components/src/components/Election/SpreadsheetAccess.tsx @@ -24,8 +24,8 @@ export const SpreadsheetAccess = (rest: ChakraProps) => { const { connected, clearClient } = useElection() const [loading, setLoading] = useState(false) const toast = useToast() - const { env, setSikPassword, setSikSignature } = useClient() - const { election, setClient, localize, fetchCensus } = useElection() + const { env, client: cl } = useClient() + const { election, setClient, localize, fetchCensus, sikPassword, sikSignature } = useElection() const fields: string[] = dotobject(election, 'meta.census.fields') const { register, @@ -45,7 +45,9 @@ export const SpreadsheetAccess = (rest: ChakraProps) => { } // create wallet and client - const wallet = walletFromRow(election!.organizationId, Object.values(vals)) + const hid = await cl.electionService.getNumericElectionId(election!.id) + const salt = await cl.electionService.getElectionSalt(election!.organizationId, hid) + const wallet = walletFromRow(salt, Object.values(vals)) const client = new VocdoniSDKClient({ env, wallet, @@ -63,8 +65,8 @@ export const SpreadsheetAccess = (rest: ChakraProps) => { fetchCensus() // store SIK requirements to client on anon elections if (election?.electionType.anonymous && sikp) { - setSikPassword(sikp) - setSikSignature(await client.anonymousService.signSIKPayload(wallet)) + sikPassword(sikp) + sikSignature(await client.anonymousService.signSIKPayload(wallet)) } // in case of success, set current client setClient(client) diff --git a/packages/chakra-components/src/components/Election/VoteButton.tsx b/packages/chakra-components/src/components/Election/VoteButton.tsx index db6128c0..90f216fb 100644 --- a/packages/chakra-components/src/components/Election/VoteButton.tsx +++ b/packages/chakra-components/src/components/Election/VoteButton.tsx @@ -2,14 +2,11 @@ import { Button, ButtonProps } from '@chakra-ui/button' import { Signer } from '@ethersproject/abstract-signer' import { useClient, useElection } from '@vocdoni/react-providers' import { ArchivedElection, ElectionStatus, InvalidElection } from '@vocdoni/sdk' +import { useState } from 'react' import { SpreadsheetAccess } from './SpreadsheetAccess' export const VoteButton = (props: ButtonProps) => { - const { - connected, - sik: { signature }, - setSikSignature, - } = useClient() + const { connected } = useClient() const { client, loading: { voting }, @@ -19,7 +16,10 @@ export const VoteButton = (props: ButtonProps) => { election, voted, localize, + sik: { signature }, + sikSignature, } = useElection() + const [loading, setLoading] = useState(false) const isDisabled = !client.wallet || !isAbleToVote || election?.status !== ElectionStatus.ONGOING if (election instanceof InvalidElection || election instanceof ArchivedElection) return null @@ -42,10 +42,15 @@ export const VoteButton = (props: ButtonProps) => { } if (connected && election?.electionType.anonymous && !signature) { + button.isLoading = loading button.type = 'button' - button.children = localize('vote.identify') + button.children = localize('vote.sign') button.onClick = async () => { - setSikSignature(await client.anonymousService.signSIKPayload(client.wallet as Signer)) + setLoading(true) + try { + sikSignature(await client.anonymousService.signSIKPayload(client.wallet as Signer)) + } catch (e) {} + setLoading(false) } } diff --git a/packages/chakra-components/src/components/layout/ConfirmModal/ConfirmModal.tsx b/packages/chakra-components/src/components/layout/ConfirmModal/ConfirmModal.tsx index 7cb9b72a..a7528241 100644 --- a/packages/chakra-components/src/components/layout/ConfirmModal/ConfirmModal.tsx +++ b/packages/chakra-components/src/components/layout/ConfirmModal/ConfirmModal.tsx @@ -1,38 +1,15 @@ -import { Button } from '@chakra-ui/button' -import { - Modal, - ModalBody, - ModalCloseButton, - ModalContent, - ModalFooter, - ModalHeader, - ModalOverlay, -} from '@chakra-ui/modal' +import { Modal, ModalContent, ModalOverlay } from '@chakra-ui/modal' import { useMultiStyleConfig } from '@chakra-ui/system' -import { useClient } from '@vocdoni/react-providers' import { useConfirm } from './ConfirmProvider' export const ConfirmModal = () => { const styles = useMultiStyleConfig('ConfirmModal') - const { prompt, isOpen, proceed, cancel } = useConfirm() - const { localize } = useClient() + const { prompt, isOpen, cancel } = useConfirm() return ( - - {localize('confirm.title')} - - {prompt} - - - - - + {prompt} ) } diff --git a/packages/chakra-components/src/i18n/locales.ts b/packages/chakra-components/src/i18n/locales.ts index 0f35d20b..1ecbdfc3 100644 --- a/packages/chakra-components/src/i18n/locales.ts +++ b/packages/chakra-components/src/i18n/locales.ts @@ -22,14 +22,6 @@ export const locales = { cancel: 'Cancel', confirm: 'Confirm', }, - // questions and vote button - vote: { - button_update: 'Re-submit vote', - button: 'Vote', - confirm: 'Please confirm your choices:', - voted_description: 'Your vote id is {{ id }}. You can use it to verify your vote.', - voted_title: 'Your vote was successfully cast!', - }, empty: 'Apparently this process has no questions 🤔', errors: { wrong_data_title: 'Wrong data', @@ -66,6 +58,15 @@ export const locales = { required: 'This field is required', min_length: 'This field must be at least {{ min }} characters long', }, + // questions and vote button + vote: { + button_update: 'Re-submit vote', + button: 'Vote', + confirm: 'Please confirm your choices:', + sign: 'Sign first', + voted_description: 'Your vote id is {{ id }}. You can use it to verify your vote.', + voted_title: 'Your vote was successfully cast!', + }, } export type Locale = RecursivePartial diff --git a/packages/chakra-components/src/theme/confirm.ts b/packages/chakra-components/src/theme/confirm.ts index 0232f5ef..5d863103 100644 --- a/packages/chakra-components/src/theme/confirm.ts +++ b/packages/chakra-components/src/theme/confirm.ts @@ -13,6 +13,8 @@ export const confirmAnatomy = [ 'close', ] +export const signModalAnatomy = ['body', 'description', 'footer', 'button'] + const { defineMultiStyleConfig, definePartsStyle } = createMultiStyleConfigHelpers(confirmAnatomy) const baseStyle = definePartsStyle({ diff --git a/packages/react-providers/package.json b/packages/react-providers/package.json index 6675bb9c..29fc5b94 100644 --- a/packages/react-providers/package.json +++ b/packages/react-providers/package.json @@ -24,7 +24,7 @@ }, "main": "src/index.ts", "peerDependencies": { - "@vocdoni/sdk": "~0.5.3", + "@vocdoni/sdk": "~0.6.0", "react": ">= 16.8.0" }, "devDependencies": { @@ -32,7 +32,7 @@ "@ethersproject/wallet": "^5.7.0", "@types/latinize": "^0.2.15", "@types/react": "^18.0.30", - "@vocdoni/sdk": "^0.5.3", + "@vocdoni/sdk": "~0.6.0", "eslint": "^8.42.0", "eslint-config-prettier": "^8.8.0", "eslint-config-react-app": "^7.0.1", diff --git a/packages/react-providers/src/client/use-client-provider.ts b/packages/react-providers/src/client/use-client-provider.ts index 4ca25511..38bd0aaf 100644 --- a/packages/react-providers/src/client/use-client-provider.ts +++ b/packages/react-providers/src/client/use-client-provider.ts @@ -1,5 +1,5 @@ import { Wallet } from '@ethersproject/wallet' -import { Account, AccountData, EnvOptions, VocdoniSDKClient } from '@vocdoni/sdk' +import { Account, EnvOptions, VocdoniSDKClient } from '@vocdoni/sdk' import { useEffect } from 'react' import { useLocalize } from '../i18n/localize' import { ClientReducerProps, newVocdoniSDKClient, useClientReducer } from './use-client-reducer' @@ -105,8 +105,6 @@ export const useClientProvider = ({ client: c, env: e, signer: s, options: o }: clear: actions.clear, setClient: actions.setClient, setSigner: actions.setSigner, - setSikPassword: actions.setSikPassword, - setSikSignature: actions.setSikSignature, createAccount, fetchAccount, generateSigner, diff --git a/packages/react-providers/src/client/use-client-reducer.ts b/packages/react-providers/src/client/use-client-reducer.ts index c6428068..08d21963 100644 --- a/packages/react-providers/src/client/use-client-reducer.ts +++ b/packages/react-providers/src/client/use-client-reducer.ts @@ -30,15 +30,12 @@ export const ClientClear = 'client:clear' export const ClientEnvSet = 'client:env:set' export const ClientSet = 'client:set' export const ClientSignerSet = 'client:signer:set' -export const ClientSikPasswordSet = 'client:sikpassword:set' -export const ClientSikSignatureSet = 'client:siksignature:set' export type ClientAccountErrorPayload = ErrorPayload export type ClientAccountSetPayload = AccountData export type ClientEnvSetPayload = ClientEnv export type ClientSetPayload = VocdoniSDKClient export type ClientSignerSetPayload = Signer | Wallet -export type ClientSikPayload = string | null export type ClientActionPayload = | ClientAccountErrorPayload @@ -46,7 +43,6 @@ export type ClientActionPayload = | ClientEnvSetPayload | ClientSetPayload | ClientSignerSetPayload - | ClientSikPayload export type ClientActionType = | typeof ClientAccountCreate @@ -60,8 +56,6 @@ export type ClientActionType = | typeof ClientEnvSet | typeof ClientSet | typeof ClientSignerSet - | typeof ClientSikPasswordSet - | typeof ClientSikSignatureSet export interface ClientAction { type: ClientActionType @@ -104,10 +98,6 @@ export interface ClientState { api_url?: string faucet_url?: string } - sik: { - password: string | null - signature: string | null - } } export const clientStateEmpty = ( @@ -139,10 +129,6 @@ export const clientStateEmpty = ( fetch: null, }, options, - sik: { - password: null, - signature: null, - }, }) const clientReducer: Reducer = (state: ClientState, action: ClientAction) => { @@ -212,10 +198,6 @@ const clientReducer: Reducer = (state: ClientState, a create: null, fetch: null, }, - sik: { - password: null, - signature: null, - }, } } case ClientClear: { @@ -254,26 +236,6 @@ const clientReducer: Reducer = (state: ClientState, a connected: !!client.wallet, } } - case ClientSikPasswordSet: { - const password = action.payload as ClientSikPayload - return { - ...state, - sik: { - ...state.sik, - password, - }, - } - } - case ClientSikSignatureSet: { - const signature = action.payload as ClientSikPayload - return { - ...state, - sik: { - ...state.sik, - signature, - }, - } - } default: return state } @@ -310,8 +272,6 @@ export const useClientReducer = ({ env, client, signer, options }: ClientReducer const setClient = (client: VocdoniSDKClient) => dispatch({ type: ClientSet, payload: client }) const setEnv = (env: ClientEnvSetPayload) => dispatch({ type: ClientEnvSet, payload: env }) const setSigner = (signer: Wallet | Signer) => dispatch({ type: ClientSignerSet, payload: signer }) - const setSikPassword = (password: string | null) => dispatch({ type: ClientSikPasswordSet, payload: password }) - const setSikSignature = (signature: string | null) => dispatch({ type: ClientSikSignatureSet, payload: signature }) return { state, @@ -327,8 +287,6 @@ export const useClientReducer = ({ env, client, signer, options }: ClientReducer setClient, setEnv, setSigner, - setSikPassword, - setSikSignature, }, } } diff --git a/packages/react-providers/src/election/use-election-provider.ts b/packages/react-providers/src/election/use-election-provider.ts index 1768dc0d..5122b89c 100644 --- a/packages/react-providers/src/election/use-election-provider.ts +++ b/packages/react-providers/src/election/use-election-provider.ts @@ -30,13 +30,16 @@ export const useElectionProvider = ({ autoUpdate, ...rest }: ElectionProviderProps) => { + const { client: c, localize } = useClient() + const { state, actions } = useElectionReducer(c, data) const { - client: c, - localize, + client, + csp, + election, + loading, + loaded, sik: { password, signature }, - } = useClient() - const { state, actions } = useElectionReducer(c, data) - const { client, csp, election, loading, loaded } = state + } = state const fetchElection = useCallback( async (id: string) => { @@ -127,7 +130,7 @@ export const useElectionProvider = ({ const address = await client.wallet?.getAddress() // The condition is just negated so we can return the code execution. // A less mental option is to not negate the entire condition and add - // the try/catch code inside the if + // the `await censusFetch()` execution in there if ( !( !loaded.census || @@ -215,8 +218,8 @@ export const useElectionProvider = ({ try { let vote: Vote | AnonymousVote = new Vote(values) - if (election.electionType.anonymous && password) { - vote = new AnonymousVote(values, password) + if (election.electionType.anonymous && signature) { + vote = new AnonymousVote(values, signature, password) } actions.setVote(vote) @@ -380,5 +383,7 @@ export const useElectionProvider = ({ fetchCensus: censusFetch, clearClient: actions.clearClient, setClient: actions.setClient, + sikPassword: actions.sikPassword, + sikSignature: actions.sikSignature, } } diff --git a/packages/react-providers/src/election/use-election-reducer.ts b/packages/react-providers/src/election/use-election-reducer.ts index deec0ac0..a2b7e05b 100644 --- a/packages/react-providers/src/election/use-election-reducer.ts +++ b/packages/react-providers/src/election/use-election-reducer.ts @@ -21,6 +21,8 @@ export const ElectionVoteSet = 'election:vote:set' export const ElectionVotesLeft = 'election:votes_left' export const ElectionVoting = 'election:voting' export const ElectionVotingError = 'election:voting:error' +export const SikPasswordSet = 'sik:password:set' +export const SikSignatureSet = 'sik:signature:set' export type ElectionActionType = | typeof CensusClear @@ -39,6 +41,8 @@ export type ElectionActionType = | typeof ElectionVotesLeft | typeof ElectionVoting | typeof ElectionVotingError + | typeof SikPasswordSet + | typeof SikSignatureSet export type CensusErrorPayload = ErrorPayload export type CensusIsAbleToVotePayload = undefined | boolean @@ -54,6 +58,7 @@ export type ElectionVotedPayload = string | null export type ElectionVoteSetPayload = Vote export type ElectionVotesLeftPayload = number export type ElectionVotingErrorPayload = ErrorPayload +export type SikPayload = string | undefined export type ElectionActionPayload = | CensusErrorPayload @@ -66,6 +71,7 @@ export type ElectionActionPayload = | ElectionVoteSetPayload | ElectionVotesLeftPayload | ElectionVotingErrorPayload + | SikPayload export interface ElectionAction { type: ElectionActionType @@ -103,6 +109,10 @@ export interface ElectionReducerState { token: string | undefined authToken: string | undefined } + sik: { + password: string | undefined + signature: string | undefined + } } export const electionStateEmpty = ({ @@ -142,6 +152,10 @@ export const electionStateEmpty = ({ token: undefined, authToken: undefined, }, + sik: { + password: undefined, + signature: undefined, + }, }) const isAbleToVote = (state: ElectionReducerState, payload?: boolean) => @@ -372,14 +386,34 @@ const electionReducer: Reducer = ( }, } } + case SikPasswordSet: { + const password = action.payload as SikPayload + return { + ...state, + sik: { + ...state.sik, + password, + }, + } + } + case SikSignatureSet: { + const signature = action.payload as SikPayload + return { + ...state, + sik: { + ...state.sik, + signature, + }, + } + } + default: + return state } - - return state } export const useElectionReducer = (client: VocdoniSDKClient, election?: PublishedElection) => { const initial = electionStateEmpty({ client, election }) - const { connected, setSikPassword, setSikSignature } = useClient() + const { connected } = useClient() const [state, dispatch] = useReducer(electionReducer, { ...initial, election, @@ -388,6 +422,8 @@ export const useElectionReducer = (client: VocdoniSDKClient, election?: Publishe const clear = () => dispatch({ type: CensusClear }) const setClient = (client: VocdoniSDKClient) => dispatch({ type: ElectionClientSet, payload: client }) const set = (election: PublishedElection) => dispatch({ type: ElectionSet, payload: election }) + const sikPassword = (password: SikPayload) => dispatch({ type: SikPasswordSet, payload: password }) + const sikSignature = (signature: SikPayload) => dispatch({ type: SikSignatureSet, payload: signature }) // update local client in case it's updated useEffect(() => { @@ -439,10 +475,12 @@ export const useElectionReducer = (client: VocdoniSDKClient, election?: Publishe clear, set, setClient, + sikPassword, + sikSignature, clearClient: () => { setClient(client) - setSikPassword(null) - setSikSignature(null) + sikPassword(undefined) + sikSignature(undefined) clear() }, load: (id?: string) => dispatch({ type: ElectionLoad, payload: id }), diff --git a/packages/react-providers/src/utils.ts b/packages/react-providers/src/utils.ts index 0826cec6..862d34a0 100644 --- a/packages/react-providers/src/utils.ts +++ b/packages/react-providers/src/utils.ts @@ -1,4 +1,4 @@ -import { VocdoniSDKClient, ensure0x } from '@vocdoni/sdk' +import { ensure0x, VocdoniSDKClient } from '@vocdoni/sdk' import latinize from 'latinize' /** @@ -50,8 +50,15 @@ export const normalizeText = (text?: string): string => { return latinize(result) } -export const walletFromRow = (organization: string, row: string[]) => { +/** + * Generates a Wallet from a given row of data and a salt. The row of data should be an array of strings + * + * @param {string} salt A random string to be used as salt, the more random the better + * @param {string} row The row to be used to generate the wallet + * @returns {Wallet} + */ +export const walletFromRow = (salt: string, row: string[]) => { const normalized = row.map(normalizeText) - normalized.push(organization) + normalized.push(salt) return VocdoniSDKClient.generateWalletFromData(normalized) } diff --git a/templates/chakra/package.json b/templates/chakra/package.json index 18de7e3a..5f83c9a6 100644 --- a/templates/chakra/package.json +++ b/templates/chakra/package.json @@ -16,7 +16,7 @@ "@emotion/styled": "^11.10.6", "@rainbow-me/rainbowkit": "^1.2.0", "@vocdoni/chakra-components": "*", - "@vocdoni/sdk": "~0.5.1", + "@vocdoni/sdk": "~0.6.0", "date-fns": "^2.29.3", "ethers": "^5.7.2", "formik": "^2.2.9",