diff --git a/apps/governance/src/app.tsx b/apps/governance/src/app.tsx index c75d5e602..fa206fe4c 100644 --- a/apps/governance/src/app.tsx +++ b/apps/governance/src/app.tsx @@ -16,6 +16,7 @@ import { AppStateProvider } from './contexts/app-state/app-state-provider'; import { ContractsProvider } from './contexts/contracts/contracts-provider'; import { AppRouter } from './routes'; import type { EthereumConfig } from '@vegaprotocol/web3'; +import { WithdrawalApprovalDialogContainer } from '@vegaprotocol/web3'; import { createConnectors, useEthTransactionManager, @@ -43,7 +44,7 @@ import { } from '@vegaprotocol/environment'; import { ENV } from './config'; import type { InMemoryCacheConfig } from '@apollo/client'; -import { WithdrawalDialog } from '@vegaprotocol/withdraws'; +import { CreateWithdrawalDialog } from '@vegaprotocol/withdraws'; import { SplashLoader } from './components/splash-loader'; import { ToastsManager } from './toasts-manager'; import { @@ -158,7 +159,8 @@ const Web3Container = ({ - + + diff --git a/apps/trading/pages/dialogs-container.tsx b/apps/trading/pages/dialogs-container.tsx index 6f0bbeb77..8b181e142 100644 --- a/apps/trading/pages/dialogs-container.tsx +++ b/apps/trading/pages/dialogs-container.tsx @@ -6,7 +6,10 @@ import { VegaConnectDialog } from '@vegaprotocol/wallet'; import { Connectors } from '../lib/vega-connectors'; import { CreateWithdrawalDialog } from '@vegaprotocol/withdraws'; import { DepositDialog } from '@vegaprotocol/deposits'; -import { Web3ConnectUncontrolledDialog } from '@vegaprotocol/web3'; +import { + Web3ConnectUncontrolledDialog, + WithdrawalApprovalDialogContainer, +} from '@vegaprotocol/web3'; import { WelcomeDialog } from '../components/welcome-dialog'; import { TransferDialog } from '@vegaprotocol/accounts'; @@ -27,6 +30,7 @@ const DialogsContainer = () => { + ); }; diff --git a/libs/ui-toolkit/src/components/toast/toast.tsx b/libs/ui-toolkit/src/components/toast/toast.tsx index fdddac9b8..3a5033282 100644 --- a/libs/ui-toolkit/src/components/toast/toast.tsx +++ b/libs/ui-toolkit/src/components/toast/toast.tsx @@ -3,7 +3,8 @@ import styles from './toast.module.css'; import type { IconName } from '@blueprintjs/icons'; import { IconNames } from '@blueprintjs/icons'; import classNames from 'classnames'; -import type { HTMLAttributes, HtmlHTMLAttributes } from 'react'; +import type { HTMLAttributes, HtmlHTMLAttributes, ReactNode } from 'react'; +import { useState } from 'react'; import { forwardRef, useEffect } from 'react'; import { useCallback } from 'react'; import { useLayoutEffect } from 'react'; @@ -11,6 +12,7 @@ import { useRef } from 'react'; import { Intent } from '../../utils/intent'; import { Icon } from '../icon'; import { Loader } from '../loader'; +import { t } from '@vegaprotocol/i18n'; export type ToastContent = JSX.Element | undefined; @@ -63,11 +65,88 @@ export const Panel = forwardRef>( } ); +type CollapsiblePanelProps = { + actions?: ReactNode; +}; +export const CollapsiblePanel = forwardRef< + HTMLDivElement, + CollapsiblePanelProps & HTMLAttributes +>(({ children, className, actions, ...props }, ref) => { + const [collapsed, setCollapsed] = useState(true); + return ( + // eslint-disable-next-line jsx-a11y/no-static-element-interactions +
h4]:font-bold', + 'overflow-auto', + { + 'h-[64px] overflow-hidden': collapsed, + 'pb-4': !collapsed, + }, + className + )} + aria-expanded={!collapsed} + onDoubleClick={(e) => { + e.preventDefault(); + setCollapsed(!collapsed); + }} + {...props} + > + {children} + {collapsed && ( +
+ )} +
+ {actions} + +
+
+ ); +}); + export const ToastHeading = forwardRef< HTMLHeadingElement, HtmlHTMLAttributes ->(({ children, ...props }, ref) => ( -

+>(({ children, className, ...props }, ref) => ( +

{children}

)); @@ -167,7 +246,7 @@ export const Toast = ({ }, // panel's colours { - '[&_[data-panel]]:bg-vega-light-150 [&_[data-panel]]:dark:bg-vega-dark-150 ': + '[&_[data-panel]]:bg-vega-light-150 [&_[data-panel]]:dark:bg-vega-dark-150': intent === Intent.None, '[&_[data-panel]]:bg-vega-blue-350 [&_[data-panel]]:dark:bg-vega-blue-650': intent === Intent.Primary, @@ -178,6 +257,31 @@ export const Toast = ({ '[&_[data-panel]]:bg-vega-pink-350 [&_[data-panel]]:dark:bg-vega-pink-650': intent === Intent.Danger, }, + { + '[&_[data-panel]]:to-vega-light-150 [&_[data-panel]]:dark:to-vega-dark-150': + intent === Intent.None, + '[&_[data-panel]]:to-vega-blue-350 [&_[data-panel]]:dark:to-vega-blue-650': + intent === Intent.Primary, + '[&_[data-panel]]:to-vega-green-350 [&_[data-panel]]:dark:to-vega-green-650': + intent === Intent.Success, + '[&_[data-panel]]:to-vega-orange-350 [&_[data-panel]]:dark:to-vega-orange-650': + intent === Intent.Warning, + '[&_[data-panel]]:to-vega-pink-350 [&_[data-panel]]:dark:to-vega-pink-650': + intent === Intent.Danger, + }, + // panel's actions + { + '[&_[data-panel-actions]]:bg-vega-light-200 [&_[data-panel-actions]]:dark:bg-vega-dark-100 ': + intent === Intent.None, + '[&_[data-panel-actions]]:bg-vega-blue-400 [&_[data-panel-actions]]:dark:bg-vega-blue-600': + intent === Intent.Primary, + '[&_[data-panel-actions]]:bg-vega-green-400 [&_[data-panel-actions]]:dark:bg-vega-green-600': + intent === Intent.Success, + '[&_[data-panel-actions]]:bg-vega-orange-400 [&_[data-panel-actions]]:dark:bg-vega-orange-600': + intent === Intent.Warning, + '[&_[data-panel-actions]]:bg-vega-pink-400 [&_[data-panel-actions]]:dark:bg-vega-pink-600': + intent === Intent.Danger, + }, // panels's progress bar colours '[&_[data-progress-bar]]:mt-[10px] [&_[data-progress-bar]]:mb-[4px]', { diff --git a/libs/web3/src/index.ts b/libs/web3/src/index.ts index beaedc770..687d7e3fc 100644 --- a/libs/web3/src/index.ts +++ b/libs/web3/src/index.ts @@ -9,20 +9,21 @@ export * from './lib/use-ethereum-config'; export * from './lib/use-ethereum-read-contract'; export * from './lib/use-ethereum-transaction-manager'; export * from './lib/use-ethereum-transaction-store'; +export * from './lib/use-ethereum-transaction-toasts'; export * from './lib/use-ethereum-transaction-updater'; export * from './lib/use-ethereum-transaction'; +export * from './lib/use-ethereum-withdraw-approval-toasts'; export * from './lib/use-ethereum-withdraw-approvals-manager'; export * from './lib/use-ethereum-withdraw-approvals-store'; export * from './lib/use-get-withdraw-delay'; export * from './lib/use-get-withdraw-threshold'; export * from './lib/use-token-contract'; export * from './lib/use-token-decimals'; +export * from './lib/use-vega-transaction-toasts'; export * from './lib/use-web3-disconnect'; export * from './lib/web3-connect-dialog'; export * from './lib/web3-connect-store'; export * from './lib/web3-connectors'; export * from './lib/web3-provider'; -export * from './lib/use-vega-transaction-toasts'; -export * from './lib/use-ethereum-transaction-toasts'; -export * from './lib/use-ethereum-withdraw-approval-toasts'; +export * from './lib/withdrawal-approval-dialog'; export * from './lib/withdrawal-approval-status'; diff --git a/libs/web3/src/lib/use-ethereum-withdraw-approval-toasts.tsx b/libs/web3/src/lib/use-ethereum-withdraw-approval-toasts.tsx index d80dce460..0471eb3cc 100644 --- a/libs/web3/src/lib/use-ethereum-withdraw-approval-toasts.tsx +++ b/libs/web3/src/lib/use-ethereum-withdraw-approval-toasts.tsx @@ -50,11 +50,10 @@ const EthWithdrawalApprovalToastContent = ({ ); + return ( <> - {title.length > 0 && ( - {title} - )} + {title.length > 0 && {title}} {details} diff --git a/libs/web3/src/lib/use-vega-transaction-toasts.tsx b/libs/web3/src/lib/use-vega-transaction-toasts.tsx index 5722b8f50..568e54cc6 100644 --- a/libs/web3/src/lib/use-vega-transaction-toasts.tsx +++ b/libs/web3/src/lib/use-vega-transaction-toasts.tsx @@ -49,6 +49,7 @@ import { useMarketList } from '@vegaprotocol/markets'; import type { Side } from '@vegaprotocol/types'; import { OrderStatusMapping } from '@vegaprotocol/types'; import { Size } from '@vegaprotocol/datagrid'; +import { useWithdrawalApprovalDialog } from './withdrawal-approval-dialog'; const intentMap: { [s in VegaTxStatus]: Intent } = { Default: Intent.Primary, @@ -457,20 +458,41 @@ const VegaTxCompleteToastsContent = ({ tx }: VegaTxToastContentProps) => {

); + + const dialogTrigger = ( + // It has to stay as due to the word breaking issue + // eslint-disable-next-line jsx-a11y/anchor-is-valid + { + e.preventDefault(); + if (tx.withdrawal?.id) { + useWithdrawalApprovalDialog.getState().open(tx.withdrawal?.id); + } + }} + > + {t('save your withdrawal details')} + + ); + return ( <> {t('Funds unlocked')} -

{t('Your funds have been unlocked for withdrawal')}

+

{t('Your funds have been unlocked for withdrawal.')}

{tx.txHash && ( -

- - {t('View in block explorer')} - -

+ + {t('View in block explorer')} + )} + {/* TODO: Delay message - This withdrawal is subject to a delay. Come back in 5 days to complete the withdrawal. */} +

+ {t('You can')} {dialogTrigger} {t('for extra security.')} +

{completeWithdrawalButton} @@ -638,8 +660,9 @@ export const useVegaTransactionToasts = () => { ); const fromVegaTransaction = (tx: VegaStoredTxState): Toast => { - const closeAfter = isFinal(tx) ? CLOSE_AFTER : undefined; const { intent, content } = getVegaTransactionContentIntent(tx); + const closeAfter = + isFinal(tx) && !isWithdrawTransaction(tx.body) ? CLOSE_AFTER : undefined; return { id: `vega-${tx.id}`, @@ -680,10 +703,22 @@ export const getVegaTransactionContentIntent = (tx: VegaStoredTxState) => { } // Transaction can be successful but the order can be rejected by the network + const intentForRejectedOrder = + tx.order && + !isOrderAmendmentTransaction(tx.body) && + getOrderToastIntent(tx.order.status); + + // Although the transaction is completed on the vega network the whole + // withdrawal process is not - funds are only released at this point + const intentForCompletedWithdrawal = + tx.status === VegaTxStatus.Complete && + isWithdrawTransaction(tx.body) && + Intent.Warning; + const intent = - (tx.order && - !isOrderAmendmentTransaction(tx.body) && - getOrderToastIntent(tx.order.status)) || + intentForRejectedOrder || + intentForCompletedWithdrawal || intentMap[tx.status]; + return { intent, content }; }; diff --git a/libs/web3/src/lib/withdrawal-approval-dialog.tsx b/libs/web3/src/lib/withdrawal-approval-dialog.tsx new file mode 100644 index 000000000..138e3a2bd --- /dev/null +++ b/libs/web3/src/lib/withdrawal-approval-dialog.tsx @@ -0,0 +1,187 @@ +import { t } from '@vegaprotocol/i18n'; +import { + Button, + CopyWithTooltip, + Dialog, + Icon, + KeyValueTable, + KeyValueTableRow, + Splash, + SyntaxHighlighter, + VegaIcon, + VegaIconNames, +} from '@vegaprotocol/ui-toolkit'; +import { useWithdrawalApprovalQuery } from '@vegaprotocol/wallet'; +import omit from 'lodash/omit'; +import { create } from 'zustand'; + +type WithdrawalApprovalDialogProps = { + withdrawalId: string | undefined; + trigger?: HTMLElement | null; + open: boolean; + onChange: (open: boolean) => void; + asJson?: boolean; +}; +export const WithdrawalApprovalDialog = ({ + withdrawalId, + trigger, + open, + onChange, + asJson, +}: WithdrawalApprovalDialogProps) => { + return ( + } + open={open} + onChange={(isOpen) => onChange(isOpen)} + onCloseAutoFocus={(e) => { + /** + * This mimics radix's default behaviour that focuses the dialog's + * trigger after closing itself + */ + if (trigger) { + e.preventDefault(); + trigger.focus(); + } + }} + > +
+

+ {t( + `If the network is reset or has an outage, records of your withdrawal + may be lost. We recommend you save these details in a safe place so + you can still complete your withdrawal.` + )} +

+ {withdrawalId ? ( + + ) : ( + + )} +
+
+ +
+
+ ); +}; + +type WithdrawalApprovalDialogContentProps = { + withdrawalId: string; + asJson: boolean; +}; + +const NoDataContent = ({ msg = t('No data') }) => ( +
+ {msg} +
+); + +const WithdrawalApprovalDialogContent = ({ + withdrawalId, + asJson, +}: WithdrawalApprovalDialogContentProps) => { + const { data, loading } = useWithdrawalApprovalQuery({ + variables: { + withdrawalId, + }, + }); + + if (loading) { + return ; + } + + if (data?.erc20WithdrawalApproval) { + const details = omit(data.erc20WithdrawalApproval, '__typename'); + + if (asJson) { + return ( +
+ +
+ ); + } else { + return ( +
+
+ + + +
+ + {Object.entries(details).map(([key, value]) => ( + +
{key}
+
+ {value} +
+
+ ))} +
+
+ ); + } + } + + return ; +}; + +export type WithdrawalApprovalDialogStore = { + isOpen: boolean; + id: string; + trigger: HTMLElement | null | undefined; + asJson: boolean; + setOpen: (isOpen: boolean) => void; + open: (id: string, trigger?: HTMLElement | null, asJson?: boolean) => void; +}; + +export const useWithdrawalApprovalDialog = + create()((set) => ({ + isOpen: false, + id: '', + trigger: null, + asJson: false, + setOpen: (isOpen) => { + set({ isOpen: isOpen }); + }, + open: (id, trigger?, asJson = false) => { + set({ + isOpen: true, + id, + trigger, + asJson, + }); + }, + })); + +export const WithdrawalApprovalDialogContainer = () => { + const { isOpen, id, trigger, setOpen, asJson } = + useWithdrawalApprovalDialog(); + return ( + + ); +}; diff --git a/libs/withdraws/src/index.ts b/libs/withdraws/src/index.ts index 6e061ea1c..12fe6d7fc 100644 --- a/libs/withdraws/src/index.ts +++ b/libs/withdraws/src/index.ts @@ -1,13 +1,13 @@ +export * from './lib/__generated__/Erc20Approval'; +export * from './lib/__generated__/Withdrawal'; export * from './lib/create-withdrawal-dialog'; -export * from './lib/withdrawal-dialog'; -export * from './lib/withdraw-form'; -export * from './lib/withdraw-form-container'; -export * from './lib/withdraw-manager'; -export * from './lib/withdrawals-table'; -export * from './lib/withdrawal-feedback'; export * from './lib/use-complete-withdraw'; export * from './lib/use-create-withdraw'; export * from './lib/use-verify-withdrawal'; +export * from './lib/withdraw-form-container'; +export * from './lib/withdraw-form'; +export * from './lib/withdraw-manager'; +export * from './lib/withdrawal-dialog'; +export * from './lib/withdrawal-feedback'; export * from './lib/withdrawals-provider'; -export * from './lib/__generated__/Withdrawal'; -export * from './lib/__generated__/Erc20Approval'; +export * from './lib/withdrawals-table'; diff --git a/libs/withdraws/src/lib/withdrawals-table.tsx b/libs/withdraws/src/lib/withdrawals-table.tsx index 802cb858d..9a080e509 100644 --- a/libs/withdraws/src/lib/withdrawals-table.tsx +++ b/libs/withdraws/src/lib/withdrawals-table.tsx @@ -9,7 +9,16 @@ import { } from '@vegaprotocol/utils'; import { useBottomPlaceholder } from '@vegaprotocol/datagrid'; import { t } from '@vegaprotocol/i18n'; -import { ButtonLink } from '@vegaprotocol/ui-toolkit'; +import { + ButtonLink, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + Icon, + VegaIcon, + VegaIconNames, +} from '@vegaprotocol/ui-toolkit'; import type { TypedDataAgGrid, VegaICellRendererParams, @@ -18,7 +27,10 @@ import type { import { AgGridLazy as AgGrid } from '@vegaprotocol/datagrid'; import { EtherscanLink } from '@vegaprotocol/environment'; import type { WithdrawalFieldsFragment } from './__generated__/Withdrawal'; -import { useEthWithdrawApprovalsStore } from '@vegaprotocol/web3'; +import { + useEthWithdrawApprovalsStore, + useWithdrawalApprovalDialog, +} from '@vegaprotocol/web3'; import * as Schema from '@vegaprotocol/types'; export const WithdrawalsTable = ( @@ -119,6 +131,7 @@ export const WithdrawalsTable = ( headerName={t('Transaction')} field="txHash" flex={2} + type="rightAligned" cellRendererParams={{ complete: (withdrawal: WithdrawalFieldsFragment) => { createWithdrawApproval(withdrawal); @@ -139,18 +152,51 @@ export type CompleteCellProps = { complete: (withdrawal: WithdrawalFieldsFragment) => void; }; export const CompleteCell = ({ data, complete }: CompleteCellProps) => { + const open = useWithdrawalApprovalDialog((state) => state.open); + const ref = useRef(null); + if (!data) { return null; } return data.pendingOnForeignChain ? ( '-' ) : ( - complete(data)} - > - {t('Complete withdrawal')} - +
+ complete(data)} + > + {t('Complete withdrawal')} + + + + + + } + > + + { + if (data.id) { + open(data.id, ref.current, false); + } + }} + > + + {t('View withdrawal details')} + + + + +
); };