From fef34d8369fb78da6308a431622623eaccffee5f Mon Sep 17 00:00:00 2001 From: Anastasios Date: Fri, 2 Aug 2024 09:40:57 +0400 Subject: [PATCH] feat: add handle network item menu, closes #5124 --- package.json | 8 +-- pnpm-lock.yaml | 64 ++++++++++++++----- .../features/add-network/add-network-form.tsx | 5 +- .../features/add-network/use-add-network.tsx | 47 +++++++++++++- .../components/network-list-item-menu.tsx | 58 +++++++++++++++++ .../components/network-list-item.layout.tsx | 48 +++++++------- .../settings/network/network-list-item.tsx | 3 + src/app/features/settings/network/network.tsx | 9 ++- src/app/features/settings/settings.tsx | 3 +- tests/mocks/mock-apis.ts | 4 +- tests/mocks/mock-utxos.ts | 6 +- tests/page-object-models/send.page.ts | 4 ++ tests/specs/send/send-stx.spec.ts | 4 ++ 13 files changed, 209 insertions(+), 54 deletions(-) create mode 100644 src/app/features/settings/network/components/network-list-item-menu.tsx diff --git a/package.json b/package.json index c7ad7ff641b..8765bfdea34 100644 --- a/package.json +++ b/package.json @@ -137,11 +137,11 @@ "@leather.io/bitcoin": "0.10.0", "@leather.io/constants": "0.8.2", "@leather.io/crypto": "1.1.0", - "@leather.io/models": "0.10.2", - "@leather.io/query": "2.1.0", + "@leather.io/models": "0.11.0", + "@leather.io/query": "2.2.0", "@leather.io/tokens": "0.7.0", "@leather.io/ui": "1.9.2", - "@leather.io/utils": "0.11.0", + "@leather.io/utils": "0.11.1", "@ledgerhq/hw-transport-webusb": "6.27.19", "@noble/hashes": "1.4.0", "@noble/secp256k1": "2.1.0", @@ -252,7 +252,7 @@ "@leather.io/eslint-config": "0.6.1", "@leather.io/panda-preset": "0.3.4", "@leather.io/prettier-config": "0.5.0", - "@leather.io/rpc": "2.1.1", + "@leather.io/rpc": "2.1.2", "@ls-lint/ls-lint": "2.2.3", "@mdx-js/loader": "3.0.0", "@pandacss/dev": "0.40.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3fc4591d12f..7751c61f318 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -43,11 +43,11 @@ importers: specifier: 1.1.0 version: 1.1.0 '@leather.io/models': - specifier: 0.10.2 - version: 0.10.2 + specifier: 0.11.0 + version: 0.11.0 '@leather.io/query': - specifier: 2.1.0 - version: 2.1.0(@stacks/network@6.13.0(encoding@0.1.13))(encoding@0.1.13)(react@18.3.1) + specifier: 2.2.0 + version: 2.2.0(@stacks/network@6.13.0(encoding@0.1.13))(encoding@0.1.13)(react@18.3.1) '@leather.io/tokens': specifier: 0.7.0 version: 0.7.0 @@ -55,8 +55,8 @@ importers: specifier: 1.9.2 version: 1.9.2(@babel/core@7.24.9)(@babel/preset-env@7.24.8(@babel/core@7.24.9))(@swc/core@1.7.0)(@types/react-dom@18.3.0)(@types/react@18.3.3)(encoding@0.1.13)(expo-modules-autolinking@1.11.1)(postcss@8.4.38)(ts-node@10.9.2(@swc/core@1.7.0)(@types/node@20.12.12)(typescript@5.4.5))(typescript@5.4.5) '@leather.io/utils': - specifier: 0.11.0 - version: 0.11.0 + specifier: 0.11.1 + version: 0.11.1 '@ledgerhq/hw-transport-webusb': specifier: 6.27.19 version: 6.27.19 @@ -383,8 +383,8 @@ importers: specifier: 0.5.0 version: 0.5.0(@vue/compiler-sfc@3.4.32) '@leather.io/rpc': - specifier: 2.1.1 - version: 2.1.1 + specifier: 2.1.2 + version: 2.1.2 '@ls-lint/ls-lint': specifier: 2.2.3 version: 2.2.3 @@ -2601,12 +2601,18 @@ packages: '@leather.io/bitcoin@0.10.0': resolution: {integrity: sha512-Po5MBZzOBCQ9cc27BXkwzpJ2hPUsD4rVtjHZ7jDX3a3Q/v3jGu1jBgyxTglhIpPU/M6ja+a3eXw0XfOW2oKFVQ==} + '@leather.io/bitcoin@0.10.1': + resolution: {integrity: sha512-rZdilqmVcsMYGG/H+bRT6DNvL65Ivme+W6Ihli+nBRw09gNcqB7z8CD+H1is0QFP84o6mxvKAUbZyucrIJB/Gg==} + '@leather.io/constants@0.8.2': resolution: {integrity: sha512-W4Q8e4H7scLlhnA2UFWFsnQYGu4NPGQmmIuq5N13wvOhzZD9iebrgN9I/VtE1GJwiQmBYdzhJ45EmEAM90zFhA==} '@leather.io/crypto@1.1.0': resolution: {integrity: sha512-iI5skLZN745rdeqishDnoMKxtVdT4rCrHUVox4TOywhqTzCw1aOmMpXXkVIen7fNnVrIjuDP0Ek4LBgzhIiDrw==} + '@leather.io/crypto@1.1.1': + resolution: {integrity: sha512-5hAJk4tdwvHBcUEC6loozFmLnuaxYumcR8eyvS5jZrC8CMSWsCKvk0OHMrAI30AuO4BRhiOR+nQfVK3ws4JGmg==} + '@leather.io/eslint-config@0.6.1': resolution: {integrity: sha512-NLvT7wpDR02jFZdp9g08ja5mRxdz/xCcB/tmXWJL8uDt2l+ebjHqaq45dZroAPm+EciLd0HU3jFfsSA7Txgh9w==} @@ -2622,8 +2628,8 @@ packages: '@leather.io/prettier-config@0.5.0': resolution: {integrity: sha512-Pul+4MAyBKnQvqgcKJLbZl4DHnS4kCJzSuaYFW6cfHdre7BFn/iY6Er/Dvm9F8g7VMtkdYu68jEYxQ1Xc7A0KQ==} - '@leather.io/query@2.1.0': - resolution: {integrity: sha512-iMM47shEtq4dVx/MlwQInv+oJk9s9dAXDiO3j56QT3HBnOzguStrpgEO5eXvsnASx6xQwguh5xheA4S/DBwhYg==} + '@leather.io/query@2.2.0': + resolution: {integrity: sha512-1JkMXzQcSQNmpXKvSlhBqksFy2lwlZAD+xPsFEoywrgpHm2WDkvLSd7XUuwmB8+VAi9OLqb2Ad+hG3bHUWj0hg==} peerDependencies: react: '*' @@ -17235,6 +17241,28 @@ snapshots: transitivePeerDependencies: - encoding + '@leather.io/bitcoin@0.10.1(encoding@0.1.13)': + dependencies: + '@bitcoinerlab/secp256k1': 1.0.2 + '@leather.io/constants': 0.8.2 + '@leather.io/crypto': 1.1.1 + '@leather.io/models': 0.11.0 + '@leather.io/utils': 0.11.1 + '@noble/hashes': 1.4.0 + '@noble/secp256k1': 2.1.0 + '@scure/base': 1.1.6 + '@scure/bip32': 1.4.0 + '@scure/bip39': 1.3.0 + '@scure/btc-signer': 1.3.2 + '@stacks/common': 6.13.0 + '@stacks/transactions': 6.15.0(encoding@0.1.13) + bip32: 4.0.0 + bitcoinjs-lib: 6.1.5 + ecpair: 2.1.0 + varuint-bitcoin: 1.1.2 + transitivePeerDependencies: + - encoding + '@leather.io/constants@0.8.2': {} '@leather.io/crypto@1.1.0': @@ -17243,6 +17271,12 @@ snapshots: '@scure/bip32': 1.4.0 '@scure/bip39': 1.3.0 + '@leather.io/crypto@1.1.1': + dependencies: + '@leather.io/utils': 0.11.1 + '@scure/bip32': 1.4.0 + '@scure/bip39': 1.3.0 + '@leather.io/eslint-config@0.6.1(typescript@5.4.5)': dependencies: '@typescript-eslint/eslint-plugin': 6.9.0(@typescript-eslint/parser@6.9.0(eslint@8.56.0)(typescript@5.4.5))(eslint@8.56.0)(typescript@5.4.5) @@ -17279,15 +17313,15 @@ snapshots: - '@vue/compiler-sfc' - supports-color - '@leather.io/query@2.1.0(@stacks/network@6.13.0(encoding@0.1.13))(encoding@0.1.13)(react@18.3.1)': + '@leather.io/query@2.2.0(@stacks/network@6.13.0(encoding@0.1.13))(encoding@0.1.13)(react@18.3.1)': dependencies: '@fungible-systems/zone-file': 2.0.0 '@hirosystems/token-metadata-api-client': 1.2.0(encoding@0.1.13) - '@leather.io/bitcoin': 0.10.0(encoding@0.1.13) + '@leather.io/bitcoin': 0.10.1(encoding@0.1.13) '@leather.io/constants': 0.8.2 - '@leather.io/models': 0.10.2 - '@leather.io/rpc': 2.1.1 - '@leather.io/utils': 0.11.0 + '@leather.io/models': 0.11.0 + '@leather.io/rpc': 2.1.2 + '@leather.io/utils': 0.11.1 '@noble/hashes': 1.4.0 '@scure/base': 1.1.6 '@scure/bip32': 1.4.0 diff --git a/src/app/features/add-network/add-network-form.tsx b/src/app/features/add-network/add-network-form.tsx index 0372ea738e4..913831f1cef 100644 --- a/src/app/features/add-network/add-network-form.tsx +++ b/src/app/features/add-network/add-network-form.tsx @@ -39,7 +39,8 @@ const networks: { ]; export function AddNetworkForm() { - const { handleChange, setFieldValue, values } = useFormikContext(); + const { handleChange, setFieldValue, values, initialValues } = + useFormikContext(); const setStacksUrl = useCallback( (value: string) => { @@ -92,7 +93,7 @@ export function AddNetworkForm() { Bitcoin API { void setFieldValue('bitcoinNetwork', value); }} diff --git a/src/app/features/add-network/use-add-network.tsx b/src/app/features/add-network/use-add-network.tsx index e0aba75008f..0d20529ab81 100644 --- a/src/app/features/add-network/use-add-network.tsx +++ b/src/app/features/add-network/use-add-network.tsx @@ -1,9 +1,14 @@ import { useState } from 'react'; -import { useNavigate } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router-dom'; import { ChainID } from '@stacks/transactions'; -import type { BitcoinNetworkModes, DefaultNetworkConfigurations } from '@leather.io/models'; +import { + type BitcoinNetworkModes, + type DefaultNetworkConfigurations, + type NetworkConfiguration, + networkConfigurationSchema, +} from '@leather.io/models'; import { RouteUrls } from '@shared/route-urls'; import { isValidUrl } from '@shared/utils/validate-url'; @@ -39,16 +44,45 @@ const initialFormValues: AddNetworkFormValues = { bitcoinNetwork: 'mainnet', }; +function useInitialValues() { + const { state } = useLocation(); + + if (!state) { + return initialFormValues; + } + + const network = state.network as NetworkConfiguration | undefined; + + if (!network) { + return initialFormValues; + } + + const isProperStateProvided = networkConfigurationSchema.safeParse(network).success; + + if (!isProperStateProvided) { + return initialFormValues; + } + + return { + key: network.id, + name: network.name, + stacksUrl: network.chain.stacks.url, + bitcoinUrl: network.chain.bitcoin.bitcoinUrl, + bitcoinNetwork: network.chain.bitcoin.bitcoinNetwork, + }; +} + export function useAddNetwork() { const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const navigate = useNavigate(); const network = useCurrentStacksNetworkState(); const networksActions = useNetworksActions(); + const initialValues = useInitialValues(); return { error, - initialFormValues, + initialFormValues: initialValues, loading, onSubmit: async (values: AddNetworkFormValues) => { const { name, stacksUrl, bitcoinUrl, key, bitcoinNetwork } = values; @@ -113,10 +147,16 @@ export function useAddNetwork() { isSubnet && (parentNetworkId === PeerNetworkID.Mainnet || parentNetworkId === PeerNetworkID.Testnet); + function removeEditedNetwork() { + if (initialValues.key) { + networksActions.removeNetwork(initialValues.key); + } + } // Currently, only subnets of mainnet and testnet are supported in the wallet if (isFirstLevelSubnet) { const parentChainId = parentNetworkId === PeerNetworkID.Mainnet ? ChainID.Mainnet : ChainID.Testnet; + removeEditedNetwork(); networksActions.addNetwork({ id: key as DefaultNetworkConfigurations, name: name, @@ -128,6 +168,7 @@ export function useAddNetwork() { }); navigate(RouteUrls.Home); } else if (chainId === ChainID.Mainnet || chainId === ChainID.Testnet) { + removeEditedNetwork(); networksActions.addNetwork({ id: key as DefaultNetworkConfigurations, name: name, diff --git a/src/app/features/settings/network/components/network-list-item-menu.tsx b/src/app/features/settings/network/components/network-list-item-menu.tsx new file mode 100644 index 00000000000..f5f1c74ed1e --- /dev/null +++ b/src/app/features/settings/network/components/network-list-item-menu.tsx @@ -0,0 +1,58 @@ +import { SettingsSelectors } from '@tests/selectors/settings.selectors'; +import { css } from 'leather-styles/css'; +import { HStack, styled } from 'leather-styles/jsx'; + +import { DotsVerticalIcon, DropdownMenu, PenIcon, TrashIcon } from '@leather.io/ui'; + +interface Props { + onEditNetwork(): void; + onClickDeleteNetwork(): void; +} + +export function NetworkItemMenu({ onClickDeleteNetwork, onEditNetwork }: Props) { + return ( + + + + + + + + { + e.stopPropagation(); + onEditNetwork(); + }} + > + + + Edit + + + { + e.stopPropagation(); + onClickDeleteNetwork(); + }} + > + + + Delete + + + + + + + ); +} diff --git a/src/app/features/settings/network/components/network-list-item.layout.tsx b/src/app/features/settings/network/components/network-list-item.layout.tsx index b0b4afef823..b0aea3769be 100644 --- a/src/app/features/settings/network/components/network-list-item.layout.tsx +++ b/src/app/features/settings/network/components/network-list-item.layout.tsx @@ -1,11 +1,13 @@ import { NetworkSelectors } from '@tests/selectors/network.selectors'; import { SettingsSelectors } from '@tests/selectors/settings.selectors'; -import { Flex, Stack, styled } from 'leather-styles/jsx'; +import { Flex, HStack, Stack, styled } from 'leather-styles/jsx'; import type { NetworkConfiguration } from '@leather.io/models'; -import { Button, CheckmarkIcon, CloudOffIcon, TrashIcon } from '@leather.io/ui'; +import { Button, CheckmarkIcon, CloudOffIcon } from '@leather.io/ui'; -import { getUrlHostname } from '@app/common/utils'; +import { getUrlHostname, truncateString } from '@app/common/utils'; + +import { NetworkItemMenu } from './network-list-item-menu'; interface NetworkListItemLayoutProps { networkId: string; @@ -14,8 +16,10 @@ interface NetworkListItemLayoutProps { isCustom: boolean; network: NetworkConfiguration; onSelectNetwork(): void; + onEditNetwork(): void; onRemoveNetwork(id: string): void; } + export function NetworkListItemLayout({ networkId, isOnline, @@ -24,6 +28,7 @@ export function NetworkListItemLayout({ isCustom, onRemoveNetwork, onSelectNetwork, + onEditNetwork, }: NetworkListItemLayoutProps) { const unselectable = !isOnline || isActive; return ( @@ -49,31 +54,28 @@ export function NetworkListItemLayout({ > - - {network.name} - + + {truncateString(network.name, 20)} + {isActive && ( + + )} + + {getUrlHostname(network.chain.stacks.url)} - {!isOnline ? ( - - ) : isActive ? ( - - ) : null} + {!isOnline ? : null} + {isOnline && isCustom && ( + onRemoveNetwork(network.id)} + onEditNetwork={onEditNetwork} + /> + )} - {isCustom && ( - - )} ); diff --git a/src/app/features/settings/network/network-list-item.tsx b/src/app/features/settings/network/network-list-item.tsx index 6134f12b195..3a399d1a4de 100644 --- a/src/app/features/settings/network/network-list-item.tsx +++ b/src/app/features/settings/network/network-list-item.tsx @@ -10,12 +10,14 @@ interface NetworkListItemProps { isCustom: boolean; onNetworkSelected(networkId: string): void; onRemoveNetwork(networkId: string): void; + onEditNetwork(): void; } export function NetworkListItem({ networkId, onNetworkSelected, onRemoveNetwork, isCustom, + onEditNetwork, }: NetworkListItemProps) { const currentNetworkId = useCurrentNetworkId(); const networks = useNetworks(); @@ -31,6 +33,7 @@ export function NetworkListItem({ networkId={networkId} isCustom={isCustom} onSelectNetwork={() => onNetworkSelected(networkId)} + onEditNetwork={onEditNetwork} onRemoveNetwork={onRemoveNetwork} /> ); diff --git a/src/app/features/settings/network/network.tsx b/src/app/features/settings/network/network.tsx index 30ecbfa1c15..7a3ff0fa32b 100644 --- a/src/app/features/settings/network/network.tsx +++ b/src/app/features/settings/network/network.tsx @@ -22,7 +22,6 @@ interface NetworkDialogProps { export function NetworkDialog({ onClose }: NetworkDialogProps) { const navigate = useNavigate(); const networks = useNetworks(); - const networksActions = useNetworksActions(); const currentNetwork = useCurrentNetworkState(); @@ -74,6 +73,14 @@ export function NetworkDialog({ onClose }: NetworkDialogProps) { if (id === currentNetwork.id) networksActions.changeNetwork('mainnet'); removeNetwork(id); }} + onEditNetwork={() => { + onClose(); + navigate(RouteUrls.AddNetwork, { + state: { + network: networks[id], + }, + }); + }} /> ))} diff --git a/src/app/features/settings/settings.tsx b/src/app/features/settings/settings.tsx index 84f8a3e345b..8b99dbb45cc 100644 --- a/src/app/features/settings/settings.tsx +++ b/src/app/features/settings/settings.tsx @@ -27,6 +27,7 @@ import { analytics } from '@shared/utils/analytics'; import { useKeyActions } from '@app/common/hooks/use-key-actions'; import { useModifierKey } from '@app/common/hooks/use-modifier-key'; import { useWalletType } from '@app/common/use-wallet-type'; +import { truncateString } from '@app/common/utils'; import { openInNewTab, openIndexPageInNewTab } from '@app/common/utils/open-in-new-tab'; import { AppVersion } from '@app/components/app-version'; import { Divider } from '@app/components/layout/divider'; @@ -130,7 +131,7 @@ export function Settings({ triggerButton, toggleSwitchAccount }: SettingsProps) Change network - {currentNetworkId} + {truncateString(currentNetworkId.toString(), 15)} diff --git a/tests/mocks/mock-apis.ts b/tests/mocks/mock-apis.ts index 8954790995a..26ced78e51f 100644 --- a/tests/mocks/mock-apis.ts +++ b/tests/mocks/mock-apis.ts @@ -2,7 +2,7 @@ import { Page } from '@playwright/test'; import { json } from '@tests/utils'; import { mockStacksFeeRequests } from './mock-stacks-fees'; -import { mockMainnetTestAccountBlockstreamRequests } from './mock-utxos'; +import { mockMainnetTestAccountBitcoinRequests } from './mock-utxos'; export async function setupMockApis(page: Page) { await Promise.all([ @@ -10,7 +10,7 @@ export async function setupMockApis(page: Page) { page.route(/github/, route => route.fulfill(json({}))), page.route('https://api.hiro.so/', route => route.fulfill()), page.route('https://api.testnet.hiro.so/', route => route.fulfill()), - mockMainnetTestAccountBlockstreamRequests(page), + mockMainnetTestAccountBitcoinRequests(page), mockStacksFeeRequests(page), ]); } diff --git a/tests/mocks/mock-utxos.ts b/tests/mocks/mock-utxos.ts index a74a8d3361f..afbe5bafac0 100644 --- a/tests/mocks/mock-utxos.ts +++ b/tests/mocks/mock-utxos.ts @@ -113,15 +113,15 @@ const mockMainnetNsTransactionsTestAccount = [ }, ]; -export async function mockMainnetTestAccountBlockstreamRequests(page: Page) { +export async function mockMainnetTestAccountBitcoinRequests(page: Page) { await Promise.all([ - page.route('**/blockstream.info/api/address/**/utxo', route => + page.route('**/leather.mempool.space/api/address/**/utxo', route => route.fulfill({ json: [], }) ), page.route( - `**/blockstream.info/api/address/${TEST_ACCOUNT_1_NATIVE_SEGWIT_ADDRESS}/txs`, + `**/leather.mempool.space/api/address/${TEST_ACCOUNT_1_NATIVE_SEGWIT_ADDRESS}/txs`, route => route.fulfill({ json: mockMainnetNsTransactionsTestAccount, diff --git a/tests/page-object-models/send.page.ts b/tests/page-object-models/send.page.ts index d89e993482e..70f05055e91 100644 --- a/tests/page-object-models/send.page.ts +++ b/tests/page-object-models/send.page.ts @@ -105,4 +105,8 @@ export class SendPage { async clickInfoCardButton() { await this.infoCardButton.click(); } + + async waitForFeeRow() { + await this.page.getByTestId(SharedComponentsSelectors.FeeRow).waitFor({ state: 'attached' }); + } } diff --git a/tests/specs/send/send-stx.spec.ts b/tests/specs/send/send-stx.spec.ts index 79cb1911fcd..cc83ccc146d 100644 --- a/tests/specs/send/send-stx.spec.ts +++ b/tests/specs/send/send-stx.spec.ts @@ -110,6 +110,10 @@ test.describe('send stx: tests on testnet', () => { }); test.describe('send form validation', () => { + test.beforeEach(async ({ sendPage }) => { + await sendPage.waitForFeeRow(); + }); + test('that the amount must be a number', async ({ sendPage }) => { await sendPage.amountInput.fill('aaaaaa'); await sendPage.amountInput.blur();