From 2d0a944097cda50f226e7644c7ea017182e69618 Mon Sep 17 00:00:00 2001 From: Aleka Cheung Date: Mon, 21 Aug 2023 17:17:48 -0400 Subject: [PATCH 1/3] transfers history table --- src/constants/abacus.ts | 2 + src/constants/localization/app.ts | 2 + src/lib/abacus/stateNotification.ts | 6 + src/localization/en/app.json | 2 + src/pages/portfolio/History.tsx | 11 +- src/pages/portfolio/Portfolio.tsx | 8 +- src/pages/portfolio/PortfolioNavMobile.tsx | 12 +- src/state/account.ts | 7 + src/state/accountSelectors.ts | 6 + src/styles/tradeViewMixins.ts | 4 + src/views/tables/TransferHistoryTable.tsx | 184 +++++++++++++++++++++ 11 files changed, 230 insertions(+), 14 deletions(-) create mode 100644 src/views/tables/TransferHistoryTable.tsx diff --git a/src/constants/abacus.ts b/src/constants/abacus.ts index 55c0ad7..551274c 100644 --- a/src/constants/abacus.ts +++ b/src/constants/abacus.ts @@ -112,6 +112,8 @@ export type SubaccountFill = Abacus.exchange.dydx.abacus.output.SubaccountFill; export type SubaccountFundingPayment = Abacus.exchange.dydx.abacus.output.SubaccountFundingPayment; export type SubaccountFundingPayments = Abacus.exchange.dydx.abacus.output.SubaccountFundingPayment[]; +export type SubaccountTransfer = Abacus.exchange.dydx.abacus.output.SubaccountTransfer; +export type SubaccountTransfers = Abacus.exchange.dydx.abacus.output.SubaccountTransfer[]; // ------ Historical PnL ------ // export type SubAccountHistoricalPNL = Abacus.exchange.dydx.abacus.output.SubaccountHistoricalPNL; diff --git a/src/constants/localization/app.ts b/src/constants/localization/app.ts index 93914c9..a392340 100644 --- a/src/constants/localization/app.ts +++ b/src/constants/localization/app.ts @@ -177,6 +177,7 @@ export const APP_STRING_KEYS = { RECEIVE: 'GENERAL.RECEIVE', RECENT: 'GENERAL.RECENT', RECENT_TRADES_SHORT: 'GENERAL.RECENT_TRADES_SHORT', + RECIPIENT: 'GENERAL.RECIPIENT', REFERRAL_CODE: 'GENERAL.REFERRAL_CODE', REFERRALS: 'GENERAL.REFERRALS', REFERRER_PERCENT_OFF: 'GENERAL.REFERRER_PERCENT_OFF', @@ -188,6 +189,7 @@ export const APP_STRING_KEYS = { SELECT_NETWORK: 'GENERAL.SELECT_NETWORK', SELL: 'GENERAL.SELL', SEND: 'GENERAL.SEND', + SENDER: 'GENERAL.SENDER', SHARE: 'GENERAL.SHARE', SHORT_POSITION_SHORT: 'GENERAL.SHORT_POSITION_SHORT', SIDE: 'GENERAL.SIDE', diff --git a/src/lib/abacus/stateNotification.ts b/src/lib/abacus/stateNotification.ts index ef08749..fbadc75 100644 --- a/src/lib/abacus/stateNotification.ts +++ b/src/lib/abacus/stateNotification.ts @@ -19,6 +19,7 @@ import { setFundingPayments, setHistoricalPnl, setSubaccount, + setTransfers, setWallet, } from '@/state/account'; @@ -102,6 +103,11 @@ class AbacusStateNotifier implements AbacusStateNotificationProtocol { dispatch(setFundingPayments(fundingPayments)); } + if (changes.has(Changes.transfers)) { + const transfers = updatedState.subaccountTransfers(subaccountId)?.toArray() || []; + dispatch(setTransfers(transfers)); + } + if (changes.has(Changes.historicalPnl)) { const historicalPnl = updatedState.subaccountHistoricalPnl(subaccountId)?.toArray() || []; diff --git a/src/localization/en/app.json b/src/localization/en/app.json index 41a0027..a492d6b 100644 --- a/src/localization/en/app.json +++ b/src/localization/en/app.json @@ -182,6 +182,7 @@ "RECEIVE": "Receive", "RECENT": "Recent", "RECENT_TRADES_SHORT": "Trades", + "RECIPIENT": "Recipient", "REFERRAL_CODE": "Referral Code", "REFERRALS": "Referrals", "REFERRER_PERCENT_OFF": "{DISCOUNT}% off", @@ -193,6 +194,7 @@ "SELECT_NETWORK": "Select Network", "SELL": "Sell", "SEND": "Send", + "SENDER": "Sender", "SHARE": "Share", "SHORT_POSITION_SHORT": "Short", "SIDE": "Side", diff --git a/src/pages/portfolio/History.tsx b/src/pages/portfolio/History.tsx index 72fffa2..78f85aa 100644 --- a/src/pages/portfolio/History.tsx +++ b/src/pages/portfolio/History.tsx @@ -28,13 +28,14 @@ export const History = () => { label:

{stringGetter({ key: STRING_KEYS.TRADES })}

, href: HistoryRoute.Trades, }, + { + value: HistoryRoute.Transfers, + label:

{stringGetter({ key: STRING_KEYS.TRANSFERS })}

, + href: HistoryRoute.Transfers, + tag: 'USDC', + }, // TODO - TRCL-1693 - // { - // value: HistoryRoute.Transfers, - // label:

{stringGetter({ key: STRING_KEYS.TRANSFERS })}

, - // href: HistoryRoute.Transfers, - // }, - // { // value: HistoryRoute.Payments, // label:

{stringGetter({ key: STRING_KEYS.PAYMENTS })}

, // href: HistoryRoute.Payments, diff --git a/src/pages/portfolio/Portfolio.tsx b/src/pages/portfolio/Portfolio.tsx index 227af41..d8969b8 100644 --- a/src/pages/portfolio/Portfolio.tsx +++ b/src/pages/portfolio/Portfolio.tsx @@ -9,6 +9,7 @@ import { useBreakpoints, useDocumentTitle, useStringGetter } from '@/hooks'; import { FillsTable, FillsTableColumnKey } from '@/views/tables/FillsTable'; import { FundingPaymentsTable } from '@/views/tables/FundingPaymentsTable'; +import { TransferHistoryTable } from '@/views/tables/TransferHistoryTable'; import { Icon, IconName } from '@/components/Icon'; import { NavigationMenu } from '@/components/NavigationMenu'; import { WithSidebar } from '@/components/WithSidebar'; @@ -61,7 +62,10 @@ export default () => { /> } /> - } /> + } + /> } @@ -118,8 +122,6 @@ export default () => { }, ], }, - // TODO(aforaleka) Add back subitems when there are clearer designs - // or when transfers and payments are ready ]} /> ) diff --git a/src/pages/portfolio/PortfolioNavMobile.tsx b/src/pages/portfolio/PortfolioNavMobile.tsx index ebfaf5a..0ce1a65 100644 --- a/src/pages/portfolio/PortfolioNavMobile.tsx +++ b/src/pages/portfolio/PortfolioNavMobile.tsx @@ -40,12 +40,12 @@ export const PortfolioNavMobile = () => { label: stringGetter({ key: STRING_KEYS.TRADES }), description: stringGetter({ key: STRING_KEYS.TRADES_DESCRIPTION }), }, - // TODO: TRCL-1693 - re-enable when Payments and Transfers are ready - // { - // value: `${AppRoute.Portfolio}/${PortfolioRoute.History}/${HistoryRoute.Transfers}`, - // label: stringGetter({ key: STRING_KEYS.TRANSFERS }), - // description: stringGetter({ key: STRING_KEYS.TRANSFERS_DESCRIPTION }), - // }, + { + value: `${AppRoute.Portfolio}/${PortfolioRoute.History}/${HistoryRoute.Transfers}`, + label: stringGetter({ key: STRING_KEYS.TRANSFERS }), + description: stringGetter({ key: STRING_KEYS.TRANSFERS_DESCRIPTION }), + }, + // TODO: TRCL-1693 - re-enable when Payments are ready // { // value: `${AppRoute.Portfolio}/${PortfolioRoute.History}/${HistoryRoute.Payments}`, // label: stringGetter({ key: STRING_KEYS.PAYMENTS }), diff --git a/src/state/account.ts b/src/state/account.ts index b722363..5fab6e3 100644 --- a/src/state/account.ts +++ b/src/state/account.ts @@ -9,6 +9,7 @@ import type { SubaccountFundingPayments, Wallet, SubaccountOrder, + SubaccountTransfers, HistoricalPnlPeriods, SubAccountHistoricalPNLs, } from '@/constants/abacus'; @@ -22,6 +23,7 @@ import { getLocalStorage } from '@/lib/localStorage'; export type AccountState = { fills?: SubaccountFills; fundingPayments?: SubaccountFundingPayments; + transfers?: SubaccountTransfers; clearedOrderIds?: string[]; uncommittedOrderClientIds?: number[]; hasUnseenFillUpdates: boolean; @@ -38,6 +40,7 @@ export type AccountState = { const initialState: AccountState = { fills: undefined, fundingPayments: undefined, + transfers: undefined, clearedOrderIds: undefined, uncommittedOrderClientIds: undefined, hasUnseenFillUpdates: false, @@ -81,6 +84,9 @@ export const accountSlice = createSlice({ setFundingPayments: (state, action: PayloadAction) => { state.fundingPayments = action.payload; }, + setTransfers: (state, action: PayloadAction) => { + state.transfers = action.payload; + }, clearOrder: (state, action: PayloadAction) => ({ ...state, clearedOrderIds: [...(state.clearedOrderIds || []), action.payload], @@ -148,6 +154,7 @@ export const accountSlice = createSlice({ export const { setFills, setFundingPayments, + setTransfers, clearOrder, setOnboardingGuard, setOnboardingState, diff --git a/src/state/accountSelectors.ts b/src/state/accountSelectors.ts index c16f039..29aa72e 100644 --- a/src/state/accountSelectors.ts +++ b/src/state/accountSelectors.ts @@ -193,6 +193,12 @@ export const getCurrentMarketFills = createSelector( !currentMarketId ? [] : marketFills[currentMarketId] ); +/** + * @param state + * @returns list of transfers for the currently connected subaccount + */ +export const getSubaccountTransfers = (state: RootState) => state.account?.transfers; + /** * @param state * @returns list of funding payments for the currently connected subaccount diff --git a/src/styles/tradeViewMixins.ts b/src/styles/tradeViewMixins.ts index fa64498..a847b81 100644 --- a/src/styles/tradeViewMixins.ts +++ b/src/styles/tradeViewMixins.ts @@ -18,5 +18,9 @@ export const tradeViewMixins: Record< tbody { font: var(--font-small-book); } + + thead tr { + box-shadow: none; + } `, }; diff --git a/src/views/tables/TransferHistoryTable.tsx b/src/views/tables/TransferHistoryTable.tsx new file mode 100644 index 0000000..f5df2a9 --- /dev/null +++ b/src/views/tables/TransferHistoryTable.tsx @@ -0,0 +1,184 @@ +import styled, { type AnyStyledComponent, css } from 'styled-components'; +import { shallowEqual, useDispatch, useSelector } from 'react-redux'; +import type { ColumnSize } from '@react-types/table'; + +import { type SubaccountTransfer } from '@/constants/abacus'; +import { STRING_KEYS, StringGetterFunction } from '@/constants/localization'; + +import { useBreakpoints, useStringGetter } from '@/hooks'; + +import { layoutMixins } from '@/styles/layoutMixins'; +import { tradeViewMixins } from '@/styles/tradeViewMixins'; + +import { Icon } from '@/components/Icon'; +import { Output, OutputType } from '@/components/Output'; +import { Table, TableCell, TableColumnHeader, type ColumnDef } from '@/components/Table'; + +import { getSubaccountTransfers } from '@/state/accountSelectors'; + +import { truncateAddress } from '@/lib/wallet'; +import { Button } from '@/components/Button'; +import { ButtonAction } from '@/constants/buttons'; +import { openDialog } from '@/state/dialogs'; +import { DialogTypes } from '@/constants/dialogs'; +import { Link } from '@/components/Link'; +import { calculateCanAccountTrade } from '@/state/accountCalculators'; +import { OnboardingTriggerButton } from '../dialogs/OnboardingTriggerButton'; + +const MOBILE_TRANSFERS_PER_PAGE = 50; + +export enum TransferHistoryTableColumnKey { + Time = 'Time', + Action = 'Action', + SenderRecipient = 'Sender-Recipient', + Amount = 'Amount', + TxHash = 'TxHash', +} + +const getTransferHistoryTableColumnDef = ({ + key, + stringGetter, + width, +}: { + key: TransferHistoryTableColumnKey; + isTablet?: boolean; + stringGetter: StringGetterFunction; + width?: ColumnSize; +}): ColumnDef => ({ + width, + ...( + { + [TransferHistoryTableColumnKey.Time]: { + columnKey: TransferHistoryTableColumnKey.Time, + getCellValue: (row) => row.updatedAtMilliseconds, + label: stringGetter({ key: STRING_KEYS.TIME }), + renderCell: ({ updatedAtMilliseconds }) => ( + + ), + }, + [TransferHistoryTableColumnKey.Action]: { + columnKey: TransferHistoryTableColumnKey.Action, + getCellValue: (row) => row.resources.typeStringKey, + label: stringGetter({ key: STRING_KEYS.ACTION }), + renderCell: ({ resources }) => + resources.typeStringKey && stringGetter({ key: resources.typeStringKey }), + }, + [TransferHistoryTableColumnKey.SenderRecipient]: { + columnKey: TransferHistoryTableColumnKey.SenderRecipient, + getCellValue: (row) => `${row.fromAddress}-${row.toAddress}`, + label: ( + + {stringGetter({ key: STRING_KEYS.SENDER })} + {stringGetter({ key: STRING_KEYS.RECIPIENT })} + + ), + renderCell: ({ fromAddress, toAddress }) => ( + + {fromAddress ? truncateAddress(fromAddress) : '-'} + {toAddress ? truncateAddress(toAddress) : '-'} + + ), + }, + [TransferHistoryTableColumnKey.Amount]: { + columnKey: TransferHistoryTableColumnKey.Amount, + getCellValue: (row) => row.amount, + label: stringGetter({ key: STRING_KEYS.AMOUNT }), + renderCell: ({ amount }) => , + }, + [TransferHistoryTableColumnKey.TxHash]: { + columnKey: TransferHistoryTableColumnKey.TxHash, + getCellValue: (row) => row.transactionHash, + label: stringGetter({ key: STRING_KEYS.TRANSACTION }), + renderCell: ({ transactionHash }) => + transactionHash ? {transactionHash} : '-', + }, + } as Record> + )[key], +}); + +type ElementProps = { + columnKeys?: TransferHistoryTableColumnKey[]; + columnWidths?: Partial>; +}; + +type StyleProps = { + withOuterBorder?: boolean; + withInnerBorders?: boolean; +}; + +export const TransferHistoryTable = ({ + columnKeys = Object.values(TransferHistoryTableColumnKey), + columnWidths, + withOuterBorder, + withInnerBorders = true, +}: ElementProps & StyleProps) => { + const stringGetter = useStringGetter(); + const dispatch = useDispatch(); + const { isMobile, isTablet } = useBreakpoints(); + + const canAccountTrade = useSelector(calculateCanAccountTrade, shallowEqual); + + const transfers = useSelector(getSubaccountTransfers, shallowEqual) ?? []; + + return ( + row.id} + columns={columnKeys.map((key: TransferHistoryTableColumnKey) => + getTransferHistoryTableColumnDef({ + key, + isTablet, + stringGetter, + width: columnWidths?.[key], + }) + )} + slotEmpty={ + <> + {stringGetter({ key: STRING_KEYS.TRANSFERS_EMPTY_STATE })} + {canAccountTrade ? ( + + ) : ( + + )} + + } + selectionBehavior="replace" + withOuterBorder={withOuterBorder} + withInnerBorders={withInnerBorders} + withScrollSnapColumns + withScrollSnapRows + /> + ); +}; + +const Styled: Record = {}; + +Styled.Table = styled(Table)` + ${tradeViewMixins.horizontalTable} +`; + +Styled.InlineRow = styled.div` + ${layoutMixins.inlineRow} +`; + +Styled.Icon = styled(Icon)` + font-size: 3em; +`; + +Styled.TimeOutput = styled(Output)` + color: var(--color-text-0); +`; + +Styled.TxHash = styled(Link)` + justify-content: flex-end; +`; From e30940e7dfdd46d8b17d419a0b007ee2ae9533d7 Mon Sep 17 00:00:00 2001 From: Aleka Cheung Date: Wed, 23 Aug 2023 17:53:47 -0400 Subject: [PATCH 2/3] bump abacus + make addresses copyable --- package.json | 2 +- pnpm-lock.yaml | 22 +++++----- src/views/tables/TransferHistoryTable.tsx | 53 ++++++++++++++++++++--- 3 files changed, 60 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 88420c0..c200637 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "@cosmjs/encoding": "^0.31.0", "@cosmjs/proto-signing": "^0.31.0", "@cosmjs/stargate": "^0.31.0", - "@dydxprotocol/abacus": "^0.2.28", + "@dydxprotocol/abacus": "^0.2.37", "@cosmjs/tendermint-rpc": "^0.31.0", "@dydxprotocol/v4-client": "^0.29.0", "@ethersproject/providers": "^5.7.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 83de3b2..a7f94ab 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,9 +1,5 @@ lockfileVersion: '6.0' -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - dependencies: '@0xsquid/sdk': specifier: ^1.7.2 @@ -27,8 +23,8 @@ dependencies: specifier: ^0.31.0 version: 0.31.0 '@dydxprotocol/abacus': - specifier: ^0.2.28 - version: 0.2.28 + specifier: ^0.2.37 + version: 0.2.37 '@dydxprotocol/v4-client': specifier: ^0.29.0 version: 0.29.0 @@ -1143,8 +1139,8 @@ packages: resolution: {integrity: sha512-RpfLEtTlyIxeNPGKcokS+p3BZII/Q3bYxryFRglh5H3A3T8q9fsLYm72VYAMEOOIBLEa8o93kFLiBDUWKrwXZA==} dev: true - /@dydxprotocol/abacus@0.2.28: - resolution: {integrity: sha512-mGmhwQM8aiV/cD+d3lvW76ysoPN7uVsDyAiVcRdzGqgj+gbM0IIJDYXG14xHruOTi+gK1Aga5MUz2pdvmPDhYA==} + /@dydxprotocol/abacus@0.2.37: + resolution: {integrity: sha512-amoq9aVXo+lVTQGu2jsyy5/0bniOMEXpyJDmaAr2TpbrtQtw2WgyAP+se9Q5wt3yh0Mzdtsh7P2h090kH7/bZQ==} dev: false /@dydxprotocol/dydxjs@0.3.0: @@ -8277,7 +8273,7 @@ packages: resolution: {integrity: sha512-WIdaQ8uW1vIbYvNnAVunkC6yxTrneJC7VQ5UUQ0kuw8b0C0A39KTIpoQHCfc8tV7o9vF4niwRhdXEdfAgQEsQQ==} dependencies: cosmos-directory-types: 0.0.6 - node-fetch-native: 1.2.0 + node-fetch-native: 1.4.0 dev: false /cosmos-directory-types@0.0.6: @@ -12462,8 +12458,8 @@ packages: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} - /node-fetch-native@1.2.0: - resolution: {integrity: sha512-5IAMBTl9p6PaAjYCnMv5FmqIF6GcZnawAVnzaCG0rX2aYZJ4CxEkZNtVPuTRug7fL7wyM5BQYTlAzcyMPi6oTQ==} + /node-fetch-native@1.4.0: + resolution: {integrity: sha512-F5kfEj95kX8tkDhUCYdV8dg3/8Olx/94zB8+ZNthFs6Bz31UpUi8Xh40TN3thLwXgrwXry1pEg9lJ++tLWTcqA==} dev: false /node-fetch@2.6.12: @@ -15855,3 +15851,7 @@ packages: release-it: 15.11.0 semver: 7.5.1 dev: false + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false diff --git a/src/views/tables/TransferHistoryTable.tsx b/src/views/tables/TransferHistoryTable.tsx index f5df2a9..59fa60b 100644 --- a/src/views/tables/TransferHistoryTable.tsx +++ b/src/views/tables/TransferHistoryTable.tsx @@ -1,3 +1,4 @@ +import { useState } from 'react'; import styled, { type AnyStyledComponent, css } from 'styled-components'; import { shallowEqual, useDispatch, useSelector } from 'react-redux'; import type { ColumnSize } from '@react-types/table'; @@ -10,7 +11,7 @@ import { useBreakpoints, useStringGetter } from '@/hooks'; import { layoutMixins } from '@/styles/layoutMixins'; import { tradeViewMixins } from '@/styles/tradeViewMixins'; -import { Icon } from '@/components/Icon'; +import { Icon, IconName } from '@/components/Icon'; import { Output, OutputType } from '@/components/Output'; import { Table, TableCell, TableColumnHeader, type ColumnDef } from '@/components/Table'; @@ -78,8 +79,8 @@ const getTransferHistoryTableColumnDef = ({ ), renderCell: ({ fromAddress, toAddress }) => ( - {fromAddress ? truncateAddress(fromAddress) : '-'} - {toAddress ? truncateAddress(toAddress) : '-'} + + ), }, @@ -93,8 +94,14 @@ const getTransferHistoryTableColumnDef = ({ columnKey: TransferHistoryTableColumnKey.TxHash, getCellValue: (row) => row.transactionHash, label: stringGetter({ key: STRING_KEYS.TRANSACTION }), - renderCell: ({ transactionHash }) => - transactionHash ? {transactionHash} : '-', + renderCell: ({ transactionHash, resources }) => + transactionHash ? ( + + {truncateAddress(transactionHash, '')} + + ) : ( + '-' + ), }, } as Record> )[key], @@ -161,6 +168,26 @@ export const TransferHistoryTable = ({ ); }; +const CopyableAddress = ({ address }: { address?: string }) => { + const [copied, setCopied] = useState(false); + + const onCopy = () => { + if (!address) return; + setCopied(true); + navigator.clipboard.writeText(address); + setTimeout(() => setCopied(false), 500); + }; + + return address ? ( + + {truncateAddress(address)} + + + ) : ( + '-' + ); +}; + const Styled: Record = {}; Styled.Table = styled(Table)` @@ -182,3 +209,19 @@ Styled.TimeOutput = styled(Output)` Styled.TxHash = styled(Link)` justify-content: flex-end; `; + +Styled.CopyableAddress = styled(Styled.InlineRow)<{ copied: boolean }>` + cursor: pointer; + + ${({ copied }) => + copied + ? css` + filter: brightness(0.8); + ` + : css` + &:hover { + filter: brightness(1.1); + text-decoration: underline; + } + `} +`; From 64f663ed5bc8ca4ca1ae17868c21b94ca07b1814 Mon Sep 17 00:00:00 2001 From: Aleka Cheung Date: Fri, 25 Aug 2023 14:07:03 -0400 Subject: [PATCH 3/3] add CopyButton component --- src/components/CopyButton.stories.tsx | 28 ++++++++++ src/components/CopyButton.tsx | 65 ++++++++++++++++++++++ src/views/dialogs/MnemonicExportDialog.tsx | 16 +----- src/views/dialogs/ReceiveDialog.tsx | 17 +----- src/views/tables/TransferHistoryTable.tsx | 62 +++++---------------- 5 files changed, 112 insertions(+), 76 deletions(-) create mode 100644 src/components/CopyButton.stories.tsx create mode 100644 src/components/CopyButton.tsx diff --git a/src/components/CopyButton.stories.tsx b/src/components/CopyButton.stories.tsx new file mode 100644 index 0000000..71f4070 --- /dev/null +++ b/src/components/CopyButton.stories.tsx @@ -0,0 +1,28 @@ +import type { Story } from '@ladle/react'; + +import { CopyButton, type CopyButtonProps } from '@/components/CopyButton'; + +import { StoryWrapper } from '.ladle/components'; + +export const CopyButtonStory: Story = (args) => ( + + + +); + +CopyButtonStory.args = { + value: 'some text to copy', +}; + +CopyButtonStory.argTypes = { + shownAsText: { + options: [true, false], + control: { type: 'select' }, + defaultValue: false, + }, + children: { + options: ['some text to copy'], + control: { type: 'select' }, + defaultValue: undefined, + } +}; diff --git a/src/components/CopyButton.tsx b/src/components/CopyButton.tsx new file mode 100644 index 0000000..5da154e --- /dev/null +++ b/src/components/CopyButton.tsx @@ -0,0 +1,65 @@ +import { useState } from 'react'; +import styled, { css, type AnyStyledComponent } from 'styled-components'; + +import { ButtonAction } from '@/constants/buttons'; +import { STRING_KEYS } from '@/constants/localization'; + +import { useStringGetter } from '@/hooks'; +import { layoutMixins } from '@/styles/layoutMixins'; + +import { Button, ButtonProps } from './Button'; +import { Icon, IconName } from './Icon'; + +export type CopyButtonProps = { + value?: string; + shownAsText?: boolean; + children?: React.ReactNode; +} & ButtonProps; + +export const CopyButton = ({ value, shownAsText, children, ...buttonProps }: CopyButtonProps) => { + const stringGetter = useStringGetter(); + const [copied, setCopied] = useState(false); + + const onCopy = () => { + if (!value) return; + + setCopied(true); + navigator.clipboard.writeText(value); + setTimeout(() => setCopied(false), 500); + }; + + return shownAsText ? ( + + {children} + + + ) : ( + + ); +}; + +const Styled: Record = {}; + +Styled.InlineRow = styled.div<{ copied: boolean }>` + ${layoutMixins.inlineRow} + cursor: pointer; + + ${({ copied }) => + copied + ? css` + filter: brightness(0.8); + ` + : css` + &:hover { + filter: brightness(1.1); + text-decoration: underline; + } + `} +`; diff --git a/src/views/dialogs/MnemonicExportDialog.tsx b/src/views/dialogs/MnemonicExportDialog.tsx index b654170..a53c7e0 100644 --- a/src/views/dialogs/MnemonicExportDialog.tsx +++ b/src/views/dialogs/MnemonicExportDialog.tsx @@ -9,7 +9,7 @@ import { breakpoints } from '@/styles'; import { layoutMixins } from '@/styles/layoutMixins'; import { AlertMessage } from '@/components/AlertMessage'; -import { Button } from '@/components/Button'; +import { CopyButton } from '@/components/CopyButton'; import { Dialog } from '@/components/Dialog'; import { Checkbox } from '@/components/Checkbox'; import { Icon, IconName } from '@/components/Icon'; @@ -30,21 +30,12 @@ export const MnemonicExportDialog = ({ setIsOpen }: ElementProps) => { const [hasAcknowledged, setHasAcknowledged] = useState(false); const [currentStep, setCurrentStep] = useState(MnemonicExportStep.AcknowledgeRisk); const [isShowing, setIsShowing] = useState(false); - const [copied, setCopied] = useState(false); const stringGetter = useStringGetter(); const { hdKey } = useAccounts(); const { mnemonic } = hdKey ?? {}; - const onCopy = () => { - setCopied(true); - if (mnemonic) { - navigator.clipboard.writeText(mnemonic); - } - setTimeout(() => setCopied(false), 500); - }; - const title = { [MnemonicExportStep.AcknowledgeRisk]: stringGetter({ key: STRING_KEYS.REVEAL_SECRET_PHRASE }), [MnemonicExportStep.DisplayMnemonic]: stringGetter({ key: STRING_KEYS.EXPORT_SECRET_PHRASE }), @@ -116,10 +107,7 @@ export const MnemonicExportDialog = ({ setIsOpen }: ElementProps) => { } > - + ), diff --git a/src/views/dialogs/ReceiveDialog.tsx b/src/views/dialogs/ReceiveDialog.tsx index 0f8db5e..b9546fa 100644 --- a/src/views/dialogs/ReceiveDialog.tsx +++ b/src/views/dialogs/ReceiveDialog.tsx @@ -1,7 +1,6 @@ import { useState } from 'react'; import styled, { type AnyStyledComponent } from 'styled-components'; -import { ButtonAction } from '@/constants/buttons'; import { STRING_KEYS } from '@/constants/localization'; import { DydxChainAsset } from '@/constants/wallets'; @@ -10,9 +9,8 @@ import { layoutMixins } from '@/styles/layoutMixins'; import { useAccounts, useStringGetter } from '@/hooks'; import { AssetIcon } from '@/components/AssetIcon'; -import { Button } from '@/components/Button'; +import { CopyButton } from '@/components/CopyButton'; import { Dialog } from '@/components/Dialog'; -import { Icon, IconName } from '@/components/Icon'; import { QrCode } from '@/components/QrCode'; import { SelectItem, SelectMenu } from '@/components/SelectMenu'; import { WithDetailsReceipt } from '@/components/WithDetailsReceipt'; @@ -31,14 +29,6 @@ export const ReceiveDialog = ({ selectedAsset = DydxChainAsset.DYDX, setIsOpen } const { dydxAddress } = useAccounts(); const [asset, setAsset] = useState(selectedAsset); - const [copied, setCopied] = useState(false); - - const onCopy = () => { - if (!dydxAddress) return; - setCopied(true); - navigator.clipboard.writeText(dydxAddress); - setTimeout(() => setCopied(false), 500); - }; const assetOptions = [ { @@ -89,10 +79,7 @@ export const ReceiveDialog = ({ selectedAsset = DydxChainAsset.DYDX, setIsOpen } > - + )} diff --git a/src/views/tables/TransferHistoryTable.tsx b/src/views/tables/TransferHistoryTable.tsx index 59fa60b..cf7c9c3 100644 --- a/src/views/tables/TransferHistoryTable.tsx +++ b/src/views/tables/TransferHistoryTable.tsx @@ -1,9 +1,10 @@ -import { useState } from 'react'; import styled, { type AnyStyledComponent, css } from 'styled-components'; import { shallowEqual, useDispatch, useSelector } from 'react-redux'; import type { ColumnSize } from '@react-types/table'; import { type SubaccountTransfer } from '@/constants/abacus'; +import { ButtonAction } from '@/constants/buttons'; +import { DialogTypes } from '@/constants/dialogs'; import { STRING_KEYS, StringGetterFunction } from '@/constants/localization'; import { useBreakpoints, useStringGetter } from '@/hooks'; @@ -11,20 +12,19 @@ import { useBreakpoints, useStringGetter } from '@/hooks'; import { layoutMixins } from '@/styles/layoutMixins'; import { tradeViewMixins } from '@/styles/tradeViewMixins'; -import { Icon, IconName } from '@/components/Icon'; +import { Button } from '@/components/Button'; +import { CopyButton } from '@/components/CopyButton'; +import { Icon } from '@/components/Icon'; +import { Link } from '@/components/Link'; import { Output, OutputType } from '@/components/Output'; import { Table, TableCell, TableColumnHeader, type ColumnDef } from '@/components/Table'; +import { OnboardingTriggerButton } from '@/views/dialogs/OnboardingTriggerButton'; import { getSubaccountTransfers } from '@/state/accountSelectors'; +import { calculateCanAccountTrade } from '@/state/accountCalculators'; +import { openDialog } from '@/state/dialogs'; import { truncateAddress } from '@/lib/wallet'; -import { Button } from '@/components/Button'; -import { ButtonAction } from '@/constants/buttons'; -import { openDialog } from '@/state/dialogs'; -import { DialogTypes } from '@/constants/dialogs'; -import { Link } from '@/components/Link'; -import { calculateCanAccountTrade } from '@/state/accountCalculators'; -import { OnboardingTriggerButton } from '../dialogs/OnboardingTriggerButton'; const MOBILE_TRANSFERS_PER_PAGE = 50; @@ -79,8 +79,12 @@ const getTransferHistoryTableColumnDef = ({ ), renderCell: ({ fromAddress, toAddress }) => ( - - + + {fromAddress ? truncateAddress(fromAddress) : '-'} + {' '} + + {toAddress ? truncateAddress(toAddress) : '-'} + ), }, @@ -168,26 +172,6 @@ export const TransferHistoryTable = ({ ); }; -const CopyableAddress = ({ address }: { address?: string }) => { - const [copied, setCopied] = useState(false); - - const onCopy = () => { - if (!address) return; - setCopied(true); - navigator.clipboard.writeText(address); - setTimeout(() => setCopied(false), 500); - }; - - return address ? ( - - {truncateAddress(address)} - - - ) : ( - '-' - ); -}; - const Styled: Record = {}; Styled.Table = styled(Table)` @@ -209,19 +193,3 @@ Styled.TimeOutput = styled(Output)` Styled.TxHash = styled(Link)` justify-content: flex-end; `; - -Styled.CopyableAddress = styled(Styled.InlineRow)<{ copied: boolean }>` - cursor: pointer; - - ${({ copied }) => - copied - ? css` - filter: brightness(0.8); - ` - : css` - &:hover { - filter: brightness(1.1); - text-decoration: underline; - } - `} -`;