From 71b0d39820ceb5a127d1f579a0ad099af29b6b10 Mon Sep 17 00:00:00 2001 From: Justin Kim Date: Sun, 31 Jan 2021 22:52:59 -0800 Subject: [PATCH] 355 - Purchase Confirmation Services (#558) * various changes * main-container-header mixin * did everything outside of the purchaseConfirmationService page * flip focus and hover color for DropdownMenuButton * added the select input * added the checkbox logic * fixed a bug where if you're switching banks, the page did not go back to 1 * keep track on the indices of the selectedValidators, so that you can order them later by that * PaginatedTable * Updated the PurchaseConfirmationServicesModal * Testing connection in the beginning * Some style changes * fetch bankConfig at the beginning for both BulkPurchaseConfirmationServicesModal and PurchaseConfirmationServicesModal * disable amount input for PVs and validators without daily_rates (at least until the BE is updated) * done everything except handleSubmit * disable submit button when entered amount exceeds available balance * finished handleSubmit * remove 'active' from primaryValidator * use getBankTxFee for the bulk purchase modal * bankConfig change * fixed a bug where bank tx's fee was wrong if the selectedBank was the activeBank --- .eslintrc.json | 1 + public/index.html | 4 +- .../DropdownMenuButton.scss | 2 +- .../components/FormComponents/Form/index.tsx | 17 +- .../FormComponents/FormInput/index.tsx | 4 +- .../FormComponents/FormRadioGroup/index.tsx | 4 +- .../FormComponents/FormSelect/index.tsx | 4 +- .../FormSelectDetailed/index.tsx | 6 +- .../FormComponents/FormTextArea/index.tsx | 4 +- .../CheckableInput.scss} | 2 +- .../FormElements/CheckableInput/index.tsx | 103 ++++++ .../FormElements/Checkbox/index.tsx | 22 ++ .../components/FormElements/Radio/index.tsx | 76 +---- .../components/FormElements/index.tsx | 3 + src/renderer/components/Icon/index.tsx | 8 + src/renderer/components/Modal/Modal.scss | 13 +- src/renderer/components/Modal/index.tsx | 30 +- .../components/PageHeader/PageHeader.scss | 6 +- .../components/PageLayout/PageLayout.scss | 2 +- .../components/PageTable/PageTable.scss | 60 ++-- src/renderer/components/PageTable/index.tsx | 104 ++++-- .../components/PageTabs/PageTabs.scss | 1 - .../PaginatedTable/PaginatedTable.scss | 8 + .../Pagination/Pagination.scss | 0 .../{ => PaginatedTable}/Pagination/index.tsx | 4 +- .../PaginationSummary/PaginationSummary.scss | 0 .../PaginationSummary/index.tsx | 4 +- .../components/PaginatedTable/index.tsx | 61 ++++ .../RequiredAsterisk/RequiredAsterisk.scss | 1 + src/renderer/components/Tiles/Tile/Tile.scss | 1 + .../Tiles/TileDailyRate/TileDailyRate.scss | 11 - .../components/Tiles/TileDailyRate/index.tsx | 25 -- .../TilePrimaryAmount/TilePrimaryAmount.scss | 11 - .../Tiles/TilePrimaryAmount/index.tsx | 25 -- .../Tiles/TileWithAmount/TileWithAmount.scss | 16 + .../components/Tiles/TileWithAmount/index.tsx | 31 ++ src/renderer/components/Tiles/index.tsx | 6 +- .../constants/{index.ts => actions.ts} | 0 src/renderer/constants/form-validation.ts | 3 + src/renderer/containers/Account/Account.scss | 3 + .../Account/AccountOverview/index.tsx | 4 +- .../Account/AccountTransactions/index.tsx | 22 +- .../SendCoinsModal/SendCoinsModal.scss | 0 .../SendCoinsModalFields.scss | 0 .../SendCoinsModalFields/index.tsx | 18 +- .../{ => Account}/SendCoinsModal/index.tsx | 21 +- src/renderer/containers/Account/index.tsx | 6 +- src/renderer/containers/Bank/Bank.scss | 4 +- .../containers/Bank/BankAccounts/index.tsx | 20 +- .../containers/Bank/BankBanks/index.tsx | 43 ++- .../containers/Bank/BankBlocks/index.tsx | 18 +- .../Bank/BankConfirmationBlocks/index.tsx | 18 +- .../Bank/BankInvalidBlocks/index.tsx | 18 +- .../Bank/BankOverview/BankOverview.scss | 6 + .../containers/Bank/BankOverview/index.tsx | 28 +- .../Bank/BankTransactions/index.tsx | 18 +- .../index.tsx | 18 +- .../containers/Bank/BankValidators/index.tsx | 36 +- .../ChangeActiveBankModal.scss | 0 .../ChangeActiveBankModalFields.tsx | 0 .../ChangeActiveBankModal/index.tsx | 0 .../{ => Layout}/LeftMenu/LeftMenu.scss | 4 + .../LeftMenu/LeftSubmenu/LeftSubmenu.scss | 0 .../LeftMenu/LeftSubmenu/index.tsx | 0 .../LeftSubmenuItem/LeftSubmenuItem.scss | 0 .../LeftMenu/LeftSubmenuItem/index.tsx | 17 +- .../LeftSubmenuItemStatus.scss | 0 .../LeftMenu/LeftSubmenuItemStatus/index.tsx | 20 +- .../{ => Layout}/LeftMenu/index.tsx | 74 +++-- .../{ => Layout}/TopNav/TopNav.scss | 0 .../containers/{ => Layout}/TopNav/index.tsx | 10 +- src/renderer/containers/Layout/index.tsx | 8 +- ...BulkPurchaseConfirmationServicesModal.scss | 9 + ...rchaseConfirmationServicesModalFields.scss | 129 ++++++++ .../index.tsx | 307 ++++++++++++++++++ .../index.tsx | 97 ++++++ .../ConnectionStatus/ConnectionStatus.scss | 30 +- .../ConnectionStatus/index.tsx | 71 ++++ .../PurchaseConfirmationServices.scss | 28 ++ .../PurchaseConfirmationServicesModal.scss | 0 ...rchaseConfirmationServicesModalFields.scss | 0 .../index.tsx | 72 ++-- .../index.tsx | 172 ++++++++++ .../PurchaseConfirmationServicesTable.scss | 5 + .../index.tsx | 125 +++++++ .../PurchaseConfirmationServices/index.tsx | 119 +++++++ .../PurchaseConfirmationServices/utils.ts | 202 ++++++++++++ .../ConnectionStatus/index.tsx | 56 ---- .../index.tsx | 289 ----------------- .../containers/Validator/Validator.scss | 4 +- .../Validator/ValidatorAccounts/index.tsx | 18 +- .../Validator/ValidatorBanks/index.tsx | 20 +- .../Validator/ValidatorOverview/index.tsx | 13 +- .../Validator/ValidatorValidators/index.tsx | 20 +- src/renderer/dispatchers/balances.ts | 7 +- src/renderer/dispatchers/banks.ts | 2 +- src/renderer/dispatchers/validators.ts | 2 +- src/renderer/hooks/useAddress.tsx | 5 +- src/renderer/hooks/useNetworkCleanFetcher.tsx | 7 +- .../hooks/useNetworkConfigFetcher.tsx | 2 +- src/renderer/hooks/useNetworkCrawlFetcher.tsx | 7 +- .../hooks/usePaginatedNetworkDataFetcher.tsx | 21 +- src/renderer/hooks/useSocketAddress.tsx | 3 +- src/renderer/selectors/app.ts | 26 +- src/renderer/selectors/banks.ts | 17 +- src/renderer/selectors/index.ts | 12 +- src/renderer/selectors/validators.ts | 4 +- src/renderer/store/accountBalances/index.ts | 2 +- src/renderer/store/app/managedAccounts.ts | 2 +- src/renderer/store/app/managedBanks.ts | 2 +- src/renderer/store/app/managedFriends.ts | 2 +- src/renderer/store/app/managedValidators.ts | 2 +- src/renderer/store/banks/bankAccounts.ts | 2 +- .../store/banks/bankBankTransactions.ts | 2 +- src/renderer/store/banks/bankBanks.ts | 2 +- src/renderer/store/banks/bankBlocks.ts | 2 +- src/renderer/store/banks/bankConfigs.ts | 2 +- .../store/banks/bankConfirmationBlocks.ts | 2 +- src/renderer/store/banks/bankInvalidBlocks.ts | 2 +- .../bankValidatorConfirmationServices.ts | 2 +- src/renderer/store/banks/bankValidators.ts | 2 +- src/renderer/store/banks/index.ts | 2 +- .../store/managedAccountBalances/index.ts | 2 +- src/renderer/store/notifications/index.ts | 2 +- src/renderer/store/sockets/cleanSockets.ts | 2 +- src/renderer/store/sockets/crawlSockets.ts | 2 +- src/renderer/store/validators/index.ts | 2 +- .../store/validators/validatorAccounts.ts | 2 +- .../store/validators/validatorBanks.ts | 2 +- .../store/validators/validatorConfigs.ts | 2 +- .../store/validators/validatorValidators.ts | 2 +- src/renderer/styles/core.scss | 7 + src/renderer/styles/forms.scss | 2 + src/renderer/styles/layout.scss | 15 +- src/renderer/styles/main.scss | 1 + src/renderer/styles/modal.scss | 4 + src/renderer/types/forms.ts | 2 +- src/renderer/types/index.ts | 3 + src/renderer/types/params.ts | 11 + src/renderer/utils/address.ts | 4 + src/renderer/utils/api.ts | 2 + src/renderer/utils/blocks.ts | 71 ++-- src/renderer/utils/forms/formComponents.tsx | 31 +- src/renderer/utils/transactions.ts | 10 +- 144 files changed, 2281 insertions(+), 948 deletions(-) rename src/renderer/components/FormElements/{Radio/Radio.scss => CheckableInput/CheckableInput.scss} (88%) create mode 100644 src/renderer/components/FormElements/CheckableInput/index.tsx create mode 100644 src/renderer/components/FormElements/Checkbox/index.tsx create mode 100644 src/renderer/components/PaginatedTable/PaginatedTable.scss rename src/renderer/components/{ => PaginatedTable}/Pagination/Pagination.scss (100%) rename src/renderer/components/{ => PaginatedTable}/Pagination/index.tsx (96%) rename src/renderer/components/{ => PaginatedTable}/PaginationSummary/PaginationSummary.scss (100%) rename src/renderer/components/{ => PaginatedTable}/PaginationSummary/index.tsx (80%) create mode 100644 src/renderer/components/PaginatedTable/index.tsx delete mode 100644 src/renderer/components/Tiles/TileDailyRate/TileDailyRate.scss delete mode 100644 src/renderer/components/Tiles/TileDailyRate/index.tsx delete mode 100644 src/renderer/components/Tiles/TilePrimaryAmount/TilePrimaryAmount.scss delete mode 100644 src/renderer/components/Tiles/TilePrimaryAmount/index.tsx create mode 100644 src/renderer/components/Tiles/TileWithAmount/TileWithAmount.scss create mode 100644 src/renderer/components/Tiles/TileWithAmount/index.tsx rename src/renderer/constants/{index.ts => actions.ts} (100%) rename src/renderer/containers/{ => Account}/SendCoinsModal/SendCoinsModal.scss (100%) rename src/renderer/containers/{ => Account}/SendCoinsModal/SendCoinsModalFields/SendCoinsModalFields.scss (100%) rename src/renderer/containers/{ => Account}/SendCoinsModal/SendCoinsModalFields/index.tsx (88%) rename src/renderer/containers/{ => Account}/SendCoinsModal/index.tsx (87%) rename src/renderer/containers/{ => Bank}/ChangeActiveBankModal/ChangeActiveBankModal.scss (100%) rename src/renderer/containers/{ => Bank}/ChangeActiveBankModal/ChangeActiveBankModalFields.tsx (100%) rename src/renderer/containers/{ => Bank}/ChangeActiveBankModal/index.tsx (100%) rename src/renderer/containers/{ => Layout}/LeftMenu/LeftMenu.scss (83%) rename src/renderer/containers/{ => Layout}/LeftMenu/LeftSubmenu/LeftSubmenu.scss (100%) rename src/renderer/containers/{ => Layout}/LeftMenu/LeftSubmenu/index.tsx (100%) rename src/renderer/containers/{ => Layout}/LeftMenu/LeftSubmenuItem/LeftSubmenuItem.scss (100%) rename src/renderer/containers/{ => Layout}/LeftMenu/LeftSubmenuItem/index.tsx (64%) rename src/renderer/containers/{ => Layout}/LeftMenu/LeftSubmenuItemStatus/LeftSubmenuItemStatus.scss (100%) rename src/renderer/containers/{ => Layout}/LeftMenu/LeftSubmenuItemStatus/index.tsx (60%) rename src/renderer/containers/{ => Layout}/LeftMenu/index.tsx (79%) rename src/renderer/containers/{ => Layout}/TopNav/TopNav.scss (100%) rename src/renderer/containers/{ => Layout}/TopNav/index.tsx (93%) create mode 100644 src/renderer/containers/PurchaseConfirmationServices/BulkPurchaseConfirmationServicesModal/BulkPurchaseConfirmationServicesModal.scss create mode 100644 src/renderer/containers/PurchaseConfirmationServices/BulkPurchaseConfirmationServicesModal/BulkPurchaseConfirmationServicesModalFields/BulkPurchaseConfirmationServicesModalFields.scss create mode 100644 src/renderer/containers/PurchaseConfirmationServices/BulkPurchaseConfirmationServicesModal/BulkPurchaseConfirmationServicesModalFields/index.tsx create mode 100644 src/renderer/containers/PurchaseConfirmationServices/BulkPurchaseConfirmationServicesModal/index.tsx rename src/renderer/containers/{PurchaseConfirmationServicesModal => PurchaseConfirmationServices}/ConnectionStatus/ConnectionStatus.scss (61%) create mode 100644 src/renderer/containers/PurchaseConfirmationServices/ConnectionStatus/index.tsx create mode 100644 src/renderer/containers/PurchaseConfirmationServices/PurchaseConfirmationServices.scss rename src/renderer/containers/{ => PurchaseConfirmationServices}/PurchaseConfirmationServicesModal/PurchaseConfirmationServicesModal.scss (100%) rename src/renderer/containers/{ => PurchaseConfirmationServices}/PurchaseConfirmationServicesModal/PurchaseConfirmationServicesModalFields/PurchaseConfirmationServicesModalFields.scss (100%) rename src/renderer/containers/{ => PurchaseConfirmationServices}/PurchaseConfirmationServicesModal/PurchaseConfirmationServicesModalFields/index.tsx (64%) create mode 100644 src/renderer/containers/PurchaseConfirmationServices/PurchaseConfirmationServicesModal/index.tsx create mode 100644 src/renderer/containers/PurchaseConfirmationServices/PurchaseConfirmationServicesTable/PurchaseConfirmationServicesTable.scss create mode 100644 src/renderer/containers/PurchaseConfirmationServices/PurchaseConfirmationServicesTable/index.tsx create mode 100644 src/renderer/containers/PurchaseConfirmationServices/index.tsx create mode 100644 src/renderer/containers/PurchaseConfirmationServices/utils.ts delete mode 100644 src/renderer/containers/PurchaseConfirmationServicesModal/ConnectionStatus/index.tsx delete mode 100644 src/renderer/containers/PurchaseConfirmationServicesModal/index.tsx create mode 100644 src/renderer/styles/modal.scss create mode 100644 src/renderer/types/params.ts diff --git a/.eslintrc.json b/.eslintrc.json index 83d674f0..01c327d7 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -40,6 +40,7 @@ "import/prefer-default-export": "off", "jsx-a11y/click-events-have-key-events": "off", "jsx-a11y/control-has-associated-label": "off", + "jsx-a11y/label-has-associated-control": "off", "jsx-a11y/no-noninteractive-tabindex": "off", "jsx-a11y/no-static-element-interactions": "off", "no-param-reassign": "off", diff --git a/public/index.html b/public/index.html index 7bcda236..11bbb7ec 100644 --- a/public/index.html +++ b/public/index.html @@ -5,7 +5,7 @@
- - + + diff --git a/src/renderer/components/DropdownMenuButton/DropdownMenuButton.scss b/src/renderer/components/DropdownMenuButton/DropdownMenuButton.scss index f49b40a5..5d9c2fe9 100644 --- a/src/renderer/components/DropdownMenuButton/DropdownMenuButton.scss +++ b/src/renderer/components/DropdownMenuButton/DropdownMenuButton.scss @@ -31,7 +31,7 @@ $button-height: 24px; cursor: pointer; &:hover { - background: var(--color-gray-100); + background: var(--color-gray-050); } } diff --git a/src/renderer/components/FormComponents/Form/index.tsx b/src/renderer/components/FormComponents/Form/index.tsx index c9d170e3..3b6ea5f3 100644 --- a/src/renderer/components/FormComponents/Form/index.tsx +++ b/src/renderer/components/FormComponents/Form/index.tsx @@ -6,12 +6,25 @@ interface ComponentProps { className?: string; initialValues?: GenericFormValues; onSubmit(values: GenericFormValues): void | Promise; + validateOnMount?: boolean; validationSchema?: any; } -const Form: FC = ({children, className, onSubmit, initialValues = {}, validationSchema}) => { +const Form: FC = ({ + children, + className, + onSubmit, + initialValues = {}, + validateOnMount = false, + validationSchema, +}) => { return ( - + {() => ( {children} diff --git a/src/renderer/components/FormComponents/FormInput/index.tsx b/src/renderer/components/FormComponents/FormInput/index.tsx index 34af60c2..62da5d34 100644 --- a/src/renderer/components/FormComponents/FormInput/index.tsx +++ b/src/renderer/components/FormComponents/FormInput/index.tsx @@ -24,9 +24,9 @@ const FormInput: FC = ({ return (
- {renderFormLabel(name, className, label, required)} + {renderFormLabel({className, label, name, required})} - {hideErrorBlock ? null : renderFormError(name, className, hideErrorText)} + {hideErrorBlock ? null : renderFormError({className, hideErrorText, name})}
); }; diff --git a/src/renderer/components/FormComponents/FormRadioGroup/index.tsx b/src/renderer/components/FormComponents/FormRadioGroup/index.tsx index d2121927..eaa92a5a 100644 --- a/src/renderer/components/FormComponents/FormRadioGroup/index.tsx +++ b/src/renderer/components/FormComponents/FormRadioGroup/index.tsx @@ -59,7 +59,7 @@ const FormRadioGroup: FC = ({ return (
- {renderFormLabel(name, className, label, required)} + {renderFormLabel({className, label, name, required})} {options.map((option, index) => { const optionIsFocused = focused && index === focusedIndex; const selected = selectedOption?.value === option.value; @@ -92,7 +92,7 @@ const FormRadioGroup: FC = ({
); })} - {renderFormError(name, className, hideErrorText)} + {renderFormError({className, hideErrorText, name})} ); }; diff --git a/src/renderer/components/FormComponents/FormSelect/index.tsx b/src/renderer/components/FormComponents/FormSelect/index.tsx index ed290238..e73f9e5b 100644 --- a/src/renderer/components/FormComponents/FormSelect/index.tsx +++ b/src/renderer/components/FormComponents/FormSelect/index.tsx @@ -16,7 +16,7 @@ const FormSelect: FC = ({hideErrorText = false, label, required, return (
- {renderFormLabel(name, className, label, required)} + {renderFormLabel({className, label, name, required})} + + ); +}; + +export default CheckableInput; diff --git a/src/renderer/components/FormElements/Checkbox/index.tsx b/src/renderer/components/FormElements/Checkbox/index.tsx new file mode 100644 index 00000000..3d52c71e --- /dev/null +++ b/src/renderer/components/FormElements/Checkbox/index.tsx @@ -0,0 +1,22 @@ +/* eslint-disable react/jsx-props-no-spreading */ + +import React, {FC} from 'react'; +import clsx from 'clsx'; + +import CheckableInput, {BaseCheckableInputProps} from '../CheckableInput'; + +export type BaseCheckboxProps = BaseCheckableInputProps; + +const Checkbox: FC = ({className, size = 22, ...props}) => { + return ( + + ); +}; + +export default Checkbox; diff --git a/src/renderer/components/FormElements/Radio/index.tsx b/src/renderer/components/FormElements/Radio/index.tsx index 03bcb4dd..78b47fa6 100644 --- a/src/renderer/components/FormElements/Radio/index.tsx +++ b/src/renderer/components/FormElements/Radio/index.tsx @@ -1,76 +1,14 @@ -import React, {FC, useEffect, useRef} from 'react'; -import clsx from 'clsx'; - -import Icon, {IconType} from '@renderer/components/Icon'; -import {getCustomClassNames} from '@renderer/utils/components'; +/* eslint-disable react/jsx-props-no-spreading */ -import './Radio.scss'; - -export interface BaseRadioProps { - checked: boolean; - className?: string; - disabled?: boolean; - error?: boolean; - focused?: boolean; - name?: string; - onClick?(e?: React.MouseEvent): void; - onKeyDown?(e?: React.KeyboardEvent): void; - size?: number; - unfocusable?: boolean; - value: string; -} +import React, {FC} from 'react'; +import clsx from 'clsx'; -const Radio: FC = ({ - checked, - className, - disabled = false, - error = false, - focused = false, - name, - onClick, - onKeyDown, - size = 24, - unfocusable = false, - value, -}) => { - const radioRef = useRef(null); +import CheckableInput, {BaseCheckableInputProps} from '../CheckableInput'; - useEffect(() => { - if (focused) { - radioRef.current?.focus(); - } - }, [focused, radioRef]); +export type BaseRadioProps = BaseCheckableInputProps; - return ( - <> - - - - ); +const Radio: FC = ({className, size = 24, ...props}) => { + return ; }; export default Radio; diff --git a/src/renderer/components/FormElements/index.tsx b/src/renderer/components/FormElements/index.tsx index 8d4aea60..e09d1173 100644 --- a/src/renderer/components/FormElements/index.tsx +++ b/src/renderer/components/FormElements/index.tsx @@ -1,4 +1,5 @@ import Button, {BaseButtonProps} from './Button'; +import Checkbox, {BaseCheckboxProps} from './Checkbox'; import Input, {BaseInputProps} from './Input'; import Loader from './Loader'; import Radio, {BaseRadioProps} from './Radio'; @@ -8,10 +9,12 @@ import TextArea from './TextArea'; export { BaseButtonProps, + BaseCheckboxProps, BaseInputProps, BaseRadioProps, BaseSelectProps, Button, + Checkbox, Input, Loader, Radio, diff --git a/src/renderer/components/Icon/index.tsx b/src/renderer/components/Icon/index.tsx index 43b9d116..d1647bb4 100644 --- a/src/renderer/components/Icon/index.tsx +++ b/src/renderer/components/Icon/index.tsx @@ -10,6 +10,8 @@ import ArrowRightIcon from 'mdi-react/ArrowRightIcon'; import BellIcon from 'mdi-react/BellIcon'; import CheckboxBlankCircleIcon from 'mdi-react/CheckboxBlankCircleIcon'; import CheckboxBlankCircleOutlineIcon from 'mdi-react/CheckboxBlankCircleOutlineIcon'; +import CheckboxBlankOutlineIcon from 'mdi-react/CheckboxBlankOutlineIcon'; +import CheckboxMarkedIcon from 'mdi-react/CheckboxMarkedIcon'; import ChevronLeftIcon from 'mdi-react/ChevronLeftIcon'; import ChevronRightIcon from 'mdi-react/ChevronRightIcon'; import CloseIcon from 'mdi-react/CloseIcon'; @@ -45,6 +47,8 @@ export enum IconType { bell, checkboxBlankCircle, checkboxBlankCircleOutline, + checkboxBlankOutline, + checkboxMarked, chevronLeft, chevronRight, close, @@ -122,6 +126,10 @@ const Icon = forwardRef( return ; case IconType.checkboxBlankCircleOutline: return ; + case IconType.checkboxBlankOutline: + return ; + case IconType.checkboxMarked: + return ; case IconType.chevronLeft: return ; case IconType.chevronRight: diff --git a/src/renderer/components/Modal/Modal.scss b/src/renderer/components/Modal/Modal.scss index 73ecce23..d4de8ef1 100644 --- a/src/renderer/components/Modal/Modal.scss +++ b/src/renderer/components/Modal/Modal.scss @@ -1,6 +1,3 @@ -$modal-header-height: 48px; -$modal-footer-height: 60px; - @keyframes addOverlay { from { background: rgba(0, 0, 0, 0); @@ -29,9 +26,13 @@ $modal-footer-height: 60px; &__content { background: var(--color-gray-050); - max-height: calc(85vh - #{$modal-header-height} - #{$modal-footer-height}); + max-height: calc(85vh - var(--modal-header-height) - var(--modal-footer-height)); overflow-y: auto; padding: 12px; + + &--no-footer { + border-radius: 0 0 3px 3px; + } } &--default-position { @@ -48,7 +49,7 @@ $modal-footer-height: 60px; &__footer { align-items: center; display: flex; - height: $modal-footer-height; + height: var(--modal-footer-height); justify-content: flex-end; padding: 0 12px; } @@ -56,7 +57,7 @@ $modal-footer-height: 60px; &__header { align-items: center; display: flex; - height: $modal-header-height; + height: var(--modal-header-height); padding: 0 12px; position: relative; } diff --git a/src/renderer/components/Modal/index.tsx b/src/renderer/components/Modal/index.tsx index 7fd71f42..1a514189 100644 --- a/src/renderer/components/Modal/index.tsx +++ b/src/renderer/components/Modal/index.tsx @@ -26,12 +26,14 @@ interface ComponentProps { displaySubmitButton?: boolean; footer?: ReactNode; header?: ReactNode; + hideFooter?: boolean; ignoreDirty?: boolean; initialValues?: GenericFormValues; - onSubmit: GenericFunction; + onSubmit?: GenericFunction; style?: CSSProperties; submitButton?: ModalButtonProps | string; submitting?: boolean; + validateOnMount?: boolean; validationSchema?: any; } @@ -45,6 +47,7 @@ const Modal: FC = ({ displaySubmitButton = true, footer, header, + hideFooter = false, displayCloseButton = true, ignoreDirty: ignoreDirtyProps = false, initialValues = {}, @@ -52,6 +55,7 @@ const Modal: FC = ({ style, submitButton, submitting = false, + validateOnMount, validationSchema, }) => { const ignoreDirty = useMemo(() => ignoreDirtyProps || Object.keys(initialValues).length === 0, [ @@ -176,13 +180,27 @@ const Modal: FC = ({ /> )}
-
-
+ +
{children}
-
- {footer || renderDefaultFooter()} -
+ {!hideFooter && ( +
+ {footer || renderDefaultFooter()} +
+ )}
, diff --git a/src/renderer/components/PageHeader/PageHeader.scss b/src/renderer/components/PageHeader/PageHeader.scss index 3c29d3d3..3095b0d1 100644 --- a/src/renderer/components/PageHeader/PageHeader.scss +++ b/src/renderer/components/PageHeader/PageHeader.scss @@ -1,8 +1,8 @@ +@use 'src/renderer/styles/layout'; + .PageHeader { - display: flex; - justify-content: space-between; + @include layout.main-container-header; margin-bottom: 30px; - padding: 12px 12px 0 12px; &__DropdownMenuButton { margin-left: 8px; diff --git a/src/renderer/components/PageLayout/PageLayout.scss b/src/renderer/components/PageLayout/PageLayout.scss index 1d044c4c..c0c167f1 100644 --- a/src/renderer/components/PageLayout/PageLayout.scss +++ b/src/renderer/components/PageLayout/PageLayout.scss @@ -7,7 +7,7 @@ background-color: var(--color-gray-100); border: none; height: 1px; - margin: 0 10px; + margin: 0 -2px; } &__content { diff --git a/src/renderer/components/PageTable/PageTable.scss b/src/renderer/components/PageTable/PageTable.scss index 66814d3d..ecd918ab 100644 --- a/src/renderer/components/PageTable/PageTable.scss +++ b/src/renderer/components/PageTable/PageTable.scss @@ -1,53 +1,53 @@ .PageTable { + $self: &; font-family: var(--font-family-mono); padding: var(--page-table-padding); width: 100%; - td, - th { - border-bottom: 1px solid var(--color-gray-100); - } - &__ArrowToggle { height: 16px; width: 16px; } - &__PaginationSummary { - margin: 12px 0; - padding: 2px 12px; - } - &__row { - td { - max-width: 120px; - overflow: hidden; - padding: 6px 4px; - text-overflow: ellipsis; - vertical-align: top; - white-space: nowrap; - - &:first-of-type { - width: 32px; - } - } - &--expanded { - td { + #{$self}__td { white-space: normal; word-wrap: break-word; } } } + &__td, + &__th { + border-bottom: 1px solid var(--color-gray-100); + } + + &__td { + max-width: 120px; + overflow: hidden; + padding: 6px 4px; + text-overflow: ellipsis; + vertical-align: top; + white-space: nowrap; + + &--checkbox { + padding: 3px 4px; + } + + &--toggle { + width: 32px; + } + } + + &__th { + padding: 0 4px 6px; + white-space: nowrap; + } + &__thead { color: var(--color-gray-500); - font-weight: 500; + font-family: var(--font-family-default); text-align: left; - - td { - padding: 0 4px 6px; - white-space: nowrap; - } } } diff --git a/src/renderer/components/PageTable/index.tsx b/src/renderer/components/PageTable/index.tsx index 763f04fb..c8263ed1 100644 --- a/src/renderer/components/PageTable/index.tsx +++ b/src/renderer/components/PageTable/index.tsx @@ -2,14 +2,14 @@ import React, {FC, ReactNode, useState} from 'react'; import clsx from 'clsx'; import ArrowToggle from '@renderer/components/ArrowToggle'; -import Loader from '@renderer/components/FormElements/Loader'; -import PaginationSummary from '@renderer/components/PaginationSummary'; +import {Checkbox} from '@renderer/components/FormElements'; +import {GenericVoidFunction} from '@renderer/types'; import {getCustomClassNames} from '@renderer/utils/components'; import './PageTable.scss'; interface Header { - [tableKey: string]: string; + [tableKey: string]: ReactNode; } export interface PageTableData { @@ -23,18 +23,21 @@ export interface PageTableItems { data: PageTableData[]; } -interface ComponentProps { +export interface PageTableProps { + alwaysExpanded?: boolean; className?: string; - count: number; - currentPage: number; + expanded?: boolean; + handleSelectRow?(i: number): GenericVoidFunction; items: PageTableItems; - loading: boolean; + selectedData?: {[key: string]: any}; } -const PageTable: FC = ({className, count, currentPage, items, loading}) => { +const PageTable: FC = ({alwaysExpanded = false, className, handleSelectRow, items, selectedData}) => { const {headers, data, orderedKeys} = items; const [expanded, setExpanded] = useState([]); + const hasCheckbox = !!handleSelectRow && !!selectedData; + const toggleExpanded = (indexToToggle: number) => (): void => { setExpanded( expanded.includes(indexToToggle) ? expanded.filter((i) => i !== indexToToggle) : [...expanded, indexToToggle], @@ -43,7 +46,7 @@ const PageTable: FC = ({className, count, currentPage, items, lo const renderRows = (): ReactNode => { return data.map((item, dataIndex) => { - const rowIsExpanded = expanded.includes(dataIndex); + const rowIsExpanded = alwaysExpanded || expanded.includes(dataIndex); return ( = ({className, count, currentPage, items, lo })} key={item.key} > - - - - {orderedKeys.map((key) => ( - {item[key] || '-'} + {hasCheckbox ? ( + + + + ) : null} + {alwaysExpanded ? null : ( + + + + )} + {orderedKeys.map((key, index) => ( + + {item[key] || '-'} + ))} ); }); }; - return loading ? ( - - ) : ( - <> - - - - - - ))} - - - {renderRows()} -
- {orderedKeys.map((key) => ( - {headers[key]}
- + return ( + + + + {hasCheckbox ? ( + + ))} + + + {renderRows()} +
+ ) : null} + {alwaysExpanded ? null : ( + + )} + {orderedKeys.map((key) => ( + + {headers[key]} +
); }; diff --git a/src/renderer/components/PageTabs/PageTabs.scss b/src/renderer/components/PageTabs/PageTabs.scss index 7ca92fb7..5d5317de 100644 --- a/src/renderer/components/PageTabs/PageTabs.scss +++ b/src/renderer/components/PageTabs/PageTabs.scss @@ -1,7 +1,6 @@ .PageTabs { $self: &; display: flex; - padding: 0 12px; text-transform: uppercase; &__tab { diff --git a/src/renderer/components/PaginatedTable/PaginatedTable.scss b/src/renderer/components/PaginatedTable/PaginatedTable.scss new file mode 100644 index 00000000..bca725f7 --- /dev/null +++ b/src/renderer/components/PaginatedTable/PaginatedTable.scss @@ -0,0 +1,8 @@ +.PaginatedTable { + &__PaginationSummary { + display: flex; + justify-content: flex-end; + margin: 12px 6px; + padding: 2px 0; + } +} diff --git a/src/renderer/components/Pagination/Pagination.scss b/src/renderer/components/PaginatedTable/Pagination/Pagination.scss similarity index 100% rename from src/renderer/components/Pagination/Pagination.scss rename to src/renderer/components/PaginatedTable/Pagination/Pagination.scss diff --git a/src/renderer/components/Pagination/index.tsx b/src/renderer/components/PaginatedTable/Pagination/index.tsx similarity index 96% rename from src/renderer/components/Pagination/index.tsx rename to src/renderer/components/PaginatedTable/Pagination/index.tsx index c1229f08..a84e2a22 100644 --- a/src/renderer/components/Pagination/index.tsx +++ b/src/renderer/components/PaginatedTable/Pagination/index.tsx @@ -6,7 +6,7 @@ import Icon, {IconType} from '@renderer/components/Icon'; import {getCustomClassNames} from '@renderer/utils/components'; import './Pagination.scss'; -interface ComponentProps { +export interface PaginationProps { className?: string; currentPage: number; setPage(page: number): () => void; @@ -15,7 +15,7 @@ interface ComponentProps { const TOTAL_VISIBLE_PAGES = 11; -const Pagination: FC = ({className, currentPage, setPage, totalPages}) => { +const Pagination: FC = ({className, currentPage, setPage, totalPages}) => { const nextIsDisabled = useMemo(() => currentPage >= totalPages, [currentPage, totalPages]); const prevIsDisabled = useMemo(() => currentPage === 1, [currentPage]); const leftEllipsesIsVisible = useMemo(() => currentPage > Math.floor(TOTAL_VISIBLE_PAGES / 2) + 1, [currentPage]); diff --git a/src/renderer/components/PaginationSummary/PaginationSummary.scss b/src/renderer/components/PaginatedTable/PaginationSummary/PaginationSummary.scss similarity index 100% rename from src/renderer/components/PaginationSummary/PaginationSummary.scss rename to src/renderer/components/PaginatedTable/PaginationSummary/PaginationSummary.scss diff --git a/src/renderer/components/PaginationSummary/index.tsx b/src/renderer/components/PaginatedTable/PaginationSummary/index.tsx similarity index 80% rename from src/renderer/components/PaginationSummary/index.tsx rename to src/renderer/components/PaginatedTable/PaginationSummary/index.tsx index d2b04dc8..a3a6c195 100644 --- a/src/renderer/components/PaginationSummary/index.tsx +++ b/src/renderer/components/PaginatedTable/PaginationSummary/index.tsx @@ -5,13 +5,13 @@ import {PAGINATED_RESULTS_LIMIT} from '@renderer/config'; import './PaginationSummary.scss'; -interface ComponentProps { +export interface PaginationSummaryProps { className?: string; count: number; currentPage: number; } -const PaginationSummary: FC = ({className, count, currentPage}) => { +const PaginationSummary: FC = ({className, count, currentPage}) => { const firstRow = Math.min((currentPage - 1) * PAGINATED_RESULTS_LIMIT + 1, count); const lastRow = Math.min(currentPage * PAGINATED_RESULTS_LIMIT, count); const summary = `${firstRow}-${lastRow} of ${count}`; diff --git a/src/renderer/components/PaginatedTable/index.tsx b/src/renderer/components/PaginatedTable/index.tsx new file mode 100644 index 00000000..c225398f --- /dev/null +++ b/src/renderer/components/PaginatedTable/index.tsx @@ -0,0 +1,61 @@ +import React, {FC} from 'react'; +import clsx from 'clsx'; + +import PageTable, {PageTableItems, PageTableData, PageTableProps} from '@renderer/components/PageTable'; +import {Loader} from '@renderer/components/FormElements'; +import {getCustomClassNames} from '@renderer/utils/components'; + +import Pagination, {PaginationProps} from './Pagination'; +import PaginationSummary, {PaginationSummaryProps} from './PaginationSummary'; +import './PaginatedTable.scss'; + +interface ComponentProps extends PageTableProps, PaginationProps, PaginationSummaryProps { + className?: string; + loading: boolean; +} + +const PaginatedTable: FC = ({ + className, + count, + currentPage, + handleSelectRow, + items, + loading, + selectedData, + setPage, + totalPages, +}) => { + return ( +
+ {loading ? ( + + ) : ( + <> + + + + )} + +
+ ); +}; + +export {PageTableData, PageTableItems}; + +export default PaginatedTable; diff --git a/src/renderer/components/RequiredAsterisk/RequiredAsterisk.scss b/src/renderer/components/RequiredAsterisk/RequiredAsterisk.scss index ef1dbb47..2f9dbae2 100644 --- a/src/renderer/components/RequiredAsterisk/RequiredAsterisk.scss +++ b/src/renderer/components/RequiredAsterisk/RequiredAsterisk.scss @@ -1,4 +1,5 @@ .RequiredAsterisk { color: var(--color-alert); + font-family: var(--font-family-default); margin-left: 3px; } diff --git a/src/renderer/components/Tiles/Tile/Tile.scss b/src/renderer/components/Tiles/Tile/Tile.scss index a948cbed..6e72fe7e 100644 --- a/src/renderer/components/Tiles/Tile/Tile.scss +++ b/src/renderer/components/Tiles/Tile/Tile.scss @@ -2,5 +2,6 @@ background: var(--color-white); border-radius: 3px; box-shadow: 0 0 3px rgba(4, 34, 53, 0.3); + margin: 3px; padding: 18px; } diff --git a/src/renderer/components/Tiles/TileDailyRate/TileDailyRate.scss b/src/renderer/components/Tiles/TileDailyRate/TileDailyRate.scss deleted file mode 100644 index 21f17eb8..00000000 --- a/src/renderer/components/Tiles/TileDailyRate/TileDailyRate.scss +++ /dev/null @@ -1,11 +0,0 @@ -.TileDailyRate { - &__amount { - color: var(--color-primary); - font-size: var(--font-size-large); - } - - &__title { - color: var(--color-gray-500); - margin-bottom: 8px; - } -} diff --git a/src/renderer/components/Tiles/TileDailyRate/index.tsx b/src/renderer/components/Tiles/TileDailyRate/index.tsx deleted file mode 100644 index 3d49f4bb..00000000 --- a/src/renderer/components/Tiles/TileDailyRate/index.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React, {FC, memo} from 'react'; -import clsx from 'clsx'; - -import Loader from '@renderer/components/FormElements/Loader'; - -import Tile from '../Tile'; -import './TileDailyRate.scss'; - -interface ComponentProps { - amount: number | string; - className?: string; - loading?: boolean; - title: string; -} - -const TileDailyRate: FC = ({amount, className, loading = false, title}) => { - return ( - -
{title}
-
{loading ? : amount}
-
- ); -}; - -export default memo(TileDailyRate); diff --git a/src/renderer/components/Tiles/TilePrimaryAmount/TilePrimaryAmount.scss b/src/renderer/components/Tiles/TilePrimaryAmount/TilePrimaryAmount.scss deleted file mode 100644 index cb1587b5..00000000 --- a/src/renderer/components/Tiles/TilePrimaryAmount/TilePrimaryAmount.scss +++ /dev/null @@ -1,11 +0,0 @@ -.TilePrimaryAmount { - &__amount { - color: var(--color-primary); - font-size: var(--font-size-large); - } - - &__title { - color: var(--color-gray-500); - margin-bottom: 8px; - } -} diff --git a/src/renderer/components/Tiles/TilePrimaryAmount/index.tsx b/src/renderer/components/Tiles/TilePrimaryAmount/index.tsx deleted file mode 100644 index e23cfb92..00000000 --- a/src/renderer/components/Tiles/TilePrimaryAmount/index.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React, {FC, memo} from 'react'; -import clsx from 'clsx'; - -import Loader from '@renderer/components/FormElements/Loader'; - -import Tile from '../Tile'; -import './TilePrimaryAmount.scss'; - -interface ComponentProps { - amount: number; - className?: string; - loading?: boolean; - title: string; -} - -const TilePrimaryAmount: FC = ({amount, className, loading = false, title}) => { - return ( - -
{title}
-
{loading ? : amount}
-
- ); -}; - -export default memo(TilePrimaryAmount); diff --git a/src/renderer/components/Tiles/TileWithAmount/TileWithAmount.scss b/src/renderer/components/Tiles/TileWithAmount/TileWithAmount.scss new file mode 100644 index 00000000..f4295715 --- /dev/null +++ b/src/renderer/components/Tiles/TileWithAmount/TileWithAmount.scss @@ -0,0 +1,16 @@ +.TileWithAmount { + .header { + display: flex; + justify-content: space-between; + margin-bottom: 8px; + + &__title { + color: var(--color-gray-500); + } + } + + &__amount { + color: var(--color-primary); + font-size: var(--font-size-large); + } +} diff --git a/src/renderer/components/Tiles/TileWithAmount/index.tsx b/src/renderer/components/Tiles/TileWithAmount/index.tsx new file mode 100644 index 00000000..dcc29c1e --- /dev/null +++ b/src/renderer/components/Tiles/TileWithAmount/index.tsx @@ -0,0 +1,31 @@ +import React, {FC, memo, ReactNode} from 'react'; +import clsx from 'clsx'; + +import Loader from '@renderer/components/FormElements/Loader'; + +import Tile from '../Tile'; +import './TileWithAmount.scss'; + +interface ComponentProps { + amount: number | string; + className?: string; + headerButton?: ReactNode; + loading?: boolean; + title: string; +} + +const TileWithAmount: FC = ({amount, className, headerButton, loading = false, title}) => { + const amountDisplay = typeof amount === 'number' ? amount.toLocaleString() : amount; + + return ( + +
+
{title}
+ {headerButton} +
+
{loading ? : amountDisplay}
+
+ ); +}; + +export default memo(TileWithAmount); diff --git a/src/renderer/components/Tiles/index.tsx b/src/renderer/components/Tiles/index.tsx index 4cfb5f83..c298c501 100644 --- a/src/renderer/components/Tiles/index.tsx +++ b/src/renderer/components/Tiles/index.tsx @@ -3,11 +3,10 @@ import TileAccountBalance from './TileAccountBalance'; import TileAccountNumber from './TileAccountNumber'; import TileBankSigningDetails from './TileBankSigningDetails'; import TileCrawlClean from './TileCrawlClean'; -import TileDailyRate from './TileDailyRate'; import TileKeyValueList from './TileKeyValueList'; -import TilePrimaryAmount from './TilePrimaryAmount'; import TileSigningKey from './TileSigningKey'; import TileValidatorSigningDetails from './TileValidatorSigningDetails'; +import TileWithAmount from './TileWithAmount'; export { Tile, @@ -15,9 +14,8 @@ export { TileAccountNumber, TileBankSigningDetails, TileCrawlClean, - TileDailyRate, TileKeyValueList, - TilePrimaryAmount, TileSigningKey, TileValidatorSigningDetails, + TileWithAmount, }; diff --git a/src/renderer/constants/index.ts b/src/renderer/constants/actions.ts similarity index 100% rename from src/renderer/constants/index.ts rename to src/renderer/constants/actions.ts diff --git a/src/renderer/constants/form-validation.ts b/src/renderer/constants/form-validation.ts index 5d08595a..2101e6f6 100644 --- a/src/renderer/constants/form-validation.ts +++ b/src/renderer/constants/form-validation.ts @@ -5,6 +5,9 @@ export const ACCOUNT_EXISTS_ERROR = 'That account already exists'; export const FRIEND_AS_OWN_ACCOUNT_ERROR = 'Unable to add your own account as a friend'; export const FRIEND_EXISTS_ERROR = "This friend's account already exists"; +export const INVALID_AMOUNT_ERROR = 'Invalid amount'; +export const MATCH_ERROR = 'Sender and recipient cannot be same'; + export const REQUIRED_FIELD_ERROR = 'This field is required'; export const SIGNING_KEY_INVALID_ACCOUNT_ERROR = 'Resulting public key does not match Account'; diff --git a/src/renderer/containers/Account/Account.scss b/src/renderer/containers/Account/Account.scss index 511cffd5..8cc9c071 100644 --- a/src/renderer/containers/Account/Account.scss +++ b/src/renderer/containers/Account/Account.scss @@ -1,2 +1,5 @@ +@use 'src/renderer/styles/layout'; + .Account { + @include layout.main-container; } diff --git a/src/renderer/containers/Account/AccountOverview/index.tsx b/src/renderer/containers/Account/AccountOverview/index.tsx index bd341df6..92191cb2 100644 --- a/src/renderer/containers/Account/AccountOverview/index.tsx +++ b/src/renderer/containers/Account/AccountOverview/index.tsx @@ -5,14 +5,14 @@ import {useParams} from 'react-router-dom'; import {TileAccountBalance, TileAccountNumber, TileSigningKey} from '@renderer/components/Tiles'; import {fetchAccountBalance} from '@renderer/dispatchers/balances'; import {getManagedAccounts, getManagedFriends} from '@renderer/selectors'; -import {AccountType, AppDispatch} from '@renderer/types'; +import {AccountNumberParams, AccountType, AppDispatch} from '@renderer/types'; import {displayErrorToast} from '@renderer/utils/toast'; import './AccountOverview.scss'; const AccountOverview: FC = () => { const dispatch = useDispatch(); - const {accountNumber} = useParams<{accountNumber: string}>(); + const {accountNumber} = useParams(); const managedAccounts = useSelector(getManagedAccounts); const managedFriends = useSelector(getManagedFriends); const managedAccount = managedAccounts[accountNumber]; diff --git a/src/renderer/containers/Account/AccountTransactions/index.tsx b/src/renderer/containers/Account/AccountTransactions/index.tsx index 58990590..bb4077e4 100644 --- a/src/renderer/containers/Account/AccountTransactions/index.tsx +++ b/src/renderer/containers/Account/AccountTransactions/index.tsx @@ -3,12 +3,11 @@ import {useSelector} from 'react-redux'; import {useParams} from 'react-router-dom'; import AccountLink from '@renderer/components/AccountLink'; -import PageTable, {PageTableData, PageTableItems} from '@renderer/components/PageTable'; -import Pagination from '@renderer/components/Pagination'; -import {BANK_BANK_TRANSACTIONS} from '@renderer/constants'; +import PaginatedTable, {PageTableData, PageTableItems} from '@renderer/components/PaginatedTable'; +import {BANK_BANK_TRANSACTIONS} from '@renderer/constants/actions'; import {usePaginatedNetworkDataFetcher} from '@renderer/hooks'; import {getActiveBankConfig} from '@renderer/selectors'; -import {BankTransaction} from '@renderer/types'; +import {AccountNumberParams, BankTransaction} from '@renderer/types'; import {formatAddressFromNode} from '@renderer/utils/address'; import {formatDate} from '@renderer/utils/dates'; @@ -22,7 +21,7 @@ enum TableKeys { } const AccountTransactions: FC = () => { - const {accountNumber} = useParams<{accountNumber: string}>(); + const {accountNumber} = useParams(); const activeBank = useSelector(getActiveBankConfig)!; const activeBankAddress = formatAddressFromNode(activeBank); const { @@ -74,10 +73,15 @@ const AccountTransactions: FC = () => { ); return ( -
- - -
+ ); }; diff --git a/src/renderer/containers/SendCoinsModal/SendCoinsModal.scss b/src/renderer/containers/Account/SendCoinsModal/SendCoinsModal.scss similarity index 100% rename from src/renderer/containers/SendCoinsModal/SendCoinsModal.scss rename to src/renderer/containers/Account/SendCoinsModal/SendCoinsModal.scss diff --git a/src/renderer/containers/SendCoinsModal/SendCoinsModalFields/SendCoinsModalFields.scss b/src/renderer/containers/Account/SendCoinsModal/SendCoinsModalFields/SendCoinsModalFields.scss similarity index 100% rename from src/renderer/containers/SendCoinsModal/SendCoinsModalFields/SendCoinsModalFields.scss rename to src/renderer/containers/Account/SendCoinsModal/SendCoinsModalFields/SendCoinsModalFields.scss diff --git a/src/renderer/containers/SendCoinsModal/SendCoinsModalFields/index.tsx b/src/renderer/containers/Account/SendCoinsModal/SendCoinsModalFields/index.tsx similarity index 88% rename from src/renderer/containers/SendCoinsModal/SendCoinsModalFields/index.tsx rename to src/renderer/containers/Account/SendCoinsModal/SendCoinsModalFields/index.tsx index e987ed06..8698f6da 100644 --- a/src/renderer/containers/SendCoinsModal/SendCoinsModalFields/index.tsx +++ b/src/renderer/containers/Account/SendCoinsModal/SendCoinsModalFields/index.tsx @@ -3,10 +3,11 @@ import {useSelector} from 'react-redux'; import {FormInput, FormSelectDetailed} from '@renderer/components/FormComponents'; import RequiredAsterisk from '@renderer/components/RequiredAsterisk'; +import {MATCH_ERROR} from '@renderer/constants/form-validation'; import {useFormContext} from '@renderer/hooks'; import { getActiveBankConfig, - getActivePrimaryValidatorConfig, + getPrimaryValidatorConfig, getManagedAccountBalances, getManagedAccounts, getManagedFriends, @@ -16,9 +17,6 @@ import {getBankTxFee, getPrimaryValidatorTxFee} from '@renderer/utils/transactio import './SendCoinsModalFields.scss'; -export const INVALID_AMOUNT_ERROR = 'Invalid amount'; -export const MATCH_ERROR = 'Sender and recipient cannot be same'; - export interface FormValues { coins: string; recipientAccountNumber: string; @@ -32,10 +30,10 @@ interface ComponentProps { const SendCoinsModalFields: FC = ({submitting}) => { const {errors, touched, values} = useFormContext(); const activeBankConfig = useSelector(getActiveBankConfig)!; - const activePrimaryValidatorConfig = useSelector(getActivePrimaryValidatorConfig); - const managedAccounts = useSelector(getManagedAccounts); const managedAccountBalances = useSelector(getManagedAccountBalances); + const managedAccounts = useSelector(getManagedAccounts); const managedFriends = useSelector(getManagedFriends); + const primaryValidatorConfig = useSelector(getPrimaryValidatorConfig); const coinsError = touched.coins ? errors.coins : ''; const matchError = errors.recipientAccountNumber === MATCH_ERROR; @@ -66,15 +64,15 @@ const SendCoinsModalFields: FC = ({submitting}) => { const renderTotal = (): string => { const {coins, senderAccountNumber} = values; - if (!activePrimaryValidatorConfig || !coins) return '-'; + if (!primaryValidatorConfig || !coins) return '-'; const bankTxFee = getBankTxFee(activeBankConfig, senderAccountNumber); - const validatorTxFee = getPrimaryValidatorTxFee(activePrimaryValidatorConfig, senderAccountNumber); + const validatorTxFee = getPrimaryValidatorTxFee(primaryValidatorConfig, senderAccountNumber); return (parseInt(coins, 10) + bankTxFee + validatorTxFee).toLocaleString(); }; const renderValidatorFee = (): number | string => { - if (!activePrimaryValidatorConfig) return '-'; - return getPrimaryValidatorTxFee(activePrimaryValidatorConfig, values?.senderAccountNumber) || '-'; + if (!primaryValidatorConfig) return '-'; + return getPrimaryValidatorTxFee(primaryValidatorConfig, values?.senderAccountNumber) || '-'; }; return ( diff --git a/src/renderer/containers/SendCoinsModal/index.tsx b/src/renderer/containers/Account/SendCoinsModal/index.tsx similarity index 87% rename from src/renderer/containers/SendCoinsModal/index.tsx rename to src/renderer/containers/Account/SendCoinsModal/index.tsx index c97441e1..d828c8da 100644 --- a/src/renderer/containers/SendCoinsModal/index.tsx +++ b/src/renderer/containers/Account/SendCoinsModal/index.tsx @@ -4,10 +4,11 @@ import {useDispatch, useSelector} from 'react-redux'; import {FormButton} from '@renderer/components/FormComponents'; import Icon, {IconType} from '@renderer/components/Icon'; import Modal from '@renderer/components/Modal'; +import {INVALID_AMOUNT_ERROR, MATCH_ERROR} from '@renderer/constants/form-validation'; import {fetchAccountBalance} from '@renderer/dispatchers/balances'; import { getActiveBankConfig, - getActivePrimaryValidatorConfig, + getPrimaryValidatorConfig, getManagedAccountBalances, getManagedAccounts, } from '@renderer/selectors'; @@ -17,7 +18,7 @@ import yup from '@renderer/utils/forms/yup'; import {displayErrorToast, displayToast} from '@renderer/utils/toast'; import {getBankTxFee, getPrimaryValidatorTxFee} from '@renderer/utils/transactions'; -import SendCoinsModalFields, {FormValues, INVALID_AMOUNT_ERROR, MATCH_ERROR} from './SendCoinsModalFields'; +import SendCoinsModalFields, {FormValues} from './SendCoinsModalFields'; import './SendCoinsModal.scss'; const COIN_AMOUNT_CEILING = 100_000_000; @@ -32,7 +33,7 @@ const SendCoinsModal: FC = ({close, initialRecipient, initialSen const dispatch = useDispatch(); const [submitting, setSubmitting] = useState(false); const activeBank = useSelector(getActiveBankConfig)!; - const activePrimaryValidator = useSelector(getActivePrimaryValidatorConfig)!; + const primaryValidator = useSelector(getPrimaryValidatorConfig)!; const managedAccounts = useSelector(getManagedAccounts); const managedAccountBalances = useSelector(getManagedAccountBalances); @@ -42,12 +43,10 @@ const SendCoinsModal: FC = ({close, initialRecipient, initialSen const {balance} = managedAccountBalances[accountNumber]; if (!balance) return false; const totalCost = - getBankTxFee(activeBank, accountNumber) + - getPrimaryValidatorTxFee(activePrimaryValidator, accountNumber) + - coins; + getBankTxFee(activeBank, accountNumber) + getPrimaryValidatorTxFee(primaryValidator, accountNumber) + coins; return totalCost <= balance; }, - [activeBank, activePrimaryValidator, managedAccountBalances], + [activeBank, primaryValidator, managedAccountBalances], ); const initialValues = useMemo( @@ -62,14 +61,12 @@ const SendCoinsModal: FC = ({close, initialRecipient, initialSen const handleSubmit = async ({coins, recipientAccountNumber, senderAccountNumber}: FormValues): Promise => { try { setSubmitting(true); - const coinAmount = parseInt(coins, 10); await sendBlock( activeBank, - activePrimaryValidator, - coinAmount, - managedAccounts, - recipientAccountNumber, + primaryValidator, + managedAccounts[senderAccountNumber].signing_key, senderAccountNumber, + [{accountNumber: recipientAccountNumber, amount: parseInt(coins, 10)}], ); await Promise.all([ dispatch(fetchAccountBalance(senderAccountNumber)), diff --git a/src/renderer/containers/Account/index.tsx b/src/renderer/containers/Account/index.tsx index 6b8430d3..5f6d8d84 100644 --- a/src/renderer/containers/Account/index.tsx +++ b/src/renderer/containers/Account/index.tsx @@ -7,10 +7,10 @@ import PageLayout from '@renderer/components/PageLayout'; import PageTabs from '@renderer/components/PageTabs'; import {Button} from '@renderer/components/FormElements'; import {DropdownMenuOption} from '@renderer/components/DropdownMenuButton'; -import SendCoinsModal from '@renderer/containers/SendCoinsModal'; +import SendCoinsModal from '@renderer/containers/Account/SendCoinsModal'; import {useBooleanState} from '@renderer/hooks'; import {getManagedAccounts, getManagedFriends} from '@renderer/selectors'; -import {AccountType, ManagedAccount, ManagedFriend} from '@renderer/types'; +import {AccountNumberParams, AccountType, ManagedAccount, ManagedFriend} from '@renderer/types'; import AccountOverview from './AccountOverview'; import AccountTransactions from './AccountTransactions'; @@ -20,7 +20,7 @@ import EditAccountNicknameModal from './EditAccountNicknameModal'; import './Account.scss'; const Account: FC = () => { - const {accountNumber} = useParams<{accountNumber: string}>(); + const {accountNumber} = useParams(); const {path, url} = useRouteMatch(); const [deleteAccountModalIsOpen, toggleDeleteAccountModal] = useBooleanState(false); const [deleteFriendModalIsOpen, toggleDeleteFriendModal] = useBooleanState(false); diff --git a/src/renderer/containers/Bank/Bank.scss b/src/renderer/containers/Bank/Bank.scss index 351b7822..f33310ee 100644 --- a/src/renderer/containers/Bank/Bank.scss +++ b/src/renderer/containers/Bank/Bank.scss @@ -1,5 +1,7 @@ +@use 'src/renderer/styles/layout'; + .Bank { - height: 100%; + @include layout.main-container; &__Badge { margin-right: 12px; diff --git a/src/renderer/containers/Bank/BankAccounts/index.tsx b/src/renderer/containers/Bank/BankAccounts/index.tsx index 3de07893..3fa7d22e 100644 --- a/src/renderer/containers/Bank/BankAccounts/index.tsx +++ b/src/renderer/containers/Bank/BankAccounts/index.tsx @@ -2,10 +2,9 @@ import React, {FC, useCallback, useMemo, useState} from 'react'; import AccountLink from '@renderer/components/AccountLink'; import Icon, {IconType} from '@renderer/components/Icon'; -import PageTable, {PageTableData, PageTableItems} from '@renderer/components/PageTable'; -import Pagination from '@renderer/components/Pagination'; +import PaginatedTable, {PageTableData, PageTableItems} from '@renderer/components/PaginatedTable'; import EditTrustModal from '@renderer/containers/EditTrustModal'; -import {BANK_ACCOUNTS} from '@renderer/constants'; +import {BANK_ACCOUNTS} from '@renderer/constants/actions'; import {useAddress, useBooleanState, usePaginatedNetworkDataFetcher} from '@renderer/hooks'; import {BankAccount, ManagedNode} from '@renderer/types'; import {formatDate} from '@renderer/utils/dates'; @@ -95,9 +94,16 @@ const BankAccounts: FC = ({managedBank}) => { ); return ( -
- - + <> + {editTrustModalIsOpen && !!editTrustAccount && !!managedBank && ( = ({managedBank}) => { trust={editTrustAccount.trust} /> )} -
+ ); }; diff --git a/src/renderer/containers/Bank/BankBanks/index.tsx b/src/renderer/containers/Bank/BankBanks/index.tsx index d35af1b2..4ec93018 100644 --- a/src/renderer/containers/Bank/BankBanks/index.tsx +++ b/src/renderer/containers/Bank/BankBanks/index.tsx @@ -1,13 +1,11 @@ import React, {FC, useCallback, useMemo, useState} from 'react'; import AccountLink from '@renderer/components/AccountLink'; -import {Loader} from '@renderer/components/FormElements'; import Icon, {IconType} from '@renderer/components/Icon'; import NodeLink from '@renderer/components/NodeLink'; -import PageTable, {PageTableData, PageTableItems} from '@renderer/components/PageTable'; -import Pagination from '@renderer/components/Pagination'; +import PaginatedTable, {PageTableData, PageTableItems} from '@renderer/components/PaginatedTable'; import EditTrustModal from '@renderer/containers/EditTrustModal'; -import {BANK_BANKS} from '@renderer/constants'; +import {BANK_BANKS} from '@renderer/constants/actions'; import {useAddress, useBooleanState, usePaginatedNetworkDataFetcher} from '@renderer/hooks'; import {ManagedNode, Node} from '@renderer/types'; @@ -104,25 +102,26 @@ const BankBanks: FC = ({managedBank}) => { ); return ( -
- {loading ? ( - - ) : ( - <> - - - {editTrustModalIsOpen && !!editTrustBank && !!managedBank && ( - - )} - + <> + + {editTrustModalIsOpen && !!editTrustBank && !!managedBank && ( + )} -
+ ); }; diff --git a/src/renderer/containers/Bank/BankBlocks/index.tsx b/src/renderer/containers/Bank/BankBlocks/index.tsx index 513f73b0..2b277146 100644 --- a/src/renderer/containers/Bank/BankBlocks/index.tsx +++ b/src/renderer/containers/Bank/BankBlocks/index.tsx @@ -1,9 +1,8 @@ import React, {FC, useMemo} from 'react'; import AccountLink from '@renderer/components/AccountLink'; -import PageTable, {PageTableData, PageTableItems} from '@renderer/components/PageTable'; -import Pagination from '@renderer/components/Pagination'; -import {BANK_BLOCKS} from '@renderer/constants'; +import PaginatedTable, {PageTableData, PageTableItems} from '@renderer/components/PaginatedTable'; +import {BANK_BLOCKS} from '@renderer/constants/actions'; import {useAddress, usePaginatedNetworkDataFetcher} from '@renderer/hooks'; import {BlockResponse} from '@renderer/types'; import {formatDate} from '@renderer/utils/dates'; @@ -66,10 +65,15 @@ const BankBlocks: FC = () => { ); return ( -
- - -
+ ); }; diff --git a/src/renderer/containers/Bank/BankConfirmationBlocks/index.tsx b/src/renderer/containers/Bank/BankConfirmationBlocks/index.tsx index 45a220d9..2f69814e 100644 --- a/src/renderer/containers/Bank/BankConfirmationBlocks/index.tsx +++ b/src/renderer/containers/Bank/BankConfirmationBlocks/index.tsx @@ -1,8 +1,7 @@ import React, {FC, useMemo} from 'react'; -import PageTable, {PageTableData, PageTableItems} from '@renderer/components/PageTable'; -import Pagination from '@renderer/components/Pagination'; -import {BANK_CONFIRMATION_BLOCKS} from '@renderer/constants'; +import PaginatedTable, {PageTableData, PageTableItems} from '@renderer/components/PaginatedTable'; +import {BANK_CONFIRMATION_BLOCKS} from '@renderer/constants/actions'; import {useAddress, usePaginatedNetworkDataFetcher} from '@renderer/hooks'; import {BankConfirmationBlock} from '@renderer/types'; @@ -51,10 +50,15 @@ const BankConfirmationBlocks: FC = () => { ); return ( -
- - -
+ ); }; diff --git a/src/renderer/containers/Bank/BankInvalidBlocks/index.tsx b/src/renderer/containers/Bank/BankInvalidBlocks/index.tsx index fed1efe3..de738f3d 100644 --- a/src/renderer/containers/Bank/BankInvalidBlocks/index.tsx +++ b/src/renderer/containers/Bank/BankInvalidBlocks/index.tsx @@ -1,8 +1,7 @@ import React, {FC, useMemo} from 'react'; -import PageTable, {PageTableData, PageTableItems} from '@renderer/components/PageTable'; -import Pagination from '@renderer/components/Pagination'; -import {BANK_INVALID_BLOCKS} from '@renderer/constants'; +import PaginatedTable, {PageTableData, PageTableItems} from '@renderer/components/PaginatedTable'; +import {BANK_INVALID_BLOCKS} from '@renderer/constants/actions'; import {useAddress, usePaginatedNetworkDataFetcher} from '@renderer/hooks'; import {InvalidBlock} from '@renderer/types'; @@ -51,10 +50,15 @@ const BankInvalidBlocks: FC = () => { ); return ( -
- - -
+ ); }; diff --git a/src/renderer/containers/Bank/BankOverview/BankOverview.scss b/src/renderer/containers/Bank/BankOverview/BankOverview.scss index b4cfd948..288992bf 100644 --- a/src/renderer/containers/Bank/BankOverview/BankOverview.scss +++ b/src/renderer/containers/Bank/BankOverview/BankOverview.scss @@ -10,4 +10,10 @@ margin-bottom: 12px; } } + + &__purchase-confirmation-services { + &:hover { + text-decoration: underline; + } + } } diff --git a/src/renderer/containers/Bank/BankOverview/index.tsx b/src/renderer/containers/Bank/BankOverview/index.tsx index 1cdaed7e..58474031 100644 --- a/src/renderer/containers/Bank/BankOverview/index.tsx +++ b/src/renderer/containers/Bank/BankOverview/index.tsx @@ -1,8 +1,9 @@ -import React, {FC, useRef} from 'react'; +import React, {FC, ReactNode, useCallback, useRef} from 'react'; +import {Link, useParams} from 'react-router-dom'; import {Loader} from '@renderer/components/FormElements'; -import {TileBankSigningDetails, TileCrawlClean, TileKeyValueList, TilePrimaryAmount} from '@renderer/components/Tiles'; -import {BANK_CONFIGS, BANK_VALIDATOR_CONFIRMATION_SERVICES} from '@renderer/constants'; +import {TileBankSigningDetails, TileCrawlClean, TileKeyValueList, TileWithAmount} from '@renderer/components/Tiles'; +import {BANK_CONFIGS, BANK_VALIDATOR_CONFIRMATION_SERVICES} from '@renderer/constants/actions'; import { useAddress, useNetworkConfigFetcher, @@ -10,7 +11,7 @@ import { useNetworkCleanFetcher, usePaginatedNetworkDataFetcher, } from '@renderer/hooks'; -import {BankConfig, ManagedNode, ValidatorConfirmationService} from '@renderer/types'; +import {AddressParams, BankConfig, ManagedNode, ValidatorConfirmationService} from '@renderer/types'; import './BankOverview.scss'; @@ -34,9 +35,23 @@ const BankOverview: FC = ({isAuthenticated, managedBank}) => { count: confirmationServiceCount, loading: confirmationServiceLoading, } = usePaginatedNetworkDataFetcher(BANK_VALIDATOR_CONFIRMATION_SERVICES, address); + const {ipAddress, port, protocol} = useParams(); const bankAccountNumberRef = useRef(null); const bankNetworkIdRef = useRef(null); + const renderConfirmationServiceButton = useCallback((): ReactNode => { + if (!isAuthenticated) return null; + + return ( + + Purchase + + ); + }, [ipAddress, isAuthenticated, port, protocol]); + return (
{loadingConfig || !bankConfig ? ( @@ -44,9 +59,10 @@ const BankOverview: FC = ({isAuthenticated, managedBank}) => { ) : ( <>
- - + diff --git a/src/renderer/containers/Bank/BankTransactions/index.tsx b/src/renderer/containers/Bank/BankTransactions/index.tsx index 626145ca..07356ab2 100644 --- a/src/renderer/containers/Bank/BankTransactions/index.tsx +++ b/src/renderer/containers/Bank/BankTransactions/index.tsx @@ -1,9 +1,8 @@ import React, {FC, useMemo} from 'react'; import AccountLink from '@renderer/components/AccountLink'; -import PageTable, {PageTableData, PageTableItems} from '@renderer/components/PageTable'; -import Pagination from '@renderer/components/Pagination'; -import {BANK_BANK_TRANSACTIONS} from '@renderer/constants'; +import PaginatedTable, {PageTableData, PageTableItems} from '@renderer/components/PaginatedTable'; +import {BANK_BANK_TRANSACTIONS} from '@renderer/constants/actions'; import {useAddress, usePaginatedNetworkDataFetcher} from '@renderer/hooks'; import {BankTransaction} from '@renderer/types'; @@ -55,10 +54,15 @@ const BankTransactions: FC = () => { ); return ( -
- - -
+ ); }; diff --git a/src/renderer/containers/Bank/BankValidatorConfirmationServices/index.tsx b/src/renderer/containers/Bank/BankValidatorConfirmationServices/index.tsx index 9291ecca..5562c90d 100644 --- a/src/renderer/containers/Bank/BankValidatorConfirmationServices/index.tsx +++ b/src/renderer/containers/Bank/BankValidatorConfirmationServices/index.tsx @@ -1,8 +1,7 @@ import React, {FC, useMemo} from 'react'; -import PageTable, {PageTableData, PageTableItems} from '@renderer/components/PageTable'; -import Pagination from '@renderer/components/Pagination'; -import {BANK_VALIDATOR_CONFIRMATION_SERVICES} from '@renderer/constants'; +import PaginatedTable, {PageTableData, PageTableItems} from '@renderer/components/PaginatedTable'; +import {BANK_VALIDATOR_CONFIRMATION_SERVICES} from '@renderer/constants/actions'; import {useAddress, usePaginatedNetworkDataFetcher} from '@renderer/hooks'; import {ValidatorConfirmationService} from '@renderer/types'; import {formatDate} from '@renderer/utils/dates'; @@ -65,10 +64,15 @@ const BankValidatorConfirmationServices: FC = () => { ); return ( -
- - -
+ ); }; diff --git a/src/renderer/containers/Bank/BankValidators/index.tsx b/src/renderer/containers/Bank/BankValidators/index.tsx index 5793d274..02ae2fa1 100644 --- a/src/renderer/containers/Bank/BankValidators/index.tsx +++ b/src/renderer/containers/Bank/BankValidators/index.tsx @@ -4,13 +4,12 @@ import {useSelector} from 'react-redux'; import AccountLink from '@renderer/components/AccountLink'; import Icon, {IconType} from '@renderer/components/Icon'; import NodeLink from '@renderer/components/NodeLink'; -import PageTable, {PageTableData, PageTableItems} from '@renderer/components/PageTable'; -import Pagination from '@renderer/components/Pagination'; +import PaginatedTable, {PageTableData, PageTableItems} from '@renderer/components/PaginatedTable'; import EditTrustModal from '@renderer/containers/EditTrustModal'; -import PurchaseConfirmationServicesModal from '@renderer/containers/PurchaseConfirmationServicesModal'; -import {BANK_VALIDATORS} from '@renderer/constants'; +import PurchaseConfirmationServicesModal from '@renderer/containers/PurchaseConfirmationServices/PurchaseConfirmationServicesModal'; +import {BANK_VALIDATORS} from '@renderer/constants/actions'; import {useAddress, useBooleanState, usePaginatedNetworkDataFetcher} from '@renderer/hooks'; -import {getActivePrimaryValidatorConfig} from '@renderer/selectors'; +import {getPrimaryValidatorConfig} from '@renderer/selectors'; import {BaseValidator, ManagedNode} from '@renderer/types'; import './BankValidators.scss'; @@ -48,7 +47,7 @@ const BankValidators: FC = ({managedBank}) => { const [editTrustValidator, setEditTrustValidator] = useState(null); const [purchaseServicesModalIsOpen, togglePurchaseServicesModal] = useBooleanState(false); const [purchaseServicesValidator, setPurchaseServicesValidator] = useState(null); - const activePrimaryValidator = useSelector(getActivePrimaryValidatorConfig); + const primaryValidator = useSelector(getPrimaryValidatorConfig); const handleEditTrustButton = useCallback( (validator: BaseValidator) => (): void => { @@ -70,7 +69,7 @@ const BankValidators: FC = ({managedBank}) => { const renderValidatorDailyRate = useCallback( (validator) => { - if (activePrimaryValidator?.node_identifier === validator.node_identifier || !validator.daily_confirmation_rate) { + if (primaryValidator?.node_identifier === validator.node_identifier || !validator.daily_confirmation_rate) { return '-'; } return hasSigningKey ? ( @@ -81,7 +80,7 @@ const BankValidators: FC = ({managedBank}) => { validator.daily_confirmation_rate ); }, - [activePrimaryValidator, handlePurchaseServicesClick, hasSigningKey], + [handlePurchaseServicesClick, hasSigningKey, primaryValidator], ); const bankValidatorsTableData = useMemo( @@ -153,9 +152,16 @@ const BankValidators: FC = ({managedBank}) => { ); return ( -
- - + <> + {editTrustModalIsOpen && !!editTrustValidator && !!managedBank && ( = ({managedBank}) => { /> )} {purchaseServicesModalIsOpen && !!purchaseServicesValidator && ( - + )} -
+ ); }; diff --git a/src/renderer/containers/ChangeActiveBankModal/ChangeActiveBankModal.scss b/src/renderer/containers/Bank/ChangeActiveBankModal/ChangeActiveBankModal.scss similarity index 100% rename from src/renderer/containers/ChangeActiveBankModal/ChangeActiveBankModal.scss rename to src/renderer/containers/Bank/ChangeActiveBankModal/ChangeActiveBankModal.scss diff --git a/src/renderer/containers/ChangeActiveBankModal/ChangeActiveBankModalFields.tsx b/src/renderer/containers/Bank/ChangeActiveBankModal/ChangeActiveBankModalFields.tsx similarity index 100% rename from src/renderer/containers/ChangeActiveBankModal/ChangeActiveBankModalFields.tsx rename to src/renderer/containers/Bank/ChangeActiveBankModal/ChangeActiveBankModalFields.tsx diff --git a/src/renderer/containers/ChangeActiveBankModal/index.tsx b/src/renderer/containers/Bank/ChangeActiveBankModal/index.tsx similarity index 100% rename from src/renderer/containers/ChangeActiveBankModal/index.tsx rename to src/renderer/containers/Bank/ChangeActiveBankModal/index.tsx diff --git a/src/renderer/containers/LeftMenu/LeftMenu.scss b/src/renderer/containers/Layout/LeftMenu/LeftMenu.scss similarity index 83% rename from src/renderer/containers/LeftMenu/LeftMenu.scss rename to src/renderer/containers/Layout/LeftMenu/LeftMenu.scss index 2cfbb607..b4e8b99f 100644 --- a/src/renderer/containers/LeftMenu/LeftMenu.scss +++ b/src/renderer/containers/Layout/LeftMenu/LeftMenu.scss @@ -19,4 +19,8 @@ margin-bottom: 4px; } } + + &__purchase-confirmation-services { + color: var(--color-secondary); + } } diff --git a/src/renderer/containers/LeftMenu/LeftSubmenu/LeftSubmenu.scss b/src/renderer/containers/Layout/LeftMenu/LeftSubmenu/LeftSubmenu.scss similarity index 100% rename from src/renderer/containers/LeftMenu/LeftSubmenu/LeftSubmenu.scss rename to src/renderer/containers/Layout/LeftMenu/LeftSubmenu/LeftSubmenu.scss diff --git a/src/renderer/containers/LeftMenu/LeftSubmenu/index.tsx b/src/renderer/containers/Layout/LeftMenu/LeftSubmenu/index.tsx similarity index 100% rename from src/renderer/containers/LeftMenu/LeftSubmenu/index.tsx rename to src/renderer/containers/Layout/LeftMenu/LeftSubmenu/index.tsx diff --git a/src/renderer/containers/LeftMenu/LeftSubmenuItem/LeftSubmenuItem.scss b/src/renderer/containers/Layout/LeftMenu/LeftSubmenuItem/LeftSubmenuItem.scss similarity index 100% rename from src/renderer/containers/LeftMenu/LeftSubmenuItem/LeftSubmenuItem.scss rename to src/renderer/containers/Layout/LeftMenu/LeftSubmenuItem/LeftSubmenuItem.scss diff --git a/src/renderer/containers/LeftMenu/LeftSubmenuItem/index.tsx b/src/renderer/containers/Layout/LeftMenu/LeftSubmenuItem/index.tsx similarity index 64% rename from src/renderer/containers/LeftMenu/LeftSubmenuItem/index.tsx rename to src/renderer/containers/Layout/LeftMenu/LeftSubmenuItem/index.tsx index b8e610fc..20a18dfa 100644 --- a/src/renderer/containers/LeftMenu/LeftSubmenuItem/index.tsx +++ b/src/renderer/containers/Layout/LeftMenu/LeftSubmenuItem/index.tsx @@ -3,17 +3,19 @@ import {NavLink, RouteComponentProps, useHistory, withRouter} from 'react-router import clsx from 'clsx'; import Icon, {IconType} from '@renderer/components/Icon'; +import {getCustomClassNames} from '@renderer/utils/components'; import './LeftSubmenuItem.scss'; export interface LeftSubmenuItemProps extends RouteComponentProps { baseUrl: string; + className?: string; key: string; label: ReactNode; relatedNodePath?: string; to: string; } -const LeftSubmenuItem: FC = ({baseUrl, key, label, location, relatedNodePath, to}) => { +const LeftSubmenuItem: FC = ({baseUrl, className, key, label, location, relatedNodePath, to}) => { const history = useHistory(); const linkIconRef = useRef(null); @@ -30,7 +32,9 @@ const LeftSubmenuItem: FC = ({baseUrl, key, label, locatio if (!relatedNodePath) return null; return ( = ({baseUrl, key, label, locatio return ( {renderLinkIcon()} -
{label}
+
+ {label} +
); }; diff --git a/src/renderer/containers/LeftMenu/LeftSubmenuItemStatus/LeftSubmenuItemStatus.scss b/src/renderer/containers/Layout/LeftMenu/LeftSubmenuItemStatus/LeftSubmenuItemStatus.scss similarity index 100% rename from src/renderer/containers/LeftMenu/LeftSubmenuItemStatus/LeftSubmenuItemStatus.scss rename to src/renderer/containers/Layout/LeftMenu/LeftSubmenuItemStatus/LeftSubmenuItemStatus.scss diff --git a/src/renderer/containers/LeftMenu/LeftSubmenuItemStatus/index.tsx b/src/renderer/containers/Layout/LeftMenu/LeftSubmenuItemStatus/index.tsx similarity index 60% rename from src/renderer/containers/LeftMenu/LeftSubmenuItemStatus/index.tsx rename to src/renderer/containers/Layout/LeftMenu/LeftSubmenuItemStatus/index.tsx index 198d06a5..46e9ba4b 100644 --- a/src/renderer/containers/LeftMenu/LeftSubmenuItemStatus/index.tsx +++ b/src/renderer/containers/Layout/LeftMenu/LeftSubmenuItemStatus/index.tsx @@ -3,12 +3,13 @@ import {NavLink, RouteComponentProps, withRouter} from 'react-router-dom'; import clsx from 'clsx'; import StatusBadge from '@renderer/components/StatusBadge'; - +import {getCustomClassNames} from '@renderer/utils/components'; import './LeftSubmenuItemStatus.scss'; export interface LeftSubmenuItemStatusProps extends RouteComponentProps { badge: 'active-bank' | 'primary-validator' | null; baseUrl: string; + className?: string; isOnline: boolean; key: string; label: ReactNode; @@ -18,6 +19,7 @@ export interface LeftSubmenuItemStatusProps extends RouteComponentProps { const LeftSubmenuItemStatus: FC = ({ badge, baseUrl, + className, isOnline, key, label, @@ -33,6 +35,9 @@ const LeftSubmenuItemStatus: FC = ({ className={clsx('LeftSubmenuItemStatus__badge', { 'LeftSubmenuItemStatus__badge--active-bank': badge === 'active-bank', 'LeftSubmenuItemStatus__badge--primary-validator': badge === 'primary-validator', + ...getCustomClassNames(className, '__badge', true), + ...getCustomClassNames(className, '__badge--active-bank', badge === 'active-bank'), + ...getCustomClassNames(className, '__badge--primary-validator', badge === 'primary-validator'), })} > {badge === 'active-bank' ? 'Active' : 'Primary'} @@ -42,16 +47,21 @@ const LeftSubmenuItemStatus: FC = ({ return ( - +
{label} diff --git a/src/renderer/containers/LeftMenu/index.tsx b/src/renderer/containers/Layout/LeftMenu/index.tsx similarity index 79% rename from src/renderer/containers/LeftMenu/index.tsx rename to src/renderer/containers/Layout/LeftMenu/index.tsx index d4e5bd86..5d514666 100644 --- a/src/renderer/containers/LeftMenu/index.tsx +++ b/src/renderer/containers/Layout/LeftMenu/index.tsx @@ -4,15 +4,13 @@ import {useDispatch, useSelector} from 'react-redux'; import CreateAccountModal from '@renderer/containers/Account/CreateAccountModal'; import AddBankModal from '@renderer/containers/Bank/AddBankModal'; import AddFriendModal from '@renderer/containers/Account/AddFriendModal'; -import LeftSubmenuItem from '@renderer/containers/LeftMenu/LeftSubmenuItem'; -import LeftSubmenuItemStatus from '@renderer/containers/LeftMenu/LeftSubmenuItemStatus'; import AddValidatorModal from '@renderer/containers/Validator/AddValidatorModal'; import {fetchAccountBalance} from '@renderer/dispatchers/balances'; import {useBooleanState} from '@renderer/hooks'; import { - getActivePrimaryValidatorConfig, getBankConfigs, getCoinBalance, + getHasAuthenticatedBanks, getManagedAccounts, getManagedBanks, getManagedFriends, @@ -25,12 +23,12 @@ import {sortByBooleanKey, sortDictValuesByPreferredKey} from '@renderer/utils/so import {displayErrorToast} from '@renderer/utils/toast'; import LeftSubmenu from './LeftSubmenu'; - +import LeftSubmenuItem from './LeftSubmenuItem'; +import LeftSubmenuItemStatus from './LeftSubmenuItemStatus'; import './LeftMenu.scss'; const LeftMenuSelector = (state: RootState) => { return { - activePrimaryValidator: getActivePrimaryValidatorConfig(state), bankConfigs: getBankConfigs(state), coinBalance: getCoinBalance(state), managedAccounts: getManagedAccounts(state), @@ -56,6 +54,7 @@ const LeftMenu: FC = () => { const [addValidatorModalIsOpen, toggleAddValidatorModal] = useBooleanState(false); const [createAccountModalIsOpen, toggleCreateAccountModal] = useBooleanState(false); const dispatch = useDispatch(); + const hasAuthenticatedBanks = useSelector(getHasAuthenticatedBanks); const [loadingBalance, setLoadingBalance] = useState(true); useEffect(() => { @@ -100,30 +99,43 @@ const LeftMenu: FC = () => { )); }, [managedAccounts, managedBanks, managedValidators]); - const bankMenuItems = useMemo( - () => - sortDictValuesByPreferredKey(managedBanks, 'nickname', 'ip_address') - .sort(sortByBooleanKey('is_default')) - .map((managedBank) => ({ - baseUrl: `/bank/${formatPathFromNode(managedBank)}`, - isDefault: managedBank.is_default || false, - isOnline: bankConfigs[formatAddressFromNode(managedBank)]?.error === null || false, - key: managedBank.ip_address, - label: managedBank.nickname || managedBank.ip_address, - to: `/bank/${formatPathFromNode(managedBank)}/overview`, - })) - .map(({baseUrl, isDefault, isOnline, key, label, to}) => ( - - )), - [bankConfigs, managedBanks], - ); + const bankMenuItems = useMemo(() => { + const banks = sortDictValuesByPreferredKey(managedBanks, 'nickname', 'ip_address') + .sort(sortByBooleanKey('is_default')) + .map((managedBank) => ({ + baseUrl: `/bank/${formatPathFromNode(managedBank)}`, + isDefault: managedBank.is_default || false, + isOnline: bankConfigs[formatAddressFromNode(managedBank)]?.error === null || false, + key: formatAddressFromNode(managedBank), + label: managedBank.nickname || formatAddressFromNode(managedBank), + to: `/bank/${formatPathFromNode(managedBank)}/overview`, + })) + .map(({baseUrl, isDefault, isOnline, key, label, to}) => ( + + )); + + if (hasAuthenticatedBanks) { + return [ + ...banks, + , + ]; + } + + return banks; + }, [bankConfigs, hasAuthenticatedBanks, managedBanks]); const friendMenuItems = useMemo( () => @@ -146,8 +158,8 @@ const LeftMenu: FC = () => { baseUrl: `/validator/${formatPathFromNode(managedValidator)}`, isDefault: managedValidator.is_default || false, isOnline: validatorConfigs[formatAddressFromNode(managedValidator)]?.error === null || false, - key: managedValidator.ip_address, - label: managedValidator.nickname || managedValidator.ip_address, + key: formatAddressFromNode(managedValidator), + label: managedValidator.nickname || formatAddressFromNode(managedValidator), to: `/validator/${formatPathFromNode(managedValidator)}/overview`, })) .map(({baseUrl, isDefault, isOnline, key, label, to}) => ( diff --git a/src/renderer/containers/TopNav/TopNav.scss b/src/renderer/containers/Layout/TopNav/TopNav.scss similarity index 100% rename from src/renderer/containers/TopNav/TopNav.scss rename to src/renderer/containers/Layout/TopNav/TopNav.scss diff --git a/src/renderer/containers/TopNav/index.tsx b/src/renderer/containers/Layout/TopNav/index.tsx similarity index 93% rename from src/renderer/containers/TopNav/index.tsx rename to src/renderer/containers/Layout/TopNav/index.tsx index b4085002..fb95d380 100644 --- a/src/renderer/containers/TopNav/index.tsx +++ b/src/renderer/containers/Layout/TopNav/index.tsx @@ -5,11 +5,11 @@ import {ipcRenderer} from 'electron'; import DropdownMenuButton, {DropdownMenuDirection, DropdownMenuOption} from '@renderer/components/DropdownMenuButton'; import Icon, {IconType} from '@renderer/components/Icon'; import Modal from '@renderer/components/Modal'; -import ChangeActiveBankModal from '@renderer/containers/ChangeActiveBankModal'; -import Notifications from '@renderer/containers//Notifications'; +import ChangeActiveBankModal from '@renderer/containers/Bank/ChangeActiveBankModal'; +import Notifications from '@renderer/containers/Notifications'; import {clearLocalState} from '@renderer/dispatchers/app'; import {useBooleanState, useIpcEffect, useNavigationalHistory, useReadIpc, useWriteIpc} from '@renderer/hooks'; -import {getActivePrimaryValidatorConfig} from '@renderer/selectors'; +import {getPrimaryValidatorConfig} from '@renderer/selectors'; import localStore from '@renderer/store/local'; import {AppDispatch, LocalStore} from '@renderer/types'; import {displayToast} from '@renderer/utils/toast'; @@ -45,7 +45,7 @@ const TopNav: FC = () => { useIpcEffect(getSuccessChannel(IpcChannel.restartApp), restartAppSuccessToast); useIpcEffect(getFailChannel(IpcChannel.restartApp), restartAppFailToast); const {back, backEnabled, forward, forwardEnabled, reload} = useNavigationalHistory(); - const activePrimaryValidator = useSelector(getActivePrimaryValidatorConfig); + const primaryValidatorConfig = useSelector(getPrimaryValidatorConfig); const handleImportSuccessCallback = useCallback((event: any, storeData: LocalStore) => { localStore.clear(); @@ -91,7 +91,7 @@ const TopNav: FC = () => { ); const renderRight = (): ReactNode => { - if (!activePrimaryValidator) return null; + if (!primaryValidatorConfig) return null; return (
diff --git a/src/renderer/containers/Layout/index.tsx b/src/renderer/containers/Layout/index.tsx index 9a4ac460..82e46e6d 100644 --- a/src/renderer/containers/Layout/index.tsx +++ b/src/renderer/containers/Layout/index.tsx @@ -4,12 +4,13 @@ import {Redirect, Route, Switch} from 'react-router-dom'; import Account from '@renderer/containers/Account'; import Bank from '@renderer/containers/Bank'; -import LeftMenu from '@renderer/containers/LeftMenu'; -import TopNav from '@renderer/containers/TopNav'; +import PurchaseConfirmationServices from '@renderer/containers/PurchaseConfirmationServices'; import Validator from '@renderer/containers/Validator'; import {getActiveBankConfig} from '@renderer/selectors'; import {formatPathFromNode} from '@renderer/utils/address'; +import LeftMenu from './LeftMenu'; +import TopNav from './TopNav'; import './Layout.scss'; export const Layout: FC = () => { @@ -37,6 +38,9 @@ export const Layout: FC = () => { + + + diff --git a/src/renderer/containers/PurchaseConfirmationServices/BulkPurchaseConfirmationServicesModal/BulkPurchaseConfirmationServicesModal.scss b/src/renderer/containers/PurchaseConfirmationServices/BulkPurchaseConfirmationServicesModal/BulkPurchaseConfirmationServicesModal.scss new file mode 100644 index 00000000..d522375c --- /dev/null +++ b/src/renderer/containers/PurchaseConfirmationServices/BulkPurchaseConfirmationServicesModal/BulkPurchaseConfirmationServicesModal.scss @@ -0,0 +1,9 @@ +.BulkPurchaseConfirmationServicesModal { + width: 1141px; + + &__content { + max-height: calc(85vh - var(--modal-header-height)); + overflow-y: hidden; + padding: 0; + } +} diff --git a/src/renderer/containers/PurchaseConfirmationServices/BulkPurchaseConfirmationServicesModal/BulkPurchaseConfirmationServicesModalFields/BulkPurchaseConfirmationServicesModalFields.scss b/src/renderer/containers/PurchaseConfirmationServices/BulkPurchaseConfirmationServicesModal/BulkPurchaseConfirmationServicesModalFields/BulkPurchaseConfirmationServicesModalFields.scss new file mode 100644 index 00000000..dc564aaf --- /dev/null +++ b/src/renderer/containers/PurchaseConfirmationServices/BulkPurchaseConfirmationServicesModal/BulkPurchaseConfirmationServicesModalFields/BulkPurchaseConfirmationServicesModalFields.scss @@ -0,0 +1,129 @@ +.BulkPurchaseConfirmationServicesModalFields { + display: flex; + + &__bank-address { + color: var(--color-gray-500); + margin-bottom: 12px; + } + + &__bank-nickname { + font-size: var(--font-size-large); + font-weight: bold; + margin-bottom: 30px; + } + + &__close-button { + transition: color 0.3s; + + &:hover { + color: var(--color-red-500); + } + } + + &__Input { + width: 100%; + } + + &__left { + flex: 1; + margin-left: 12px; + max-height: calc(85vh - var(--modal-header-height) - 24px); + overflow-y: auto; + position: relative; + } + + &__purchase-button { + width: 100%; + } + + &__right { + background: var(--color-white); + border-left: 1px solid var(--color-gray-100); + height: calc(85vh - var(--modal-header-height) - 24px); + padding: 12px 18px; + width: 334px; + } + + &__summary-table { + margin-bottom: 42px; + + td { + border-bottom: 1px solid var(--color-gray-100); + min-width: 140px; + overflow-wrap: anywhere; + padding: 6px 0; + text-align: left; + vertical-align: top; + + &:nth-child(1) { + color: var(--color-gray-500); + font-weight: 400; + width: 175px; + } + } + + tr:first-child td { + border-top: 1px solid var(--color-gray-100); + } + + tr:nth-last-child(3) td { + border-bottom-width: 3px; + } + + tr:nth-last-child(1) td, + tr:nth-last-child(2) td { + border-bottom: 0; + } + + .total-tx--error { + color: var(--color-red-500); + } + } + + &__validator-table { + &__td { + // nodeIdentifier column + &--0 { + max-width: 285px; + width: 285px; + } + + // ConnectionStatus column + &--1 { + padding-right: 12px; + max-width: 220px; + width: 220px; + } + + // dailyRate column + &--2 { + max-width: 110px; + width: 110px; + } + + // amount column + &--3 { + max-width: 130px; + width: 130px; + } + + // delete row column + &--4 { + align-items: center; + display: flex; + justify-content: center; + max-width: 50px; + width: 50px; + } + } + + &__th { + background: var(--color-gray-050); + height: 35px; + padding: 6px 4px; + position: sticky; + top: 0; + z-index: 1; + } + } +} diff --git a/src/renderer/containers/PurchaseConfirmationServices/BulkPurchaseConfirmationServicesModal/BulkPurchaseConfirmationServicesModalFields/index.tsx b/src/renderer/containers/PurchaseConfirmationServices/BulkPurchaseConfirmationServicesModal/BulkPurchaseConfirmationServicesModalFields/index.tsx new file mode 100644 index 00000000..66d9a733 --- /dev/null +++ b/src/renderer/containers/PurchaseConfirmationServices/BulkPurchaseConfirmationServicesModal/BulkPurchaseConfirmationServicesModalFields/index.tsx @@ -0,0 +1,307 @@ +import React, {ChangeEvent, Dispatch, FC, useCallback, useEffect, useMemo} from 'react'; +import {useDispatch, useSelector} from 'react-redux'; +import clsx from 'clsx'; +import noop from 'lodash/noop'; + +import {Button, Input, Loader} from '@renderer/components/FormElements'; +import Icon, {IconType} from '@renderer/components/Icon'; +import PageTable from '@renderer/components/PageTable'; +import {PageTableData, PageTableItems} from '@renderer/components/PaginatedTable'; +import RequiredAsterisk from '@renderer/components/RequiredAsterisk'; +import {fetchAccountBalance} from '@renderer/dispatchers/balances'; +import { + getAccountBalances, + getPrimaryValidator, + getPrimaryValidatorConfig, + getBankConfigs, + getActiveBankConfig, +} from '@renderer/selectors'; +import {AppDispatch, ManagedNode} from '@renderer/types'; +import {formatAddressFromNode, isSameNode} from '@renderer/utils/address'; +import {getKeyPairFromSigningKeyHex} from '@renderer/utils/signing'; +import {displayErrorToast} from '@renderer/utils/toast'; +import {getBankTxFee, getPrimaryValidatorTxFee} from '@renderer/utils/transactions'; + +import ConnectionStatus from '../../ConnectionStatus'; +import { + checkConnectionBankToValidator, + checkConnectionValidatorToBank, + removeValidatorInForm, + SelectedValidatorState, + setValidatorInForm, + ValidatorConnectionStatus, + ValidatorFormAction, + ValidatorFormState, +} from '../../utils'; +import './BulkPurchaseConfirmationServicesModalFields.scss'; + +interface ComponentProps { + dispatchFormValues: Dispatch; + formValues: ValidatorFormState; + handleSubmit(): Promise; + initialValidatorsData: SelectedValidatorState; + orderedNodeIdentifiers: string[]; + selectedBank: ManagedNode; + submitting: boolean; +} + +enum TableKeys { + amount, + dailyRate, + nodeIdentifier, + removeValidator, + status, +} + +const BulkPurchaseConfirmationServicesModalFields: FC = ({ + dispatchFormValues, + formValues, + handleSubmit, + initialValidatorsData, + orderedNodeIdentifiers, + selectedBank, + submitting, +}) => { + const selectedBankAddress = formatAddressFromNode(selectedBank); + const dispatch = useDispatch(); + const accountBalances = useSelector(getAccountBalances); + const activeBankConfig = useSelector(getActiveBankConfig)!; + const bankConfigs = useSelector(getBankConfigs); + const primaryValidator = useSelector(getPrimaryValidator)!; + const primaryValidatorConfig = useSelector(getPrimaryValidatorConfig)!; + const {data: selectedBankConfig} = bankConfigs[selectedBankAddress]; + const {node_identifier: selectedBankNodeIdentifier} = selectedBankConfig; + const {publicKeyHex: selectedBankAccountNumber} = getKeyPairFromSigningKeyHex(selectedBank.account_signing_key); + const selectedBankBalance = accountBalances[selectedBankAccountNumber]; + + const testConnection = useCallback( + async (validatorNodeIdentifier: string) => { + const validator = initialValidatorsData[validatorNodeIdentifier]; + const formValue = formValues[validatorNodeIdentifier]; + try { + await Promise.all([ + checkConnectionBankToValidator(selectedBank, validator), + checkConnectionValidatorToBank(selectedBank, validator, selectedBankNodeIdentifier), + ]); + dispatchFormValues( + setValidatorInForm({ + amount: formValue.amount, + nodeIdentifier: validatorNodeIdentifier, + status: ValidatorConnectionStatus.connected, + }), + ); + } catch (error) { + dispatchFormValues( + setValidatorInForm({ + amount: '', + nodeIdentifier: validatorNodeIdentifier, + status: ValidatorConnectionStatus.notConnected, + }), + ); + } + }, + [selectedBank, selectedBankNodeIdentifier, dispatchFormValues, formValues, initialValidatorsData], + ); + + useEffect(() => { + for (const nodeIdentifier of orderedNodeIdentifiers) { + testConnection(nodeIdentifier); + } + // ensure that this only runs once + // eslint-disable-next-line + }, []); + + useEffect(() => { + try { + dispatch(fetchAccountBalance(selectedBankAccountNumber)); + } catch (error) { + displayErrorToast(error); + } + }, [selectedBankAccountNumber, dispatch]); + + const handleAmountChange = useCallback( + (nodeIdentifier: string) => (e: ChangeEvent): void => { + e.preventDefault(); + if (submitting) return; + const formValue = formValues[nodeIdentifier]; + dispatchFormValues(setValidatorInForm({amount: e.target.value, nodeIdentifier, status: formValue.status})); + }, + [dispatchFormValues, formValues, submitting], + ); + + const handleRemoveValidator = useCallback( + (nodeIdentifier: string) => (): void => { + if (submitting) return; + dispatchFormValues(removeValidatorInForm({nodeIdentifier})); + }, + [dispatchFormValues, submitting], + ); + + const validatorsTableData = useMemo( + () => + orderedNodeIdentifiers + .filter((nodeIdentifier) => !!formValues[nodeIdentifier]) + .map((nodeIdentifier) => { + const formData = formValues[nodeIdentifier]; + const validatorData = initialValidatorsData[nodeIdentifier]; + + const noDailyRate = !validatorData.daily_confirmation_rate; + const validatorIsPv = primaryValidator && isSameNode(primaryValidator, validatorData); + const amountIsDisabled = + noDailyRate || validatorIsPv || formData.status !== ValidatorConnectionStatus.connected || submitting; + + return { + key: nodeIdentifier, + [TableKeys.amount]: ( + + ), + [TableKeys.dailyRate]: validatorData.daily_confirmation_rate, + [TableKeys.nodeIdentifier]: nodeIdentifier, + [TableKeys.removeValidator]: ( + + ), + [TableKeys.status]: , + }; + }), + [ + formValues, + handleAmountChange, + handleRemoveValidator, + initialValidatorsData, + orderedNodeIdentifiers, + primaryValidator, + submitting, + ], + ); + + const pageTableItems = useMemo( + () => ({ + data: validatorsTableData, + headers: { + [TableKeys.amount]: ( + + Amount + + + ), + [TableKeys.dailyRate]: 'Daily Rate', + [TableKeys.nodeIdentifier]: 'Validator Network ID', + [TableKeys.removeValidator]: '', + [TableKeys.status]: 'Status', + }, + orderedKeys: [ + TableKeys.nodeIdentifier, + TableKeys.status, + TableKeys.dailyRate, + TableKeys.amount, + TableKeys.removeValidator, + ], + }), + [validatorsTableData], + ); + + const bankFee = useMemo(() => getBankTxFee(activeBankConfig, selectedBankConfig.account_number), [ + activeBankConfig, + selectedBankConfig, + ]); + + const pvFee = useMemo(() => getPrimaryValidatorTxFee(primaryValidatorConfig, selectedBankAddress), [ + selectedBankAddress, + primaryValidatorConfig, + ]); + + const totalAmount = useMemo( + () => + Object.values(formValues).reduce((sum, {amount, status}) => { + if (amount && status === ValidatorConnectionStatus.connected) { + return sum + parseInt(amount, 10); + } + return sum; + }, 0), + [formValues], + ); + + const totalValidators = useMemo( + () => + Object.values(formValues).reduce((count, {amount}) => { + if (amount) { + return count + 1; + } + return count; + }, 0), + [formValues], + ); + + const totalTx = bankFee + pvFee + totalAmount; + + const totalExceedsAvailableBalance = totalTx > (selectedBankBalance?.balance || 0); + + return ( +
+
+ +
+
+
{selectedBankAddress}
+
{selectedBank.nickname}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Account Balance{selectedBankBalance?.balance.toLocaleString() || '-'}
Active Bank Fee{bankFee?.toLocaleString() || '-'}
Primary Validator Fee{pvFee.toLocaleString()}
Total Amount{totalAmount.toLocaleString()}
TOTAL Tx + + {totalTx.toLocaleString()} + +
TOTAL Validators + {totalValidators.toLocaleString()} +
+ +
+
+ ); +}; + +export default BulkPurchaseConfirmationServicesModalFields; diff --git a/src/renderer/containers/PurchaseConfirmationServices/BulkPurchaseConfirmationServicesModal/index.tsx b/src/renderer/containers/PurchaseConfirmationServices/BulkPurchaseConfirmationServicesModal/index.tsx new file mode 100644 index 00000000..cacd74af --- /dev/null +++ b/src/renderer/containers/PurchaseConfirmationServices/BulkPurchaseConfirmationServicesModal/index.tsx @@ -0,0 +1,97 @@ +import React, {FC, useEffect, useMemo, useReducer, useState} from 'react'; +import {useDispatch, useSelector} from 'react-redux'; + +import Modal from '@renderer/components/Modal'; +import {fetchBankConfig} from '@renderer/dispatchers/banks'; +import {getActiveBankConfig, getPrimaryValidatorConfig} from '@renderer/selectors'; +import {AppDispatch, ManagedNode} from '@renderer/types'; +import {formatAddressFromNode} from '@renderer/utils/address'; +import {sendBlock} from '@renderer/utils/blocks'; +import {getKeyPairFromSigningKeyHex} from '@renderer/utils/signing'; +import {displayErrorToast, displayToast} from '@renderer/utils/toast'; + +import BulkPurchaseConfirmationServicesModalFields from './BulkPurchaseConfirmationServicesModalFields'; +import {SelectedValidatorState, ValidatorConnectionStatus, validatorFormReducer} from '../utils'; +import './BulkPurchaseConfirmationServicesModal.scss'; + +interface ComponentProps { + bank: ManagedNode; + close(): void; + selectedValidators: SelectedValidatorState; +} + +const BulkPurchaseConfirmationServicesModal: FC = ({bank, close, selectedValidators}) => { + const dispatch = useDispatch(); + const activeBankConfig = useSelector(getActiveBankConfig)!; + const primaryValidator = useSelector(getPrimaryValidatorConfig)!; + + const [submitting, setSubmitting] = useState(false); + const [formValues, dispatchFormValues] = useReducer( + validatorFormReducer, + Object.keys(selectedValidators).reduce( + (acc, nodeIdentifier) => ({ + ...acc, + [nodeIdentifier]: {amount: '', status: ValidatorConnectionStatus.checking}, + }), + {}, + ), + ); + + useEffect(() => { + const bankAddress = formatAddressFromNode(bank); + dispatch(fetchBankConfig(bankAddress)); + }, [bank, dispatch]); + + const handleSubmit = async (): Promise => { + try { + setSubmitting(true); + const {publicKeyHex: bankAccountNumber} = getKeyPairFromSigningKeyHex(bank.account_signing_key); + const recipients = Object.entries(formValues) + .map(([nodeIdentifier, {amount}]) => { + const validatorData = selectedValidators[nodeIdentifier]; + return { + accountNumber: validatorData.account_number, + amount: parseInt(amount, 10), + }; + }) + .filter(({amount}) => !!amount); + + await sendBlock(activeBankConfig, primaryValidator, bank.account_signing_key, bankAccountNumber, recipients); + displayToast(`You have purchased ${recipients.length} services`, 'success'); + close(); + } catch (error) { + displayErrorToast(error); + setSubmitting(false); + } + }; + + const orderedValidatorNodeIdentifiers = useMemo(() => { + return Object.entries(selectedValidators) + .sort(([, {index: indexA}], [, {index: indexB}]) => { + return indexA - indexB; + }) + .map(([nodeIdentifier]) => nodeIdentifier); + }, [selectedValidators]); + + return ( + + + + ); +}; + +export default BulkPurchaseConfirmationServicesModal; diff --git a/src/renderer/containers/PurchaseConfirmationServicesModal/ConnectionStatus/ConnectionStatus.scss b/src/renderer/containers/PurchaseConfirmationServices/ConnectionStatus/ConnectionStatus.scss similarity index 61% rename from src/renderer/containers/PurchaseConfirmationServicesModal/ConnectionStatus/ConnectionStatus.scss rename to src/renderer/containers/PurchaseConfirmationServices/ConnectionStatus/ConnectionStatus.scss index 8985a5af..1079a394 100644 --- a/src/renderer/containers/PurchaseConfirmationServicesModal/ConnectionStatus/ConnectionStatus.scss +++ b/src/renderer/containers/PurchaseConfirmationServices/ConnectionStatus/ConnectionStatus.scss @@ -7,10 +7,20 @@ } } -.ConnectionStatus { +@mixin connection-status { + align-items: center; border-radius: 3px; - margin-bottom: 24px; - padding: 10px 18px; + display: flex; + font-family: var(--font-family-default); +} + +@mixin title { + font-weight: bold; +} + +.ConnectionStatus { + @include connection-status; + padding: 6px; &--checking { background: var(--color-yellow-100); @@ -27,8 +37,10 @@ color: var(--color-red-500); } - &__badge { - display: flex; + &--subtitled { + @include connection-status; + margin-bottom: 24px; + padding: 10px 18px; } &__icon { @@ -40,8 +52,12 @@ } &__main-title { - font-weight: bold; - margin-bottom: 3px; + @include title; + + &--subtitled { + @include title; + margin-bottom: 3px; + } } &__sub-title { diff --git a/src/renderer/containers/PurchaseConfirmationServices/ConnectionStatus/index.tsx b/src/renderer/containers/PurchaseConfirmationServices/ConnectionStatus/index.tsx new file mode 100644 index 00000000..bbb9d7c6 --- /dev/null +++ b/src/renderer/containers/PurchaseConfirmationServices/ConnectionStatus/index.tsx @@ -0,0 +1,71 @@ +import React, {FC, useMemo} from 'react'; +import clsx from 'clsx'; + +import Icon, {IconType} from '@renderer/components/Icon'; +import {ValidatorConnectionStatus} from '../utils'; +import './ConnectionStatus.scss'; + +interface ComponentProps { + className?: string; + showDescription?: boolean; + status: ValidatorConnectionStatus; +} + +const ConnectionStatus: FC = ({className, showDescription = false, status}) => { + const {icon, mainTitle, subTitle} = useMemo(() => { + if (status === ValidatorConnectionStatus.checking) { + return { + icon: IconType.sync, + mainTitle: 'Checking Connection', + subTitle: 'Please wait while we check the connection', + }; + } + if (status === ValidatorConnectionStatus.connected) { + return { + icon: IconType.lanConnect, + mainTitle: 'Connected', + subTitle: 'Purchase Confirmation Services below', + }; + } + return { + icon: IconType.lanDisconnect, + mainTitle: 'Not Connected', + subTitle: 'Unable to connect to Confirmation Validator', + }; + }, [status]); + + const renderIcon = () => { + const size = showDescription ? 32 : 22; + return ( + + ); + }; + + if (showDescription) { + return ( +
+ {renderIcon()} +
+
{mainTitle}
+
{subTitle}
+
+
+ ); + } + + return ( +
+ {renderIcon()} +
{mainTitle}
+
+ ); +}; + +export default ConnectionStatus; diff --git a/src/renderer/containers/PurchaseConfirmationServices/PurchaseConfirmationServices.scss b/src/renderer/containers/PurchaseConfirmationServices/PurchaseConfirmationServices.scss new file mode 100644 index 00000000..60dddc4e --- /dev/null +++ b/src/renderer/containers/PurchaseConfirmationServices/PurchaseConfirmationServices.scss @@ -0,0 +1,28 @@ +@use 'src/renderer/styles/layout'; + +.PurchaseConfirmationServices { + @include layout.main-container; + + &__no-authenticated-banks { + color: var(--color-red-500); + font-weight: bold; + } + + .header { + @include layout.main-container-header; + margin-bottom: 18px; + } + + .bank-select { + align-items: center; + display: flex; + + &__label { + margin-right: 6px; + } + + &__input { + width: 420px; + } + } +} diff --git a/src/renderer/containers/PurchaseConfirmationServicesModal/PurchaseConfirmationServicesModal.scss b/src/renderer/containers/PurchaseConfirmationServices/PurchaseConfirmationServicesModal/PurchaseConfirmationServicesModal.scss similarity index 100% rename from src/renderer/containers/PurchaseConfirmationServicesModal/PurchaseConfirmationServicesModal.scss rename to src/renderer/containers/PurchaseConfirmationServices/PurchaseConfirmationServicesModal/PurchaseConfirmationServicesModal.scss diff --git a/src/renderer/containers/PurchaseConfirmationServicesModal/PurchaseConfirmationServicesModalFields/PurchaseConfirmationServicesModalFields.scss b/src/renderer/containers/PurchaseConfirmationServices/PurchaseConfirmationServicesModal/PurchaseConfirmationServicesModalFields/PurchaseConfirmationServicesModalFields.scss similarity index 100% rename from src/renderer/containers/PurchaseConfirmationServicesModal/PurchaseConfirmationServicesModalFields/PurchaseConfirmationServicesModalFields.scss rename to src/renderer/containers/PurchaseConfirmationServices/PurchaseConfirmationServicesModal/PurchaseConfirmationServicesModalFields/PurchaseConfirmationServicesModalFields.scss diff --git a/src/renderer/containers/PurchaseConfirmationServicesModal/PurchaseConfirmationServicesModalFields/index.tsx b/src/renderer/containers/PurchaseConfirmationServices/PurchaseConfirmationServicesModal/PurchaseConfirmationServicesModalFields/index.tsx similarity index 64% rename from src/renderer/containers/PurchaseConfirmationServicesModal/PurchaseConfirmationServicesModalFields/index.tsx rename to src/renderer/containers/PurchaseConfirmationServices/PurchaseConfirmationServicesModal/PurchaseConfirmationServicesModalFields/index.tsx index 010659e9..7ee5e916 100644 --- a/src/renderer/containers/PurchaseConfirmationServicesModal/PurchaseConfirmationServicesModalFields/index.tsx +++ b/src/renderer/containers/PurchaseConfirmationServices/PurchaseConfirmationServicesModal/PurchaseConfirmationServicesModalFields/index.tsx @@ -1,17 +1,20 @@ -import React, {FC, useMemo} from 'react'; -import {useSelector} from 'react-redux'; +import React, {FC, useEffect, useMemo} from 'react'; +import {useDispatch, useSelector} from 'react-redux'; import {FormInput, FormSelectDetailed} from '@renderer/components/FormComponents'; import RequiredAsterisk from '@renderer/components/RequiredAsterisk'; +import {fetchBankConfig} from '@renderer/dispatchers/banks'; +import {fetchAccountBalance} from '@renderer/dispatchers/balances'; import {useFormContext} from '@renderer/hooks'; import { + getAccountBalances, getActiveBankConfig, - getActivePrimaryValidatorConfig, - getBankConfigs, - getManagedAccountBalances, - getManagedBanks, + getPrimaryValidatorConfig, + getAuthenticatedBanks, } from '@renderer/selectors'; -import {BaseValidator, InputOption} from '@renderer/types'; +import {AppDispatch, BaseValidator, InputOption} from '@renderer/types'; +import {getKeyPairFromSigningKeyHex} from '@renderer/utils/signing'; +import {displayErrorToast} from '@renderer/utils/toast'; import {getBankTxFee, getPrimaryValidatorTxFee} from '@renderer/utils/transactions'; import './PurchaseConfirmationServicesModalFields.scss'; @@ -27,40 +30,42 @@ interface ComponentProps { } const PurchaseConfirmationServicesModalFields: FC = ({submitting, validator}) => { + const dispatch = useDispatch(); const {values} = useFormContext(); const activeBankConfig = useSelector(getActiveBankConfig)!; - const activePrimaryValidatorConfig = useSelector(getActivePrimaryValidatorConfig)!; - const bankConfigs = useSelector(getBankConfigs); - const managedAccountBalances = useSelector(getManagedAccountBalances); - const managedBanks = useSelector(getManagedBanks); + const primaryValidatorConfig = useSelector(getPrimaryValidatorConfig)!; + const authenticatedBanks = useSelector(getAuthenticatedBanks); + const accountBalances = useSelector(getAccountBalances); + const selectedBank = authenticatedBanks[values.bankAddress]; + const {publicKeyHex: accountNumber} = getKeyPairFromSigningKeyHex(selectedBank.account_signing_key); + const selectedAccountBalance = accountBalances[accountNumber]; - const getBanksAccountNumberFromAddress = (bankAddress: string) => { - const { - data: {account_number: accountNumber}, - } = bankConfigs[bankAddress]; - return accountNumber; - }; + useEffect(() => { + if (values.bankAddress) { + dispatch(fetchBankConfig(values.bankAddress)); + } + }, [dispatch, values.bankAddress]); + + useEffect(() => { + try { + dispatch(fetchAccountBalance(accountNumber)); + } catch (error) { + displayErrorToast(error); + } + }, [accountNumber, dispatch]); const getFromOptions = useMemo( () => - Object.entries(managedBanks).map(([key, managedBank]) => ({ + Object.entries(authenticatedBanks).map(([key, managedBank]) => ({ label: managedBank.nickname, value: key, })), - [managedBanks], + [authenticatedBanks], ); const renderActiveBankFee = (): string => { if (!values?.bankAddress) return activeBankConfig.default_transaction_fee.toLocaleString(); - return getBankTxFee(activeBankConfig, getBanksAccountNumberFromAddress(values.bankAddress)).toLocaleString() || '-'; - }; - - const renderBanksAccountBalance = (): string => { - const {bankAddress} = values; - if (!bankAddress) return '-'; - const accountNumber = getBanksAccountNumberFromAddress(bankAddress); - const managedAccountBalance = managedAccountBalances[accountNumber]; - return managedAccountBalance?.balance.toLocaleString() || '0'; + return getBankTxFee(activeBankConfig, accountNumber).toLocaleString() || '-'; }; const renderDays = (): string => { @@ -72,11 +77,10 @@ const PurchaseConfirmationServicesModalFields: FC = ({submitting }; const renderTotal = (): string => { - const {amount, bankAddress} = values; - if (!amount || !bankAddress) return '-'; - const accountNumber = getBanksAccountNumberFromAddress(bankAddress); + const {amount} = values; + if (!amount) return '-'; const bankTxFee = getBankTxFee(activeBankConfig, accountNumber); - const validatorTxFee = getPrimaryValidatorTxFee(activePrimaryValidatorConfig, accountNumber); + const validatorTxFee = getPrimaryValidatorTxFee(primaryValidatorConfig, accountNumber); return (parseInt(amount, 10) + bankTxFee + validatorTxFee).toLocaleString(); }; @@ -101,7 +105,7 @@ const PurchaseConfirmationServicesModalFields: FC = ({submitting Account Balance - {renderBanksAccountBalance()} + {selectedAccountBalance?.balance.toLocaleString() || '-'} @@ -130,7 +134,7 @@ const PurchaseConfirmationServicesModalFields: FC = ({submitting Primary Validator Fee - {getPrimaryValidatorTxFee(activePrimaryValidatorConfig, values?.bankAddress) || '-'} + {getPrimaryValidatorTxFee(primaryValidatorConfig, values?.bankAddress) || '-'} Total Tx Cost diff --git a/src/renderer/containers/PurchaseConfirmationServices/PurchaseConfirmationServicesModal/index.tsx b/src/renderer/containers/PurchaseConfirmationServices/PurchaseConfirmationServicesModal/index.tsx new file mode 100644 index 00000000..4508c71d --- /dev/null +++ b/src/renderer/containers/PurchaseConfirmationServices/PurchaseConfirmationServicesModal/index.tsx @@ -0,0 +1,172 @@ +import React, {FC, useCallback, useMemo, useState} from 'react'; +import {useSelector} from 'react-redux'; + +import Modal from '@renderer/components/Modal'; +import {INVALID_AMOUNT_ERROR} from '@renderer/constants/form-validation'; +import { + getAccountBalances, + getActiveBankConfig, + getPrimaryValidatorConfig, + getAuthenticatedBanks, + getBankConfigs, +} from '@renderer/selectors'; +import {BaseValidator} from '@renderer/types'; +import {sendBlock} from '@renderer/utils/blocks'; +import yup from '@renderer/utils/forms/yup'; +import {getKeyPairFromSigningKeyHex} from '@renderer/utils/signing'; +import {displayErrorToast, displayToast} from '@renderer/utils/toast'; +import {getBankTxFee, getPrimaryValidatorTxFee} from '@renderer/utils/transactions'; + +import ConnectionStatus from '../ConnectionStatus'; +import {checkConnectionBankToValidator, checkConnectionValidatorToBank, ValidatorConnectionStatus} from '../utils'; +import PurchaseConfirmationServicesModalFields, {FormValues} from './PurchaseConfirmationServicesModalFields'; +import './PurchaseConfirmationServicesModal.scss'; + +interface ComponentProps { + close(): void; + initialBankAddress: string; + validator: BaseValidator; +} + +export interface KnownStatus { + [key: string]: boolean; +} + +const PurchaseConfirmationServicesModal: FC = ({close, initialBankAddress, validator}) => { + const [connectionStatus, setConnectionStatus] = useState(null); + const [knownStatuses, setKnownStatuses] = useState({}); + const [submitting, setSubmitting] = useState(false); + const activeBankConfig = useSelector(getActiveBankConfig)!; + const activePrimaryValidator = useSelector(getPrimaryValidatorConfig)!; + const authenticatedBanks = useSelector(getAuthenticatedBanks); + const bankConfigs = useSelector(getBankConfigs); + const accountBalances = useSelector(getAccountBalances); + + const handleSubmit = async ({amount, bankAddress}: FormValues): Promise => { + try { + setSubmitting(true); + const selectedBank = authenticatedBanks[bankAddress]; + const {publicKeyHex: bankAccountNumber} = getKeyPairFromSigningKeyHex(selectedBank.account_signing_key); + await sendBlock(activeBankConfig, activePrimaryValidator, selectedBank.account_signing_key, bankAccountNumber, [ + {accountNumber: validator.account_number, amount: parseInt(amount, 10)}, + ]); + displayToast('Your payment has been sent', 'success'); + close(); + } catch (error) { + displayErrorToast(error); + setSubmitting(false); + } + }; + + const testConnection = useCallback( + async (bankAddress: string | any) => { + if (!bankAddress) return true; + const knownStatus = knownStatuses[bankAddress]; + if (knownStatus) return knownStatus; + + try { + setConnectionStatus(ValidatorConnectionStatus.checking); + setSubmitting(true); + + const managedBank = authenticatedBanks[bankAddress]; + const { + data: {node_identifier: bankNodeIdentifier}, + } = bankConfigs[bankAddress]; + + await Promise.all([ + checkConnectionBankToValidator(managedBank, validator), + checkConnectionValidatorToBank(managedBank, validator, bankNodeIdentifier), + ]); + + setConnectionStatus(ValidatorConnectionStatus.connected); + setKnownStatuses({ + ...knownStatuses, + [bankAddress]: true, + }); + setSubmitting(false); + } catch (error) { + setConnectionStatus(ValidatorConnectionStatus.notConnected); + setKnownStatuses({ + ...knownStatuses, + [bankAddress]: false, + }); + setSubmitting(false); + } + + return false; + }, + [authenticatedBanks, bankConfigs, knownStatuses, validator], + ); + + const checkCoinsWithBalance = useCallback( + (amount: number, bankAddress: string): boolean => { + if (!amount || !bankAddress) return true; + + const selectedBank = authenticatedBanks[bankAddress]; + const {publicKeyHex: accountNumber} = getKeyPairFromSigningKeyHex(selectedBank.account_signing_key); + + const managedAccountBalance = accountBalances[accountNumber]; + if (!managedAccountBalance) return false; + + const totalCost = + getBankTxFee(activeBankConfig, accountNumber) + + getPrimaryValidatorTxFee(activePrimaryValidator, accountNumber) + + amount; + + return totalCost <= managedAccountBalance.balance; + }, + [accountBalances, activeBankConfig, activePrimaryValidator, authenticatedBanks], + ); + + const validationSchema = useMemo(() => { + const bankAddressRef = yup.ref('bankAddress'); + + return yup.object().shape({ + amount: yup + .number() + .callbackWithRef(bankAddressRef, checkCoinsWithBalance, INVALID_AMOUNT_ERROR) + .moreThan(0, 'Amount must be greater than 0') + .required('Amount is a required field'), + bankAddress: yup + .string() + .test( + 'bank-has-unique-account-number', + 'The account number for this bank matches the validators account number.', + (address) => { + if (!address) return true; + const selectedBank = authenticatedBanks[address]; + const {publicKeyHex: accountNumber} = getKeyPairFromSigningKeyHex(selectedBank.account_signing_key); + return accountNumber !== validator.account_number; + }, + ) + .test('bank-is-connected', '', testConnection) + .required('This field is required'), + }); + }, [authenticatedBanks, checkCoinsWithBalance, testConnection, validator.account_number]); + + const initialValues = useMemo(() => { + return { + amount: '', + bankAddress: initialBankAddress, + }; + }, [initialBankAddress]); + + return ( + + {connectionStatus && } + + + ); +}; + +export default PurchaseConfirmationServicesModal; diff --git a/src/renderer/containers/PurchaseConfirmationServices/PurchaseConfirmationServicesTable/PurchaseConfirmationServicesTable.scss b/src/renderer/containers/PurchaseConfirmationServices/PurchaseConfirmationServicesTable/PurchaseConfirmationServicesTable.scss new file mode 100644 index 00000000..cf9b6d3c --- /dev/null +++ b/src/renderer/containers/PurchaseConfirmationServices/PurchaseConfirmationServicesTable/PurchaseConfirmationServicesTable.scss @@ -0,0 +1,5 @@ +.PurchaseConfirmationServicesTable { + &__node-identifier { + cursor: pointer; + } +} diff --git a/src/renderer/containers/PurchaseConfirmationServices/PurchaseConfirmationServicesTable/index.tsx b/src/renderer/containers/PurchaseConfirmationServices/PurchaseConfirmationServicesTable/index.tsx new file mode 100644 index 00000000..160d929e --- /dev/null +++ b/src/renderer/containers/PurchaseConfirmationServices/PurchaseConfirmationServicesTable/index.tsx @@ -0,0 +1,125 @@ +import React, {Dispatch, FC, useCallback, useMemo} from 'react'; +import clsx from 'clsx'; + +import PaginatedTable, {PageTableData, PageTableItems} from '@renderer/components/PaginatedTable'; +import {PAGINATED_RESULTS_LIMIT} from '@renderer/config'; +import {BANK_VALIDATORS} from '@renderer/constants/actions'; +import {usePaginatedNetworkDataFetcher} from '@renderer/hooks'; +import {BaseValidator} from '@renderer/types'; + +import {SelectedValidatorAction, SelectedValidatorState, toggleSelectedValidator} from '../utils'; +import './PurchaseConfirmationServicesTable.scss'; + +interface ComponentProps { + bankAddress: string; + className?: string; + dispatchSelectedValidators: Dispatch; + selectedValidators: SelectedValidatorState; +} + +enum TableKeys { + accountNumber, + dailyConfirmationRate, + defaultTransactionFee, + ipAddress, + nodeIdentifier, + port, + protocol, + trust, + version, +} + +const PurchaseConfirmationServicesTable: FC = ({ + bankAddress, + className, + dispatchSelectedValidators, + selectedValidators, +}) => { + const { + count, + currentPage, + loading, + results: bankValidators, + setPage, + totalPages, + } = usePaginatedNetworkDataFetcher(BANK_VALIDATORS, bankAddress); + + const handleCheckboxClick = useCallback( + (validatorIndex: number) => () => { + const validator = bankValidators[validatorIndex]; + dispatchSelectedValidators( + toggleSelectedValidator({ + ...validator, + index: (currentPage - 1) * PAGINATED_RESULTS_LIMIT + validatorIndex, + }), + ); + }, + [bankValidators, currentPage, dispatchSelectedValidators], + ); + + const bankValidatorsTableData = useMemo( + () => + bankValidators.map((validator, index) => ({ + key: validator.node_identifier, + [TableKeys.accountNumber]: validator.account_number, + [TableKeys.dailyConfirmationRate]: validator.daily_confirmation_rate, + [TableKeys.defaultTransactionFee]: validator.default_transaction_fee, + [TableKeys.ipAddress]: validator.ip_address, + [TableKeys.nodeIdentifier]: ( + + {validator.node_identifier} + + ), + [TableKeys.port]: validator.port, + [TableKeys.protocol]: validator.protocol, + [TableKeys.trust]: validator.trust, + [TableKeys.version]: validator.version, + })) || [], + [bankValidators, handleCheckboxClick], + ); + + const pageTableItems = useMemo( + () => ({ + data: bankValidatorsTableData, + headers: { + [TableKeys.accountNumber]: 'Account Number', + [TableKeys.dailyConfirmationRate]: 'Daily Rate', + [TableKeys.defaultTransactionFee]: 'Tx Fee', + [TableKeys.ipAddress]: 'IP Address', + [TableKeys.nodeIdentifier]: 'Network ID', + [TableKeys.port]: 'Port', + [TableKeys.protocol]: 'Protocol', + [TableKeys.trust]: 'Trust', + [TableKeys.version]: 'Version', + }, + orderedKeys: [ + TableKeys.nodeIdentifier, + TableKeys.dailyConfirmationRate, + TableKeys.accountNumber, + TableKeys.defaultTransactionFee, + TableKeys.protocol, + TableKeys.ipAddress, + TableKeys.port, + TableKeys.trust, + TableKeys.version, + ], + }), + [bankValidatorsTableData], + ); + + return ( + + ); +}; + +export default PurchaseConfirmationServicesTable; diff --git a/src/renderer/containers/PurchaseConfirmationServices/index.tsx b/src/renderer/containers/PurchaseConfirmationServices/index.tsx new file mode 100644 index 00000000..2c78a62e --- /dev/null +++ b/src/renderer/containers/PurchaseConfirmationServices/index.tsx @@ -0,0 +1,119 @@ +import React, {FC, ReactNode, useMemo, useReducer, useState} from 'react'; +import {useSelector} from 'react-redux'; + +import {Button, Select} from '@renderer/components/FormElements'; +import {useAddress, useBooleanState} from '@renderer/hooks'; +import {getAuthenticatedBanks} from '@renderer/selectors'; +import {Dict, InputOption} from '@renderer/types'; + +import BulkPurchaseConfirmationServicesModal from './BulkPurchaseConfirmationServicesModal'; +import PurchaseConfirmationServicesTable from './PurchaseConfirmationServicesTable'; +import {clearSelectedValidator, selectedValidatorReducer} from './utils'; +import './PurchaseConfirmationServices.scss'; + +const PurchaseConfirmationServices: FC = () => { + const initialAddress = useAddress(); + const authenticatedBanks = useSelector(getAuthenticatedBanks); + const initialBankIsAuthenticated = !!authenticatedBanks[initialAddress]; + + const bankOptionsObject = useMemo>(() => { + return Object.entries(authenticatedBanks).reduce((acc, [bankAddress, bank]) => { + const label = `${bank.nickname ? `${bank.nickname} - ` : ''}${bankAddress}`; + + return { + ...acc, + [bankAddress]: { + label, + value: bankAddress, + }, + }; + }, {}); + }, [authenticatedBanks]); + + const bankOptionsList = useMemo(() => { + return Object.values(bankOptionsObject); + }, [bankOptionsObject]); + + const [purchaseModalIsOpen, togglePurchaseModal] = useBooleanState(false); + const [selectedBankOption, setSelectedBankOption] = useState( + initialBankIsAuthenticated ? bankOptionsObject[initialAddress] : bankOptionsList[0] || null, + ); + const [selectedValidators, dispatchSelectedValidators] = useReducer(selectedValidatorReducer, {}); + + const selectedBank = selectedBankOption ? authenticatedBanks[selectedBankOption.value] : null; + + const selectedCount = useMemo(() => { + return Object.keys(selectedValidators).length; + }, [selectedValidators]); + + const handleBankChange = (option: InputOption): void => { + if (!selectedBankOption) return; + + if (option.value !== selectedBankOption.value) { + dispatchSelectedValidators(clearSelectedValidator()); + setSelectedBankOption(option); + } + }; + + const handlePurchaseClick = (): void => { + if (!selectedCount) return; + + togglePurchaseModal(); + }; + + const renderMainContent = (): ReactNode => { + if (!selectedBankOption) { + return ( +
+ You don't have any authenticated banks +
+ ); + } + + return ( + <> +
+ +