diff --git a/apps/browser-extension-wallet/src/features/dapp/components/ConfirmTransaction.tsx b/apps/browser-extension-wallet/src/features/dapp/components/ConfirmTransaction.tsx index 37186dde44..e711aa50ed 100644 --- a/apps/browser-extension-wallet/src/features/dapp/components/ConfirmTransaction.tsx +++ b/apps/browser-extension-wallet/src/features/dapp/components/ConfirmTransaction.tsx @@ -14,16 +14,21 @@ import { consumeRemoteApi, exposeApi, RemoteApiPropertyType } from '@cardano-sdk import { DappDataService } from '@lib/scripts/types'; import { DAPP_CHANNELS } from '@src/utils/constants'; import { runtime } from 'webextension-polyfill'; -import { useRedirection } from '@hooks'; -import { assetsBurnedInspector, assetsMintedInspector, createTxInspector } from '@cardano-sdk/core'; +import { useFetchCoinPrice, useRedirection } from '@hooks'; +import { + assetsBurnedInspector, + assetsMintedInspector, + createTxInspector, + AssetsMintedInspection, + MintedAsset +} from '@cardano-sdk/core'; import { Skeleton } from 'antd'; import { dAppRoutePaths } from '@routes'; import { UserPromptService } from '@lib/scripts/background/services'; import { of } from 'rxjs'; -import { CardanoTxOut } from '@src/types'; import { getAssetsInformation, TokenInfo } from '@src/utils/get-assets-information'; import * as HardwareLedger from '../../../../../../node_modules/@cardano-sdk/hardware-ledger/dist/cjs'; -import { useAnalyticsContext } from '@providers'; +import { useCurrencyStore, useAnalyticsContext } from '@providers'; import { TX_CREATION_TYPE_KEY, TxCreationType } from '@providers/AnalyticsProvider/analyticsTracker'; import { txSubmitted$ } from '@providers/AnalyticsProvider/onChain'; @@ -39,6 +44,45 @@ const dappDataApi = consumeRemoteApi>( { logger: console, runtime } ); +const convertMetadataArrayToObj = (arr: unknown[]): Record => { + const result: Record = {}; + for (const item of arr) { + if (typeof item === 'object' && !Array.isArray(item) && item !== null) { + Object.assign(result, item); + } + } + return result; +}; + +// eslint-disable-next-line complexity, sonarjs/cognitive-complexity +const getAssetNameFromMintMetadata = (asset: MintedAsset, metadata: Wallet.Cardano.TxMetadata): string | undefined => { + if (!asset || !metadata) return; + const decodedAssetName = Buffer.from(asset.assetName, 'hex').toString(); + + // Tries to find the asset name in the tx metadata under label 721 or 20 + for (const [key, value] of metadata.entries()) { + // eslint-disable-next-line no-magic-numbers + if (key !== BigInt(721) && key !== BigInt(20)) return; + const cip25Metadata = Wallet.cardanoMetadatumToObj(value); + if (!Array.isArray(cip25Metadata)) return; + + // cip25Metadata should be an array containing all policies for the minted assets in the tx + const policyLevelMetadata = convertMetadataArrayToObj(cip25Metadata)[asset.policyId]; + if (!Array.isArray(policyLevelMetadata)) return; + + // policyLevelMetadata should be an array of objects with the minted assets names as key + // e.g. "policyId" = [{ "AssetName1": { ...metadataAsset1 } }, { "AssetName2": { ...metadataAsset2 } }]; + const assetProperties = convertMetadataArrayToObj(policyLevelMetadata)?.[decodedAssetName]; + if (!Array.isArray(assetProperties)) return; + + // assetProperties[decodedAssetName] should be an array of objects with the properties as keys + // e.g. [{ "name": "Asset Name" }, { "description": "An asset" }, ...] + const assetMetadataName = convertMetadataArrayToObj(assetProperties)?.name; + // eslint-disable-next-line consistent-return + return typeof assetMetadataName === 'string' ? assetMetadataName : undefined; + } +}; + // eslint-disable-next-line sonarjs/cognitive-complexity export const ConfirmTransaction = withAddressBookContext((): React.ReactElement => { const { @@ -49,9 +93,12 @@ export const ConfirmTransaction = withAddressBookContext((): React.ReactElement walletInfo, inMemoryWallet, getKeyAgentType, - blockchainProvider: { assetProvider } + blockchainProvider: { assetProvider }, + walletUI: { cardanoCoin } } = useWalletStore(); + const { fiatCurrency } = useCurrencyStore(); const { list: addressList } = useAddressBookContext(); + const { priceResult } = useFetchCoinPrice(); const analytics = useAnalyticsContext(); const [tx, setTx] = useState(); @@ -67,20 +114,23 @@ export const ConfirmTransaction = withAddressBookContext((): React.ReactElement const [assetsInfo, setAssetsInfo] = useState(); const [dappInfo, setDappInfo] = useState(); - const getTransactionAssetsId = (outputs: CardanoTxOut[]) => { - const assetIds: Wallet.Cardano.AssetId[] = []; - const assetMaps = outputs.map((output) => output.value.assets); + // All assets' ids in the transaction body. Used to fetch their info from cardano services + const assetIds = useMemo(() => { + const uniqueAssetIds = new Set(); + // Merge all assets (TokenMaps) from the tx outputs and mint + const assetMaps = tx?.body?.outputs?.map((output) => output.value.assets) ?? []; + if (tx?.body?.mint?.size > 0) assetMaps.push(tx.body.mint); + + // Extract all unique asset ids from the array of TokenMaps for (const asset of assetMaps) { if (asset) { for (const id of asset.keys()) { - !assetIds.includes(id) && assetIds.push(id); + !uniqueAssetIds.has(id) && uniqueAssetIds.add(id); } } } - return assetIds; - }; - - const assetIds = useMemo(() => tx?.body?.outputs && getTransactionAssetsId(tx.body.outputs), [tx?.body?.outputs]); + return [...uniqueAssetIds.values()]; + }, [tx]); useEffect(() => { if (assetIds?.length > 0) { @@ -154,16 +204,38 @@ export const ConfirmTransaction = withAddressBookContext((): React.ReactElement }); }, []); + const createMintedList = useCallback( + (mintedAssets: AssetsMintedInspection) => { + if (!assetsInfo) return []; + return mintedAssets.map((asset) => { + const assetId = Wallet.Cardano.AssetId.fromParts(asset.policyId, asset.assetName); + const assetInfo = assets.get(assetId) || assetsInfo?.get(assetId); + // If it's a new asset or the name is being updated we should be getting it from the tx metadata + const metadataName = getAssetNameFromMintMetadata(asset, tx?.auxiliaryData?.blob); + return { + name: assetInfo?.name.toString() || asset.fingerprint || assetId, + ticker: + metadataName ?? + assetInfo?.nftMetadata?.name ?? + assetInfo?.tokenMetadata?.ticker ?? + assetInfo?.tokenMetadata?.name ?? + asset.fingerprint.toString(), + amount: Wallet.util.calculateAssetBalance(asset.quantity, assetInfo) + }; + }); + }, + [assets, assetsInfo, tx] + ); + const createAssetList = useCallback( (txAssets: Wallet.Cardano.TokenMap) => { if (!assetsInfo) return []; const assetList: Wallet.Cip30SignTxAssetItem[] = []; - // eslint-disable-next-line unicorn/no-array-for-each txAssets.forEach(async (value, key) => { const walletAsset = assets.get(key) || assetsInfo?.get(key); assetList.push({ - name: walletAsset.name.toString() || key.toString(), - ticker: walletAsset.tokenMetadata?.ticker || walletAsset.nftMetadata?.name, + name: walletAsset?.name.toString() || key.toString(), + ticker: walletAsset?.tokenMetadata?.ticker || walletAsset?.nftMetadata?.name, amount: Wallet.util.calculateAssetBalance(value, walletAsset) }); }); @@ -185,17 +257,9 @@ export const ConfirmTransaction = withAddressBookContext((): React.ReactElement }); const { minted, burned } = inspector(tx as Wallet.Cardano.HydratedTx); - const isMintTransaction = minted.length > 0; - const isBurnTransaction = burned.length > 0; + const isMintTransaction = minted.length > 0 || burned.length > 0; - let txType: 'Send' | 'Mint' | 'Burn'; - if (isMintTransaction) { - txType = 'Mint'; - } else if (isBurnTransaction) { - txType = 'Burn'; - } else { - txType = 'Send'; - } + const txType = isMintTransaction ? 'Mint' : 'Send'; const externalOutputs = tx.body.outputs.filter((output) => { if (txType === 'Send') { @@ -223,17 +287,11 @@ export const ConfirmTransaction = withAddressBookContext((): React.ReactElement return { fee: Wallet.util.lovelacesToAdaString(tx.body.fee.toString()), outputs: txSummaryOutputs, - type: txType + type: txType, + mintedAssets: createMintedList(minted), + burnedAssets: createMintedList(burned) }; - }, [tx, walletInfo.addresses, createAssetList, addressToNameMap]); - - const translations = { - transaction: t('core.dappTransaction.transaction'), - amount: t('core.dappTransaction.amount'), - recipient: t('core.dappTransaction.recipient'), - fee: t('core.dappTransaction.fee'), - adaFollowingNumericValue: t('general.adaFollowingNumericValue') - }; + }, [tx, walletInfo.addresses, createAssetList, createMintedList, addressToNameMap]); const onConfirm = () => { analytics.sendEventToPostHog(PostHogAction.SendTransactionSummaryConfirmClick, { @@ -256,7 +314,9 @@ export const ConfirmTransaction = withAddressBookContext((): React.ReactElement transaction={txSummary} dappInfo={dappInfo} errorMessage={errorMessage} - translations={translations} + fiatCurrencyCode={fiatCurrency?.code} + fiatCurrencyPrice={priceResult?.cardano?.price} + coinSymbol={cardanoCoin.symbol} /> ) : ( diff --git a/apps/browser-extension-wallet/src/features/dapp/components/__tests__/ConfirmTransaction.test.tsx b/apps/browser-extension-wallet/src/features/dapp/components/__tests__/ConfirmTransaction.test.tsx index c82c6724d6..7acabf64d1 100644 --- a/apps/browser-extension-wallet/src/features/dapp/components/__tests__/ConfirmTransaction.test.tsx +++ b/apps/browser-extension-wallet/src/features/dapp/components/__tests__/ConfirmTransaction.test.tsx @@ -1,5 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable import/imports-first */ +import * as CurrencyProvider from '@providers/currency'; + const mockGetKeyAgentType = jest.fn(); const mockUseWalletStore = jest.fn(); const error = 'error in getSignTxData'; @@ -7,6 +9,7 @@ const mockConsumeRemoteApi = jest.fn().mockReturnValue({ getSignTxData: async () => await Promise.reject(error) }); const mockCreateTxInspector = jest.fn().mockReturnValue(() => ({ minted: [] as any, burned: [] as any })); +const mockUseCurrencyStore = jest.fn().mockReturnValue({ fiatCurrency: { code: 'usd', symbol: '$' } }); import * as React from 'react'; import { cleanup, render, waitFor } from '@testing-library/react'; import { ConfirmTransaction } from '../ConfirmTransaction'; @@ -31,6 +34,8 @@ import { postHogClientMocks } from '@src/utils/mocks/test-helpers'; const assetInfo$ = new BehaviorSubject(new Map()); const available$ = new BehaviorSubject([]); +const tokenPrices$ = new BehaviorSubject({}); +const adaPrices$ = new BehaviorSubject({}); const assetProvider = { getAsset: () => ({}), @@ -50,6 +55,11 @@ jest.mock('@src/stores', () => ({ useWalletStore: mockUseWalletStore })); +jest.mock('@providers/currency', (): typeof CurrencyProvider => ({ + ...jest.requireActual('@providers/currency'), + useCurrencyStore: mockUseCurrencyStore +})); + jest.mock('@cardano-sdk/web-extension', () => { const original = jest.requireActual('@cardano-sdk/web-extension'); return { @@ -74,7 +84,8 @@ const testIds = { const backgroundService = { getBackgroundStorage: jest.fn(), - setBackgroundStorage: jest.fn() + setBackgroundStorage: jest.fn(), + coinPrices: { tokenPrices$, adaPrices$ } } as unknown as BackgroundServiceAPIProviderProps['value']; const getWrapper = diff --git a/apps/browser-extension-wallet/src/lib/translations/en.json b/apps/browser-extension-wallet/src/lib/translations/en.json index a383db68f8..fcc7e662a1 100644 --- a/apps/browser-extension-wallet/src/lib/translations/en.json +++ b/apps/browser-extension-wallet/src/lib/translations/en.json @@ -69,6 +69,18 @@ "noMatchPassword": "Oops! The passwords don't match.", "secondLevelPasswordStrengthFeedback": "Getting there! Add some symbols and numbers to make it stronger.", "firstLevelPasswordStrengthFeedback": "Weak password. Add some numbers and characters to make it stronger." + }, + "dappTransaction": { + "asset": "Asset", + "burn": "Burn", + "fee": "Transaction Fee", + "insufficientFunds": "You do not have enough funds to complete the transaction", + "mint": "Mint", + "quantity": "Quantity", + "recipient": "Recipient", + "send": "Send", + "sending": "Sending", + "transaction": "Transaction" } }, "tab.main.title": "Tab extension", diff --git a/packages/cardano/src/wallet/types.ts b/packages/cardano/src/wallet/types.ts index 75707709dd..ab4164389f 100644 --- a/packages/cardano/src/wallet/types.ts +++ b/packages/cardano/src/wallet/types.ts @@ -35,7 +35,9 @@ export type Cip30SignTxSummary = { recipient: string; assets?: Cip30SignTxAssetItem[]; }[]; - type: 'Send' | 'Mint' | 'Burn'; + type: 'Send' | 'Mint'; + mintedAssets?: Cip30SignTxAssetItem[]; + burnedAssets?: Cip30SignTxAssetItem[]; }; export type Cip30SignTxAssetItem = { diff --git a/packages/core/package.json b/packages/core/package.json index a74b1549fc..6001c10540 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -39,6 +39,7 @@ "dependencies": { "@ant-design/icons": "^4.7.0", "@lace/common": "0.1.0", + "@lace/ui": "^0.1.0", "antd": "^4.24.10", "axios": "0.21.4", "axios-cache-adapter": "2.7.3", diff --git a/packages/core/src/ui/components/ActivityDetail/TransactionDetails.module.scss b/packages/core/src/ui/components/ActivityDetail/TransactionDetails.module.scss index a11c7e8b65..73fdb278f2 100644 --- a/packages/core/src/ui/components/ActivityDetail/TransactionDetails.module.scss +++ b/packages/core/src/ui/components/ActivityDetail/TransactionDetails.module.scss @@ -149,27 +149,6 @@ $border-bottom: 1px solid var(--light-mode-light-grey-plus, var(--dark-mode-mid- } } - .txFeeContainer { - display: flex; - align-items: center; - justify-content: center; - gap: size_unit(1); - - .txfee { - color: var(--text-color-primary); - font-size: var(--body, 16px); - font-weight: 600; - line-height: size_unit(3); - } - - .infoIcon { - width: size_unit(2); - height: size_unit(2); - margin-bottom: -#{size_unit(0.5)}; - color: var(--text-color-secondary); - } - } - .metadataLabel { display: flex; flex: 0 0 50%; @@ -266,7 +245,7 @@ $border-bottom: 1px solid var(--light-mode-light-grey-plus, var(--dark-mode-mid- margin-bottom: size_unit(2); .poolsTitle { - @include text-body-semi-bold + @include text-body-semi-bold; } .poolsList { @@ -280,7 +259,7 @@ $border-bottom: 1px solid var(--light-mode-light-grey-plus, var(--dark-mode-mid- .poolHeading { display: flex; justify-content: flex-end; - gap: size_unit(1) + gap: size_unit(1); } .poolRewardAmount { diff --git a/packages/core/src/ui/components/ActivityDetail/TransactionDetails.tsx b/packages/core/src/ui/components/ActivityDetail/TransactionDetails.tsx index e54e20cc15..bc40229b6c 100644 --- a/packages/core/src/ui/components/ActivityDetail/TransactionDetails.tsx +++ b/packages/core/src/ui/components/ActivityDetail/TransactionDetails.tsx @@ -1,16 +1,15 @@ /* eslint-disable no-magic-numbers */ import React from 'react'; import cn from 'classnames'; -import { InfoCircleOutlined } from '@ant-design/icons'; -import { Tooltip } from 'antd'; -import styles from './TransactionDetails.module.scss'; -import { TransactionDetailAsset, TxOutputInput, TransactionMetadataProps, TxSummary } from './TransactionDetailAsset'; -import type { ActivityStatus } from '../Activity'; +import { TransactionDetailAsset, TransactionMetadataProps, TxOutputInput, TxSummary } from './TransactionDetailAsset'; import { Ellipsis, toast } from '@lace/common'; -import { ReactComponent as Info } from '../../assets/icons/info-icon.component.svg'; -import { TransactionInputOutput } from './TransactionInputOutput'; +import { Box } from '@lace/ui'; import { useTranslate } from '@src/ui/hooks'; import CopyToClipboard from 'react-copy-to-clipboard'; +import type { ActivityStatus } from '../Activity'; +import styles from './TransactionDetails.module.scss'; +import { TransactionInputOutput } from './TransactionInputOutput'; +import { TransactionFee } from './TransactionFee'; import { ActivityDetailHeader } from './ActivityDetailHeader'; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -265,31 +264,11 @@ export const TransactionDetails = ({  {includedTime} - {fee && fee !== '-' && ( -
-
-
{t('package.core.activityDetails.transactionFee')}
- - {Info ? ( - - ) : ( - - )} - -
- -
-
- {`${fee} ${coinSymbol}`} - - {amountTransformer(fee)} - -
-
-
+ + + )} - {deposit && renderDepositValueSection({ value: deposit, label: t('package.core.activityDetails.deposit') })} {depositReclaim && renderDepositValueSection({ diff --git a/packages/core/src/ui/components/ActivityDetail/TransactionFee.module.scss b/packages/core/src/ui/components/ActivityDetail/TransactionFee.module.scss new file mode 100644 index 0000000000..7ba4758637 --- /dev/null +++ b/packages/core/src/ui/components/ActivityDetail/TransactionFee.module.scss @@ -0,0 +1,109 @@ +@import '../../styles/theme.scss'; +@import '../../../../../common/src/ui/styles/abstracts/typography'; + +.txFeeContainer { + display: flex; + align-items: center; + justify-content: center; + gap: size_unit(1); +} + +.txfee { + color: var(--text-color-primary); + font-size: var(--body, 16px); + font-weight: 600; + line-height: size_unit(3); +} + +.details { + color: var(--text-color-primary, #ffffff); + display: flex; + justify-content: space-between; + align-items: flex-start; + width: 100%; + + .title { + display: flex; + flex: 0 0 50%; + align-self: baseline; + color: var(--text-color-primary, #ffffff); + @include text-body-semi-bold; + } + + .detail { + align-items: end; + display: flex; + flex-direction: column; + gap: size_unit(2); + color: var(--text-color-primary, #ffffff); + text-align: right; + word-break: break-all; + @include text-body-medium; + + @media (max-width: $breakpoint-popup) { + flex-direction: column; + } + + &.hash { + @include text-address; + font-weight: 500; + text-align: right; + cursor: pointer; + } + &.txLink { + color: var(--text-color-blue, #3489f7); + line-height: 17px; + @media (max-width: $breakpoint-popup) { + flex: 60%; + } + } + &.poolId { + color: var(--text-color-secondary); + font-size: var(--bodySmall); + font-weight: 500; + line-height: 17px; + } + } + + .timestamp { + flex: 0 0 35%; + } + + .amount { + display: flex; + flex-direction: column; + width: 100%; + align-items: flex-end; + + .ada { + color: var(--text-color-primary, #ffffff); + } + + .fiat { + color: var(--text-color-secondary, #878e9e); + } + + .addrName { + margin-bottom: size_unit(1); + } + } + + .addressDetail { + font-size: var(--bodySmall, 14px); + font-weight: 400; + line-height: size_unit(2); + text-align: right; + margin-bottom: size_unit(5); + @media (max-width: $breakpoint-popup) { + margin-bottom: size_unit(6); + } + } + + .metadataLabel { + display: flex; + flex: 0 0 50%; + align-self: baseline; + @include text-bodyLarge-bold; + color: var(--text-color-primary); + } +} diff --git a/packages/core/src/ui/components/ActivityDetail/TransactionFee.tsx b/packages/core/src/ui/components/ActivityDetail/TransactionFee.tsx new file mode 100644 index 0000000000..bab28f0a2c --- /dev/null +++ b/packages/core/src/ui/components/ActivityDetail/TransactionFee.tsx @@ -0,0 +1,36 @@ +/* eslint-disable no-magic-numbers */ +import React from 'react'; +import { InfoCircleOutlined } from '@ant-design/icons'; +import { Tooltip } from 'antd'; +import styles from './TransactionFee.module.scss'; +import { ReactComponent as Info } from '../../assets/icons/info-icon.component.svg'; +import { useTranslate } from '@src/ui/hooks'; + +export interface TransactionFeeProps { + fee: string; + amountTransformer: (amount: string) => string; + coinSymbol: string; +} +export const TransactionFee = ({ fee, amountTransformer, coinSymbol }: TransactionFeeProps): React.ReactElement => { + const { t } = useTranslate(); + + return ( +
+
+
{t('package.core.activityDetails.transactionFee')}
+ + {Info ? : } + +
+ +
+
+ {`${fee} ${coinSymbol}`} + + {amountTransformer(fee)} + +
+
+
+ ); +}; diff --git a/packages/core/src/ui/components/ActivityDetail/index.ts b/packages/core/src/ui/components/ActivityDetail/index.ts index 26ccb7a4b5..7ba95eea83 100644 --- a/packages/core/src/ui/components/ActivityDetail/index.ts +++ b/packages/core/src/ui/components/ActivityDetail/index.ts @@ -3,3 +3,4 @@ export * from './RewardsDetails'; export * from './ActivityTypeIcon'; export * from './TransactionDetailAsset'; export * from './TransactionInputOutput'; +export * from './TransactionFee'; diff --git a/packages/core/src/ui/components/DappTransaction/DappTransaction.module.scss b/packages/core/src/ui/components/DappTransaction/DappTransaction.module.scss index ac80eec8e0..ea1eb99b34 100644 --- a/packages/core/src/ui/components/DappTransaction/DappTransaction.module.scss +++ b/packages/core/src/ui/components/DappTransaction/DappTransaction.module.scss @@ -1,5 +1,6 @@ @import '../../styles/theme.scss'; @import '../../../../../common/src/ui/styles/abstracts/_typography.scss'; + .dappInfo { margin: size_unit(1) 0px; } @@ -12,92 +13,14 @@ margin: size_unit(4) 0 size_unit(2) 0px; padding: size_unit(3) 0; border-top: 2px solid var(--light-mode-light-grey-plus, var(--dark-mode-mid-grey)); + gap: size_unit(3); } .error { margin: size_unit(2) 0px; } -.header { - font-size: var(--bodyLarge); - letter-spacing: -0.015em; - margin-bottom: size_unit(1); - display: flex; - justify-content: space-between; - align-items: center; - - .title { - font-weight: 600; - line-height: size_unit(3); - /* or 133% */ - /* Secondary - Black */ - color: var(--text-color-primary); - } - .type { - font-weight: 500; - line-height: size_unit(4); - /* or 178% */ - text-align: right; - /* Primary - Purple */ - color: var(--primary-default, #7f5af0); - } -} -.body { - display: flex; - flex-direction: column; - gap: size_unit(2); -} -.detail { - display: flex; - justify-content: space-between; - align-items: baseline; - - > * { - display: flex; - flex: 0 1 50%; - min-width: 0; - } - - .title { - font-weight: 500; - font-size: var(--body); - line-height: size_unit(3); - /* or 150% */ - /* Secondary - Black */ - color: var(--text-color-primary); - text-align: right; - } - .value { - display: flex; - align-items: flex-end; - flex-direction: column; - - font-size: var(--bodySmall); - font-weight: 500; - line-height: size_unit(2); - /* Secondary - Black */ - color: var(--text-color-primary); - - .bold { - font-weight: 600; - line-height: size_unit(3); - font-size: var(--body); - word-break: break-all; - } - - .rightAligned { - text-align: right; - > div { - justify-content: flex-end; - } - div, - p { - text-align: right; - } - } - } -} .warningAlert { flex-direction: row; background: var(--lace-cream); @@ -118,15 +41,12 @@ margin: 0; } } -:global(.__react_component_tooltip) { - @include tooltip-default; -} -.sub { - @include text-bodySmall-medium; - /* or 171% */ - letter-spacing: -0.015em; - /* Data - Dark Grey */ +.feeContainer { + display: flex; + flex-direction: row; + align-items: flex-start; + justify-content: space-between; + @include text-body-semi-bold; color: var(--text-color-primary); - text-align: right; } diff --git a/packages/core/src/ui/components/DappTransaction/DappTransaction.tsx b/packages/core/src/ui/components/DappTransaction/DappTransaction.tsx index d7c4da265c..9f5717baae 100644 --- a/packages/core/src/ui/components/DappTransaction/DappTransaction.tsx +++ b/packages/core/src/ui/components/DappTransaction/DappTransaction.tsx @@ -1,21 +1,21 @@ +/* eslint-disable sonarjs/no-duplicate-string */ import React from 'react'; -import { Ellipsis, ErrorPane } from '@lace/common'; +import { ErrorPane } from '@lace/common'; +import { Wallet } from '@lace/cardano'; import { DappInfo, DappInfoProps } from '../DappInfo'; +import { DappTxHeader } from './DappTxHeader/DappTxHeader'; +import { DappTxAsset, DappTxAssetProps } from './DappTxAsset/DappTxAsset'; +import { DappTxOutput, DappTxOutputProps } from './DappTxOutput/DappTxOutput'; import styles from './DappTransaction.module.scss'; -import { TranslationsFor } from '@ui/utils/types'; +import { useTranslate } from '@src/ui/hooks'; +import { TransactionFee } from '@ui/components/ActivityDetail'; type TransactionDetails = { fee: string; - outputs: { - coins: string; - recipient: string; - assets?: { - name: string; - amount: string; - ticker?: string; - }[]; - }[]; - type: 'Send' | 'Mint' | 'Burn'; + outputs: DappTxOutputProps[]; + type: 'Send' | 'Mint'; + mintedAssets?: DappTxAssetProps[]; + burnedAssets?: DappTxAssetProps[]; }; export interface DappTransactionProps { @@ -25,67 +25,68 @@ export interface DappTransactionProps { dappInfo: Omit; /** Optional error message */ errorMessage?: string; - translations: TranslationsFor<'transaction' | 'amount' | 'recipient' | 'fee' | 'adaFollowingNumericValue'>; + fiatCurrencyCode?: string; + fiatCurrencyPrice?: number; + coinSymbol?: string; } export const DappTransaction = ({ - transaction: { type, outputs, fee }, + transaction: { type, outputs, fee, mintedAssets, burnedAssets }, dappInfo, errorMessage, - translations -}: DappTransactionProps): React.ReactElement => ( -
- - {errorMessage && } -
-
-
- {translations.transaction} -
-
- {type} -
+ fiatCurrencyCode, + fiatCurrencyPrice, + coinSymbol +}: DappTransactionProps): React.ReactElement => { + const { t } = useTranslate(); + return ( +
+ + {errorMessage && } +
+ {type === 'Mint' && mintedAssets?.length > 0 && ( + <> + + {mintedAssets.map((asset) => ( + + ))} + + )} + {type === 'Mint' && burnedAssets?.length > 0 && ( + <> + 0 ? undefined : t('package.core.dappTransaction.transaction')} + subtitle={t('package.core.dappTransaction.burn')} + /> + {burnedAssets.map((asset) => ( + + ))} + + )} + {type === 'Send' && ( + <> + + {outputs.map((output) => ( + + ))} + + )} + {fee && fee !== '-' && ( + + `${Wallet.util.convertAdaToFiat({ ada, fiat: fiatCurrencyPrice })} ${fiatCurrencyCode}` + } + coinSymbol={coinSymbol} + /> + )}
- {outputs.map((output) => ( -
-
-
- {translations.amount} -
-
-
- {output.coins.toString()} ADA -
- {outputs.length === 1 && ( -
- {translations.fee}: {fee.toString()} ADA -
- )} - {output.assets && - output.assets.map((asset) => ( -
- {asset.amount} {asset.ticker || asset.name} -
- ))} -
-
-
-
- {translations.recipient} -
-
- -
-
-
- ))} - {outputs.length > 1 && ( -
-
- {translations.fee}: {fee.toString()} ADA -
-
- )}
-
-); + ); +}; diff --git a/packages/core/src/ui/components/DappTransaction/DappTxAsset/DappTxAsset.module.scss b/packages/core/src/ui/components/DappTransaction/DappTxAsset/DappTxAsset.module.scss new file mode 100644 index 0000000000..0d5bf78af1 --- /dev/null +++ b/packages/core/src/ui/components/DappTransaction/DappTxAsset/DappTxAsset.module.scss @@ -0,0 +1,41 @@ +@import '../../../styles/theme.scss'; +@import '../../../../../../common/src/ui/styles/abstracts/_typography.scss'; + +.body { + display: flex; + flex-direction: column; + gap: size_unit(3); + background-color: var(--light-mode-light-grey, var(--dark-mode-grey, #f9f9f9)); + border-radius: size_unit(2); + padding: size_unit(2); + + .detail { + @include text-body-semi-bold; + display: flex; + justify-content: space-between; + align-items: center; + + .title { + color: var(--text-color-primary); + line-height: 1; + } + + .value { + display: flex; + max-width: 50%; + align-items: flex-end; + flex-direction: column; + gap: size_unit(2); + + font-size: var(--bodySmall); + font-weight: 500; + line-height: 1; + /* Secondary - Black */ + color: var(--text-color-primary); + } + + .ellipsis > p { + margin: 0; + } + } +} diff --git a/packages/core/src/ui/components/DappTransaction/DappTxAsset/DappTxAsset.tsx b/packages/core/src/ui/components/DappTransaction/DappTxAsset/DappTxAsset.tsx new file mode 100644 index 0000000000..f421b19c95 --- /dev/null +++ b/packages/core/src/ui/components/DappTransaction/DappTxAsset/DappTxAsset.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import styles from './DappTxAsset.module.scss'; +import { Ellipsis } from '@lace/common'; +import { useTranslate } from '@src/ui/hooks'; + +export interface DappTxAssetProps { + name: string; + amount: string; + ticker?: string; +} + +export const DappTxAsset = ({ amount, name, ticker }: DappTxAssetProps): React.ReactElement => { + const { t } = useTranslate(); + return ( +
+
+
{t('package.core.dappTransaction.asset')}
+
+ +
+
+
+
{t('package.core.dappTransaction.quantity')}
+
{amount}
+
+
+ ); +}; diff --git a/packages/core/src/ui/components/DappTransaction/DappTxHeader/DappTxHeader.module.scss b/packages/core/src/ui/components/DappTransaction/DappTxHeader/DappTxHeader.module.scss new file mode 100644 index 0000000000..94eefef49f --- /dev/null +++ b/packages/core/src/ui/components/DappTransaction/DappTxHeader/DappTxHeader.module.scss @@ -0,0 +1,27 @@ +@import '../../../styles/theme.scss'; +@import '../../../../../../common/src/ui/styles/abstracts/_typography.scss'; + +.header { + font-size: var(--bodyLarge); + letter-spacing: -0.015em; + margin-bottom: size_unit(1); + display: flex; + justify-content: space-between; + align-items: center; + + .title { + font-weight: 600; + line-height: size_unit(3); + /* or 133% */ + /* Secondary - Black */ + color: var(--text-color-primary); + } + .type { + font-weight: 500; + line-height: size_unit(4); + /* or 178% */ + text-align: right; + /* Primary - Purple */ + color: var(--primary-default, #7f5af0); + } +} diff --git a/packages/core/src/ui/components/DappTransaction/DappTxHeader/DappTxHeader.tsx b/packages/core/src/ui/components/DappTransaction/DappTxHeader/DappTxHeader.tsx new file mode 100644 index 0000000000..8117fcd3e0 --- /dev/null +++ b/packages/core/src/ui/components/DappTransaction/DappTxHeader/DappTxHeader.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import styles from './DappTxHeader.module.scss'; + +export interface DappTxHeaderProps { + title?: string; + subtitle?: string; +} + +export const DappTxHeader = (props: DappTxHeaderProps): React.ReactElement => ( +
+
+ {props?.title ?? ''} +
+ {props?.subtitle && ( +
+ {props.subtitle} +
+ )} +
+); diff --git a/packages/core/src/ui/components/DappTransaction/DappTxOutput/DappTxOutput.module.scss b/packages/core/src/ui/components/DappTransaction/DappTxOutput/DappTxOutput.module.scss new file mode 100644 index 0000000000..04c3b12f0a --- /dev/null +++ b/packages/core/src/ui/components/DappTransaction/DappTxOutput/DappTxOutput.module.scss @@ -0,0 +1,67 @@ +@import '../../../styles/theme.scss'; +@import '../../../../../../common/src/ui/styles/abstracts/_typography.scss'; + +.body { + display: flex; + flex-direction: column; + gap: size_unit(3); + background-color: var(--light-mode-light-grey, var(--dark-mode-grey, #f9f9f9)); + border-radius: size_unit(2); + padding: size_unit(2); +} + +.detail { + display: flex; + justify-content: space-between; + align-items: baseline; + + > * { + display: flex; + flex: 0 1 50%; + min-width: 0; + } + + .title { + font-weight: 500; + font-size: var(--body); + line-height: size_unit(3); + /* or 150% */ + /* Secondary - Black */ + color: var(--text-color-primary); + text-align: right; + } + .value { + display: flex; + align-items: flex-end; + flex-direction: column; + gap: size_unit(2); + + font-size: var(--bodySmall); + font-weight: 500; + line-height: size_unit(2); + /* Secondary - Black */ + color: var(--text-color-primary); + + .bold { + font-weight: 600; + line-height: size_unit(3); + font-size: var(--body); + word-break: break-all; + } + + .rightAligned { + text-align: right; + > div { + justify-content: flex-end; + } + div, + p { + text-align: right; + } + } + } +} + +:global(.__react_component_tooltip) { + @include tooltip-default; +} diff --git a/packages/core/src/ui/components/DappTransaction/DappTxOutput/DappTxOutput.tsx b/packages/core/src/ui/components/DappTransaction/DappTxOutput/DappTxOutput.tsx new file mode 100644 index 0000000000..d608929168 --- /dev/null +++ b/packages/core/src/ui/components/DappTransaction/DappTxOutput/DappTxOutput.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { Ellipsis } from '@lace/common'; +import { DappTxAssetProps } from '../DappTxAsset/DappTxAsset'; +import styles from './DappTxOutput.module.scss'; +import { useTranslate } from '@src/ui/hooks'; + +export interface DappTxOutputProps { + coins: string; + recipient: string; + assets?: DappTxAssetProps[]; +} + +export const DappTxOutput = ({ recipient, coins, assets }: DappTxOutputProps): React.ReactElement => { + const { t } = useTranslate(); + return ( +
+
+
+ {t('package.core.dappTransaction.sending')} +
+
+
+ {coins.toString()} ADA +
+ {assets?.map((asset) => ( +
+ {asset.amount} {asset.ticker || asset.name} +
+ ))} +
+
+
+
+ {t('package.core.dappTransaction.recipient')} +
+
+ +
+
+
+ ); +}; diff --git a/packages/core/src/ui/lib/translations/en.json b/packages/core/src/ui/lib/translations/en.json index c73e862cbd..7336752ffd 100644 --- a/packages/core/src/ui/lib/translations/en.json +++ b/packages/core/src/ui/lib/translations/en.json @@ -100,6 +100,18 @@ "noMatchPassword": "Oops! The passwords don't match.", "secondLevelPasswordStrengthFeedback": "Getting there! Add some symbols and numbers to make it stronger.", "firstLevelPasswordStrengthFeedback": "Weak password. Add some numbers and characters to make it stronger." + }, + "dappTransaction": { + "asset": "Asset", + "burn": "Burn", + "fee": "Transaction Fee", + "insufficientFunds": "You do not have enough funds to complete the transaction", + "mint": "Mint", + "quantity": "Quantity", + "recipient": "Recipient", + "send": "Send", + "sending": "Sending", + "transaction": "Transaction" } } } diff --git a/yarn.lock b/yarn.lock index 89c685363e..bab5d8ed6e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7962,6 +7962,7 @@ __metadata: dependencies: "@ant-design/icons": ^4.7.0 "@lace/common": 0.1.0 + "@lace/ui": ^0.1.0 "@types/debounce-promise": ^3.1.6 antd: ^4.24.10 axios: 0.21.4