From e391f96a626feb1912a0aebfa5710264631110fe Mon Sep 17 00:00:00 2001 From: fairlight <31534717+fairlighteth@users.noreply.github.com> Date: Mon, 21 Aug 2023 15:19:21 +0100 Subject: [PATCH] feat(trezor): control for selecting account (#3048) * chore: trezor setup * feat(trezor): sign and send transaction * feat(trezor): display approve tx error in modal * fix(trezor): use gas price for tx * feat(trezor): sign typed data * fix(trezor): display icon in pending tx modal * chore: fix trezor icon * fix(trezor): allow switching network * feat(trezor): selector for account index * refactor(trezor): extract sendTransactionHandler * refactor(trezor): extract TrezorConnector * chore: fix double activation * fix: fix metamask approving banner conditions * chore: fix gasPrice * feat(wallets): display connection error in modal * fix: dispose trezor on connection fail * fix: deactivate connector on trezor disconnect * chore: merge develop * chore: fix * chore: temp fix * chore: temp fix * refactor: address code review issues * chore: revert fix * fix: clean state on deactivate * chore: refactor * feat(trezor): load first 100 accounts * feat(trezor): select account index in one click * feat(trezor): hide the wallet under feature-flag * chore: hide Trezor by feature-flag * feat(trezor): display accounts balances * feat: add trezor account select functionality * chore: fix files location * refactor: integrate hardware wallet account selector * refactor: refactor AccountSelectorModal * fix: short addresses in list * fix: indicate account changes * feat(wallets): display snackbar on hardware wallet account changes * chore: deprecate old module * chore: docs * feat: display wallet name and icon * chore: fix tests for new jest * feat: add trezor account select functionality * feat: add trezor account select functionality --------- Co-authored-by: shoom3301 --- .vscode/extensions.json | 3 +- .../useSendOnChainCancellation.test.ts.snap | 28 +-- .../src/common/pure/ButtonSecondary/index.tsx | 21 ++ .../src/common/pure/Modal/index.tsx | 2 +- .../src/common/pure/Modal/styled.tsx | 2 +- .../src/common/pure/SelectDropdown/index.tsx | 29 +++ .../cowswap-frontend/src/cosmos.decorator.tsx | 4 + apps/cowswap-frontend/src/cow-react/index.tsx | 3 + .../legacy/components/Popups/PopupItem.tsx | 3 + .../src/legacy/components/Popups/index.tsx | 3 + .../src/legacy/state/application/hooks.ts | 13 +- .../src/legacy/theme/baseTheme.tsx | 12 ++ .../AccountDetails/index.cosmos.tsx | 39 ++++ .../containers/AccountDetails/index.tsx | 95 ++++----- .../containers/AccountDetails/styled.ts | 92 +++++++- .../account/containers/OrdersPanel/index.tsx | 6 +- .../tradeReceiveAmount.test.ts.snap | 12 +- .../useTradeQuotePolling.test.tsx.snap | 4 +- .../createTwapOrderTxs.test.ts.snap | 48 ++--- .../extensibleFallbackSetupTxs.test.tsx.snap | 6 +- .../HwAccountIndexSelector/index.tsx | 43 ---- .../api/pure/AccountIndexSelect/index.tsx | 50 ----- .../wallet/api/pure/WalletModal/index.tsx | 17 +- .../wallet/api/pure/WalletModal/styled.tsx | 25 +++ .../modules/wallet/api/utils/connection.ts | 32 +++ .../src/modules/wallet/index.ts | 3 +- .../TrezorConnector/getAccountsList.ts | 30 ++- .../connectors/TrezorConnector/index.ts | 21 +- .../AccountSelectorModal/accountsLoaders.ts | 19 ++ .../containers/AccountSelectorModal/index.tsx | 84 ++++++++ .../containers/AccountSelectorModal/state.ts | 11 + .../AccountSelectorModal/styled.tsx | 38 ++++ .../containers/WalletModal/index.tsx | 13 +- .../containers/Web3Status/index.tsx | 3 +- .../pure/AccountIndexSelect/index.cosmos.tsx | 30 ++- .../pure/AccountIndexSelect/index.tsx | 95 +++++++++ .../pure/AccountIndexSelect/styled.tsx | 63 ++++++ apps/cowswap-frontend/tsconfig.json | 3 +- libs/snackbars/.babelrc | 20 ++ libs/snackbars/.eslintrc.json | 18 ++ libs/snackbars/README.md | 42 ++++ libs/snackbars/demo.png | Bin 0 -> 57902 bytes libs/snackbars/jest.config.ts | 11 + libs/snackbars/package.json | 12 ++ libs/snackbars/project.json | 46 ++++ .../src/containers/SnackbarsWidget/index.tsx | 99 +++++++++ libs/snackbars/src/hooks/useAddSnackbar.ts | 6 + libs/snackbars/src/index.ts | 3 + .../src/pure/SnackbarPopup/index.tsx | 87 ++++++++ libs/snackbars/src/state/snackbarsAtom.ts | 27 +++ libs/snackbars/tsconfig.json | 21 ++ libs/snackbars/tsconfig.lib.json | 19 ++ libs/snackbars/tsconfig.spec.json | 20 ++ libs/snackbars/vite.config.ts | 49 +++++ nx.json | 40 +--- package.json | 7 +- tsconfig.base.json | 3 +- yarn.lock | 197 +++++++++++++++++- 58 files changed, 1439 insertions(+), 293 deletions(-) create mode 100644 apps/cowswap-frontend/src/common/pure/ButtonSecondary/index.tsx create mode 100644 apps/cowswap-frontend/src/common/pure/SelectDropdown/index.tsx create mode 100644 apps/cowswap-frontend/src/modules/account/containers/AccountDetails/index.cosmos.tsx delete mode 100644 apps/cowswap-frontend/src/modules/wallet/api/container/HwAccountIndexSelector/index.tsx delete mode 100644 apps/cowswap-frontend/src/modules/wallet/api/pure/AccountIndexSelect/index.tsx create mode 100644 apps/cowswap-frontend/src/modules/wallet/web3-react/containers/AccountSelectorModal/accountsLoaders.ts create mode 100644 apps/cowswap-frontend/src/modules/wallet/web3-react/containers/AccountSelectorModal/index.tsx create mode 100644 apps/cowswap-frontend/src/modules/wallet/web3-react/containers/AccountSelectorModal/state.ts create mode 100644 apps/cowswap-frontend/src/modules/wallet/web3-react/containers/AccountSelectorModal/styled.tsx rename apps/cowswap-frontend/src/modules/wallet/{api => web3-react}/pure/AccountIndexSelect/index.cosmos.tsx (59%) create mode 100644 apps/cowswap-frontend/src/modules/wallet/web3-react/pure/AccountIndexSelect/index.tsx create mode 100644 apps/cowswap-frontend/src/modules/wallet/web3-react/pure/AccountIndexSelect/styled.tsx create mode 100644 libs/snackbars/.babelrc create mode 100644 libs/snackbars/.eslintrc.json create mode 100644 libs/snackbars/README.md create mode 100644 libs/snackbars/demo.png create mode 100644 libs/snackbars/jest.config.ts create mode 100644 libs/snackbars/package.json create mode 100644 libs/snackbars/project.json create mode 100644 libs/snackbars/src/containers/SnackbarsWidget/index.tsx create mode 100644 libs/snackbars/src/hooks/useAddSnackbar.ts create mode 100644 libs/snackbars/src/index.ts create mode 100644 libs/snackbars/src/pure/SnackbarPopup/index.tsx create mode 100644 libs/snackbars/src/state/snackbarsAtom.ts create mode 100644 libs/snackbars/tsconfig.json create mode 100644 libs/snackbars/tsconfig.lib.json create mode 100644 libs/snackbars/tsconfig.spec.json create mode 100644 libs/snackbars/vite.config.ts diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 462e29bbf4..6a302fe536 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -2,6 +2,7 @@ "recommendations": [ "nrwl.angular-console", "esbenp.prettier-vscode", - "dbaeumer.vscode-eslint" + "dbaeumer.vscode-eslint", + "firsttris.vscode-jest-runner" ] } diff --git a/apps/cowswap-frontend/src/common/hooks/useCancelOrder/__snapshots__/useSendOnChainCancellation.test.ts.snap b/apps/cowswap-frontend/src/common/hooks/useCancelOrder/__snapshots__/useSendOnChainCancellation.test.ts.snap index f11aaf954b..dc22626777 100644 --- a/apps/cowswap-frontend/src/common/hooks/useCancelOrder/__snapshots__/useSendOnChainCancellation.test.ts.snap +++ b/apps/cowswap-frontend/src/common/hooks/useCancelOrder/__snapshots__/useSendOnChainCancellation.test.ts.snap @@ -1,10 +1,10 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`useSendOnChainCancellation() + useGetOnChainCancellation() When a transaction is sent Then should change an order status, set a tx hash to order and add the transaction to store 1`] = ` -Array [ - Object { +[ + { "hash": "0xcfwj23g4fwe111", - "onChainCancellation": Object { + "onChainCancellation": { "orderId": "xx1", "sellTokenSymbol": "COW", }, @@ -13,8 +13,8 @@ Array [ `; exports[`useSendOnChainCancellation() + useGetOnChainCancellation() When a transaction is sent Then should change an order status, set a tx hash to order and add the transaction to store 2`] = ` -Array [ - Object { +[ + { "chainId": 1, "id": "xx1", }, @@ -22,8 +22,8 @@ Array [ `; exports[`useSendOnChainCancellation() + useGetOnChainCancellation() When a transaction is sent Then should change an order status, set a tx hash to order and add the transaction to store 3`] = ` -Array [ - Object { +[ + { "chainId": 1, "hash": "0xcfwj23g4fwe111", "id": "xx1", @@ -32,8 +32,8 @@ Array [ `; exports[`useSendOnChainCancellation() + useGetOnChainCancellation() When is ETH-flow order, then should call eth-flow contract 1`] = ` -Array [ - Object { +[ + { "appData": "0x001", "buyAmount": "2", "buyToken": "0xDEf1CA1fb7FBcDC777520aa7f396b4E015F497aB", @@ -44,8 +44,8 @@ Array [ "sellAmount": "1", "validTo": "34245345432", }, - Object { - "gasLimit": Object { + { + "gasLimit": { "hex": "0x78", "type": "BigNumber", }, @@ -54,10 +54,10 @@ Array [ `; exports[`useSendOnChainCancellation() + useGetOnChainCancellation() When is NOT ETH-flow order, then should call settlement contract 1`] = ` -Array [ +[ "xx1", - Object { - "gasLimit": Object { + { + "gasLimit": { "hex": "0xf0", "type": "BigNumber", }, diff --git a/apps/cowswap-frontend/src/common/pure/ButtonSecondary/index.tsx b/apps/cowswap-frontend/src/common/pure/ButtonSecondary/index.tsx new file mode 100644 index 0000000000..e134f59500 --- /dev/null +++ b/apps/cowswap-frontend/src/common/pure/ButtonSecondary/index.tsx @@ -0,0 +1,21 @@ +import styled from 'styled-components/macro' + +export const ButtonSecondary = styled.button` + background: var(--cow-color-lightBlue-opacity-90); + color: var(--cow-color-lightBlue); + font-size: 12px; + font-weight: 600; + border: 0; + box-shadow: none; + border-radius: 12px; + position: relative; + transition: background 0.2s ease-in-out; + min-height: 35px; + padding: 0 12px; + cursor: pointer; + white-space: nowrap; + + &:hover { + background: var(--cow-color-lightBlue-opacity-80); + } +` diff --git a/apps/cowswap-frontend/src/common/pure/Modal/index.tsx b/apps/cowswap-frontend/src/common/pure/Modal/index.tsx index 74afd0ada3..b0aec21d42 100644 --- a/apps/cowswap-frontend/src/common/pure/Modal/index.tsx +++ b/apps/cowswap-frontend/src/common/pure/Modal/index.tsx @@ -136,7 +136,7 @@ export const CowModal = styled(Modal)<{ ${ContentWrapper} { ${({ theme }) => theme.mediaWidth.upToSmall` - margin: 62px auto 0; + margin: 82px auto 0; `} } } diff --git a/apps/cowswap-frontend/src/common/pure/Modal/styled.tsx b/apps/cowswap-frontend/src/common/pure/Modal/styled.tsx index d5cc6332d3..29ef71105b 100644 --- a/apps/cowswap-frontend/src/common/pure/Modal/styled.tsx +++ b/apps/cowswap-frontend/src/common/pure/Modal/styled.tsx @@ -8,7 +8,7 @@ import styled, { css } from 'styled-components/macro' export const HeaderRow = styled.div` ${({ theme }) => theme.flexRowNoWrap}; padding: 1rem 1rem; - font-weight: 500; + font-weight: 600; color: ${(props) => (props.color === 'blue' ? ({ theme }) => theme.primary1 : 'inherit')}; ${({ theme }) => theme.mediaWidth.upToMedium` padding: 1rem; diff --git a/apps/cowswap-frontend/src/common/pure/SelectDropdown/index.tsx b/apps/cowswap-frontend/src/common/pure/SelectDropdown/index.tsx new file mode 100644 index 0000000000..4782f378d4 --- /dev/null +++ b/apps/cowswap-frontend/src/common/pure/SelectDropdown/index.tsx @@ -0,0 +1,29 @@ +import styled from 'styled-components/macro' + +export const SelectDropdown = styled.select` + border-radius: 12px; + padding: 8px 34px 8px 8px; + border-radius: 12px; + appearance: none; + cursor: pointer; + transition: background 0.2s ease-in-out; + width: 100%; + outline: none; + border: 1px solid var(--cow-color-border); + color: var(--cow-color-text1); + background: linear-gradient(45deg, transparent 50%, var(--cow-color-lightBlue) 50%) calc(100% - 13px) calc(13px) / 5px + 5px no-repeat, + linear-gradient(135deg, var(--cow-color-lightBlue) 50%, transparent 50%) calc(100% - 8px) calc(13px) / 5px 5px + no-repeat, + linear-gradient(to right, var(--cow-color-lightBlue-opacity-90), var(--cow-color-lightBlue-opacity-90)) 100% 0 / + 26px 100% no-repeat; + + &:hover { + background: linear-gradient(45deg, transparent 50%, var(--cow-color-lightBlue) 50%) calc(100% - 13px) calc(13px) / + 5px 5px no-repeat, + linear-gradient(135deg, var(--cow-color-lightBlue) 50%, transparent 50%) calc(100% - 8px) calc(13px) / 5px 5px + no-repeat, + linear-gradient(to right, var(--cow-color-lightBlue-opacity-80), var(--cow-color-lightBlue-opacity-80)) 100% 0 / + 26px 100% no-repeat; + } +` diff --git a/apps/cowswap-frontend/src/cosmos.decorator.tsx b/apps/cowswap-frontend/src/cosmos.decorator.tsx index 9666f023c2..3460702d23 100644 --- a/apps/cowswap-frontend/src/cosmos.decorator.tsx +++ b/apps/cowswap-frontend/src/cosmos.decorator.tsx @@ -1,4 +1,5 @@ import '@reach/dialog/styles.css' +import './polyfills' import React, { StrictMode, useCallback, useContext, ReactNode } from 'react' @@ -22,6 +23,8 @@ import { injectedConnection } from 'modules/wallet/web3-react/connection/injecte import { BlockNumberProvider } from 'lib/hooks/useBlockNumber' +import { WalletUpdater } from './modules/wallet' + const DarkModeToggleButton = styled.button` display: flex; align-items: center; @@ -102,6 +105,7 @@ const Fixture = ({ children }: { children: ReactNode }) => { + {children} diff --git a/apps/cowswap-frontend/src/cow-react/index.tsx b/apps/cowswap-frontend/src/cow-react/index.tsx index c48e841cc0..f20b500682 100644 --- a/apps/cowswap-frontend/src/cow-react/index.tsx +++ b/apps/cowswap-frontend/src/cow-react/index.tsx @@ -6,6 +6,8 @@ import 'utils/sentry' import { Provider as AtomProvider } from 'jotai' import { StrictMode } from 'react' +import { SnackbarsWidget } from '@cowswap/snackbars' + import { LanguageProvider } from 'i18n' import { createRoot } from 'react-dom/client' import { Provider } from 'react-redux' @@ -58,6 +60,7 @@ root.render( + diff --git a/apps/cowswap-frontend/src/legacy/components/Popups/PopupItem.tsx b/apps/cowswap-frontend/src/legacy/components/Popups/PopupItem.tsx index a2578b25bb..9042d107ca 100644 --- a/apps/cowswap-frontend/src/legacy/components/Popups/PopupItem.tsx +++ b/apps/cowswap-frontend/src/legacy/components/Popups/PopupItem.tsx @@ -11,6 +11,9 @@ import { PopupContent } from 'legacy/state/application/reducer' import { AnimatedFader, PopupWrapper, StyledClose } from './styled' import { TransactionPopup } from './TransactionPopup' +/** + * @deprecated use @cowswap/snackbars instead + */ export function PopupItem({ removeAfterMs, content, diff --git a/apps/cowswap-frontend/src/legacy/components/Popups/index.tsx b/apps/cowswap-frontend/src/legacy/components/Popups/index.tsx index 45cc2d7ff0..3ba3b376d0 100644 --- a/apps/cowswap-frontend/src/legacy/components/Popups/index.tsx +++ b/apps/cowswap-frontend/src/legacy/components/Popups/index.tsx @@ -74,6 +74,9 @@ const FixedPopupColumn = styled(AutoColumn)<{ extraPadding: boolean; xlPadding: } ` +/** + * @deprecated use @cowswap/snackbars instead + */ export function Popups() { // get all popups const activePopups = useActivePopups() diff --git a/apps/cowswap-frontend/src/legacy/state/application/hooks.ts b/apps/cowswap-frontend/src/legacy/state/application/hooks.ts index 7c5fe7e9c6..cb8eb4da2b 100644 --- a/apps/cowswap-frontend/src/legacy/state/application/hooks.ts +++ b/apps/cowswap-frontend/src/legacy/state/application/hooks.ts @@ -65,7 +65,9 @@ export function useTogglePrivacyPolicy(): () => void { return useToggleModal(ApplicationModal.PRIVACY_POLICY) } -// returns a function that allows removing a popup via its key +/** + * @deprecated use @cowswap/snackbars instead + */ export function useRemovePopup(): (key: string) => void { const dispatch = useAppDispatch() return useCallback( @@ -76,7 +78,9 @@ export function useRemovePopup(): (key: string) => void { ) } -// get the list of active popups +/** + * @deprecated use @cowswap/snackbars instead + */ export function useActivePopups(): AppState['application']['popupList'] { const list = useAppSelector((state: AppState) => state.application.popupList) return useMemo(() => list.filter((item) => item.show), [list]) @@ -97,8 +101,9 @@ export function useCloseModals(): () => void { return useCallback(() => dispatch(setOpenModal(null)), [dispatch]) } -// mod: add removeAfterMs change -// returns a function that allows adding a popup +/** + * @deprecated use @cowswap/snackbars instead + */ export function useAddPopup(): (content: PopupContent, key?: string, removeAfterMs?: number | null) => void { const dispatch = useAppDispatch() diff --git a/apps/cowswap-frontend/src/legacy/theme/baseTheme.tsx b/apps/cowswap-frontend/src/legacy/theme/baseTheme.tsx index 1bd5b83cc2..8f2d7c3195 100644 --- a/apps/cowswap-frontend/src/legacy/theme/baseTheme.tsx +++ b/apps/cowswap-frontend/src/legacy/theme/baseTheme.tsx @@ -385,6 +385,18 @@ export const UniFixedGlobalStyle = css` ` export const UniThemedGlobalStyle = css` + :root { + // CSS Variables + --cow-color-text1: ${({ theme }) => theme.text1}; + --cow-color-text1-opacity-25: ${({ theme }) => theme.text1 + '40'}; + --cow-color-white: ${({ theme }) => theme.white}; + --cow-color-blue: ${({ theme }) => theme.bg2}; + --cow-color-border: ${({ theme }) => theme.grey1}; + --cow-color-lightBlue: ${({ theme }) => theme.information}; + --cow-color-lightBlue-opacity-90: ${({ theme }) => transparentize(0.9, theme.information)}; + --cow-color-lightBlue-opacity-80: ${({ theme }) => transparentize(0.8, theme.information)}; + } + html { color: ${({ theme }) => theme.text1}; background-color: ${({ theme }) => theme.bg2}; diff --git a/apps/cowswap-frontend/src/modules/account/containers/AccountDetails/index.cosmos.tsx b/apps/cowswap-frontend/src/modules/account/containers/AccountDetails/index.cosmos.tsx new file mode 100644 index 0000000000..62e180d83b --- /dev/null +++ b/apps/cowswap-frontend/src/modules/account/containers/AccountDetails/index.cosmos.tsx @@ -0,0 +1,39 @@ +import { useSelect } from 'react-cosmos/client' +import styled from 'styled-components/macro' + +import { AccountDetails } from './index' + +const defaultProps = { + pendingTransactions: [], + confirmedTransactions: [], + toggleWalletModal: () => void 0, + toggleAccountSelectorModal: () => void 0, + handleCloseOrdersPanel: () => void 0, +} + +const Wrapper = styled.div` + width: 800px; + margin: 100px auto; + padding: 20px; +` + +// const chainId = 5 + +function Host() { + const [isHardWare] = useSelect('Is hardware wallet', { + options: ['true', 'false'], + defaultValue: 'false', + }) + + return ( + + + + ) +} + +const Fixtures = { + default: , +} + +export default Fixtures diff --git a/apps/cowswap-frontend/src/modules/account/containers/AccountDetails/index.tsx b/apps/cowswap-frontend/src/modules/account/containers/AccountDetails/index.tsx index c250531586..54c3cf8987 100644 --- a/apps/cowswap-frontend/src/modules/account/containers/AccountDetails/index.tsx +++ b/apps/cowswap-frontend/src/modules/account/containers/AccountDetails/index.tsx @@ -20,18 +20,14 @@ import { isMobile } from 'legacy/utils/userAgent' import Activity from 'modules/account/containers/Transaction' import { ConnectionType, useDisconnectWallet, useWalletInfo, WalletDetails } from 'modules/wallet' -import { HwAccountIndexSelector } from 'modules/wallet' -import CoinbaseWalletIcon from 'modules/wallet/api/assets/coinbase.svg' -import FortmaticIcon from 'modules/wallet/api/assets/formatic.png' -import KeystoneImage from 'modules/wallet/api/assets/keystone.svg' -import LedgerIcon from 'modules/wallet/api/assets/ledger.svg' -import TallyIcon from 'modules/wallet/api/assets/tally.svg' -import TrezorIcon from 'modules/wallet/api/assets/trezor.svg' -import TrustIcon from 'modules/wallet/api/assets/trust.svg' -import WalletConnectIcon from 'modules/wallet/api/assets/walletConnectIcon.svg' import { Identicon } from 'modules/wallet/api/container/Identicon' import { useWalletDetails } from 'modules/wallet/api/hooks' -import { getConnectionName, getIsCoinbaseWallet, getIsMetaMask } from 'modules/wallet/api/utils/connection' +import { + getConnectionIcon, + getConnectionName, + getIsCoinbaseWallet, + getIsMetaMask, +} from 'modules/wallet/api/utils/connection' import { getIsHardWareWallet, getWeb3ReactConnection } from 'modules/wallet/web3-react/connection' import { walletConnectConnection } from 'modules/wallet/web3-react/connection/walletConnect' import { walletConnectConnectionV2 } from 'modules/wallet/web3-react/connection/walletConnectV2' @@ -54,6 +50,7 @@ import { WalletActions, WalletName, WalletNameAddress, + WalletSelector, WalletSecondaryActions, WalletWrapper, Wrapper, @@ -81,27 +78,6 @@ export function renderActivities(activities: ActivityDescriptors[]) { ) } -const IDENTICON_KEY = 'Identicon' - -const walletIcons: Record = { - [ConnectionType.INJECTED]: IDENTICON_KEY, - [ConnectionType.INJECTED_WIDGET]: IDENTICON_KEY, - [ConnectionType.GNOSIS_SAFE]: IDENTICON_KEY, - [ConnectionType.NETWORK]: IDENTICON_KEY, - [ConnectionType.ZENGO]: IDENTICON_KEY, - [ConnectionType.AMBIRE]: IDENTICON_KEY, - [ConnectionType.ALPHA]: IDENTICON_KEY, - [ConnectionType.COINBASE_WALLET]: CoinbaseWalletIcon, - [ConnectionType.FORTMATIC]: FortmaticIcon, - [ConnectionType.TRUST]: TrustIcon, - [ConnectionType.TALLY]: TallyIcon, - [ConnectionType.LEDGER]: LedgerIcon, - [ConnectionType.TREZOR]: TrezorIcon, - [ConnectionType.KEYSTONE]: KeystoneImage, - [ConnectionType.WALLET_CONNECT]: WalletConnectIcon, - [ConnectionType.WALLET_CONNECT_V2]: WalletConnectIcon, -} - export function getStatusIcon(connector: Connector, walletDetails?: WalletDetails, size?: number) { const connectionType = getWeb3ReactConnection(connector) @@ -124,9 +100,9 @@ export function getStatusIcon(connector: Connector, walletDetails?: WalletDetail ) } - const icon = walletIcons[connectionType.type] + const icon = getConnectionIcon(connectionType.type) - if (icon === IDENTICON_KEY) { + if (icon === 'Identicon') { return } @@ -141,7 +117,9 @@ export interface AccountDetailsProps { pendingTransactions: string[] confirmedTransactions: string[] ENSName?: string + forceHardwareWallet?: boolean toggleWalletModal: () => void + toggleAccountSelectorModal: () => void handleCloseOrdersPanel: () => void } @@ -150,7 +128,9 @@ export function AccountDetails({ confirmedTransactions = [], ENSName, toggleWalletModal, + toggleAccountSelectorModal, handleCloseOrdersPanel, + forceHardwareWallet, }: AccountDetailsProps) { const { account, chainId } = useWalletInfo() const { connector } = useWeb3React() @@ -194,8 +174,7 @@ export function AccountDetails({ } const networkLabel = NETWORK_LABELS[chainId] - - const isHardWareWallet = getIsHardWareWallet(connectionType.type) + const isHardWareWallet = forceHardwareWallet || getIsHardWareWallet(connectionType.type) return ( @@ -203,17 +182,25 @@ export function AccountDetails({ - {getStatusIcon(connector, walletDetails, 24)} + { + if (isHardWareWallet) { + toggleAccountSelectorModal() + } + }} + > + {getStatusIcon(connector, walletDetails, 24)} + {(ENSName || account) && ( + {ENSName ? ENSName : account && shortenAddress(account)} + )} + {(ENSName || account) && ( - - {ENSName ? ENSName : account && shortenAddress(account)} - + )} - {isHardWareWallet && } - {' '} {networkLabel && !isChainIdUnsupported && ( @@ -228,26 +215,26 @@ export function AccountDetails({ {!isInjectedMobileBrowser && ( <> - - Disconnect - + {account && !isChainIdUnsupported && ( + + {explorerLabel} ↗ + + )} {connection.type !== ConnectionType.GNOSIS_SAFE && ( Change Wallet )} - - )} - {account && !isChainIdUnsupported && ( - - {explorerLabel} ↗ - + + Disconnect + + )} diff --git a/apps/cowswap-frontend/src/modules/account/containers/AccountDetails/styled.ts b/apps/cowswap-frontend/src/modules/account/containers/AccountDetails/styled.ts index 5d0e078f97..5130334565 100644 --- a/apps/cowswap-frontend/src/modules/account/containers/AccountDetails/styled.ts +++ b/apps/cowswap-frontend/src/modules/account/containers/AccountDetails/styled.ts @@ -45,22 +45,34 @@ export const TransactionListWrapper = styled.div` ${({ theme }) => theme.flexColumnNoWrap}; ` +export const WalletIconSmall = styled.img` + width: 16px; + height: 16px; +` + export const WalletAction = styled(ButtonSecondary)` width: fit-content; font-weight: 400; margin-left: 8px; font-size: 0.825rem; padding: 4px 6px; + :hover { cursor: pointer; text-decoration: underline; } + + ${WalletIconSmall} { + margin-left: 5px; + } ` export const WalletActions = styled.div` display: flex; - flex-flow: row wrap; margin: 10px 0 0; + flex-flow: column wrap; + gap: 10px; + align-items: flex-start; ` export const AddressLink = styled(ExternalLink)<{ hasENS: boolean; isENS: boolean }>` @@ -107,7 +119,7 @@ export const WalletSecondaryActions = styled.div`` export const WalletNameAddress = styled.div` width: 100%; - font-size: 23px; + font-size: 20px; font-weight: 500; margin: 0 0 0 8px; ` @@ -132,7 +144,7 @@ export const Wrapper = styled.div` color: ${({ theme }) => theme.text1}; opacity: 0.85; transition: color 0.2s ease-in-out, opacity 0.2s ease-in-out; - margin: 0; + margin: auto; padding: 0; border: 0; font-size: 14px; @@ -199,15 +211,12 @@ export const Wrapper = styled.div` min-height: initial; cursor: pointer; animation: none; + margin: 0; &:hover { cursor: pointer; } } - - > a:not(:last-child) { - margin: 0 0 5px; - } } ${AccountControl} ${WalletActions} { @@ -443,7 +452,7 @@ export const NetworkCard = styled(NetworkCardUni)` color: ${({ theme }) => theme.text1}; padding: 6px 8px; font-size: 13px; - margin: 0 8px 0 0; + margin: 0; letter-spacing: 0.7px; min-width: initial; flex: 0 0 fit-content; @@ -557,3 +566,70 @@ export const SurplusCardWrapper = styled.div` } } ` + +export const WalletIconWrapper = styled.div` + --size: 12px; + display: flex; + width: var(--size); + height: var(--size); + align-items: center; + justify-content: center; + background: transparent; + padding: 0; + margin: 0 0 0 5px; + + > svg { + width: 100%; + height: 100%; + } + + > svg > path { + --color: var(--cow-color-text1); + fill: var(--color); + stroke: var(--color); + stroke-width: 0.5px; + } +` + +interface WalletSelectorProps { + isHardWareWallet?: boolean; + onClick?: () => void; +} + +export const WalletSelector = styled.div` + display: flex; + border-radius: 16px; + align-items: center; + justify-content: center; + transition: background 0.2s ease-in-out; + + ${({ isHardWareWallet }) => + isHardWareWallet && + ` + cursor: pointer; + border: 1px solid var(--cow-color-text1-opacity-25); + background: transparent; + padding: 6px 10px; + + &:after { + content: ''; + display: block; + width: 0; + height: 0; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 4px solid var(--cow-color-text1); + margin-left: 8px; + opacity: 0.5; + transition: opacity 0.2s ease-in-out; + } + + &:hover { + background: var(--cow-color-text1-opacity-25); + } + + &:hover::after { + opacity: 1; + } + `} +` diff --git a/apps/cowswap-frontend/src/modules/account/containers/OrdersPanel/index.tsx b/apps/cowswap-frontend/src/modules/account/containers/OrdersPanel/index.tsx index 1e7b63eec4..3917bfd50f 100644 --- a/apps/cowswap-frontend/src/modules/account/containers/OrdersPanel/index.tsx +++ b/apps/cowswap-frontend/src/modules/account/containers/OrdersPanel/index.tsx @@ -1,10 +1,12 @@ +import { useSetAtom } from 'jotai' + import { transparentize } from 'polished' import styled from 'styled-components/macro' import { ReactComponent as Close } from 'legacy/assets/images/x.svg' import { useToggleWalletModal } from 'legacy/state/application/hooks' -import { useWalletDetails, useWalletInfo } from 'modules/wallet' +import { toggleAccountSelectorModalAtom, useWalletDetails, useWalletInfo } from 'modules/wallet' import { useCategorizeRecentActivity } from 'common/hooks/useCategorizeRecentActivity' @@ -126,6 +128,7 @@ export function OrdersPanel({ handleCloseOrdersPanel }: OrdersPanelProps) { const { active } = useWalletInfo() const { ensName } = useWalletDetails() const toggleWalletModal = useToggleWalletModal() + const toggleAccountSelectorModal = useSetAtom(toggleAccountSelectorModalAtom) const { pendingActivity, confirmedActivity } = useCategorizeRecentActivity() @@ -150,6 +153,7 @@ export function OrdersPanel({ handleCloseOrdersPanel }: OrdersPanelProps) { pendingTransactions={pendingActivity} confirmedTransactions={confirmedActivity} toggleWalletModal={toggleWalletModal} + toggleAccountSelectorModal={toggleAccountSelectorModal} handleCloseOrdersPanel={handleCloseOrdersPanel} /> diff --git a/apps/cowswap-frontend/src/modules/swap/helpers/__snapshots__/tradeReceiveAmount.test.ts.snap b/apps/cowswap-frontend/src/modules/swap/helpers/__snapshots__/tradeReceiveAmount.test.ts.snap index 2a19364c54..2aea16f68f 100644 --- a/apps/cowswap-frontend/src/modules/swap/helpers/__snapshots__/tradeReceiveAmount.test.ts.snap +++ b/apps/cowswap-frontend/src/modules/swap/helpers/__snapshots__/tradeReceiveAmount.test.ts.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Helpers to build ReceiveAmountInfo getInputReceiveAmountInfo() When selling, if the fee is bigger than the traded amount Then amountBeforeFees should be zero 1`] = ` -Object { +{ "amount": CurrencyAmount { "currency": Token { "address": "0xDEf1CA1fb7FBcDC777520aa7f396b4E015F497aB", @@ -26,14 +26,14 @@ Object { `; exports[`Helpers to build ReceiveAmountInfo getInputReceiveAmountInfo() When selling, if the fee is bigger than the traded amount Then amountBeforeFees should be zero 2`] = ` -Object { +{ "amount": null, "defaultValue": "0", } `; exports[`Helpers to build ReceiveAmountInfo getInputReceiveAmountInfo() When selling, if the fee is bigger than the traded amount Then amountBeforeFees should be zero 3`] = ` -Object { +{ "amount": CurrencyAmount { "currency": Token { "address": "0xDEf1CA1fb7FBcDC777520aa7f396b4E015F497aB", @@ -62,7 +62,7 @@ Object { `; exports[`Helpers to build ReceiveAmountInfo getOutputReceiveAmountInfo() Should match a snapshot 1`] = ` -Object { +{ "amount": CurrencyAmount { "currency": Token { "address": "0xDEf1CA1fb7FBcDC777520aa7f396b4E015F497aB", @@ -91,7 +91,7 @@ Object { `; exports[`Helpers to build ReceiveAmountInfo getOutputReceiveAmountInfo() Should match a snapshot 2`] = ` -Object { +{ "amount": CurrencyAmount { "currency": Token { "address": "0xDEf1CA1fb7FBcDC777520aa7f396b4E015F497aB", @@ -120,7 +120,7 @@ Object { `; exports[`Helpers to build ReceiveAmountInfo getOutputReceiveAmountInfo() Should match a snapshot 3`] = ` -Object { +{ "amount": CurrencyAmount { "currency": Token { "address": "0xDEf1CA1fb7FBcDC777520aa7f396b4E015F497aB", diff --git a/apps/cowswap-frontend/src/modules/tradeQuote/hooks/__snapshots__/useTradeQuotePolling.test.tsx.snap b/apps/cowswap-frontend/src/modules/tradeQuote/hooks/__snapshots__/useTradeQuotePolling.test.tsx.snap index bf1f6c0756..126e1be152 100644 --- a/apps/cowswap-frontend/src/modules/tradeQuote/hooks/__snapshots__/useTradeQuotePolling.test.tsx.snap +++ b/apps/cowswap-frontend/src/modules/tradeQuote/hooks/__snapshots__/useTradeQuotePolling.test.tsx.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`useTradeQuotePolling() When wallet is NOT connected Then the "useAddress" field in the quote request should be 0x000...0000 1`] = ` -Object { +{ "amount": "10000000", "buyToken": "0x91056D4A53E1faa1A84306D4deAEc71085394bC8", "chainId": 1, @@ -17,7 +17,7 @@ Object { `; exports[`useTradeQuotePolling() When wallet is connected Then should put account address into "useAddress" field in the quote request 1`] = ` -Object { +{ "amount": "10000000", "buyToken": "0x91056D4A53E1faa1A84306D4deAEc71085394bC8", "chainId": 1, diff --git a/apps/cowswap-frontend/src/modules/twap/services/__snapshots__/createTwapOrderTxs.test.ts.snap b/apps/cowswap-frontend/src/modules/twap/services/__snapshots__/createTwapOrderTxs.test.ts.snap index 3f5afc8f40..8736ed1f9b 100644 --- a/apps/cowswap-frontend/src/modules/twap/services/__snapshots__/createTwapOrderTxs.test.ts.snap +++ b/apps/cowswap-frontend/src/modules/twap/services/__snapshots__/createTwapOrderTxs.test.ts.snap @@ -1,10 +1,10 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Create TWAP order When sell token is NOT approved AND token needs zero approval, then should generate 2 approvals and creation transactions 1`] = ` -Array [ +[ "createWithContext", - Array [ - Object { + [ + { "handler": "0x6cF1e9cA41f7611dEf408122793c358a3d11E5a5", "salt": "0x00000000000000000000000000000000000000000000000000000015c90b9b2a", "staticInput": "0x00000000000000000000000091056d4a53e1faa1a84306d4deaec71085394bc8000000000000000000000000b4fbf271143f4fbf7b91a5ded31805e42b2208d6000000000000000000000000b4fbf271143f4fbf7b91a5ded31805e42b2208d600000000000000000000000000000000000000000000000000000007c2d24d55000000000000000000000000000000000000000000000000000000000001046a00000000000000000000000000000000000000000000000000000000646b782c000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000002580000000000000000000000000000000000000000000000000000000000000000bace6e636671f2df6e59ef1eb4e63dce6b646a70c51b004ae5a07a9fbe4356fc", @@ -17,9 +17,9 @@ Array [ `; exports[`Create TWAP order When sell token is NOT approved AND token needs zero approval, then should generate 2 approvals and creation transactions 2`] = ` -Array [ +[ "approve", - Array [ + [ "0xB4FBF271143F4FBf7B91A5ded31805e42b222222", "100000000000", ], @@ -27,9 +27,9 @@ Array [ `; exports[`Create TWAP order When sell token is NOT approved AND token needs zero approval, then should generate 2 approvals and creation transactions 3`] = ` -Array [ +[ "approve", - Array [ + [ "0xB4FBF271143F4FBf7B91A5ded31805e42b222222", "0", ], @@ -37,20 +37,20 @@ Array [ `; exports[`Create TWAP order When sell token is NOT approved AND token needs zero approval, then should generate 2 approvals and creation transactions 4`] = ` -Array [ - Object { +[ + { "data": "0xAPPROVE_TX_DATA", "operation": 0, "to": "0x91056D4A53E1faa1A84306D4deAEc71085394bC8", "value": "0", }, - Object { + { "data": "0xAPPROVE_TX_DATA", "operation": 0, "to": "0x91056D4A53E1faa1A84306D4deAEc71085394bC8", "value": "0", }, - Object { + { "data": "0xCREATE_COW_TX_DATA", "operation": 0, "to": "0xfdaFc9d1902f4e0b84f65F49f244b32b31013b74", @@ -60,10 +60,10 @@ Array [ `; exports[`Create TWAP order When sell token is NOT approved, then should generate approval and creation transactions 1`] = ` -Array [ +[ "createWithContext", - Array [ - Object { + [ + { "handler": "0x6cF1e9cA41f7611dEf408122793c358a3d11E5a5", "salt": "0x00000000000000000000000000000000000000000000000000000015c90b9b2a", "staticInput": "0x00000000000000000000000091056d4a53e1faa1a84306d4deaec71085394bc8000000000000000000000000b4fbf271143f4fbf7b91a5ded31805e42b2208d6000000000000000000000000b4fbf271143f4fbf7b91a5ded31805e42b2208d600000000000000000000000000000000000000000000000000000007c2d24d55000000000000000000000000000000000000000000000000000000000001046a00000000000000000000000000000000000000000000000000000000646b782c000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000002580000000000000000000000000000000000000000000000000000000000000000bace6e636671f2df6e59ef1eb4e63dce6b646a70c51b004ae5a07a9fbe4356fc", @@ -76,9 +76,9 @@ Array [ `; exports[`Create TWAP order When sell token is NOT approved, then should generate approval and creation transactions 2`] = ` -Array [ +[ "approve", - Array [ + [ "0xB4FBF271143F4FBf7B91A5ded31805e42b222222", "100000000000", ], @@ -86,14 +86,14 @@ Array [ `; exports[`Create TWAP order When sell token is NOT approved, then should generate approval and creation transactions 3`] = ` -Array [ - Object { +[ + { "data": "0xAPPROVE_TX_DATA", "operation": 0, "to": "0x91056D4A53E1faa1A84306D4deAEc71085394bC8", "value": "0", }, - Object { + { "data": "0xCREATE_COW_TX_DATA", "operation": 0, "to": "0xfdaFc9d1902f4e0b84f65F49f244b32b31013b74", @@ -103,10 +103,10 @@ Array [ `; exports[`Create TWAP order When sell token is approved, then should generate only creation transaction 1`] = ` -Array [ +[ "createWithContext", - Array [ - Object { + [ + { "handler": "0x6cF1e9cA41f7611dEf408122793c358a3d11E5a5", "salt": "0x00000000000000000000000000000000000000000000000000000015c90b9b2a", "staticInput": "0x00000000000000000000000091056d4a53e1faa1a84306d4deaec71085394bc8000000000000000000000000b4fbf271143f4fbf7b91a5ded31805e42b2208d6000000000000000000000000b4fbf271143f4fbf7b91a5ded31805e42b2208d600000000000000000000000000000000000000000000000000000007c2d24d55000000000000000000000000000000000000000000000000000000000001046a00000000000000000000000000000000000000000000000000000000646b782c000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000002580000000000000000000000000000000000000000000000000000000000000000bace6e636671f2df6e59ef1eb4e63dce6b646a70c51b004ae5a07a9fbe4356fc", @@ -119,8 +119,8 @@ Array [ `; exports[`Create TWAP order When sell token is approved, then should generate only creation transaction 2`] = ` -Array [ - Object { +[ + { "data": "0xCREATE_COW_TX_DATA", "operation": 0, "to": "0xfdaFc9d1902f4e0b84f65F49f244b32b31013b74", diff --git a/apps/cowswap-frontend/src/modules/twap/services/__snapshots__/extensibleFallbackSetupTxs.test.tsx.snap b/apps/cowswap-frontend/src/modules/twap/services/__snapshots__/extensibleFallbackSetupTxs.test.tsx.snap index 2c9e631e10..9148d4ed00 100644 --- a/apps/cowswap-frontend/src/modules/twap/services/__snapshots__/extensibleFallbackSetupTxs.test.tsx.snap +++ b/apps/cowswap-frontend/src/modules/twap/services/__snapshots__/extensibleFallbackSetupTxs.test.tsx.snap @@ -1,14 +1,14 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`extensibleFallbackSetupTxs - service to generate transactions for ExtensibleFallback setup Should create a bundle of two transactions: setFallbackHandler and setDomainVerifier 1`] = ` -Array [ - Object { +[ + { "data": "0xf08a03230000000000000000000000002f55e8b20d0b9fefa187aa7d00b6cbe563605bf5", "operation": 0, "to": "0xA12D770028d7072b80BAEb6A1df962cccfd1dddd", "value": "0", }, - Object { + { "data": "0x3365582ca5b986c2f5845d520bcb903639360b147735589732066cea24a3a59678025c94000000000000000000000000fdafc9d1902f4e0b84f65f49f244b32b31013b74", "operation": 0, "to": "0xA12D770028d7072b80BAEb6A1df962cccfd1dddd", diff --git a/apps/cowswap-frontend/src/modules/wallet/api/container/HwAccountIndexSelector/index.tsx b/apps/cowswap-frontend/src/modules/wallet/api/container/HwAccountIndexSelector/index.tsx deleted file mode 100644 index f40a564da3..0000000000 --- a/apps/cowswap-frontend/src/modules/wallet/api/container/HwAccountIndexSelector/index.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { useAtom } from 'jotai' -import { useMemo } from 'react' - -import { useWeb3React } from '@web3-react/core' - -import { useNativeCurrencyBalances } from 'modules/tokens/hooks/useCurrencyBalance' - -import { getWeb3ReactConnection, HardWareWallet } from '../../../web3-react/connection' -import { trezorConnection } from '../../../web3-react/connection/trezor' -import { AccountIndexSelect } from '../../pure/AccountIndexSelect' -import { hwAccountIndexAtom } from '../../state' -import { ConnectionType } from '../../types' - -const accountsLoaders: Record string[] | null> = { - [ConnectionType.TREZOR]: () => trezorConnection.connector.getAccounts(), -} - -// TODO: add styles -export function HwAccountIndexSelector() { - const [hwAccountIndex, setHwAccountIndex] = useAtom(hwAccountIndexAtom) - const { connector } = useWeb3React() - - const accountsList = useMemo(() => { - const loader = accountsLoaders[getWeb3ReactConnection(connector).type as HardWareWallet] - - if (!loader) return null - - return loader() - }, [connector]) - - const balances = useNativeCurrencyBalances(accountsList || undefined, true) - - if (!accountsList) return null - - return ( - - ) -} diff --git a/apps/cowswap-frontend/src/modules/wallet/api/pure/AccountIndexSelect/index.tsx b/apps/cowswap-frontend/src/modules/wallet/api/pure/AccountIndexSelect/index.tsx deleted file mode 100644 index 390ec198af..0000000000 --- a/apps/cowswap-frontend/src/modules/wallet/api/pure/AccountIndexSelect/index.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { ChangeEvent, useCallback } from 'react' - -import { Currency, CurrencyAmount } from '@uniswap/sdk-core' - -import styled from 'styled-components/macro' - -import { TokenAmount } from 'common/pure/TokenAmount' - -const Wrapper = styled.form` - font-size: 14px; - margin: 10px 0; -` - -export interface AccountIndexSelectProps { - accountsList: string[] - currentIndex: number - balances: { [account: string]: CurrencyAmount | undefined } - onAccountIndexChange(index: number): void -} - -export function AccountIndexSelect(props: AccountIndexSelectProps) { - const { currentIndex, accountsList, balances, onAccountIndexChange } = props - - const onAccountIndexChangeCallback = useCallback( - (event: ChangeEvent) => { - event.preventDefault() - const index = +(event.target.value || 0) - onAccountIndexChange(index) - }, - [onAccountIndexChange] - ) - - return ( - -

Hardware account index:

- -
- ) -} diff --git a/apps/cowswap-frontend/src/modules/wallet/api/pure/WalletModal/index.tsx b/apps/cowswap-frontend/src/modules/wallet/api/pure/WalletModal/index.tsx index 15df6fd701..98493699f0 100644 --- a/apps/cowswap-frontend/src/modules/wallet/api/pure/WalletModal/index.tsx +++ b/apps/cowswap-frontend/src/modules/wallet/api/pure/WalletModal/index.tsx @@ -23,7 +23,7 @@ interface WalletModalProps { toggleModal: () => void view: WalletModalView openOptions: () => void - tryConnection: () => void // () => tryActivation(connector) + tryConnection: () => void pendingError: string | undefined // TODO: Remove dependency web3-react @@ -33,21 +33,9 @@ interface WalletModalProps { } export function WalletModal(props: WalletModalProps) { - const { - isOpen, - toggleModal, - view, - openOptions, - pendingError, - tryActivation, - tryConnection, - pendingConnector, - // account, - } = props + const { isOpen, toggleModal, view, openOptions, pendingError, tryActivation, tryConnection, pendingConnector } = props const isPending = view === 'pending' - // const isOptions = view === 'options' - // const showZengoBanner = !account && !window.ethereum && isOptions return ( @@ -73,7 +61,6 @@ export function WalletModal(props: WalletModalProps) { )} - {/*{showZengoBanner && }*/} {!pendingError && ( diff --git a/apps/cowswap-frontend/src/modules/wallet/api/pure/WalletModal/styled.tsx b/apps/cowswap-frontend/src/modules/wallet/api/pure/WalletModal/styled.tsx index 5a30c5a5f3..81eff40a82 100644 --- a/apps/cowswap-frontend/src/modules/wallet/api/pure/WalletModal/styled.tsx +++ b/apps/cowswap-frontend/src/modules/wallet/api/pure/WalletModal/styled.tsx @@ -66,3 +66,28 @@ export const OptionGrid = styled.div` grid-template-columns: repeat(2, 1fr); `} ` + +export const IconWrapper = styled.div` + --size: 42px; + display: flex; + width: var(--size); + height: var(--size); + border-radius: var(--size); + align-items: center; + justify-content: center; + background: transparent; + border: 1px solid var(--cow-color-border); + padding: 8px; + margin: 0 12px 0 0; + + > svg { + width: 100%; + height: 100%; + } + + > svg > path { + --color: var(--cow-color-text1); + fill: var(--color); + stroke: var(--color); + } +` diff --git a/apps/cowswap-frontend/src/modules/wallet/api/utils/connection.ts b/apps/cowswap-frontend/src/modules/wallet/api/utils/connection.ts index b8a3d1c241..43732d663c 100644 --- a/apps/cowswap-frontend/src/modules/wallet/api/utils/connection.ts +++ b/apps/cowswap-frontend/src/modules/wallet/api/utils/connection.ts @@ -1,3 +1,11 @@ +import CoinbaseWalletIcon from '../assets/coinbase.svg' +import FortmaticIcon from '../assets/formatic.png' +import KeystoneImage from '../assets/keystone.svg' +import LedgerIcon from '../assets/ledger.svg' +import TallyIcon from '../assets/tally.svg' +import TrezorIcon from '../assets/trezor.svg' +import TrustIcon from '../assets/trust.svg' +import WalletConnectIcon from '../assets/walletConnectIcon.svg' import { ConnectionType } from '../types' const connectionTypeToName: Record = { @@ -19,6 +27,30 @@ const connectionTypeToName: Record = { [ConnectionType.TREZOR]: 'Trezor', } +const IDENTICON_KEY = 'Identicon' + +const connectionTypeToIcon: Record = { + [ConnectionType.INJECTED]: IDENTICON_KEY, + [ConnectionType.INJECTED_WIDGET]: IDENTICON_KEY, + [ConnectionType.GNOSIS_SAFE]: IDENTICON_KEY, + [ConnectionType.NETWORK]: IDENTICON_KEY, + [ConnectionType.ZENGO]: IDENTICON_KEY, + [ConnectionType.AMBIRE]: IDENTICON_KEY, + [ConnectionType.ALPHA]: IDENTICON_KEY, + [ConnectionType.COINBASE_WALLET]: CoinbaseWalletIcon, + [ConnectionType.FORTMATIC]: FortmaticIcon, + [ConnectionType.TRUST]: TrustIcon, + [ConnectionType.TALLY]: TallyIcon, + [ConnectionType.LEDGER]: LedgerIcon, + [ConnectionType.TREZOR]: TrezorIcon, + [ConnectionType.KEYSTONE]: KeystoneImage, + [ConnectionType.WALLET_CONNECT]: WalletConnectIcon, + [ConnectionType.WALLET_CONNECT_V2]: WalletConnectIcon, +} + +export function getConnectionIcon(connectionType: ConnectionType): string { + return connectionTypeToIcon[connectionType] +} export function getConnectionName(connectionType: ConnectionType, isMetaMask?: boolean): string { if (connectionType === ConnectionType.INJECTED && isMetaMask) return 'MetaMask' diff --git a/apps/cowswap-frontend/src/modules/wallet/index.ts b/apps/cowswap-frontend/src/modules/wallet/index.ts index 9201947eda..f05c881e06 100644 --- a/apps/cowswap-frontend/src/modules/wallet/index.ts +++ b/apps/cowswap-frontend/src/modules/wallet/index.ts @@ -13,7 +13,8 @@ export * from './api/updaters/HwAccountIndexUpdater' // Components export * from './web3-react/containers/Web3Status' +export * from './web3-react/containers/AccountSelectorModal' +export * from './web3-react/containers/AccountSelectorModal/state' export * from './web3-react/containers/WalletModal' export * from './api/container/Identicon' export * from './web3-react/containers/AddToMetamask' -export * from './api/container/HwAccountIndexSelector' diff --git a/apps/cowswap-frontend/src/modules/wallet/web3-react/connectors/TrezorConnector/getAccountsList.ts b/apps/cowswap-frontend/src/modules/wallet/web3-react/connectors/TrezorConnector/getAccountsList.ts index 72919df33e..f08d0deae1 100644 --- a/apps/cowswap-frontend/src/modules/wallet/web3-react/connectors/TrezorConnector/getAccountsList.ts +++ b/apps/cowswap-frontend/src/modules/wallet/web3-react/connectors/TrezorConnector/getAccountsList.ts @@ -9,12 +9,12 @@ import type { TrezorConnect } from '@trezor/connect-web' * This file contains cherry-picked code from import { TrezorSubprovider } from '@0x/subproviders' */ -export async function getAccountsAsync(trezorConnect: TrezorConnect, numberOfAccounts = 100): Promise { +export async function getAccountsList(trezorConnect: TrezorConnect, offset = 0, limit = 100): Promise { const initialDerivedKeyInfo = await initialDerivedKeyInfoAsync(trezorConnect) if (!initialDerivedKeyInfo) return null - const derivedKeyInfos = calculateDerivedHDKeyInfos(initialDerivedKeyInfo, numberOfAccounts) + const derivedKeyInfos = calculateDerivedHDKeyInfos(initialDerivedKeyInfo, offset, limit) return derivedKeyInfos.map((k) => k.address) } @@ -29,10 +29,10 @@ interface DerivedHDKeyInfo { class DerivedHDKeyInfoIterator { private index = 0 - constructor(private parentDerivedKeyInfo: DerivedHDKeyInfo, private searchLimit = 1000) {} + constructor(private parentDerivedKeyInfo: DerivedHDKeyInfo, private offset = 0, private limit = 100) {} next() { const baseDerivationPath = this.parentDerivedKeyInfo.baseDerivationPath - const derivationIndex = this.index + const derivationIndex = this.offset + this.index const fullDerivationPath = `m/${baseDerivationPath}/${derivationIndex}` const path = `m/${derivationIndex}` const hdKey = this.parentDerivedKeyInfo.hdKey.derive(path) @@ -43,7 +43,7 @@ class DerivedHDKeyInfoIterator { baseDerivationPath, derivationPath: fullDerivationPath, } - const isDone = this.index === this.searchLimit + const isDone = this.index === this.limit this.index++ return { done: isDone, @@ -55,7 +55,13 @@ class DerivedHDKeyInfoIterator { } } +const derivedKeyInfoCache = new Map() + async function initialDerivedKeyInfoAsync(trezorConnect: TrezorConnect): Promise { + if (derivedKeyInfoCache.has(trezorConnect)) { + return derivedKeyInfoCache.get(trezorConnect) || null + } + const response = await trezorConnect.getPublicKey({ path: TREZOR_DERIVATION_PATH, }) @@ -68,17 +74,25 @@ async function initialDerivedKeyInfoAsync(trezorConnect: TrezorConnect): Promise hdKey.chainCode = new Buffer(payload.chainCode, 'hex') const address = addressOfHDKey(hdKey) - return { + const info: DerivedHDKeyInfo = { hdKey, address, derivationPath: TREZOR_DERIVATION_PATH, baseDerivationPath: TREZOR_DERIVATION_PATH.slice(2), } + + derivedKeyInfoCache.set(trezorConnect, info) + + return info } -function calculateDerivedHDKeyInfos(parentDerivedKeyInfo: DerivedHDKeyInfo, numberOfKeys: number): DerivedHDKeyInfo[] { +function calculateDerivedHDKeyInfos( + parentDerivedKeyInfo: DerivedHDKeyInfo, + offset: number, + limit: number +): DerivedHDKeyInfo[] { const derivedKeys: DerivedHDKeyInfo[] = [] - const derivedKeyIterator = new DerivedHDKeyInfoIterator(parentDerivedKeyInfo, numberOfKeys) + const derivedKeyIterator = new DerivedHDKeyInfoIterator(parentDerivedKeyInfo, offset, limit) for (const key of derivedKeyIterator) { derivedKeys.push(key) diff --git a/apps/cowswap-frontend/src/modules/wallet/web3-react/connectors/TrezorConnector/index.ts b/apps/cowswap-frontend/src/modules/wallet/web3-react/connectors/TrezorConnector/index.ts index 0de402a4ea..5091f5a351 100644 --- a/apps/cowswap-frontend/src/modules/wallet/web3-react/connectors/TrezorConnector/index.ts +++ b/apps/cowswap-frontend/src/modules/wallet/web3-react/connectors/TrezorConnector/index.ts @@ -22,6 +22,8 @@ const trezorConfig: Parameters[0] = { }, } +const ACCOUNTS_LIMIT = 100 + export class TrezorConnector extends Connector { public customProvider?: TrezorProvider @@ -33,6 +35,8 @@ export class TrezorConnector extends Connector { private accounts: string[] | null = null + private accountsOffset = 0 + private cancelActivation: () => void = () => void 0 connectEagerly(...args: unknown[]) { @@ -93,11 +97,26 @@ export class TrezorConnector extends Connector { deactivate(): Promise | void { this.activatedNetwork = null + this.accountsOffset = 0 this.cancelActivation() return this.trezorConnect?.dispose() } + async loadMoreAccounts(): Promise { + await this.loadAccounts(this.accountsOffset + ACCOUNTS_LIMIT) + } + + async loadAccounts(offset: number): Promise { + this.accountsOffset = offset + + const accounts = await import('./getAccountsList').then((module) => + module.getAccountsList(this.trezorConnect!, offset, ACCOUNTS_LIMIT) + ) + + this.accounts = (this.accounts || []).concat(accounts || []) + } + private getCurrentAccount(): string { if (!this.accounts) { throw new Error('Cannot load Trezor accounts') @@ -118,7 +137,7 @@ export class TrezorConnector extends Connector { trezorConnect: TrezorConnect, _transformTypedData: typeof transformTypedData ) { - this.accounts = await import('./getAccountsList').then((module) => module.getAccountsAsync(trezorConnect)) + await this.loadAccounts(0) const account = this.getCurrentAccount() const customProvider = new TrezorProvider(url, this.accounts!, trezorConnect, _transformTypedData) diff --git a/apps/cowswap-frontend/src/modules/wallet/web3-react/containers/AccountSelectorModal/accountsLoaders.ts b/apps/cowswap-frontend/src/modules/wallet/web3-react/containers/AccountSelectorModal/accountsLoaders.ts new file mode 100644 index 0000000000..b25ed641fb --- /dev/null +++ b/apps/cowswap-frontend/src/modules/wallet/web3-react/containers/AccountSelectorModal/accountsLoaders.ts @@ -0,0 +1,19 @@ +import { ConnectionType } from '../../../api/types' +import { HardWareWallet } from '../../connection' +import { trezorConnection } from '../../connection/trezor' + +interface WalletAccountsLoader { + getAccounts(): string[] | null + loadMoreAccounts(): Promise +} + +export const accountsLoaders: Record = { + [ConnectionType.TREZOR]: { + getAccounts() { + return trezorConnection.connector.getAccounts() + }, + loadMoreAccounts() { + return trezorConnection.connector.loadMoreAccounts() + }, + }, +} diff --git a/apps/cowswap-frontend/src/modules/wallet/web3-react/containers/AccountSelectorModal/index.tsx b/apps/cowswap-frontend/src/modules/wallet/web3-react/containers/AccountSelectorModal/index.tsx new file mode 100644 index 0000000000..8ac22e48b3 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/wallet/web3-react/containers/AccountSelectorModal/index.tsx @@ -0,0 +1,84 @@ +import { useAtom, useAtomValue, useSetAtom } from 'jotai' +import React, { useCallback, useEffect, useMemo, useState } from 'react' + +import { useAddSnackbar } from '@cowswap/snackbars' +import { useWeb3React } from '@web3-react/core' + +import { Trans } from '@lingui/macro' + +import { useNativeCurrencyBalances } from 'modules/tokens/hooks/useCurrencyBalance' + +import { CowModal } from 'common/pure/Modal' + +import { accountsLoaders } from './accountsLoaders' +import { accountSelectorModalAtom, toggleAccountSelectorModalAtom } from './state' +import * as styledEl from './styled' + +import { hwAccountIndexAtom } from '../../../api/state' +import { getConnectionIcon, getConnectionName } from '../../../api/utils/connection' +import { getWeb3ReactConnection, HardWareWallet } from '../../connection' +import { AccountIndexSelect } from '../../pure/AccountIndexSelect' + +export function AccountSelectorModal() { + const { isOpen } = useAtomValue(accountSelectorModalAtom) + const closeModal = useSetAtom(toggleAccountSelectorModalAtom) + + const [hwAccountIndex, setHwAccountIndex] = useAtom(hwAccountIndexAtom) + const { connector } = useWeb3React() + const addSnackbar = useAddSnackbar() + + const connectionType = useMemo(() => getWeb3ReactConnection(connector).type, [connector]) + + const walletIcon = useMemo(() => getConnectionIcon(connectionType), [connectionType]) + const walletName = useMemo(() => getConnectionName(connectionType), [connectionType]) + + const accountsLoader = useMemo(() => accountsLoaders[connectionType as HardWareWallet], [connectionType]) + + const [accountsList, setAccountsList] = useState(null) + + const balances = useNativeCurrencyBalances(accountsList || undefined, true) + + const loadMoreAccounts = useCallback(async () => { + if (!accountsLoader) return + + return accountsLoader.loadMoreAccounts().then(() => { + setAccountsList(accountsLoader.getAccounts()) + }) + }, [accountsLoader]) + + const onAccountIndexChange = useCallback( + (index: number) => { + setHwAccountIndex(index) + closeModal() + + addSnackbar({ content: {walletName} account changed, id: 'account-changed', icon: 'success' }) + }, + [walletName, addSnackbar, setHwAccountIndex, closeModal] + ) + + useEffect(() => { + setAccountsList(accountsLoader?.getAccounts() || null) + }, [accountsLoader]) + + if (!accountsList || !accountsLoader) return null + + return ( + + + +

+ Select {walletName} Account +

+ +
+ +
+
+ ) +} diff --git a/apps/cowswap-frontend/src/modules/wallet/web3-react/containers/AccountSelectorModal/state.ts b/apps/cowswap-frontend/src/modules/wallet/web3-react/containers/AccountSelectorModal/state.ts new file mode 100644 index 0000000000..6a2bae21f5 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/wallet/web3-react/containers/AccountSelectorModal/state.ts @@ -0,0 +1,11 @@ +import { atom } from 'jotai' + +export interface AccountSelectorModalState { + isOpen: boolean +} + +export const accountSelectorModalAtom = atom({ isOpen: false }) + +export const toggleAccountSelectorModalAtom = atom(null, (get, set) => { + set(accountSelectorModalAtom, { isOpen: !get(accountSelectorModalAtom).isOpen }) +}) diff --git a/apps/cowswap-frontend/src/modules/wallet/web3-react/containers/AccountSelectorModal/styled.tsx b/apps/cowswap-frontend/src/modules/wallet/web3-react/containers/AccountSelectorModal/styled.tsx new file mode 100644 index 0000000000..d5502835d2 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/wallet/web3-react/containers/AccountSelectorModal/styled.tsx @@ -0,0 +1,38 @@ +import styled from 'styled-components/macro' + +import { ReactComponent as Close } from 'legacy/assets/images/x.svg' + +export const CloseIcon = styled(Close)` + opacity: 0.6; + transition: opacity 0.3s ease-in-out; + stroke: ${({ theme }) => theme.text1}; + width: 24px; + height: 24px; + cursor: pointer; + + &:hover { + opacity: 1; + } +` + +export const Header = styled.div` + display: flex; + justify-content: space-between; + margin-bottom: 20px; + + h3 { + display: flex; + gap: 10px; + margin: 0; + } +` + +export const Wrapper = styled.div` + padding: 20px; + width: 100%; +` + +export const WalletIcon = styled.img` + width: 24px; + height: 24px; +` diff --git a/apps/cowswap-frontend/src/modules/wallet/web3-react/containers/WalletModal/index.tsx b/apps/cowswap-frontend/src/modules/wallet/web3-react/containers/WalletModal/index.tsx index 58021c25c3..43f58c5ec2 100644 --- a/apps/cowswap-frontend/src/modules/wallet/web3-react/containers/WalletModal/index.tsx +++ b/apps/cowswap-frontend/src/modules/wallet/web3-react/containers/WalletModal/index.tsx @@ -1,3 +1,4 @@ +import { useSetAtom } from 'jotai' import { useCallback, useEffect, useState } from 'react' import { useWeb3React } from '@web3-react/core' @@ -11,9 +12,9 @@ import { updateConnectionError } from 'legacy/state/connection/reducer' import { useAppDispatch, useAppSelector } from 'legacy/state/hooks' import { updateSelectedWallet } from 'legacy/state/user/reducer' -import { ConnectionType, useWalletInfo } from 'modules/wallet' +import { ConnectionType, toggleAccountSelectorModalAtom, useWalletInfo } from 'modules/wallet' import { WalletModal as WalletModalPure, WalletModalView } from 'modules/wallet/api/pure/WalletModal' -import { getWeb3ReactConnection } from 'modules/wallet/web3-react/connection' +import { getIsHardWareWallet, getWeb3ReactConnection } from 'modules/wallet/web3-react/connection' import { walletConnectConnection } from 'modules/wallet/web3-react/connection/walletConnect' export function WalletModal() { @@ -30,6 +31,7 @@ export function WalletModal() { const walletModalOpen = useModalIsOpen(ApplicationModal.WALLET) const toggleWalletModal = useToggleWalletModal() + const toggleAccountSelectorModal = useSetAtom(toggleAccountSelectorModalAtom) const openOptions = useCallback(() => { setWalletView('options') @@ -72,6 +74,7 @@ export function WalletModal() { const tryActivation = useCallback( async (connector: Connector) => { const connectionType = getWeb3ReactConnection(connector).type + const isHardWareWallet = getIsHardWareWallet(connectionType) changeWalletAnalytics('Todo: wallet name') @@ -102,6 +105,10 @@ export function WalletModal() { } dispatch(updateSelectedWallet({ wallet: connectionType })) + + if (isHardWareWallet) { + toggleAccountSelectorModal() + } } catch (error: any) { console.error(`[tryActivation] web3-react connection error`, error) dispatch(updateSelectedWallet({ wallet: undefined })) @@ -116,7 +123,7 @@ export function WalletModal() { ) } }, - [dispatch, toggleWalletModal] + [dispatch, toggleWalletModal, toggleAccountSelectorModal] ) return ( diff --git a/apps/cowswap-frontend/src/modules/wallet/web3-react/containers/Web3Status/index.tsx b/apps/cowswap-frontend/src/modules/wallet/web3-react/containers/Web3Status/index.tsx index 4f521d6c99..29915da1e3 100644 --- a/apps/cowswap-frontend/src/modules/wallet/web3-react/containers/Web3Status/index.tsx +++ b/apps/cowswap-frontend/src/modules/wallet/web3-react/containers/Web3Status/index.tsx @@ -5,7 +5,7 @@ import { STORAGE_KEY_LAST_PROVIDER } from 'legacy/constants' import { useToggleWalletModal } from 'legacy/state/application/hooks' import { useAppSelector } from 'legacy/state/hooks' -import { useWalletDetails, useWalletInfo, WalletModal } from 'modules/wallet' +import { useWalletDetails, useWalletInfo, WalletModal, AccountSelectorModal } from 'modules/wallet' import { Web3StatusInner } from 'modules/wallet/api/pure/Web3StatusInner' import { Wrapper } from 'modules/wallet/api/pure/Web3StatusInner/styled' import { getWeb3ReactConnection } from 'modules/wallet/web3-react/connection' @@ -45,6 +45,7 @@ export function Web3Status() { connectionType={connectionType} /> +
) } diff --git a/apps/cowswap-frontend/src/modules/wallet/api/pure/AccountIndexSelect/index.cosmos.tsx b/apps/cowswap-frontend/src/modules/wallet/web3-react/pure/AccountIndexSelect/index.cosmos.tsx similarity index 59% rename from apps/cowswap-frontend/src/modules/wallet/api/pure/AccountIndexSelect/index.cosmos.tsx rename to apps/cowswap-frontend/src/modules/wallet/web3-react/pure/AccountIndexSelect/index.cosmos.tsx index 69a515c0ef..82b5a0fc62 100644 --- a/apps/cowswap-frontend/src/modules/wallet/api/pure/AccountIndexSelect/index.cosmos.tsx +++ b/apps/cowswap-frontend/src/modules/wallet/web3-react/pure/AccountIndexSelect/index.cosmos.tsx @@ -1,5 +1,7 @@ import { CurrencyAmount } from '@uniswap/sdk-core' +import styled from 'styled-components/macro' + import { WETH_GOERLI } from 'legacy/utils/goerli/constants' import { AccountIndexSelect } from './index' @@ -18,14 +20,30 @@ const balances = { '0xefcce23bfbef24cc4fb2dcb2bbc4f6f83c6bda98': CurrencyAmount.fromRawAmount(WETH_GOERLI, 40_000_000), } +const Wrapper = styled.div` + width: 500px; + margin: 100px auto; + padding: 20px; + + background: ${({ theme }) => theme.bg1}; +` + const Fixtures = { default: ( - console.log('onAccountIndexChange', index)} - /> + + console.log('onAccountIndexChange', index)} + loadMoreAccounts={() => { + return new Promise((resolve) => { + console.log('loadMoreAccounts') + setTimeout(resolve, 2000) + }) + }} + /> + ), } diff --git a/apps/cowswap-frontend/src/modules/wallet/web3-react/pure/AccountIndexSelect/index.tsx b/apps/cowswap-frontend/src/modules/wallet/web3-react/pure/AccountIndexSelect/index.tsx new file mode 100644 index 0000000000..c5414dfff3 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/wallet/web3-react/pure/AccountIndexSelect/index.tsx @@ -0,0 +1,95 @@ +import { useCallback, useRef, useState } from 'react' + +import { Currency, CurrencyAmount } from '@uniswap/sdk-core' + +import { Trans } from '@lingui/macro' + +import { ButtonPrimary } from 'legacy/components/Button' +import Loader from 'legacy/components/Loader' +import { shortenAddress } from 'legacy/utils' + +import { SelectDropdown } from 'common/pure/SelectDropdown' +import { TokenAmount } from 'common/pure/TokenAmount' + +import * as styledEl from './styled' + +export interface AccountIndexSelectProps { + accountsList: string[] + currentIndex: number + balances: { [account: string]: CurrencyAmount | undefined } + onAccountIndexChange(index: number): void + loadMoreAccounts(): Promise +} + +export function AccountIndexSelect(props: AccountIndexSelectProps) { + const { currentIndex, accountsList, balances, onAccountIndexChange, loadMoreAccounts } = props + const selectRef = useRef(null) + const [loadingAccounts, setLoadingAccounts] = useState(false) + + const onAccountIndexChangeCallback = useCallback(() => { + const index = +(selectRef.current?.value || 0) + + onAccountIndexChange(index) + }, [onAccountIndexChange]) + + const loadMoreAccountsCallback = useCallback(async () => { + setLoadingAccounts(true) + + try { + await loadMoreAccounts() + } catch (e) { + console.error('Loading more accounts error:', e) + } + + setLoadingAccounts(false) + }, [loadMoreAccounts]) + + return ( + + + + <> + + Please select which account you would like to use: + + + + {accountsList.map((address, index) => { + const balance = balances[address] + + return ( + + ) + })} + + + + {loadingAccounts ? ( + <> + + Loading... + + ) : ( + Load more + )} + + + + + + Connect selected account + + + + + + ) +} diff --git a/apps/cowswap-frontend/src/modules/wallet/web3-react/pure/AccountIndexSelect/styled.tsx b/apps/cowswap-frontend/src/modules/wallet/web3-react/pure/AccountIndexSelect/styled.tsx new file mode 100644 index 0000000000..3ff7fa9229 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/wallet/web3-react/pure/AccountIndexSelect/styled.tsx @@ -0,0 +1,63 @@ +import styled from 'styled-components/macro' + +import { ButtonSecondary } from 'common/pure/ButtonSecondary' + +export const Wrapper = styled.div` + ${({ theme }) => theme.flexColumnNoWrap}; + align-items: center; + justify-content: center; + width: 100%; +` + +export const LoaderContainer = styled(ButtonSecondary)` + display: flex; + gap: 8px; + align-items: center; + justify-content: center; + + &[disabled] { + cursor: default; + background: var(--cow-color-lightBlue-opacity-90); + } +` + +export const LoadingMessage = styled.div` + ${({ theme }) => theme.flexRowNoWrap}; + width: 100%; + align-items: center; + justify-content: center; + border-radius: 12px; +` + +export const LoadingWrapper = styled.div` + ${({ theme }) => theme.flexColumnNoWrap}; + width: 100%; + align-items: center; + justify-content: center; +` + +export const SelectWrapper = styled.div` + ${({ theme }) => theme.flexRowNoWrap}; + align-items: center; + justify-content: center; + width: 100%; + margin: 8px 0 0; + gap: 8px; + + ${({ theme }) => theme.mediaWidth.upToSmall` + flex-flow: column wrap; + + ${ButtonSecondary} { + width: 100%; + } + `} +` + +export const TextWrapper = styled.div` + ${({ theme }) => theme.flexColumnNoWrap}; + align-items: flex-start; + justify-content: flex-start; + width: 100%; + margin: 0 0 24px; + font-size: 14px; +` diff --git a/apps/cowswap-frontend/tsconfig.json b/apps/cowswap-frontend/tsconfig.json index 2e07716699..65e6c88429 100644 --- a/apps/cowswap-frontend/tsconfig.json +++ b/apps/cowswap-frontend/tsconfig.json @@ -13,7 +13,8 @@ "@cowswap/ui": ["../../../libs/ui/src/index.ts"], "@cowswap/ui-utils": ["../../../libs/ui-utils/src/index.ts"], "@cowswap/widget-lib": ["../../../libs/widget-lib/src/index.ts"], - "@cowswap/widget-react": ["../../../libs/widget-react/src/index.ts"] + "@cowswap/widget-react": ["../../../libs/widget-react/src/index.ts"], + "@cowswap/snackbars": ["../../../libs/snackbars/src/index.ts"] } }, "files": [], diff --git a/libs/snackbars/.babelrc b/libs/snackbars/.babelrc new file mode 100644 index 0000000000..ef4889c1ab --- /dev/null +++ b/libs/snackbars/.babelrc @@ -0,0 +1,20 @@ +{ + "presets": [ + [ + "@nx/react/babel", + { + "runtime": "automatic", + "useBuiltIns": "usage" + } + ] + ], + "plugins": [ + [ + "styled-components", + { + "pure": true, + "ssr": true + } + ] + ] +} diff --git a/libs/snackbars/.eslintrc.json b/libs/snackbars/.eslintrc.json new file mode 100644 index 0000000000..a39ac5d057 --- /dev/null +++ b/libs/snackbars/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["plugin:@nx/react", "../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/snackbars/README.md b/libs/snackbars/README.md new file mode 100644 index 0000000000..f80124370c --- /dev/null +++ b/libs/snackbars/README.md @@ -0,0 +1,42 @@ +# Snackbars + +![](./demo.png) + +## Usage + +```tsx +// Add the widget in the root component + +import { SnackbarsWidget } from '@cowswap/snackbars' + +export function App() { + return ( + + + + + ) +} +``` + +```tsx +// Use the hook to add a snackbar + +import { useAddSnackbar } from '@cowswap/snackbars' + +export function MyComponent() { + const addSnackbar = useAddSnackbar() + + addSnackbar({ + content: {walletName} account changed, + id: 'account-changed', + icon: 'success' + }) +} +``` + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test snackbars` to execute the unit tests via [Vitest](https://vitest.dev/). diff --git a/libs/snackbars/demo.png b/libs/snackbars/demo.png new file mode 100644 index 0000000000000000000000000000000000000000..dfd8b8d31fc3778a3a54de0bf8a1d296e6c02420 GIT binary patch literal 57902 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 literal 0 HcmV?d00001 diff --git a/libs/snackbars/jest.config.ts b/libs/snackbars/jest.config.ts new file mode 100644 index 0000000000..21f311b68a --- /dev/null +++ b/libs/snackbars/jest.config.ts @@ -0,0 +1,11 @@ +/* eslint-disable */ +export default { + displayName: 'snackbars', + preset: '../../jest.preset.js', + transform: { + '^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nx/react/plugins/jest', + '^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/react/babel'] }], + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], + coverageDirectory: '../../coverage/libs/snackbars', +} diff --git a/libs/snackbars/package.json b/libs/snackbars/package.json new file mode 100644 index 0000000000..ada384c9d3 --- /dev/null +++ b/libs/snackbars/package.json @@ -0,0 +1,12 @@ +{ + "name": "@cowswap/snackbars", + "version": "0.0.1", + "main": "./index.js", + "types": "./index.d.ts", + "exports": { + ".": { + "import": "./index.mjs", + "require": "./index.js" + } + } +} diff --git a/libs/snackbars/project.json b/libs/snackbars/project.json new file mode 100644 index 0000000000..748a6c5a84 --- /dev/null +++ b/libs/snackbars/project.json @@ -0,0 +1,46 @@ +{ + "name": "@cowswap/snackbars", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/snackbars/src", + "projectType": "library", + "tags": [], + "targets": { + "lint": { + "executor": "@nx/linter:eslint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["libs/snackbars/**/*.{ts,tsx,js,jsx}"] + } + }, + "build": { + "executor": "@nx/vite:build", + "outputs": ["{options.outputPath}"], + "defaultConfiguration": "production", + "options": { + "outputPath": "dist/libs/snackbars" + }, + "configurations": { + "development": { + "mode": "development" + }, + "production": { + "mode": "production" + } + } + }, + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/snackbars/jest.config.ts", + "passWithNoTests": true + }, + "configurations": { + "ci": { + "ci": true, + "codeCoverage": true + } + } + } + } +} diff --git a/libs/snackbars/src/containers/SnackbarsWidget/index.tsx b/libs/snackbars/src/containers/SnackbarsWidget/index.tsx new file mode 100644 index 0000000000..db41555fe6 --- /dev/null +++ b/libs/snackbars/src/containers/SnackbarsWidget/index.tsx @@ -0,0 +1,99 @@ +import { useAtomValue, useSetAtom } from 'jotai' +import { IconType, removeSnackbarAtom, snackbarsAtom } from '../../state/snackbarsAtom' +import styled from 'styled-components/macro' +import { SnackbarPopup } from '../../pure/SnackbarPopup' +import ms from 'ms.macro' +import { AlertCircle, CheckCircle } from 'react-feather' +import { ReactElement, useCallback, useMemo } from 'react' +import { transparentize } from 'polished' +import { useResetAtom } from 'jotai/utils' + +const Overlay = styled.div` + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 4; + background: ${({ theme }) => transparentize(0.2, theme.bg1)}; +` + +const List = styled.div` + position: relative; + z-index: 5; +` + +const Host = styled.div` + position: fixed; + top: 0; + right: 0; + padding: 20px; + z-index: 6; + min-width: 300px; + max-width: 800px; + + ${({ theme }) => theme.mediaWidth.upToSmall` + width: 100%; + + ${Overlay} { + display: block; + } + `} +` + +const SuccessIcon = styled(CheckCircle)` + color: ${({ theme }) => theme.green1}; +` + +const AlertIcon = styled(AlertCircle)` + color: ${({ theme }) => theme.red1}; +` + +const DEFAULT_DURATION = ms`6s` + +const icons: Record = { + success: , + alert: , + custom: undefined, +} + +export function SnackbarsWidget() { + const snackbarsState = useAtomValue(snackbarsAtom) + const resetSnackbarsState = useResetAtom(snackbarsAtom) + const removeSnackbar = useSetAtom(removeSnackbarAtom) + + const snackbars = useMemo(() => { + return Object.values(snackbarsState) + }, [snackbarsState]) + + const onExpire = useCallback( + (id: string) => { + removeSnackbar(id) + }, + [removeSnackbar] + ) + + return ( + + + {snackbars.map((snackbar) => { + const icon = snackbar.icon + ? snackbar.icon === 'custom' + ? snackbar.customIcon + : icons[snackbar.icon] + : undefined + + const duration = snackbar.duration ?? DEFAULT_DURATION + + return ( + + {snackbar.content} + + ) + })} + + {snackbars.length > 0 && } + + ) +} diff --git a/libs/snackbars/src/hooks/useAddSnackbar.ts b/libs/snackbars/src/hooks/useAddSnackbar.ts new file mode 100644 index 0000000000..ddc1e26564 --- /dev/null +++ b/libs/snackbars/src/hooks/useAddSnackbar.ts @@ -0,0 +1,6 @@ +import { useSetAtom } from 'jotai' +import { addSnackbarAtom, SnackbarItem } from '../state/snackbarsAtom' + +export function useAddSnackbar(): (item: SnackbarItem) => void { + return useSetAtom(addSnackbarAtom) +} diff --git a/libs/snackbars/src/index.ts b/libs/snackbars/src/index.ts new file mode 100644 index 0000000000..3a29a188c1 --- /dev/null +++ b/libs/snackbars/src/index.ts @@ -0,0 +1,3 @@ +export * from './containers/SnackbarsWidget' +export * from './state/snackbarsAtom' +export * from './hooks/useAddSnackbar' diff --git a/libs/snackbars/src/pure/SnackbarPopup/index.tsx b/libs/snackbars/src/pure/SnackbarPopup/index.tsx new file mode 100644 index 0000000000..8836f69a67 --- /dev/null +++ b/libs/snackbars/src/pure/SnackbarPopup/index.tsx @@ -0,0 +1,87 @@ +import { ReactElement, useCallback, useEffect } from 'react' + +import styled from 'styled-components/macro' +import { X } from 'react-feather' +import { animated, useSpring } from '@react-spring/web' + +const Wrapper = styled.div` + display: inline-block; + width: 100%; + background-color: ${({ theme }) => theme.bg1}; + position: relative; + border-radius: 10px; + padding: 20px 40px 20px 20px; + overflow: hidden; + border: 2px solid ${({ theme }) => theme.black}; + box-shadow: 2px 2px 0 ${({ theme }) => theme.black}; + font-weight: 500; + font-size: 16px; + color: ${({ theme }) => theme.text1}; +` + +const ContentWrapper = styled.div` + display: flex; + width: 100%; + align-items: center; + gap: 20px; +` + +const StyledClose = styled(X)` + position: absolute; + right: 10px; + top: 10px; + + :hover { + cursor: pointer; + } +` + +const Fader = styled.div` + position: absolute; + bottom: 0; + left: 0; + width: 100%; + background-color: ${({ theme }) => theme.disabled}; + height: 4px; +` + +const AnimatedFader = animated(Fader) + +export interface SnackbarPopupProps { + id: string + duration: number + children: ReactElement + icon: ReactElement | undefined + onExpire(id: string): void +} + +export function SnackbarPopup(props: SnackbarPopupProps) { + const { id, children, duration, icon, onExpire } = props + + const faderStyle = useSpring({ + from: { width: '100%' }, + to: { width: '0%' }, + config: { duration }, + }) + + const removeSelf = useCallback(() => { + onExpire(id) + }, [id, onExpire]) + + useEffect(() => { + const timeout = setTimeout(removeSelf, duration) + + return () => clearTimeout(timeout) + }, [duration, removeSelf]) + + return ( + + + + {icon &&
{icon}
} +
{children}
+
+ +
+ ) +} diff --git a/libs/snackbars/src/state/snackbarsAtom.ts b/libs/snackbars/src/state/snackbarsAtom.ts new file mode 100644 index 0000000000..fe4847d321 --- /dev/null +++ b/libs/snackbars/src/state/snackbarsAtom.ts @@ -0,0 +1,27 @@ +import { atom } from 'jotai' +import { ReactElement } from 'react' +import { atomWithReset } from 'jotai/utils' + +export type IconType = 'success' | 'alert' | 'custom' + +export interface SnackbarItem { + content: ReactElement + id: string + duration?: number + icon?: IconType + customIcon?: ReactElement +} + +export const snackbarsAtom = atomWithReset<{ [key: string]: SnackbarItem }>({}) + +export const addSnackbarAtom = atom(null, (get, set, item: SnackbarItem) => { + set(snackbarsAtom, { ...get(snackbarsAtom), [item.id]: item }) +}) + +export const removeSnackbarAtom = atom(null, (get, set, id: string) => { + const state = get(snackbarsAtom) + + delete state[id] + + set(snackbarsAtom, { ...state }) +}) diff --git a/libs/snackbars/tsconfig.json b/libs/snackbars/tsconfig.json new file mode 100644 index 0000000000..bab74ff2e6 --- /dev/null +++ b/libs/snackbars/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "jsx": "react-jsx", + "allowJs": false, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "types": ["vite/client"] + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "extends": "../../tsconfig.base.json" +} diff --git a/libs/snackbars/tsconfig.lib.json b/libs/snackbars/tsconfig.lib.json new file mode 100644 index 0000000000..1b2b0c12df --- /dev/null +++ b/libs/snackbars/tsconfig.lib.json @@ -0,0 +1,19 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "types": ["node", "vite/client"] + }, + "files": ["../../node_modules/@nx/react/typings/cssmodule.d.ts", "../../node_modules/@nx/react/typings/image.d.ts"], + "exclude": [ + "**/*.spec.ts", + "**/*.test.ts", + "**/*.spec.tsx", + "**/*.test.tsx", + "**/*.spec.js", + "**/*.test.js", + "**/*.spec.jsx", + "**/*.test.jsx" + ], + "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] +} diff --git a/libs/snackbars/tsconfig.spec.json b/libs/snackbars/tsconfig.spec.json new file mode 100644 index 0000000000..26ef046ac5 --- /dev/null +++ b/libs/snackbars/tsconfig.spec.json @@ -0,0 +1,20 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx", + "src/**/*.d.ts" + ] +} diff --git a/libs/snackbars/vite.config.ts b/libs/snackbars/vite.config.ts new file mode 100644 index 0000000000..9238b65a24 --- /dev/null +++ b/libs/snackbars/vite.config.ts @@ -0,0 +1,49 @@ +/// +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import viteTsConfigPaths from 'vite-tsconfig-paths' +import dts from 'vite-plugin-dts' +import * as path from 'path' + +export default defineConfig({ + cacheDir: '../../node_modules/.vite/snackbars', + + plugins: [ + dts({ + entryRoot: 'src', + tsConfigFilePath: path.join(__dirname, 'tsconfig.lib.json'), + skipDiagnostics: true, + }), + react(), + viteTsConfigPaths({ + root: '../../', + }), + ], + + // Uncomment this if you are using workers. + // worker: { + // plugins: [ + // viteTsConfigPaths({ + // root: '../../', + // }), + // ], + // }, + + // Configuration for building your library. + // See: https://vitejs.dev/guide/build.html#library-mode + build: { + lib: { + // Could also be a dictionary or array of multiple entry points. + entry: 'src/index.ts', + name: 'snackbars', + fileName: 'index', + // Change this to the formats you want to support. + // Don't forget to update your package.json as well. + formats: ['es', 'cjs'], + }, + rollupOptions: { + // External packages that should not be bundled into your library. + external: ['react', 'react-dom', 'react/jsx-runtime'], + }, + }, +}) diff --git a/nx.json b/nx.json index caa778b0ee..9cddd6252b 100644 --- a/nx.json +++ b/nx.json @@ -4,51 +4,28 @@ "default": { "runner": "nx/tasks-runners/default", "options": { - "cacheableOperations": [ - "build", - "lint", - "test", - "e2e" - ], + "cacheableOperations": ["build", "lint", "test", "e2e"], "accessToken": "YmVmZTg2ZTQtOGNjMi00ZjQzLTk1Y2EtZTA0MDNjY2ExNDZifHJlYWQtd3JpdGU=" } } }, "targetDefaults": { "build": { - "dependsOn": [ - "^build" - ], - "inputs": [ - "production", - "^production" - ] + "dependsOn": ["^build"], + "inputs": ["production", "^production"] }, "e2e": { - "inputs": [ - "default", - "^production" - ] + "inputs": ["default", "^production"] }, "test": { - "inputs": [ - "default", - "^production" - ] + "inputs": ["default", "^production"] }, "lint": { - "inputs": [ - "default", - "{workspaceRoot}/.eslintrc.json", - "{workspaceRoot}/.eslintignore" - ] + "inputs": ["default", "{workspaceRoot}/.eslintrc.json", "{workspaceRoot}/.eslintignore"] } }, "namedInputs": { - "default": [ - "{projectRoot}/**/*", - "sharedGlobals" - ], + "default": ["{projectRoot}/**/*", "sharedGlobals"], "production": [ "default", "!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)", @@ -70,7 +47,8 @@ }, "library": { "style": "styled-components", - "linter": "eslint" + "linter": "eslint", + "unitTestRunner": "jest" } } } diff --git a/package.json b/package.json index 4b82bdbb2c..27ba77ae48 100644 --- a/package.json +++ b/package.json @@ -178,14 +178,15 @@ "web-vitals": "^2.1.4" }, "devDependencies": { + "@babel/preset-react": "^7.14.5", "@commitlint/cli": "^17.6.7", "@commitlint/config-conventional": "^17.6.7", "@lingui/swc-plugin": "^4.0.4", "@lingui/vite-plugin": "^4.3.0", "@nx/cypress": "16.4.0", "@nx/eslint-plugin": "16.4.0", - "@nx/jest": "^16.4.0", - "@nx/js": "16.4.0", + "@nx/jest": "16.5.5", + "@nx/js": "16.5.5", "@nx/linter": "16.4.0", "@nx/react": "16.4.0", "@nx/vite": "16.4.0", @@ -234,6 +235,8 @@ "eslint-plugin-unused-imports": "^3.0.0", "husky": "^8.0.3", "isomorphic-fetch": "^3.0.0", + "jest": "^29.4.1", + "jest-environment-jsdom": "^29.4.1", "jest-fetch-mock": "^3.0.3", "jest-styled-components": "^7.1.1", "jsdom": "~22.1.0", diff --git a/tsconfig.base.json b/tsconfig.base.json index a5f1d9a344..8b32512a7e 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -35,7 +35,8 @@ "@cowswap/ui": ["libs/ui/src/index.ts"], "@cowswap/ui-utils": ["libs/ui-utils/src/index.ts"], "@cowswap/widget-lib": ["libs/widget-lib/src/index.ts"], - "@cowswap/widget-react": ["libs/widget-react/src/index.ts"] + "@cowswap/widget-react": ["libs/widget-react/src/index.ts"], + "@cowswap/snackbars": ["libs/snackbars/src/index.ts"] } }, "exclude": ["node_modules", "tmp"] diff --git a/yarn.lock b/yarn.lock index 257148cebd..7e8d73367c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1176,7 +1176,7 @@ "@babel/types" "^7.4.4" esutils "^2.0.2" -"@babel/preset-react@^7.12.5", "@babel/preset-react@^7.16.0", "@babel/preset-react@^7.18.6": +"@babel/preset-react@^7.12.5", "@babel/preset-react@^7.14.5", "@babel/preset-react@^7.16.0", "@babel/preset-react@^7.18.6": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.22.5.tgz#c4d6058fbf80bccad02dd8c313a9aaa67e3c3dd6" integrity sha512-M+Is3WikOpEJHgR385HbuCITPTaPRaNkibTEa9oiofmJvIsrceb4yp9RL9Kb+TE8LznmeyZqpP+Lopwcx59xPQ== @@ -2571,6 +2571,40 @@ slash "^3.0.0" strip-ansi "^6.0.0" +"@jest/core@^29.6.2": + version "29.6.2" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-29.6.2.tgz#6f2d1dbe8aa0265fcd4fb8082ae1952f148209c8" + integrity sha512-Oj+5B+sDMiMWLhPFF+4/DvHOf+U10rgvCLGPHP8Xlsy/7QxS51aU/eBngudHlJXnaWD5EohAgJ4js+T6pa+zOg== + dependencies: + "@jest/console" "^29.6.2" + "@jest/reporters" "^29.6.2" + "@jest/test-result" "^29.6.2" + "@jest/transform" "^29.6.2" + "@jest/types" "^29.6.1" + "@types/node" "*" + ansi-escapes "^4.2.1" + chalk "^4.0.0" + ci-info "^3.2.0" + exit "^0.1.2" + graceful-fs "^4.2.9" + jest-changed-files "^29.5.0" + jest-config "^29.6.2" + jest-haste-map "^29.6.2" + jest-message-util "^29.6.2" + jest-regex-util "^29.4.3" + jest-resolve "^29.6.2" + jest-resolve-dependencies "^29.6.2" + jest-runner "^29.6.2" + jest-runtime "^29.6.2" + jest-snapshot "^29.6.2" + jest-util "^29.6.2" + jest-validate "^29.6.2" + jest-watcher "^29.6.2" + micromatch "^4.0.4" + pretty-format "^29.6.2" + slash "^3.0.0" + strip-ansi "^6.0.0" + "@jest/environment@^27.5.1": version "27.5.1" resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-27.5.1.tgz#d7425820511fe7158abbecc010140c3fd3be9c74" @@ -2680,7 +2714,7 @@ terminal-link "^2.0.0" v8-to-istanbul "^8.1.0" -"@jest/reporters@^29.4.1": +"@jest/reporters@^29.4.1", "@jest/reporters@^29.6.2": version "29.6.2" resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-29.6.2.tgz#524afe1d76da33d31309c2c4a2c8062d0c48780a" integrity sha512-sWtijrvIav8LgfJZlrGCdN0nP2EWbakglJY49J1Y5QihcQLfy7ovyxxjJBRXMNltgt4uPtEcFmIMbVshEDfFWw== @@ -3582,7 +3616,7 @@ jsonc-eslint-parser "^2.1.0" semver "7.5.3" -"@nx/jest@16.5.5", "@nx/jest@^16.4.0": +"@nx/jest@16.5.5": version "16.5.5" resolved "https://registry.yarnpkg.com/@nx/jest/-/jest-16.5.5.tgz#1781f51bd5f1ff2b2b5d86c10ed62fc36c52bec7" integrity sha512-oEPTOJf/bdzvMBETsNapqiOrRuyVblHrCuXmTjM1O2AaLeEYLyr+OeA0YlboxSqspJJ5uo4H1yQc8soYG+j2Mg== @@ -5872,6 +5906,15 @@ expect "^29.0.0" pretty-format "^29.0.0" +"@types/jsdom@^20.0.0": + version "20.0.1" + resolved "https://registry.yarnpkg.com/@types/jsdom/-/jsdom-20.0.1.tgz#07c14bc19bd2f918c1929541cdaacae894744808" + integrity sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ== + dependencies: + "@types/node" "*" + "@types/tough-cookie" "*" + parse5 "^7.0.0" + "@types/json-schema@*", "@types/json-schema@^7.0.12", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": version "7.0.12" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb" @@ -6216,6 +6259,11 @@ dependencies: csstype "^3.0.2" +"@types/tough-cookie@*": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.2.tgz#6286b4c7228d58ab7866d19716f3696e03a09397" + integrity sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw== + "@types/trusted-types@^2.0.2": version "2.0.3" resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.3.tgz#a136f83b0758698df454e328759dbd3d44555311" @@ -7591,6 +7639,14 @@ acorn-globals@^6.0.0: acorn "^7.1.1" acorn-walk "^7.1.1" +acorn-globals@^7.0.0: + version "7.0.1" + resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-7.0.1.tgz#0dbf05c44fa7c94332914c02066d5beff62c40c3" + integrity sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q== + dependencies: + acorn "^8.1.0" + acorn-walk "^8.0.2" + acorn-import-assertions@^1.9.0: version "1.9.0" resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz#507276249d684797c84e0734ef84860334cfb1ac" @@ -7606,7 +7662,7 @@ acorn-walk@^7.1.1: resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc" integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== -acorn-walk@^8.1.1, acorn-walk@^8.2.0: +acorn-walk@^8.0.2, acorn-walk@^8.1.1, acorn-walk@^8.2.0: version "8.2.0" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== @@ -7616,7 +7672,7 @@ acorn@^7.1.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== -acorn@^8.2.4, acorn@^8.4.1, acorn@^8.5.0, acorn@^8.7.1, acorn@^8.8.2, acorn@^8.9.0: +acorn@^8.1.0, acorn@^8.2.4, acorn@^8.4.1, acorn@^8.5.0, acorn@^8.7.1, acorn@^8.8.1, acorn@^8.8.2, acorn@^8.9.0: version "8.10.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== @@ -10219,6 +10275,11 @@ cssom@^0.4.4: resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10" integrity sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw== +cssom@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.5.0.tgz#d254fa92cd8b6fbd83811b9fbaed34663cc17c36" + integrity sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw== + cssom@~0.3.6: version "0.3.8" resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a" @@ -10586,6 +10647,15 @@ data-urls@^2.0.0: whatwg-mimetype "^2.3.0" whatwg-url "^8.0.0" +data-urls@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-3.0.2.tgz#9cf24a477ae22bcef5cd5f6f0bfbc1d2d3be9143" + integrity sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ== + dependencies: + abab "^2.0.6" + whatwg-mimetype "^3.0.0" + whatwg-url "^11.0.0" + data-urls@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-4.0.0.tgz#333a454eca6f9a5b7b0f1013ff89074c3f522dd4" @@ -10653,7 +10723,7 @@ decimal.js-light@^2.5.0: resolved "https://registry.yarnpkg.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz#134fd32508f19e208f4fb2f8dac0d2626a867934" integrity sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg== -decimal.js@^10.2.0, decimal.js@^10.2.1, decimal.js@^10.4.3: +decimal.js@^10.2.0, decimal.js@^10.2.1, decimal.js@^10.4.2, decimal.js@^10.4.3: version "10.4.3" resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23" integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA== @@ -14976,6 +15046,14 @@ jest-changed-files@^27.5.1: execa "^5.0.0" throat "^6.0.1" +jest-changed-files@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.5.0.tgz#e88786dca8bf2aa899ec4af7644e16d9dcf9b23e" + integrity sha512-IFG34IUMUaNBIxjQXF/iu7g6EcdMrGRRxaUSw92I/2g2YC6vCdTltl4nHvt7Ci5nSJwXIkCu8Ka1DKF+X7Z1Ag== + dependencies: + execa "^5.0.0" + p-limit "^3.1.0" + jest-circus@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-27.5.1.tgz#37a5a4459b7bf4406e53d637b49d22c65d125ecc" @@ -15045,6 +15123,24 @@ jest-cli@^27.5.1: prompts "^2.0.1" yargs "^16.2.0" +jest-cli@^29.6.2: + version "29.6.2" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-29.6.2.tgz#edb381763398d1a292cd1b636a98bfa5644b8fda" + integrity sha512-TT6O247v6dCEX2UGHGyflMpxhnrL0DNqP2fRTKYm3nJJpCTfXX3GCMQPGFjXDoj0i5/Blp3jriKXFgdfmbYB6Q== + dependencies: + "@jest/core" "^29.6.2" + "@jest/test-result" "^29.6.2" + "@jest/types" "^29.6.1" + chalk "^4.0.0" + exit "^0.1.2" + graceful-fs "^4.2.9" + import-local "^3.0.2" + jest-config "^29.6.2" + jest-util "^29.6.2" + jest-validate "^29.6.2" + prompts "^2.0.1" + yargs "^17.3.1" + jest-config@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-27.5.1.tgz#5c387de33dca3f99ad6357ddeccd91bf3a0e4a41" @@ -15075,7 +15171,7 @@ jest-config@^27.5.1: slash "^3.0.0" strip-json-comments "^3.1.1" -jest-config@^29.4.1: +jest-config@^29.4.1, jest-config@^29.6.2: version "29.6.2" resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-29.6.2.tgz#c68723f06b31ca5e63030686e604727d406cd7c3" integrity sha512-VxwFOC8gkiJbuodG9CPtMRjBUNZEHxwfQXmIudSTzFWxaci3Qub1ddTRbFNQlD/zUeaifLndh/eDccFX4wCMQw== @@ -15172,6 +15268,20 @@ jest-environment-jsdom@^27.5.1: jest-util "^27.5.1" jsdom "^16.6.0" +jest-environment-jsdom@^29.4.1: + version "29.6.2" + resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-29.6.2.tgz#4fc68836a7774a771819a2f980cb47af3b1629da" + integrity sha512-7oa/+266AAEgkzae8i1awNEfTfjwawWKLpiw2XesZmaoVVj9u9t8JOYx18cG29rbPNtkUlZ8V4b5Jb36y/VxoQ== + dependencies: + "@jest/environment" "^29.6.2" + "@jest/fake-timers" "^29.6.2" + "@jest/types" "^29.6.1" + "@types/jsdom" "^20.0.0" + "@types/node" "*" + jest-mock "^29.6.2" + jest-util "^29.6.2" + jsdom "^20.0.0" + jest-environment-node@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-27.5.1.tgz#dedc2cfe52fab6b8f5714b4808aefa85357a365e" @@ -15403,6 +15513,14 @@ jest-resolve-dependencies@^27.5.1: jest-regex-util "^27.5.1" jest-snapshot "^27.5.1" +jest-resolve-dependencies@^29.6.2: + version "29.6.2" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-29.6.2.tgz#36435269b6672c256bcc85fb384872c134cc4cf2" + integrity sha512-LGqjDWxg2fuQQm7ypDxduLu/m4+4Lb4gczc13v51VMZbVP5tSBILqVx8qfWcsdP8f0G7aIqByIALDB0R93yL+w== + dependencies: + jest-regex-util "^29.4.3" + jest-snapshot "^29.6.2" + jest-resolve@^27.4.2, jest-resolve@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-27.5.1.tgz#a2f1c5a0796ec18fe9eb1536ac3814c23617b384" @@ -15773,6 +15891,16 @@ jest@^27.4.3: import-local "^3.0.2" jest-cli "^27.5.1" +jest@^29.4.1: + version "29.6.2" + resolved "https://registry.yarnpkg.com/jest/-/jest-29.6.2.tgz#3bd55b9fd46a161b2edbdf5f1d1bd0d1eab76c42" + integrity sha512-8eQg2mqFbaP7CwfsTpCxQ+sHzw1WuNWL5UUvjnWP4hx2riGz9fPSzYOaU5q8/GqWn1TfgZIVTqYJygbGbWAANg== + dependencies: + "@jest/core" "^29.6.2" + "@jest/types" "^29.6.1" + import-local "^3.0.2" + jest-cli "^29.6.2" + jiti@^1.17.1, jiti@^1.18.2: version "1.19.1" resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.19.1.tgz#fa99e4b76a23053e0e7cde098efe1704a14c16f1" @@ -15871,6 +15999,38 @@ jsdom@^16.6.0: ws "^7.4.6" xml-name-validator "^3.0.0" +jsdom@^20.0.0: + version "20.0.3" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-20.0.3.tgz#886a41ba1d4726f67a8858028c99489fed6ad4db" + integrity sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ== + dependencies: + abab "^2.0.6" + acorn "^8.8.1" + acorn-globals "^7.0.0" + cssom "^0.5.0" + cssstyle "^2.3.0" + data-urls "^3.0.2" + decimal.js "^10.4.2" + domexception "^4.0.0" + escodegen "^2.0.0" + form-data "^4.0.0" + html-encoding-sniffer "^3.0.0" + http-proxy-agent "^5.0.0" + https-proxy-agent "^5.0.1" + is-potential-custom-element-name "^1.0.1" + nwsapi "^2.2.2" + parse5 "^7.1.1" + saxes "^6.0.0" + symbol-tree "^3.2.4" + tough-cookie "^4.1.2" + w3c-xmlserializer "^4.0.0" + webidl-conversions "^7.0.0" + whatwg-encoding "^2.0.0" + whatwg-mimetype "^3.0.0" + whatwg-url "^11.0.0" + ws "^8.11.0" + xml-name-validator "^4.0.0" + jsdom@~22.1.0: version "22.1.0" resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-22.1.0.tgz#0fca6d1a37fbeb7f4aac93d1090d782c56b611c8" @@ -17593,7 +17753,7 @@ number-to-bn@1.7.0: bn.js "4.11.6" strip-hex-prefix "1.0.0" -nwsapi@^2.2.0, nwsapi@^2.2.4: +nwsapi@^2.2.0, nwsapi@^2.2.2, nwsapi@^2.2.4: version "2.2.7" resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.7.tgz#738e0707d3128cb750dddcfe90e4610482df0f30" integrity sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ== @@ -18188,7 +18348,7 @@ parse5@6.0.1: resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== -parse5@^7.1.2: +parse5@^7.0.0, parse5@^7.1.1, parse5@^7.1.2: version "7.1.2" resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32" integrity sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw== @@ -22161,6 +22321,13 @@ tr46@^2.1.0: dependencies: punycode "^2.1.1" +tr46@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-3.0.0.tgz#555c4e297a950617e8eeddef633c87d4d9d6cbf9" + integrity sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA== + dependencies: + punycode "^2.1.1" + tr46@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/tr46/-/tr46-4.1.1.tgz#281a758dcc82aeb4fe38c7dfe4d11a395aac8469" @@ -23566,6 +23733,14 @@ whatwg-mimetype@^3.0.0: resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz#5fa1a7623867ff1af6ca3dc72ad6b8a4208beba7" integrity sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q== +whatwg-url@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-11.0.0.tgz#0a849eebb5faf2119b901bb76fd795c2848d4018" + integrity sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ== + dependencies: + tr46 "^3.0.0" + webidl-conversions "^7.0.0" + whatwg-url@^12.0.0, whatwg-url@^12.0.1: version "12.0.1" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-12.0.1.tgz#fd7bcc71192e7c3a2a97b9a8d6b094853ed8773c" @@ -24097,7 +24272,7 @@ ws@^3.0.0: safe-buffer "~5.1.0" ultron "~1.1.0" -ws@^8.13.0, ws@^8.5.0: +ws@^8.11.0, ws@^8.13.0, ws@^8.5.0: version "8.13.0" resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0" integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA== @@ -24294,7 +24469,7 @@ yargs@^16.2.0: y18n "^5.0.5" yargs-parser "^20.2.2" -yargs@^17.0.0, yargs@^17.6.2, yargs@^17.7.2: +yargs@^17.0.0, yargs@^17.3.1, yargs@^17.6.2, yargs@^17.7.2: version "17.7.2" resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==