diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/DappTransactionContainer.tsx b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/DappTransactionContainer.tsx index 5654c2393..d5de946fa 100644 --- a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/DappTransactionContainer.tsx +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/DappTransactionContainer.tsx @@ -5,7 +5,7 @@ import { Flex } from '@lace/ui'; import { useViewsFlowContext } from '@providers/ViewFlowProvider'; import { Wallet } from '@lace/cardano'; -import { withAddressBookContext } from '@src/features/address-book/context'; +import { useAddressBookContext, withAddressBookContext } from '@src/features/address-book/context'; import { useWalletStore } from '@stores'; import { useFetchCoinPrice, useChainHistoryProvider } from '@hooks'; import { @@ -23,6 +23,7 @@ import { useCurrencyStore, useAppSettingsContext } from '@providers'; import { logger } from '@lib/wallet-api-ui'; import { useComputeTxCollateral } from '@hooks/useComputeTxCollateral'; import { utxoAndBackendChainHistoryResolver } from '@src/utils/utxo-chain-history-resolver'; +import { AddressBookSchema, useDbStateValue } from '@lib/storage'; interface DappTransactionContainerProps { errorMessage?: string; @@ -43,6 +44,10 @@ export const DappTransactionContainer = withAddressBookContext( walletState } = useWalletStore(); + const ownAddresses = useObservable(inMemoryWallet.addresses$)?.map((a) => a.address); + const { list: addressBook } = useAddressBookContext() as useDbStateValue; + const addressToNameMap = new Map(addressBook?.map((entry) => [entry.address as string, entry.name])); + const { fiatCurrency } = useCurrencyStore(); const { priceResult } = useFetchCoinPrice(); @@ -146,6 +151,8 @@ export const DappTransactionContainer = withAddressBookContext( errorMessage={errorMessage} toAddress={toAddressTokens} collateral={txCollateral} + ownAddresses={ownAddresses} + addressToNameMap={addressToNameMap} /> ) : ( diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/DappTransactionContainer.test.tsx b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/DappTransactionContainer.test.tsx index 98afa976f..029399959 100644 --- a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/DappTransactionContainer.test.tsx +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/DappTransactionContainer.test.tsx @@ -30,12 +30,13 @@ import { DappTransactionContainer } from '../DappTransactionContainer'; import '@testing-library/jest-dom'; import { BehaviorSubject } from 'rxjs'; import { act } from 'react-dom/test-utils'; -import { buildMockTx } from '@src/utils/mocks/tx'; +import { buildMockTx, sendingAddress } from '@src/utils/mocks/tx'; import { Wallet } from '@lace/cardano'; import { SignTxData } from '../types'; import { getWrapper } from '../testing.utils'; import { TransactionWitnessRequest } from '@cardano-sdk/web-extension'; import { cardanoCoin } from '@src/utils/constants'; +import { AddressBookSchema } from '@lib/storage'; const { Cardano, Crypto } = Wallet; @@ -51,6 +52,7 @@ const mockedAssetsInfo = new Map([['id', 'data']]); const assetInfo$ = new BehaviorSubject(mockedAssetsInfo); const available$ = new BehaviorSubject([]); const signed$ = new BehaviorSubject([]); +const addresses$ = new BehaviorSubject([sendingAddress]); const rewardAccounts$ = new BehaviorSubject([ { // eslint-disable-next-line unicorn/consistent-destructuring @@ -65,6 +67,7 @@ const protocolParameters$ = new BehaviorSubject({ }); const inMemoryWallet = { + addresses$, assetInfo$, balance: { utxo: { @@ -133,12 +136,12 @@ jest.mock('react-i18next', () => { }; }); -const addressList = ['addressList']; +const addressBook: AddressBookSchema[] = []; jest.mock('@src/features/address-book/context', () => ({ // eslint-disable-next-line @typescript-eslint/no-explicit-any ...jest.requireActual('@src/features/address-book/context'), withAddressBookContext: mockWithAddressBookContext, - useAddressBookContext: () => ({ list: addressList }) + useAddressBookContext: () => ({ list: addressBook }) })); jest.mock('antd', () => { @@ -332,7 +335,9 @@ describe('Testing DappTransactionContainer component', () => { errorMessage, coinSymbol: 'ADA', collateral: BigInt(1_000_000), - txInspectionDetails + txInspectionDetails, + ownAddresses: [sendingAddress.address], + addressToNameMap: new Map() }, {} ); diff --git a/apps/browser-extension-wallet/src/lib/translations/en.json b/apps/browser-extension-wallet/src/lib/translations/en.json index 5a6150caf..ce7bf838d 100644 --- a/apps/browser-extension-wallet/src/lib/translations/en.json +++ b/apps/browser-extension-wallet/src/lib/translations/en.json @@ -1368,6 +1368,8 @@ "core.receive.usedAddresses.copy": "Copy Address", "core.receive.usedAddresses.addressCopied": "Address copied", "core.receive.showUsedAddresses": "Show used addresses", + "core.addressTags.own": "own", + "core.addressTags.foreign": "foreign", "addressesDiscovery.overlay.title": "Your wallet is syncing, this might take a few minutes", "addressesDiscovery.toast.errorText": "Wallet failed to sync", "addressesDiscovery.toast.successText": "Wallet synced successfully", diff --git a/apps/browser-extension-wallet/src/utils/mocks/tx.ts b/apps/browser-extension-wallet/src/utils/mocks/tx.ts index 8ea60d13c..a41522f82 100644 --- a/apps/browser-extension-wallet/src/utils/mocks/tx.ts +++ b/apps/browser-extension-wallet/src/utils/mocks/tx.ts @@ -1,11 +1,13 @@ /* eslint-disable no-magic-numbers */ import { Wallet } from '@lace/cardano'; -const sendingAddress = Wallet.Cardano.PaymentAddress( - 'addr_test1qq585l3hyxgj3nas2v3xymd23vvartfhceme6gv98aaeg9muzcjqw982pcftgx53fu5527z2cj2tkx2h8ux2vxsg475q2g7k3g' -); +export const sendingAddress = { + address: Wallet.Cardano.PaymentAddress( + 'addr_test1qq585l3hyxgj3nas2v3xymd23vvartfhceme6gv98aaeg9muzcjqw982pcftgx53fu5527z2cj2tkx2h8ux2vxsg475q2g7k3g' + ) +} as Wallet.KeyManagement.GroupedAddress; -const receivingAddress = Wallet.Cardano.PaymentAddress( +export const receivingAddress = Wallet.Cardano.PaymentAddress( 'addr_test1qpfhhfy2qgls50r9u4yh0l7z67xpg0a5rrhkmvzcuqrd0znuzcjqw982pcftgx53fu5527z2cj2tkx2h8ux2vxsg475q9gw0lz' ); @@ -34,7 +36,7 @@ export const buildMockTx = ( ]), inputs: args.inputs ?? [ { - address: sendingAddress, + address: sendingAddress.address, index: 0, txId: Wallet.Cardano.TransactionId('bb217abaca60fc0ca68c1555eca6a96d2478547818ae76ce6836133f3cc546e0') } @@ -64,7 +66,7 @@ export const buildMockTx = ( } }, { - address: sendingAddress, + address: sendingAddress.address, value: { assets: new Map([ [Wallet.Cardano.AssetId('659f2917fb63f12b33667463ee575eeac1845bbc736b9c0bbc40ba8254534c41'), BigInt(1)] diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/activity/components/TransactionDetailsProxy.tsx b/apps/browser-extension-wallet/src/views/browser-view/features/activity/components/TransactionDetailsProxy.tsx index 412cb9a52..e0223dbe9 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/activity/components/TransactionDetailsProxy.tsx +++ b/apps/browser-extension-wallet/src/views/browser-view/features/activity/components/TransactionDetailsProxy.tsx @@ -9,6 +9,7 @@ import { TxDirections } from '@src/types'; import { APP_MODE_POPUP } from '@src/utils/constants'; import { config } from '@src/config'; import { PostHogAction } from '@providers/AnalyticsProvider/analyticsTracker'; +import { useObservable } from '@lace/common'; type TransactionDetailsProxyProps = { name: string; @@ -21,13 +22,23 @@ export const TransactionDetailsProxy = withAddressBookContext( ({ name, activityInfo, direction, status, amountTransformer }: TransactionDetailsProxyProps): ReactElement => { const analytics = useAnalyticsContext(); const { + inMemoryWallet, walletInfo, environmentName, walletUI: { cardanoCoin, appMode } } = useWalletStore(); const isPopupView = appMode === APP_MODE_POPUP; const openExternalLink = useExternalLinkOpener(); + + // Prepare own addresses of active account + const walletAddresses = useObservable(inMemoryWallet.addresses$)?.map((a) => a.address); + + // Prepare address book data as Map const { list: addressList } = useAddressBookContext(); + const addressToNameMap = useMemo( + () => new Map(addressList?.map((item: AddressListType) => [item.address, item.name])), + [addressList] + ); const { CEXPLORER_BASE_URL, CEXPLORER_URL_PATHS } = config(); const explorerBaseUrl = useMemo( @@ -72,11 +83,6 @@ export const TransactionDetailsProxy = withAddressBookContext( externalLink && status === ActivityStatus.SUCCESS && openExternalLink(externalLink); }; - const addressToNameMap = useMemo( - () => new Map(addressList?.map((item: AddressListType) => [item.address, item.name])), - [addressList] - ); - return ( // eslint-disable-next-line react/jsx-pascal-case ; isPopupView?: boolean; openExternalLink?: (url: string) => void; @@ -122,6 +126,7 @@ export const TransactionDetails = ({ txSummary = [], coinSymbol, pools, + ownAddresses, addressToNameMap, isPopupView, openExternalLink, @@ -353,22 +358,19 @@ export const TransactionDetails = ({ )} {(summary.addr as string[]).map((addr) => { - const addrName = addressToNameMap?.get(addr); const address = isPopupView ? ( ) : ( {addr} ); return ( -
- {addrName ? ( -
- {addrName} - {address} -
- ) : ( - address - )} +
+ {address} + {renderAddressTag(addr, getAddressTagTranslations(t), ownAddresses, addressToNameMap)}
); })} @@ -590,6 +592,8 @@ export const TransactionDetails = ({ coinSymbol={coinSymbol} withSeparatorLine sendAnalytics={sendAnalyticsInputs} + ownAddresses={ownAddresses} + addressToNameMap={addressToNameMap} /> )} {addrOutputs?.length > 0 && ( @@ -604,6 +608,8 @@ export const TransactionDetails = ({ }} coinSymbol={coinSymbol} sendAnalytics={sendAnalyticsOutputs} + ownAddresses={ownAddresses} + addressToNameMap={addressToNameMap} /> )} {metadata?.length > 0 && ( diff --git a/packages/core/src/ui/components/ActivityDetail/TransactionInputOutput.tsx b/packages/core/src/ui/components/ActivityDetail/TransactionInputOutput.tsx index bb9369e65..0d930d140 100644 --- a/packages/core/src/ui/components/ActivityDetail/TransactionInputOutput.tsx +++ b/packages/core/src/ui/components/ActivityDetail/TransactionInputOutput.tsx @@ -2,14 +2,18 @@ import React, { useState } from 'react'; import { Tooltip } from 'antd'; import cn from 'classnames'; -import { addEllipsis, Button } from '@lace/common'; import { InfoCircleOutlined, DownOutlined } from '@ant-design/icons'; +import { addEllipsis, Button } from '@lace/common'; + import { TxOutputInput } from './TransactionDetailAsset'; import { TranslationsFor } from '../../utils/types'; import { ReactComponent as BracketDown } from '../../assets/icons/bracket-down.component.svg'; import styles from './TransactionInputOutput.module.scss'; +import { Flex } from '@lace/ui'; +import { getAddressTagTranslations, renderAddressTag } from '@ui/utils'; +import { useTranslate } from '@ui/hooks'; const rotateOpen: React.CSSProperties = { transform: 'rotate(180deg)', @@ -30,6 +34,8 @@ export interface TransactionInputOutputProps { translations: TranslationsFor<'address' | 'sent'>; coinSymbol: string; withSeparatorLine?: boolean; + ownAddresses: string[]; + addressToNameMap: Map; sendAnalytics?: () => void; } @@ -42,9 +48,12 @@ export const TransactionInputOutput = ({ translations, coinSymbol, withSeparatorLine, + ownAddresses, + addressToNameMap, sendAnalytics }: TransactionInputOutputProps): React.ReactElement => { const [isVisible, setIsVisible] = useState(); + const { t } = useTranslate(); const animation = isVisible ? rotateOpen : rotateClose; const Icon = BracketDown ? : ; @@ -79,9 +88,12 @@ export const TransactionInputOutput = ({
{translations.address}
-
- {addEllipsis(inputAddress, 8, 8)} -
+ +
+ {addEllipsis(inputAddress, 8, 8)} +
+ {renderAddressTag(inputAddress, getAddressTagTranslations(t), ownAddresses, addressToNameMap)} +
diff --git a/packages/core/src/ui/components/ActivityDetail/__tests__/TransactionDetails.test.tsx b/packages/core/src/ui/components/ActivityDetail/__tests__/TransactionDetails.test.tsx index 73522e5c2..26a3d9558 100644 --- a/packages/core/src/ui/components/ActivityDetail/__tests__/TransactionDetails.test.tsx +++ b/packages/core/src/ui/components/ActivityDetail/__tests__/TransactionDetails.test.tsx @@ -35,6 +35,7 @@ describe('Testing ActivityDetailsBrowser component', () => { ], amountTransformer: (amount) => `${amount} $`, coinSymbol: 'ADA', + ownAddresses: [], addressToNameMap: new Map() }; @@ -83,4 +84,22 @@ describe('Testing ActivityDetailsBrowser component', () => { const { queryByTestId: query } = render(); expect(query('tx-metadata')).not.toBeInTheDocument(); }); + + test('should show address tag for inputs', async () => { + // use empty addrOutputs (so we get only one toggle button for inputs) + const { findByTestId } = render(); + const inputsSectionToggle = await findByTestId('tx-addr-list_toggle'); + fireEvent.click(inputsSectionToggle); + + expect(await findByTestId('address-tag')).toBeVisible(); + }); + + test('should show address tag for outputs', async () => { + // use empty addrOutputs (so we get only one toggle button for outputs) + const { findByTestId } = render(); + const outputsSectionToggle = await findByTestId('tx-addr-list_toggle'); + fireEvent.click(outputsSectionToggle); + + expect(await findByTestId('address-tag')).toBeVisible(); + }); }); diff --git a/packages/core/src/ui/components/DappAddressSections/DappAddressSections.tsx b/packages/core/src/ui/components/DappAddressSections/DappAddressSections.tsx index 34abb6ebd..69a065701 100644 --- a/packages/core/src/ui/components/DappAddressSections/DappAddressSections.tsx +++ b/packages/core/src/ui/components/DappAddressSections/DappAddressSections.tsx @@ -8,8 +8,9 @@ import { Typography } from 'antd'; import styles from './DappAddressSections.module.scss'; import { useTranslate } from '@src/ui/hooks'; -import { TransactionAssets, SummaryExpander, DappTransactionSummary, Tooltip } from '@lace/ui'; +import { Flex, TransactionAssets, SummaryExpander, DappTransactionSummary, Tooltip } from '@lace/ui'; import classNames from 'classnames'; +import { getAddressTagTranslations, renderAddressTag } from '@ui/utils/render-address-tag'; interface GroupedAddressAssets { nfts: Array; @@ -23,6 +24,8 @@ export interface DappAddressSectionProps { isToAddressesEnabled: boolean; isFromAddressesEnabled: boolean; coinSymbol: string; + ownAddresses: string[]; + addressToNameMap?: Map; } const tryDecodeAsUtf8 = ( @@ -101,7 +104,9 @@ export const DappAddressSections = ({ groupedToAddresses, isToAddressesEnabled, isFromAddressesEnabled, - coinSymbol + coinSymbol, + ownAddresses, + addressToNameMap }: DappAddressSectionProps): React.ReactElement => { const { t } = useTranslate(); @@ -121,11 +126,14 @@ export const DappAddressSections = ({ {t('core.dappTransaction.address')} - - - {addEllipsis(address, charBeforeEllipsisName, charAfterEllipsisName)} - - + + + + {addEllipsis(address, charBeforeEllipsisName, charAfterEllipsisName)} + + + {renderAddressTag(address, getAddressTagTranslations(t), ownAddresses, addressToNameMap)} +
{(addressData.tokens.length > 0 || addressData.coins.length > 0) && ( <> @@ -179,11 +187,14 @@ export const DappAddressSections = ({ {t('core.dappTransaction.address')} - - - {addEllipsis(address, charBeforeEllipsisName, charAfterEllipsisName)} - - + + + + {addEllipsis(address, charBeforeEllipsisName, charAfterEllipsisName)} + + + {renderAddressTag(address, getAddressTagTranslations(t), ownAddresses, addressToNameMap)} +
{(addressData.tokens.length > 0 || addressData.coins.length > 0) && ( <> diff --git a/packages/core/src/ui/components/DappTransaction/DappTransaction.stories.tsx b/packages/core/src/ui/components/DappTransaction/DappTransaction.stories.tsx index f734549d4..691d4b8b8 100644 --- a/packages/core/src/ui/components/DappTransaction/DappTransaction.stories.tsx +++ b/packages/core/src/ui/components/DappTransaction/DappTransaction.stories.tsx @@ -3,6 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { DappTransaction } from './DappTransaction'; import { ComponentProps } from 'react'; import { Wallet } from '@lace/cardano'; +import { AssetInfoWithAmount, TokenTransferValue } from '@cardano-sdk/core'; const meta: Meta = { title: 'DappTransaction', @@ -15,14 +16,60 @@ const meta: Meta = { export default meta; type Story = StoryObj; +const fromAddress = Wallet.Cardano.PaymentAddress( + 'addr_test1qq585l3hyxgj3nas2v3xymd23vvartfhceme6gv98aaeg9muzcjqw982pcftgx53fu5527z2cj2tkx2h8ux2vxsg475q2g7k3g' +); + +const toAddress = Wallet.Cardano.PaymentAddress( + 'addr_test1qpfhhfy2qgls50r9u4yh0l7z67xpg0a5rrhkmvzcuqrd0znuzcjqw982pcftgx53fu5527z2cj2tkx2h8ux2vxsg475q9gw0lz' +); + +const toAddressBookAddress = Wallet.Cardano.PaymentAddress( + 'addr_test1qzqgfww9svrzelxrnlml0nmdq4yevwke7ck7ae27u5ptmq5dwuq25p4hr0yxhg4pce0d6t7v4c0msy3vr3xppygn9ktqe77950' +); + +const PXLAssetId = Wallet.Cardano.AssetId('1ec85dcee27f2d90ec1f9a1e4ce74a667dc9be8b184463223f9c960150584c'); +const PXLPolicyId = Wallet.Cardano.AssetId.getPolicyId(PXLAssetId); +const PXLAssetName = Wallet.Cardano.AssetId.getAssetName(PXLAssetId); +const PXLAssetInfo: AssetInfoWithAmount = { + // eslint-disable-next-line no-magic-numbers + amount: BigInt(100_000), + assetInfo: { + assetId: PXLAssetId, + name: PXLAssetName, + policyId: PXLPolicyId, + fingerprint: Wallet.Cardano.AssetFingerprint.fromParts(PXLPolicyId, PXLAssetName), + // eslint-disable-next-line no-magic-numbers + supply: BigInt(11_242_452_000), + quantity: BigInt(1) + } +}; + +const fromAddressTokens: TokenTransferValue = { + // eslint-disable-next-line no-magic-numbers + coins: BigInt(-100_000), + assets: new Map([[PXLAssetId, { ...PXLAssetInfo, amount: -PXLAssetInfo.amount }]]) +}; + +const toAddressTokens: TokenTransferValue = { + // eslint-disable-next-line no-magic-numbers + coins: BigInt(100_000), + assets: new Map([[PXLAssetId, PXLAssetInfo]]) +}; + const data: ComponentProps = { dappInfo: { name: 'Mint' }, coinSymbol: 'tAda', fiatCurrencyCode: 'usd', - fromAddress: new Map(), - toAddress: new Map(), + ownAddresses: [fromAddress], + addressToNameMap: new Map([[toAddressBookAddress, 'test']]), + fromAddress: new Map([[fromAddress, fromAddressTokens]]), + toAddress: new Map([ + [toAddress, toAddressTokens], + [toAddressBookAddress, toAddressTokens] + ]), // eslint-disable-next-line no-magic-numbers collateral: 150_000 as unknown as bigint, txInspectionDetails: { diff --git a/packages/core/src/ui/components/DappTransaction/DappTransaction.tsx b/packages/core/src/ui/components/DappTransaction/DappTransaction.tsx index aed6f5cfd..15ea1a1b0 100644 --- a/packages/core/src/ui/components/DappTransaction/DappTransaction.tsx +++ b/packages/core/src/ui/components/DappTransaction/DappTransaction.tsx @@ -30,6 +30,8 @@ export interface DappTransactionProps { /** tokens send to being sent to or from the user */ fromAddress: Map; toAddress: Map; + ownAddresses?: string[]; + addressToNameMap?: Map; collateral?: bigint; } @@ -57,6 +59,7 @@ const groupAddresses = (addresses: Map { const { t } = useTranslate(); @@ -205,6 +210,8 @@ export const DappTransaction = ({ groupedFromAddresses={groupedFromAddresses} groupedToAddresses={groupedToAddresses} coinSymbol={coinSymbol} + ownAddresses={ownAddresses} + addressToNameMap={addressToNameMap} />
diff --git a/packages/core/src/ui/hooks/useTranslate.tsx b/packages/core/src/ui/hooks/useTranslate.tsx index 8e20494d3..773093b4a 100644 --- a/packages/core/src/ui/hooks/useTranslate.tsx +++ b/packages/core/src/ui/hooks/useTranslate.tsx @@ -6,7 +6,7 @@ import fallbackInstance from '../lib/i18n'; // If no i18n instance found in context then use the local one setI18n(fallbackInstance); -interface UseTranslate { +export interface UseTranslate { t: (key: string | string[], defaultValue?: string, options?: TOptions) => string; Trans: typeof Trans; i18n: typeof i18n; diff --git a/packages/core/src/ui/utils/__tests__/render-address-tag.test.ts b/packages/core/src/ui/utils/__tests__/render-address-tag.test.ts new file mode 100644 index 000000000..153b40e63 --- /dev/null +++ b/packages/core/src/ui/utils/__tests__/render-address-tag.test.ts @@ -0,0 +1,33 @@ +import { render } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { Wallet } from '@lace/cardano'; +import { AddressTagTranslations, renderAddressTag } from '@ui/utils'; + +const address = Wallet.Cardano.PaymentAddress( + 'addr_test1qq585l3hyxgj3nas2v3xymd23vvartfhceme6gv98aaeg9muzcjqw982pcftgx53fu5527z2cj2tkx2h8ux2vxsg475q2g7k3g' +); + +const translations: AddressTagTranslations = { + own: 'own', + foreign: 'foreign' +}; + +describe('rendering correct tags for addresses', () => { + test('should tag own addresses', async () => { + const ownAddresses = [address]; + const { findByTestId } = render(renderAddressTag(address, translations, ownAddresses)); + expect(await findByTestId('address-tag')).toContainHTML(translations.own); + }); + + test('should tag foreign addresses', async () => { + const { findByTestId } = render(renderAddressTag(address, translations)); + expect(await findByTestId('address-tag')).toContainHTML(translations.foreign); + }); + + test('should tag address book addresses', async () => { + const addressName = 'test'; + const addressToNameMap = new Map([[address, addressName]]); + const { findByTestId } = render(renderAddressTag(address, translations, [], addressToNameMap)); + expect(await findByTestId('address-tag')).toContainHTML(`${translations.foreign}/${addressName}`); + }); +}); diff --git a/packages/core/src/ui/utils/index.ts b/packages/core/src/ui/utils/index.ts index 2983ba98d..1892c7749 100644 --- a/packages/core/src/ui/utils/index.ts +++ b/packages/core/src/ui/utils/index.ts @@ -1,3 +1,4 @@ export * from './sanitize-number'; export * from './handle'; export * from './address-form'; +export * from './render-address-tag'; diff --git a/packages/core/src/ui/utils/render-address-tag.tsx b/packages/core/src/ui/utils/render-address-tag.tsx new file mode 100644 index 000000000..b5cf677fe --- /dev/null +++ b/packages/core/src/ui/utils/render-address-tag.tsx @@ -0,0 +1,27 @@ +import * as React from 'react'; +import { AddressTag, AddressTagVariants } from '@lace/ui'; +import { UseTranslate } from '@ui/hooks'; + +export type AddressTagTranslations = { own: string; foreign: string }; + +export const getAddressTagTranslations = (t: UseTranslate['t']): AddressTagTranslations => ({ + own: t('core.addressTags.own'), + foreign: t('core.addressTags.foreign') +}); + +export const renderAddressTag = ( + address: string, + translations: AddressTagTranslations, + ownAddresses: string[] = [], + addressToNameMap: Map = new Map() // address, name +): JSX.Element => { + const matchingAddressName = addressToNameMap.get(address); + return ownAddresses.includes(address) ? ( + {translations.own} + ) : ( + + {translations.foreign} + {matchingAddressName ? `/${matchingAddressName}` : ''} + + ); +}; diff --git a/packages/core/wallaby.js b/packages/core/wallaby.js new file mode 100644 index 000000000..6b7c500d8 --- /dev/null +++ b/packages/core/wallaby.js @@ -0,0 +1,7 @@ +module.exports = () => { + return { + testFramework: { + configFile: 'test/jest.config.js' + } + }; +}; diff --git a/packages/ui/src/design-system/address-tags/address-tag.component.tsx b/packages/ui/src/design-system/address-tags/address-tag.component.tsx new file mode 100644 index 000000000..9f9e1cf89 --- /dev/null +++ b/packages/ui/src/design-system/address-tags/address-tag.component.tsx @@ -0,0 +1,31 @@ +import type { PropsWithChildren, HTMLAttributes } from 'react'; +import React from 'react'; + +import cs from 'classnames'; + +import * as cx from './address-tag.css'; + +import type { AddressTagVariants } from './types'; + +export type AddressBaseTageProps = PropsWithChildren< + HTMLAttributes & { + variant: AddressTagVariants; + testId?: string; + } +>; + +export const AddressTag = ({ + children, + className, + variant, + testId = 'address-tag', + ...restProps +}: Readonly): JSX.Element => ( +
+ {children} +
+); diff --git a/packages/ui/src/design-system/address-tags/address-tag.css.ts b/packages/ui/src/design-system/address-tags/address-tag.css.ts new file mode 100644 index 000000000..604787839 --- /dev/null +++ b/packages/ui/src/design-system/address-tags/address-tag.css.ts @@ -0,0 +1,33 @@ +import { recipe } from '@vanilla-extract/recipes'; + +import { vars } from '../../design-tokens'; + +import { AddressTagVariants } from './types'; + +export const addressTag = recipe({ + base: { + fontSize: vars.fontSizes.$12, + fontWeight: vars.fontWeights.$medium, + borderRadius: vars.radius.$medium, + padding: `0 ${vars.spacing.$8}`, + display: 'flex', + alignItems: 'center', + height: vars.spacing.$24, + }, + variants: { + scheme: { + [AddressTagVariants.Own]: { + color: vars.colors.$address_tag_own_color, + backgroundColor: vars.colors.$address_tag_own_bgColor, + }, + [AddressTagVariants.Handle]: { + color: vars.colors.$address_tag_handle_color, + backgroundColor: vars.colors.$address_tag_handle_bgColor, + }, + [AddressTagVariants.Foreign]: { + color: vars.colors.$address_tag_foreign_color, + backgroundColor: vars.colors.$address_tag_foreign_bgColor, + }, + }, + }, +}); diff --git a/packages/ui/src/design-system/address-tags/address-tags.stories.tsx b/packages/ui/src/design-system/address-tags/address-tags.stories.tsx new file mode 100644 index 000000000..227884074 --- /dev/null +++ b/packages/ui/src/design-system/address-tags/address-tags.stories.tsx @@ -0,0 +1,65 @@ +import React from 'react'; + +import type { Meta } from '@storybook/react'; + +import { ThemeColorScheme, LocalThemeProvider } from '../../design-tokens'; +import { page, Section, Variants } from '../decorators'; +import { Cell, Grid } from '../grid'; + +import { AddressTag } from './address-tag.component'; +import { AddressTagVariants } from './types'; + +export default { + title: 'AddressTag', + decorators: [ + page({ + title: 'Address Tag', + subtitle: 'Simple component to flag addresses as own, handle or foreign.', + }), + ], +} as Meta; + +export const Overview = (): JSX.Element => { + const variantsData = [ + { + Component: AddressTag, + variant: AddressTagVariants.Own, + }, + { + Component: AddressTag, + variant: AddressTagVariants.Handle, + }, + { + Component: AddressTag, + variant: AddressTagVariants.Foreign, + }, + ]; + const renderTable = (showHeader = false): JSX.Element => ( + v.variant) : []} + > + + {variantsData.map(({ Component, variant }) => ( + + {variant} + + ))} + + + ); + + return ( + + +
+ <> + {renderTable(true)} + +
{renderTable()}
+
+ +
+
+
+ ); +}; diff --git a/packages/ui/src/design-system/address-tags/index.ts b/packages/ui/src/design-system/address-tags/index.ts new file mode 100644 index 000000000..4b40d22cb --- /dev/null +++ b/packages/ui/src/design-system/address-tags/index.ts @@ -0,0 +1,2 @@ +export { AddressTag } from './address-tag.component'; +export { AddressTagVariants } from './types'; diff --git a/packages/ui/src/design-system/address-tags/types.ts b/packages/ui/src/design-system/address-tags/types.ts new file mode 100644 index 000000000..d0721894e --- /dev/null +++ b/packages/ui/src/design-system/address-tags/types.ts @@ -0,0 +1,5 @@ +export enum AddressTagVariants { + Own = 'Own', + Handle = 'Handle', + Foreign = 'Foreign', +} diff --git a/packages/ui/src/design-system/index.ts b/packages/ui/src/design-system/index.ts index dcc11a288..a4704d4d5 100644 --- a/packages/ui/src/design-system/index.ts +++ b/packages/ui/src/design-system/index.ts @@ -53,3 +53,4 @@ export { SummaryExpander } from './summary-expander'; export * from './auto-suggest-box'; export * from './table'; export { InfoBar } from './info-bar'; +export * from './address-tags'; diff --git a/packages/ui/src/design-tokens/colors.data.ts b/packages/ui/src/design-tokens/colors.data.ts index 4c9ef372b..12be7aea9 100644 --- a/packages/ui/src/design-tokens/colors.data.ts +++ b/packages/ui/src/design-tokens/colors.data.ts @@ -308,6 +308,13 @@ export const colors = { $info_bar_container_bgColor: '', $info_bar_message_color: '', $info_bar_icon_color: '', + + $address_tag_own_color: '', + $address_tag_own_bgColor: '', + $address_tag_handle_color: '', + $address_tag_handle_bgColor: '', + $address_tag_foreign_color: '', + $address_tag_foreign_bgColor: '', }; export type Colors = typeof colors; diff --git a/packages/ui/src/design-tokens/theme/dark-theme.css.ts b/packages/ui/src/design-tokens/theme/dark-theme.css.ts index 159d6e1b5..01e0b0551 100644 --- a/packages/ui/src/design-tokens/theme/dark-theme.css.ts +++ b/packages/ui/src/design-tokens/theme/dark-theme.css.ts @@ -409,6 +409,16 @@ const colors: Colors = { $info_bar_container_bgColor: darkColorScheme.$primary_dark_grey_plus, $info_bar_icon_color: darkColorScheme.$secondary_data_pink, $info_bar_message_color: darkColorScheme.$primary_light_grey, + + $address_tag_own_color: darkColorScheme.$secondary_data_pink, + $address_tag_own_bgColor: rgba(darkColorScheme.$secondary_data_pink, 0.1), + $address_tag_handle_color: darkColorScheme.$primary_accent_purple, + $address_tag_handle_bgColor: rgba( + darkColorScheme.$primary_accent_purple, + 0.1, + ), + $address_tag_foreign_color: darkColorScheme.$primary_dark_grey, + $address_tag_foreign_bgColor: darkColorScheme.$primary_light_grey, }; const elevation: Elevation = { diff --git a/packages/ui/src/design-tokens/theme/light-theme.css.ts b/packages/ui/src/design-tokens/theme/light-theme.css.ts index 8ba4cc1f0..fd380d355 100644 --- a/packages/ui/src/design-tokens/theme/light-theme.css.ts +++ b/packages/ui/src/design-tokens/theme/light-theme.css.ts @@ -437,6 +437,16 @@ const colors: Colors = { $info_bar_container_bgColor: lightColorScheme.$secondary_cream, $info_bar_icon_color: lightColorScheme.$secondary_data_pink, $info_bar_message_color: lightColorScheme.$primary_black, + + $address_tag_own_color: lightColorScheme.$secondary_data_pink, + $address_tag_own_bgColor: rgba(lightColorScheme.$secondary_data_pink, 0.1), + $address_tag_handle_color: lightColorScheme.$primary_accent_purple, + $address_tag_handle_bgColor: rgba( + lightColorScheme.$primary_accent_purple, + 0.1, + ), + $address_tag_foreign_color: lightColorScheme.$primary_dark_grey, + $address_tag_foreign_bgColor: lightColorScheme.$primary_light_grey, }; export const elevation: Elevation = {