diff --git a/apps/browser-extension-wallet/src/components/Layout/MainLayout.module.scss b/apps/browser-extension-wallet/src/components/Layout/MainLayout.module.scss index 1d6d8f2d1..984549d0d 100644 --- a/apps/browser-extension-wallet/src/components/Layout/MainLayout.module.scss +++ b/apps/browser-extension-wallet/src/components/Layout/MainLayout.module.scss @@ -21,6 +21,7 @@ display: flex; min-height: 0; width: 100%; + flex-direction: column; > * { width: 100%; } diff --git a/apps/browser-extension-wallet/src/features/dapp/components/Layout.tsx b/apps/browser-extension-wallet/src/features/dapp/components/Layout.tsx index 78b3e6b73..c89c6c1d8 100644 --- a/apps/browser-extension-wallet/src/features/dapp/components/Layout.tsx +++ b/apps/browser-extension-wallet/src/features/dapp/components/Layout.tsx @@ -3,7 +3,7 @@ import React from 'react'; import styles from './Layout.module.scss'; type layoutProps = { - title: string | React.ReactElement; + title?: string | React.ReactElement; children?: React.ReactElement | React.ReactNode; layoutClassname?: string; pageClassname?: string; diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmTransaction.module.scss b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmTransaction.module.scss index 3376b96ac..a884c336c 100644 --- a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmTransaction.module.scss +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmTransaction.module.scss @@ -3,23 +3,26 @@ .spaceBetween { justify-content: space-between; - padding-top: size_unit(2); + padding-top: size_unit(0); } -.layoutError { - padding: 0; +.transactionContainer { + display: flex; + flex-direction: column; + flex: 1; + justify-content: space-between; } .actions { @extend %flex-column; - background-color: var(--bg-color-body); - gap: size_unit(1); + height: size_unit(17.12); + justify-content: space-between; padding: size_unit(2) size_unit(3) size_unit(2) size_unit(3); - border-top: 2px solid var(--light-mode-light-grey-plus, var(--dark-mode-mid-grey)); - margin: size_unit(4) size_unit(-3) size_unit(-2) size_unit(-3); + border-top: 1px solid var(--light-mode-light-grey-plus, var(--dark-mode-mid-grey)); position: sticky; bottom: 0; z-index: 10; + background-color: var(--light-mode-body, var(--dark-mode-bg-black)); .actionBtn { width: 100%; } diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmTransaction.tsx b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmTransaction.tsx index a49bcd69a..7d3041c83 100644 --- a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmTransaction.tsx +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmTransaction.tsx @@ -88,11 +88,7 @@ export const ConfirmTransaction = (): React.ReactElement => { useOnBeforeUnload(disallowSignTx); return ( - + {req && txType ? ( setConfirmTransactionError(true)} /> ) : ( 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 a6fd22419..27e5ea00f 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 @@ -1,227 +1,188 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useState, useEffect, useMemo } from 'react'; import { useObservable } from '@lace/common'; -import { useWalletStore } from '@stores'; -import { Skeleton } from 'antd'; import { DappTransaction } from '@lace/core'; -import { TokenInfo, getAssetsInformation } from '@src/utils/get-assets-information'; -import { useAddressBookContext, withAddressBookContext } from '@src/features/address-book/context'; -import { AddressListType } from '@src/views/browser-view/features/activity'; -import { useCurrencyStore } from '@providers/currency'; -import { useFetchCoinPrice } from '@hooks'; -import { useViewsFlowContext } from '@providers'; +import { Flex } from '@lace/ui'; +import { useViewsFlowContext } from '@providers/ViewFlowProvider'; + import { Wallet } from '@lace/cardano'; +import { withAddressBookContext } from '@src/features/address-book/context'; +import { useWalletStore } from '@stores'; +import { exposeApi, RemoteApiPropertyType } from '@cardano-sdk/web-extension'; +import { DAPP_CHANNELS } from '@src/utils/constants'; +import { runtime } from 'webextension-polyfill'; +import { useFetchCoinPrice, useChainHistoryProvider } from '@hooks'; import { - AssetsMintedInspection, createTxInspector, - assetsMintedInspector, - assetsBurnedInspector, - MintedAsset + TransactionSummaryInspection, + transactionSummaryInspector, + Cardano, + TokenTransferValue, + tokenTransferInspector } from '@cardano-sdk/core'; +import { createWalletAssetProvider } from '@cardano-sdk/wallet'; +import { Skeleton } from 'antd'; +import type { UserPromptService } from '@lib/scripts/background/services'; +import { of, take } from 'rxjs'; + +import { useCurrencyStore, useAppSettingsContext } from '@providers'; +import { logger, signingCoordinator } from '@lib/wallet-api-ui'; import { useComputeTxCollateral } from '@hooks/useComputeTxCollateral'; +import { utxoAndBackendChainHistoryResolver } from '@src/utils/utxo-chain-history-resolver'; -interface Props { +interface DappTransactionContainerProps { errorMessage?: string; } -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 complexity, sonarjs/cognitive-complexity -export const DappTransactionContainer = withAddressBookContext(({ errorMessage }: Props): React.ReactElement => { - const { - walletInfo, - inMemoryWallet, - blockchainProvider: { assetProvider }, - walletUI: { cardanoCoin }, - walletState - } = useWalletStore(); - const { - signTxRequest: { request }, - dappInfo - } = useViewsFlowContext(); - const currencyStore = useCurrencyStore(); - const coinPrice = useFetchCoinPrice(); - const { list: addressList } = useAddressBookContext() as { list: AddressListType[] }; - const tx = useMemo(() => request?.transaction.toCore(), [request?.transaction]); - const assets = useObservable(inMemoryWallet.assetInfo$); - const [assetsInfo, setAssetsInfo] = useState(); - - const txCollateral = useComputeTxCollateral(walletState, tx); - - const assetIds = useMemo(() => { - if (!tx) return []; - 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()) { - !uniqueAssetIds.has(id) && uniqueAssetIds.add(id); - } - } - } - return [...uniqueAssetIds.values()]; - }, [tx]); - - useEffect(() => { - if (assetIds?.length > 0) { - getAssetsInformation(assetIds, assets, { - assetProvider, - extraData: { nftMetadata: true, tokenMetadata: true } - }) - .then((result) => setAssetsInfo(result)) - .catch((error) => { - console.error(error); - }); - } - }, [assetIds, assetProvider, assets]); - - 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) - }; +export const DappTransactionContainer = withAddressBookContext( + ({ errorMessage }: DappTransactionContainerProps): React.ReactElement => { + const { + signTxRequest: { request: req, set: setSignTxRequest }, + dappInfo + } = useViewsFlowContext(); + + const { + walletInfo, + inMemoryWallet, + blockchainProvider: { assetProvider }, + walletUI: { cardanoCoin }, + walletState + } = useWalletStore(); + + const { fiatCurrency } = useCurrencyStore(); + const { priceResult } = useFetchCoinPrice(); + + const [{ chainName }] = useAppSettingsContext(); + + const [fromAddressTokens, setFromAddressTokens] = useState< + Map | undefined + >(); + const [toAddressTokens, setToAddressTokens] = useState< + Map | undefined + >(); + const [transactionInspectionDetails, setTransactionInspectionDetails] = useState< + TransactionSummaryInspection | undefined + >(); + + const chainHistoryProvider = useChainHistoryProvider({ chainName }); + + const txInputResolver = useMemo( + () => + utxoAndBackendChainHistoryResolver({ + utxo: inMemoryWallet.utxo, + transactions: inMemoryWallet.transactions, + chainHistoryProvider + }), + [inMemoryWallet, chainHistoryProvider] + ); + + const tx = useMemo(() => req?.transaction.toCore(), [req?.transaction]); + const txCollateral = useComputeTxCollateral(walletState, tx); + + useEffect(() => { + const subscription = signingCoordinator.transactionWitnessRequest$.pipe(take(1)).subscribe(async (r) => { + setSignTxRequest(r); }); - }, - [assets, assetsInfo, tx] - ); - - const createAssetList = useCallback( - (txAssets: Wallet.Cardano.TokenMap) => { - if (!assetsInfo) return []; - const assetList: Wallet.Cip30SignTxAssetItem[] = []; - 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, - amount: Wallet.util.calculateAssetBalance(value, walletAsset) - }); - }); - return assetList; - }, - [assets, assetsInfo] - ); - - const addressToNameMap = useMemo( - () => new Map(addressList?.map((item: AddressListType) => [item.address, item.name])), - [addressList] - ); - - const [txSummary, setTxSummary] = useState(); - - useEffect(() => { - if (!tx) { - setTxSummary(void 0); - return; - } - const getTxSummary = async () => { - const inspector = createTxInspector({ - minted: assetsMintedInspector, - burned: assetsBurnedInspector - }); - - const { minted, burned } = await inspector(tx as Wallet.Cardano.HydratedTx); - const isMintTransaction = minted.length > 0 || burned.length > 0; - - const txType = isMintTransaction ? Wallet.Cip30TxType.Mint : Wallet.Cip30TxType.Send; - const externalOutputs = tx.body.outputs.filter((output) => { - if (txType === 'Send') { - return walletInfo.addresses.every((addr) => output.address !== addr.address); - } - return true; - }); + const api = exposeApi>( + { + api$: of({ + async readyToSignTx(): Promise { + return Promise.resolve(true); + } + }), + baseChannel: DAPP_CHANNELS.userPrompt, + properties: { readyToSignTx: RemoteApiPropertyType.MethodReturningPromise } + }, + { logger: console, runtime } + ); + + return () => { + subscription.unsubscribe(); + api.shutdown(); + }; + }, [setSignTxRequest]); + + const userAddresses = useMemo(() => walletInfo.addresses.map((v) => v.address), [walletInfo.addresses]); + const userRewardAccounts = useObservable(inMemoryWallet.delegation.rewardAccounts$); + const rewardAccountsAddresses = useMemo( + () => userRewardAccounts && userRewardAccounts.map((key) => key.address), + [userRewardAccounts] + ); + const protocolParameters = useObservable(inMemoryWallet?.protocolParameters$); + + useEffect(() => { + if (!req || !protocolParameters) { + setTransactionInspectionDetails(void 0); + return; + } + const getTxSummary = async () => { + const inspector = createTxInspector({ + tokenTransfer: tokenTransferInspector({ + inputResolver: txInputResolver, + fromAddressAssetProvider: createWalletAssetProvider({ + assetProvider, + assetInfo$: inMemoryWallet.assetInfo$, + logger + }), + toAddressAssetProvider: createWalletAssetProvider({ + assetProvider, + assetInfo$: inMemoryWallet.assetInfo$, + tx, + logger + }) + }), + summary: transactionSummaryInspector({ + addresses: userAddresses, + rewardAccounts: rewardAccountsAddresses, + inputResolver: txInputResolver, + protocolParameters, + assetProvider: createWalletAssetProvider({ + assetProvider, + assetInfo$: inMemoryWallet.assetInfo$, + tx, + logger + }) + }) + }); - const txSummaryOutputs: Wallet.Cip30SignTxSummary['outputs'] = externalOutputs.reduce((acc, txOut) => { - // Don't show withdrawl tx's etc - if (txOut.address.toString() === walletInfo.addresses[0].address.toString()) return acc; - - return [ - ...acc, - { - coins: Wallet.util.lovelacesToAdaString(txOut.value.coins.toString()), - recipient: addressToNameMap?.get(txOut.address.toString()) || txOut.address.toString(), - ...(txOut.value.assets?.size > 0 && { assets: createAssetList(txOut.value.assets) }) - } - ]; - }, []); - - // eslint-disable-next-line consistent-return - setTxSummary({ - fee: Wallet.util.lovelacesToAdaString(tx.body.fee.toString()), - outputs: txSummaryOutputs, - type: txType, - mintedAssets: createMintedList(minted), - burnedAssets: createMintedList(burned), - collateral: txCollateral ? Wallet.util.lovelacesToAdaString(txCollateral.toString()) : undefined - }); - }; - getTxSummary(); - }, [tx, walletInfo.addresses, createAssetList, createMintedList, addressToNameMap, setTxSummary, txCollateral]); - - return tx && txSummary ? ( - - ) : ( - - ); -}); + const { summary, tokenTransfer } = await inspector(tx as Wallet.Cardano.HydratedTx); + + const { toAddress, fromAddress } = tokenTransfer; + setToAddressTokens(toAddress); + setFromAddressTokens(fromAddress); + setTransactionInspectionDetails(summary); + }; + getTxSummary(); + }, [ + req, + walletInfo.addresses, + userAddresses, + rewardAccountsAddresses, + txInputResolver, + protocolParameters, + assetProvider, + inMemoryWallet.assetInfo$, + tx + ]); + + return ( + + {req && transactionInspectionDetails && dappInfo ? ( + + ) : ( + + )} + + ); + } +); diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/ConfirmTransaction.test.tsx b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/ConfirmTransaction.test.tsx index b7560a74d..c2847651c 100644 --- a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/ConfirmTransaction.test.tsx +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/ConfirmTransaction.test.tsx @@ -184,7 +184,6 @@ describe('Testing ConfirmTransaction component', () => { })); }); - expect(queryByTestId(testIds.layoutTitle)).toHaveTextContent(txType); expect(queryByTestId('ConfirmTransactionContent')).toBeInTheDocument(); expect(mockConfirmTransactionContent).toHaveBeenLastCalledWith( { 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 0922f14c4..98afa976f 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 @@ -19,6 +19,11 @@ const mockWithAddressBookContext = jest.fn((children) => children); const mockUseCurrencyStore = jest.fn().mockReturnValue({ fiatCurrency: { code: 'usd', symbol: '$' } }); const mockUseFetchCoinPrice = jest.fn().mockReturnValue({ priceResult: { cardano: { price: 2 }, tokens: new Map() } }); const mockUseComputeTxCollateral = jest.fn().mockReturnValue(BigInt(1_000_000)); +const mockUseChainHistoryProvider = jest.fn().mockReturnValue({ + transactionsByAddress: jest.fn().mockResolvedValue([]), + transactionsByHashes: jest.fn().mockResolvedValue([]), + blocksByHashes: jest.fn().mockResolvedValue([]) +}); import * as React from 'react'; import { cleanup, render } from '@testing-library/react'; import { DappTransactionContainer } from '../DappTransactionContainer'; @@ -35,7 +40,8 @@ import { cardanoCoin } from '@src/utils/constants'; const { Cardano, Crypto } = Wallet; const assetProvider = { - getAssets: jest.fn(() => ['assets']) + getAssets: jest.fn(() => ['assets']), + getAsset: jest.fn(() => 'asset') }; const walletInfo = { name: 'wall', @@ -44,6 +50,19 @@ const walletInfo = { const mockedAssetsInfo = new Map([['id', 'data']]); const assetInfo$ = new BehaviorSubject(mockedAssetsInfo); const available$ = new BehaviorSubject([]); +const signed$ = new BehaviorSubject([]); +const rewardAccounts$ = new BehaviorSubject([ + { + // eslint-disable-next-line unicorn/consistent-destructuring + address: Wallet.Cardano.RewardAccount('stake_test1uqrw9tjymlm8wrwq7jk68n6v7fs9qz8z0tkdkve26dylmfc2ux2hj'), + credentialStatus: 'REGISTERED', + rewardBalance: 1 + } +]); +const protocolParameters$ = new BehaviorSubject({ + stakeKeyDeposit: 1, + poolDeposit: 1 +}); const inMemoryWallet = { assetInfo$, @@ -51,7 +70,19 @@ const inMemoryWallet = { utxo: { available$ } - } + }, + utxo: { + available$ + }, + transactions: { + outgoing: { + signed$ + } + }, + delegation: { + rewardAccounts$ + }, + protocolParameters$ }; jest.mock('@src/stores', () => ({ @@ -79,6 +110,11 @@ jest.mock('@providers/currency', (): typeof CurrencyProvider => ({ useCurrencyStore: mockUseCurrencyStore })); +jest.mock('@hooks/useChainHistoryProvider', (): typeof CurrencyProvider => ({ + ...jest.requireActual('@hooks/useChainHistoryProvider'), + useChainHistoryProvider: mockUseChainHistoryProvider +})); + jest.mock('@lace/core', () => { const original = jest.requireActual('@lace/core'); return { @@ -142,8 +178,8 @@ const request = { } as any } as TransactionWitnessRequest; -jest.mock('@providers', () => ({ - ...jest.requireActual('@providers'), +jest.mock('@providers/ViewFlowProvider', () => ({ + ...jest.requireActual('@providers/ViewFlowProvider'), useViewsFlowContext: mockUseViewsFlowContext })); @@ -166,14 +202,14 @@ describe('Testing DappTransactionContainer component', () => { mockSkeleton.mockImplementation(() => ); mockUseViewsFlowContext.mockReset(); mockUseViewsFlowContext.mockImplementation(() => ({ - signTxRequest: { request }, + signTxRequest: { request, set: jest.fn() }, dappInfo })); }); afterEach(() => { jest.resetModules(); - jest.resetAllMocks(); + jest.clearAllMocks(); cleanup(); }); @@ -183,83 +219,120 @@ describe('Testing DappTransactionContainer component', () => { const errorMessage = 'errorMessage'; const props = { errorMessage }; - const txSummary = { - burnedAssets: [], - collateral: '1.00', - fee: '0.17', - mintedAssets: [ + const toAddress = new Map([ + [ + // eslint-disable-next-line unicorn/consistent-destructuring + Wallet.Cardano.PaymentAddress( + 'addr_test1qpfhhfy2qgls50r9u4yh0l7z67xpg0a5rrhkmvzcuqrd0znuzcjqw982pcftgx53fu5527z2cj2tkx2h8ux2vxsg475q9gw0lz' + ), { - amount: '3', - name: 'asset1rqluyux4nxv6kjashz626c8usp8g88unmqwnyh', - ticker: 'asset1rqluyux4nxv6kjashz626c8usp8g88unmqwnyh' + assets: new Map([ + [ + '659f2917fb63f12b33667463ee575eeac1845bbc736b9c0bbc40ba8254534c41', + { + amount: BigInt(9), + assetInfo: { + assetId: '659f2917fb63f12b33667463ee575eeac1845bbc736b9c0bbc40ba8254534c41', + fingerprint: 'asset1rqluyux4nxv6kjashz626c8usp8g88unmqwnyh', + name: '54534c41', + nftMetadata: null, + policyId: '659f2917fb63f12b33667463ee575eeac1845bbc736b9c0bbc40ba82', + quantity: BigInt(3), + supply: BigInt(3) + } + } + ], + [ + '6b8d07d69639e9413dd637a1a815a7323c69c86abbafb66dbfdb1aa7', + { + amount: BigInt(4), + assetInfo: { + assetId: '6b8d07d69639e9413dd637a1a815a7323c69c86abbafb66dbfdb1aa7', + fingerprint: 'asset1cvmyrfrc7lpht2hcjwr9lulzyyjv27uxh3kcz0', + name: '', + policyId: '6b8d07d69639e9413dd637a1a815a7323c69c86abbafb66dbfdb1aa7', + quantity: BigInt(0), + supply: BigInt(0) + } + } + ] + ]), + coins: BigInt(9_000_000) } ], - outputs: [ - { - coins: '5.00', - recipient: - 'addr_test1qpfhhfy2qgls50r9u4yh0l7z67xpg0a5rrhkmvzcuqrd0znuzcjqw982pcftgx53fu5527z2cj2tkx2h8ux2vxsg475q9gw0lz' - }, - { - assets: [ - { - amount: '3', - name: '659f2917fb63f12b33667463ee575eeac1845bbc736b9c0bbc40ba8254534c41', - ticker: undefined - }, - { - amount: '4', - name: '6b8d07d69639e9413dd637a1a815a7323c69c86abbafb66dbfdb1aa7', - ticker: undefined - } - ], - coins: '2.00', - recipient: - 'addr_test1qpfhhfy2qgls50r9u4yh0l7z67xpg0a5rrhkmvzcuqrd0znuzcjqw982pcftgx53fu5527z2cj2tkx2h8ux2vxsg475q9gw0lz' - }, + [ + // eslint-disable-next-line unicorn/consistent-destructuring + Wallet.Cardano.PaymentAddress( + 'addr_test1qq585l3hyxgj3nas2v3xymd23vvartfhceme6gv98aaeg9muzcjqw982pcftgx53fu5527z2cj2tkx2h8ux2vxsg475q2g7k3g' + ), { - assets: [ - { - amount: '6', - name: '659f2917fb63f12b33667463ee575eeac1845bbc736b9c0bbc40ba8254534c41', - ticker: undefined - } - ], - coins: '2.00', - recipient: - 'addr_test1qpfhhfy2qgls50r9u4yh0l7z67xpg0a5rrhkmvzcuqrd0znuzcjqw982pcftgx53fu5527z2cj2tkx2h8ux2vxsg475q9gw0lz' - }, - { - assets: [ - { - amount: '1', - name: '659f2917fb63f12b33667463ee575eeac1845bbc736b9c0bbc40ba8254534c41', - ticker: undefined - } - ], - coins: '2.00', - recipient: - 'addr_test1qq585l3hyxgj3nas2v3xymd23vvartfhceme6gv98aaeg9muzcjqw982pcftgx53fu5527z2cj2tkx2h8ux2vxsg475q2g7k3g' + assets: new Map([ + [ + '659f2917fb63f12b33667463ee575eeac1845bbc736b9c0bbc40ba8254534c41', + { + amount: BigInt(1), + assetInfo: { + assetId: '659f2917fb63f12b33667463ee575eeac1845bbc736b9c0bbc40ba8254534c41', + fingerprint: 'asset1rqluyux4nxv6kjashz626c8usp8g88unmqwnyh', + name: '54534c41', + nftMetadata: null, + policyId: '659f2917fb63f12b33667463ee575eeac1845bbc736b9c0bbc40ba82', + quantity: BigInt(3), + supply: BigInt(3) + } + } + ] + ]), + coins: BigInt(2_000_000) } - ], - type: 'Mint' - } as Wallet.Cip30SignTxSummary; + ] + ]); + + const txInspectionDetails = { + assets: new Map(), + coins: BigInt(0), + collateral: BigInt(0), + deposit: BigInt(1000), + fee: BigInt(170_000), + returnedDeposit: BigInt(0), + unresolved: { + inputs: [ + { + address: + 'addr_test1qq585l3hyxgj3nas2v3xymd23vvartfhceme6gv98aaeg9muzcjqw982pcftgx53fu5527z2cj2tkx2h8ux2vxsg475q2g7k3g', + index: 0, + txId: 'bb217abaca60fc0ca68c1555eca6a96d2478547818ae76ce6836133f3cc546e0' + } + ], + value: { + assets: new Map([ + ['6b8d07d69639e9413dd637a1a815a7323c69c86abbafb66dbfdb1aa7', BigInt(4)], + ['659f2917fb63f12b33667463ee575eeac1845bbc736b9c0bbc40ba8254534c41', BigInt(7)] + ]), + + coins: BigInt(11_171_000) + } + } + }; await act(async () => { ({ queryByTestId } = render(, { wrapper: getWrapper() })); }); - expect(queryByTestId('DappTransaction')).toBeInTheDocument(); + expect(mockDappTransaction).toHaveBeenLastCalledWith( { dappInfo, - transaction: txSummary, + toAddress, + fromAddress: new Map(), fiatCurrencyCode: 'usd', fiatCurrencyPrice: 2, errorMessage, - coinSymbol: 'ADA' + coinSymbol: 'ADA', + collateral: BigInt(1_000_000), + txInspectionDetails }, {} ); @@ -285,21 +358,4 @@ describe('Testing DappTransactionContainer component', () => { expect(queryByTestId('DappTransaction')).not.toBeInTheDocument(); expect(queryByTestId('skeleton')).toBeInTheDocument(); }); - - test('should render loader in case there is no txSummary', async () => { - let queryByTestId: any; - - mockUseCurrencyStore.mockRestore(); - - const signTxData = { tx: { body: {} } } as unknown as SignTxData; - - await act(async () => { - ({ queryByTestId } = render(, { - wrapper: getWrapper() - })); - }); - - expect(queryByTestId('DappTransaction')).not.toBeInTheDocument(); - expect(queryByTestId('skeleton')).toBeInTheDocument(); - }); }); diff --git a/apps/browser-extension-wallet/src/hooks/index.ts b/apps/browser-extension-wallet/src/hooks/index.ts index cccb01e6d..fccba25c0 100644 --- a/apps/browser-extension-wallet/src/hooks/index.ts +++ b/apps/browser-extension-wallet/src/hooks/index.ts @@ -21,3 +21,4 @@ export * from './useAssetInfo'; export * from './useUpdateAddressStatus'; export * from './useOnAddressSave'; export * from './useAppInit'; +export * from './useChainHistoryProvider'; diff --git a/apps/browser-extension-wallet/src/hooks/useChainHistoryProvider.ts b/apps/browser-extension-wallet/src/hooks/useChainHistoryProvider.ts new file mode 100644 index 000000000..413091435 --- /dev/null +++ b/apps/browser-extension-wallet/src/hooks/useChainHistoryProvider.ts @@ -0,0 +1,17 @@ +import { getBaseUrlForChain } from '@src/utils/chain'; +import { useMemo } from 'react'; +import { chainHistoryHttpProvider } from '@cardano-sdk/cardano-services-client'; +import { logger } from '@lib/wallet-api-ui'; + +export type NetworkType = 'Mainnet' | 'Preprod' | 'Preview' | 'Sanchonet'; + +type UseChainHistoryProviderArgs = { + chainName: NetworkType; +}; + +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export const useChainHistoryProvider = ({ chainName }: UseChainHistoryProviderArgs) => { + const baseCardanoServicesUrl = getBaseUrlForChain(chainName); + + return useMemo(() => chainHistoryHttpProvider({ baseUrl: baseCardanoServicesUrl, logger }), [baseCardanoServicesUrl]); +}; diff --git a/apps/browser-extension-wallet/src/lib/translations/en.json b/apps/browser-extension-wallet/src/lib/translations/en.json index 18624c8d8..503fb9500 100644 --- a/apps/browser-extension-wallet/src/lib/translations/en.json +++ b/apps/browser-extension-wallet/src/lib/translations/en.json @@ -123,7 +123,16 @@ "recipient": "Recipient", "send": "Send", "sending": "Sending", - "transaction": "Transaction" + "transaction": "Transaction", + "transactionSummary": "Transaction Summary", + "origin": "Origin", + "fromAddress": "From address", + "toAddress": "To address", + "deposit": "Deposit", + "returnedDeposit": "Returned deposit", + "address": "Address", + "nfts": "NFTs", + "tokens": "Tokens" } }, "tab.main.title": "Tab extension", diff --git a/apps/browser-extension-wallet/src/utils/utxo-chain-history-resolver.ts b/apps/browser-extension-wallet/src/utils/utxo-chain-history-resolver.ts new file mode 100644 index 000000000..0861c30cf --- /dev/null +++ b/apps/browser-extension-wallet/src/utils/utxo-chain-history-resolver.ts @@ -0,0 +1,23 @@ +import { Wallet } from '@lace/cardano'; +import { WitnessedTx } from '@cardano-sdk/key-management'; + +import { combineInputResolvers, createBackendInputResolver, createInputResolver } from '@cardano-sdk/wallet'; +import { Cardano } from '@cardano-sdk/core'; +import { Observable } from 'rxjs'; + +interface UtxoAndBackendChainHistoryResolverArgs { + utxo: Wallet.ObservableWallet['utxo']; + chainHistoryProvider: Wallet.ChainHistoryProvider; + transactions: { + outgoing: { + signed$: Observable; + }; + }; +} + +export const utxoAndBackendChainHistoryResolver = ({ + utxo, + transactions, + chainHistoryProvider +}: UtxoAndBackendChainHistoryResolverArgs): Cardano.InputResolver => + combineInputResolvers(createInputResolver({ utxo, transactions }), createBackendInputResolver(chainHistoryProvider)); diff --git a/packages/common/src/ui/lib/add-ellipsis.ts b/packages/common/src/ui/lib/add-ellipsis.ts index 2fa46fac8..8938ff784 100644 --- a/packages/common/src/ui/lib/add-ellipsis.ts +++ b/packages/common/src/ui/lib/add-ellipsis.ts @@ -1,6 +1,13 @@ /* eslint-disable no-magic-numbers */ const addedLength = 4; +export const truncate = (text: string, partOneLength: number, partTwoLength: number): string => { + const textMinLenght = partOneLength + partTwoLength + addedLength; + if (text.length <= textMinLenght) return text; + + return `${text.slice(0, partOneLength)}${text.slice(text.length - partTwoLength)}`; +}; + export const addEllipsis = (text: string, partOnelength: number, partTwoLength: number): string => { const textMinLenght = partOnelength + partTwoLength + addedLength; if (text.length <= textMinLenght) return text; diff --git a/packages/common/src/ui/styles/theme.scss b/packages/common/src/ui/styles/theme.scss index 9bde8195e..b2c556961 100644 --- a/packages/common/src/ui/styles/theme.scss +++ b/packages/common/src/ui/styles/theme.scss @@ -7,6 +7,7 @@ $theme-text-color-light-grey: var(--text-color-light-grey, #878e9e); $theme-light-grey: var(--data-light-grey, #f9f9f9); $theme-light-grey-border: #efefef; $theme-light-grey-border-opacity: #efefef8f; +$theme-white: #ffffff; $breakpoint-minimum: 668px; $breakpoint-xsmall: 1024px; diff --git a/packages/common/src/ui/styles/themes/_light.scss b/packages/common/src/ui/styles/themes/_light.scss index ece0c096e..a4979d456 100644 --- a/packages/common/src/ui/styles/themes/_light.scss +++ b/packages/common/src/ui/styles/themes/_light.scss @@ -19,6 +19,7 @@ if the variables are undefined we use the fallback color $light: ( 'black': #3d3b39, 'cream': #fcf5e3, + 'body':#ffffff, 'dark-grey-plus': #7e8593, 'dark-grey': #6f7786, 'mid-grey': #c0c0c0, diff --git a/packages/core/.storybook/__mocks__/cardano.ts b/packages/core/.storybook/__mocks__/cardano.ts index 6b4b8382d..7c65b255b 100644 --- a/packages/core/.storybook/__mocks__/cardano.ts +++ b/packages/core/.storybook/__mocks__/cardano.ts @@ -1,6 +1,10 @@ import { Cardano, util } from '@cardano-sdk/core'; +import * as WalletUtil from '../../../cardano/src/wallet/util'; export const Wallet = { - util, + util: { + ...util, + ...WalletUtil + }, Cardano }; diff --git a/packages/core/src/ui/components/ActivityDetail/Collateral.tsx b/packages/core/src/ui/components/ActivityDetail/Collateral.tsx index 763f165d1..631484ae7 100644 --- a/packages/core/src/ui/components/ActivityDetail/Collateral.tsx +++ b/packages/core/src/ui/components/ActivityDetail/Collateral.tsx @@ -14,6 +14,8 @@ export interface Props { collateral: string; amountTransformer: (amount: string) => string; coinSymbol: string; + displayFiat?: boolean; + className?: string; status?: CollateralStatus; } @@ -21,6 +23,8 @@ export const Collateral = ({ collateral, amountTransformer, coinSymbol, + displayFiat, + className, status = CollateralStatus.REVIEW }: Props): React.ReactElement => { const { t } = useTranslate(); @@ -44,6 +48,8 @@ export const Collateral = ({ fiatPrice={amountTransformer(collateral)} label={t('package.core.activityDetails.collateral.label')} tooltip={getTooltipText()} + displayFiat={displayFiat} + className={className} /> {status === CollateralStatus.ERROR && ( diff --git a/packages/core/src/ui/components/ActivityDetail/TransactionDetails.tsx b/packages/core/src/ui/components/ActivityDetail/TransactionDetails.tsx index 4cd059b35..2a1848aa7 100644 --- a/packages/core/src/ui/components/ActivityDetail/TransactionDetails.tsx +++ b/packages/core/src/ui/components/ActivityDetail/TransactionDetails.tsx @@ -403,7 +403,12 @@ export const TransactionDetails = ({ )} {fee && fee !== '-' && ( - + )} {deposit && renderDepositValueSection({ value: deposit, label: t('package.core.activityDetails.deposit') })} diff --git a/packages/core/src/ui/components/ActivityDetail/TransactionFee.tsx b/packages/core/src/ui/components/ActivityDetail/TransactionFee.tsx index 26312ad81..13eb8b50f 100644 --- a/packages/core/src/ui/components/ActivityDetail/TransactionFee.tsx +++ b/packages/core/src/ui/components/ActivityDetail/TransactionFee.tsx @@ -6,17 +6,36 @@ export interface TransactionFeeProps { fee: string; amountTransformer: (amount: string) => string; coinSymbol: string; + label?: string; + className?: string; + testId?: string; + tooltipInfo?: string; + displayFiat?: boolean; + highlightPositiveAmount?: boolean; } -export const TransactionFee = ({ fee, amountTransformer, coinSymbol }: TransactionFeeProps): React.ReactElement => { +export const TransactionFee = ({ + fee, + amountTransformer, + coinSymbol, + label, + className, + testId, + tooltipInfo, + displayFiat, + highlightPositiveAmount +}: TransactionFeeProps): React.ReactElement => { const { t } = useTranslate(); return ( ); }; diff --git a/packages/core/src/ui/components/DappAddressSections/DappAddressSections.module.scss b/packages/core/src/ui/components/DappAddressSections/DappAddressSections.module.scss new file mode 100644 index 000000000..74bf3b3cc --- /dev/null +++ b/packages/core/src/ui/components/DappAddressSections/DappAddressSections.module.scss @@ -0,0 +1,34 @@ +@import '../../styles/theme.scss'; +@import '../../../../../common/src/ui/styles/abstracts/_typography.scss'; + +.label { + color: var(--text-color-primary, #ffffff) !important; + font-size: 16px; + font-weight: 600; +} + +.value { + color: var(--text-color-primary, #ffffff) !important; + font-size: 14px; + font-weight: 500; +} + +.address { + display: flex; + justify-content: space-between; + padding: size_unit(2.5) 0; +} + +.summaryContent { + padding-bottom: size_unit(2.5); +} + +.tokenCount { + display: flex; + justify-content: space-between; + align-items: end; +} + +.positiveAmount { + color: var(--data-green, #2CB67D) !important; +} diff --git a/packages/core/src/ui/components/DappAddressSections/DappAddressSections.tsx b/packages/core/src/ui/components/DappAddressSections/DappAddressSections.tsx new file mode 100644 index 000000000..e11b7c79b --- /dev/null +++ b/packages/core/src/ui/components/DappAddressSections/DappAddressSections.tsx @@ -0,0 +1,219 @@ +/* eslint-disable sonarjs/no-identical-functions */ +import React from 'react'; +import { truncate, addEllipsis } from '@lace/common'; +import { Wallet } from '@lace/cardano'; +import { Cardano, AssetInfoWithAmount } from '@cardano-sdk/core'; +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 classNames from 'classnames'; + +interface GroupedAddressAssets { + nfts: Array; + tokens: Array; + coins: Array; +} + +export interface DappAddressSectionProps { + groupedFromAddresses: Map; + groupedToAddresses: Map; + isToAddressesEnabled: boolean; + isFromAddressesEnabled: boolean; + coinSymbol: string; +} + +const tryDecodeAsUtf8 = ( + value: WithImplicitCoercion | { [Symbol.toPrimitive](hint: 'string'): string } +): string => { + const bytes = Uint8Array.from(Buffer.from(value, 'hex')); + const decoder = new TextDecoder('utf-8'); + // Decode the Uint8Array to a UTF-8 string + return decoder.decode(bytes); +}; + +const getFallbackName = (asset: AssetInfoWithAmount) => + tryDecodeAsUtf8(asset.assetInfo.name) ? tryDecodeAsUtf8(asset.assetInfo.name) : asset.assetInfo.assetId; + +const isNFT = (asset: AssetInfoWithAmount) => asset.assetInfo.supply === BigInt(1); + +const getAssetTokenName = (assetWithAmount: AssetInfoWithAmount) => { + if (isNFT(assetWithAmount)) { + return assetWithAmount.assetInfo.nftMetadata?.name ?? getFallbackName(assetWithAmount); + } + return assetWithAmount.assetInfo.tokenMetadata?.ticker ?? getFallbackName(assetWithAmount); +}; + +const charBeforeEllName = 9; +const charAfterEllName = 0; + +const displayGroupedNFTs = (nfts: AssetInfoWithAmount[]) => + nfts.map((nft: AssetInfoWithAmount) => { + const imageSrc = nft.assetInfo.tokenMetadata?.icon ?? nft.assetInfo.nftMetadata?.image ?? undefined; + return ( + + ); + }); + +const displayGroupedTokens = (tokens: AssetInfoWithAmount[]) => + tokens.map((token: AssetInfoWithAmount) => { + const imageSrc = token.assetInfo.tokenMetadata?.icon ?? token.assetInfo.nftMetadata?.image ?? undefined; + + return ( + + ); + }); + +const { Title, Text } = Typography; + +const charBeforeEllipsisName = 8; +const charAfterEllipsisName = 8; + +const getStringFromLovelace = (value: bigint): string => Wallet.util.lovelacesToAdaString(value.toString()); + +const getTokenQuantity = (tokens: Array, coins: Array) => { + let quantity = tokens.length; + + if (coins.length > 0) { + quantity += 1; + } + + return quantity; +}; +export const DappAddressSections = ({ + groupedFromAddresses, + groupedToAddresses, + isToAddressesEnabled, + isFromAddressesEnabled, + coinSymbol +}: DappAddressSectionProps): React.ReactElement => { + const { t } = useTranslate(); + + const itemsCountCopy = t('package.core.dappTransaction.items'); + + return ( + <> + +
+ {[...groupedFromAddresses.entries()].map(([address, addressData]) => ( + <> +
+ + {t('package.core.dappTransaction.address')} + + + + {addEllipsis(address, charBeforeEllipsisName, charAfterEllipsisName)} + + +
+ {(addressData.tokens.length > 0 || addressData.coins.length > 0) && ( + <> +
+ + {t('package.core.dappTransaction.tokens')} + + + -{getTokenQuantity(addressData.tokens, addressData.coins)} {itemsCountCopy} + +
+ {addressData.coins.map((coin) => ( + + ))} + {displayGroupedTokens(addressData.tokens)} + + )} + + {addressData.nfts.length > 0 && ( + <> +
+ + {t('package.core.dappTransaction.nfts')} + + + -{addressData.nfts.length} {itemsCountCopy} + +
+ {displayGroupedNFTs(addressData.nfts)} + + )} + + ))} +
+
+ + +
+ {[...groupedToAddresses.entries()].map(([address, addressData]) => ( + <> +
+ + {t('package.core.dappTransaction.address')} + + + + {addEllipsis(address, charBeforeEllipsisName, charAfterEllipsisName)} + + +
+ {(addressData.tokens.length > 0 || addressData.coins.length > 0) && ( + <> +
+ + {t('package.core.dappTransaction.tokens')} + + + {getTokenQuantity(addressData.tokens, addressData.coins)} {itemsCountCopy} + +
+ {addressData.coins.map((coin) => ( + + ))} + {displayGroupedTokens(addressData.tokens)} + + )} + + {addressData.nfts.length > 0 && ( + <> +
+ + {t('package.core.dappTransaction.nfts')} + + + {addressData.nfts.length} {itemsCountCopy} + +
+ {displayGroupedNFTs(addressData.nfts)} + + )} + + ))} +
+
+ + ); +}; diff --git a/packages/core/src/ui/components/DappTransaction/DappTransaction.module.scss b/packages/core/src/ui/components/DappTransaction/DappTransaction.module.scss index ea1eb99b3..c612aad03 100644 --- a/packages/core/src/ui/components/DappTransaction/DappTransaction.module.scss +++ b/packages/core/src/ui/components/DappTransaction/DappTransaction.module.scss @@ -1,52 +1,23 @@ @import '../../styles/theme.scss'; @import '../../../../../common/src/ui/styles/abstracts/_typography.scss'; -.dappInfo { - margin: size_unit(1) 0px; -} - .details { display: flex; - flex: 1; flex-direction: column; - justify-content: space-between; - 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; } +.feeContainer { + padding-bottom: size_unit(2.5); +} -.warningAlert { - flex-direction: row; - background: var(--lace-cream); - display: flex; - align-items: center; - border-radius: size_unit(2); - padding: size_unit(2) size_unit(3); - gap: size_unit(3); - - svg { - height: size_unit(3); - width: size_unit(3); - color: var(--data-orange); - } - - p { - @include text-body-semi-bold; - margin: 0; - } +.depositContainer { + padding-bottom: size_unit(1.8) !important; } -.feeContainer { - display: flex; - flex-direction: row; - align-items: flex-start; - justify-content: space-between; - @include text-body-semi-bold; - color: var(--text-color-primary); +.transactionAssetsContainer { + margin-bottom: size_unit(2.5); } diff --git a/packages/core/src/ui/components/DappTransaction/DappTransaction.stories.tsx b/packages/core/src/ui/components/DappTransaction/DappTransaction.stories.tsx index 07525eac9..f734549d4 100644 --- a/packages/core/src/ui/components/DappTransaction/DappTransaction.stories.tsx +++ b/packages/core/src/ui/components/DappTransaction/DappTransaction.stories.tsx @@ -17,20 +17,52 @@ type Story = StoryObj; const data: ComponentProps = { dappInfo: { - logo: 'https://cdn.mint.handle.me/favicon.png', - name: 'Mint', - url: 'https://preprod.mint.handle.me' + name: 'Mint' }, - transaction: { - fee: '0.17', - outputs: [ - { - coins: '1', - recipient: - 'addr_test1qrl0s3nqfljv8dfckn7c4wkzu5rl6wn4hakkddcz2mczt3szlqss933x0aag07qcgspcaglmay6ufl4y4lalmlpe02mqhl0fx2' + coinSymbol: 'tAda', + fiatCurrencyCode: 'usd', + fromAddress: new Map(), + toAddress: new Map(), + // eslint-disable-next-line no-magic-numbers + collateral: 150_000 as unknown as bigint, + txInspectionDetails: { + assets: new Map(), + // eslint-disable-next-line no-magic-numbers + coins: 1_171_000 as unknown as bigint, + // eslint-disable-next-line no-magic-numbers + collateral: 150_000 as unknown as bigint, + // eslint-disable-next-line no-magic-numbers + deposit: 1_000_000 as unknown as bigint, + // eslint-disable-next-line no-magic-numbers + fee: 170_000 as unknown as bigint, + // eslint-disable-next-line no-magic-numbers + returnedDeposit: 90_000 as unknown as bigint, + + unresolved: { + inputs: [ + { + index: 0, + txId: Wallet.Cardano.TransactionId('bb217abaca60fc0ca68c1555eca6a96d2478547818ae76ce6836133f3cc546e0') + } + ], + value: { + assets: new Map([ + [ + Wallet.Cardano.AssetId('6b8d07d69639e9413dd637a1a815a7323c69c86abbafb66dbfdb1aa7'), + // eslint-disable-next-line no-magic-numbers + 4 as unknown as bigint + ], + [ + Wallet.Cardano.AssetId('659f2917fb63f12b33667463ee575eeac1845bbc736b9c0bbc40ba8254534c41'), + // eslint-disable-next-line no-magic-numbers + 7 as unknown as bigint + ] + ]), + + // eslint-disable-next-line no-magic-numbers + coins: 11_171_000 as unknown as bigint } - ], - type: Wallet.Cip30TxType.Mint + } } }; diff --git a/packages/core/src/ui/components/DappTransaction/DappTransaction.tsx b/packages/core/src/ui/components/DappTransaction/DappTransaction.tsx index 8a70fb94c..86e5673d8 100644 --- a/packages/core/src/ui/components/DappTransaction/DappTransaction.tsx +++ b/packages/core/src/ui/components/DappTransaction/DappTransaction.tsx @@ -1,97 +1,209 @@ -/* eslint-disable sonarjs/no-duplicate-string */ +/* eslint-disable no-console */ import React from 'react'; -import { ErrorPane } from '@lace/common'; +import { ErrorPane, truncate } from '@lace/common'; import { Wallet } from '@lace/cardano'; -import { DappInfo, DappInfoProps } from '../DappInfo'; -import { DappTxHeader } from './DappTxHeader/DappTxHeader'; -import { DappTxAsset } from './DappTxAsset/DappTxAsset'; -import { DappTxOutput } from './DappTxOutput/DappTxOutput'; + +import { Cardano, TransactionSummaryInspection, TokenTransferValue, AssetInfoWithAmount } from '@cardano-sdk/core'; + +import { DappTransactionHeader, DappTransactionHeaderProps, TransactionTypes } from '../DappTransactionHeader'; + import styles from './DappTransaction.module.scss'; import { useTranslate } from '@src/ui/hooks'; import { TransactionFee, Collateral } from '@ui/components/ActivityDetail'; +import { TransactionType, DappTransactionSummary, TransactionAssets } from '@lace/ui'; +import { DappAddressSections } from '../DappAddressSections/DappAddressSections'; + const amountTransformer = (fiat: { price: number; code: string }) => (ada: string) => `${Wallet.util.convertAdaToFiat({ ada, fiat: fiat.price })} ${fiat.code}`; export interface DappTransactionProps { /** Transaction details such as type, amount, fee and address */ - transaction: Wallet.Cip30SignTxSummary; + txInspectionDetails: TransactionSummaryInspection; /** dApp information such as logo, name and url */ - dappInfo: Omit; + dappInfo: Omit; /** Optional error message */ errorMessage?: string; fiatCurrencyCode?: string; fiatCurrencyPrice?: number; coinSymbol?: string; + /** tokens send to being sent to or from the user */ + fromAddress: Map; + toAddress: Map; + collateral?: bigint; } +const isNFT = (asset: AssetInfoWithAmount) => asset.assetInfo.supply === BigInt(1); + +interface GroupedAddressAssets { + nfts: Array; + tokens: Array; + coins: Array; +} + +const groupAddresses = (addresses: Map) => { + const groupedAddresses: Map = new Map(); + + for (const [address, value] of addresses) { + const group: GroupedAddressAssets = { + coins: [], + nfts: [], + tokens: [] + }; + + if (value.coins !== BigInt(0)) { + group.coins.push(value.coins); + } + + const addressAssets = value.assets; + for (const [, asset] of addressAssets) { + if (asset.assetInfo.supply === BigInt(1)) { + group.nfts.push(asset); + } else { + group.tokens.push(asset); + } + } + + if (group.coins.length > 0 || group.nfts.length > 0 || group.tokens.length > 0) { + groupedAddresses.set(address, group); + } + } + + return groupedAddresses; +}; + +type TransactionType = keyof typeof TransactionTypes; + +const tryDecodeAsUtf8 = ( + value: WithImplicitCoercion | { [Symbol.toPrimitive](hint: 'string'): string } +): string => { + const bytes = Uint8Array.from(Buffer.from(value, 'hex')); + const decoder = new TextDecoder('utf-8'); + // Decode the Uint8Array to a UTF-8 string + return decoder.decode(bytes); +}; + +const getFallbackName = (asset: AssetInfoWithAmount) => + tryDecodeAsUtf8(asset.assetInfo.name) ? tryDecodeAsUtf8(asset.assetInfo.name) : asset.assetInfo.assetId; + +const getAssetTokenName = (assetWithAmount: AssetInfoWithAmount) => { + if (isNFT(assetWithAmount)) { + return assetWithAmount.assetInfo.nftMetadata?.name ?? getFallbackName(assetWithAmount); + } + return assetWithAmount.assetInfo.tokenMetadata?.ticker ?? getFallbackName(assetWithAmount); +}; + +const getTxType = (coins: bigint): TransactionType => { + const balance = Wallet.util.lovelacesToAdaString(coins.toString()); + return balance.includes('-') === true ? 'Send' : 'Receive'; +}; + +const charBeforeEllName = 9; +const charAfterEllName = 0; + export const DappTransaction = ({ - transaction: { type, outputs, fee, mintedAssets, burnedAssets, collateral }, - dappInfo, + txInspectionDetails: { assets, coins, fee, deposit, returnedDeposit }, + toAddress, + fromAddress, + collateral, errorMessage, fiatCurrencyCode, fiatCurrencyPrice, - coinSymbol + coinSymbol, + dappInfo }: DappTransactionProps): React.ReactElement => { const { t } = useTranslate(); + + const groupedToAddresses = groupAddresses(toAddress); + const groupedFromAddresses = groupAddresses(fromAddress); + + const isFromAddressesEnabled = groupedFromAddresses.size > 0; + const isToAddressesEnabled = groupedToAddresses.size > 0; + return (
- {errorMessage && }
- {type === Wallet.Cip30TxType.Mint && mintedAssets?.length > 0 && ( - <> - - {mintedAssets.map((asset) => ( - - ))} - - )} - {type === Wallet.Cip30TxType.Mint && burnedAssets?.length > 0 && ( - <> - 0 ? undefined : t('package.core.dappTransaction.transaction')} - subtitle={t('package.core.dappTransaction.burn')} - /> - {burnedAssets.map((asset) => ( - - ))} - - )} - {type === Wallet.Cip30TxType.Send && ( - <> - - {outputs.map((output) => ( - - ))} - - )} - {collateral && ( + + +
+ {[...assets].map(([key, assetWithAmount]: [string, AssetInfoWithAmount]) => { + const imageSrc = + assetWithAmount.assetInfo.tokenMetadata?.icon ?? + assetWithAmount.assetInfo.nftMetadata?.image ?? + undefined; + return ( + + ); + })} +
+ + {collateral !== undefined && collateral !== BigInt(0) && ( )} - {fee && fee !== '-' && ( + + {returnedDeposit !== BigInt(0) && ( + `${Wallet.util.lovelacesToAdaString(returnedDeposit.toString())} ${fiatCurrencyCode}` + } + /> + )} + + {deposit !== BigInt(0) && ( + `${Wallet.util.lovelacesToAdaString(deposit.toString())} ${fiatCurrencyCode}`} /> )} + + `${Wallet.util.lovelacesToAdaString(fee.toString())} ${fiatCurrencyCode}`} + coinSymbol={coinSymbol} + className={styles.feeContainer} + displayFiat={false} + /> + +
); diff --git a/packages/core/src/ui/components/DappTransaction/DappTxAsset/DappTxAsset.module.scss b/packages/core/src/ui/components/DappTransaction/DappTxAsset/DappTxAsset.module.scss deleted file mode 100644 index 0d5bf78af..000000000 --- a/packages/core/src/ui/components/DappTransaction/DappTxAsset/DappTxAsset.module.scss +++ /dev/null @@ -1,41 +0,0 @@ -@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 deleted file mode 100644 index ea929db53..000000000 --- a/packages/core/src/ui/components/DappTransaction/DappTxAsset/DappTxAsset.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import React from 'react'; -import styles from './DappTxAsset.module.scss'; -import { Ellipsis } from '@lace/common'; -import { useTranslate } from '@src/ui/hooks'; -import { Wallet } from '@lace/cardano'; - -export interface DappTxAssetProps { - name: string; - amount: string; - ticker?: string; -} - -export const DappTxAsset = ({ amount, name, ticker }: Wallet.Cip30SignTxAssetItem): 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/DappTxAsset/index.ts b/packages/core/src/ui/components/DappTransaction/DappTxAsset/index.ts deleted file mode 100644 index 3418661f1..000000000 --- a/packages/core/src/ui/components/DappTransaction/DappTxAsset/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { DappTxAsset } from './DappTxAsset'; diff --git a/packages/core/src/ui/components/DappTransaction/DappTxHeader/DappTxHeader.module.scss b/packages/core/src/ui/components/DappTransaction/DappTxHeader/DappTxHeader.module.scss deleted file mode 100644 index 94eefef49..000000000 --- a/packages/core/src/ui/components/DappTransaction/DappTxHeader/DappTxHeader.module.scss +++ /dev/null @@ -1,27 +0,0 @@ -@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 deleted file mode 100644 index 8117fcd3e..000000000 --- a/packages/core/src/ui/components/DappTransaction/DappTxHeader/DappTxHeader.tsx +++ /dev/null @@ -1,20 +0,0 @@ -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 deleted file mode 100644 index 04c3b12f0..000000000 --- a/packages/core/src/ui/components/DappTransaction/DappTxOutput/DappTxOutput.module.scss +++ /dev/null @@ -1,67 +0,0 @@ -@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 deleted file mode 100644 index 9eccd4e0e..000000000 --- a/packages/core/src/ui/components/DappTransaction/DappTxOutput/DappTxOutput.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react'; -import { Ellipsis } from '@lace/common'; -import styles from './DappTxOutput.module.scss'; -import { useTranslate } from '@src/ui/hooks'; -import { Wallet } from '@lace/cardano'; - -export interface DappTxOutputProps { - coins: string; - recipient: string; - assets?: Wallet.Cip30SignTxAssetItem[]; -} - -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/components/DappTransactionHeader/DappTransactionHeader.module.scss b/packages/core/src/ui/components/DappTransactionHeader/DappTransactionHeader.module.scss new file mode 100644 index 000000000..f20879be7 --- /dev/null +++ b/packages/core/src/ui/components/DappTransactionHeader/DappTransactionHeader.module.scss @@ -0,0 +1,14 @@ +@import '../../styles/theme.scss'; +@import '../../../../../../packages/common/src/ui/styles/abstracts/_typography'; + +.dappInfo { + font-weight: 500; + line-height: 1.7; + color: var(--text-color-light-grey) !important; +} + +.dappInfoContainer { + padding: size_unit(2); + margin: size_unit(1); + background-color: var(--data-light-grey); +} \ No newline at end of file diff --git a/packages/core/src/ui/components/DappTransactionHeader/DappTransactionHeader.tsx b/packages/core/src/ui/components/DappTransactionHeader/DappTransactionHeader.tsx new file mode 100644 index 000000000..0e38fc19a --- /dev/null +++ b/packages/core/src/ui/components/DappTransactionHeader/DappTransactionHeader.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { Typography } from 'antd'; + +import styles from './DappTransactionHeader.module.scss'; +import { useTranslate } from '@src/ui/hooks'; + +import { TransactionType, SummaryExpander, Card } from '@lace/ui'; + +const { Text } = Typography; + +export enum TransactionTypes { + Withdrawal = 'withdrawal', + Receive = 'receive', + Sent = 'sent', + Send = 'send', + Sending = 'sending', + Mint = 'mint', + 'Self Transaction' = 'self' +} + +type TransactionType = keyof typeof TransactionTypes; + +export interface DappTransactionHeaderProps { + name: string; + transactionType?: TransactionType; +} + +export const DappTransactionHeader = ({ transactionType, name }: DappTransactionHeaderProps): React.ReactElement => { + const { t } = useTranslate(); + + return ( +
+ + + + + {name} + + + +
+ ); +}; diff --git a/packages/core/src/ui/components/DappTransactionHeader/index.ts b/packages/core/src/ui/components/DappTransactionHeader/index.ts new file mode 100644 index 000000000..f2108aff8 --- /dev/null +++ b/packages/core/src/ui/components/DappTransactionHeader/index.ts @@ -0,0 +1 @@ +export * from './DappTransactionHeader'; diff --git a/packages/core/src/ui/lib/translations/en.json b/packages/core/src/ui/lib/translations/en.json index 452ed9656..54da6476e 100644 --- a/packages/core/src/ui/lib/translations/en.json +++ b/packages/core/src/ui/lib/translations/en.json @@ -415,6 +415,16 @@ "recipient": "Recipient", "send": "Send", "sending": "Sending", + "toAddress": "To address", + "fromAddress": "From address", + "tokens": "Tokens", + "nfts": "NFTs", + "address": "Address", + "origin": "Origin", + "transactionSummary": "Transaction Summary", + "deposit": "Deposit", + "returnedDeposit": "Returned deposit", + "items": "item(s)", "transaction": "Transaction" }, "confirmationBanner": { diff --git a/packages/e2e-tests/src/assert/dAppConnectorAssert.ts b/packages/e2e-tests/src/assert/dAppConnectorAssert.ts index 3d388b5fb..948836d23 100644 --- a/packages/e2e-tests/src/assert/dAppConnectorAssert.ts +++ b/packages/e2e-tests/src/assert/dAppConnectorAssert.ts @@ -41,23 +41,8 @@ class DAppConnectorAssert { expect(await commonDappPageElements.betaPill.getText()).to.equal(await t('core.dapp.beta')); } - async assertSeeTitleAndDappDetails(expectedTitleKey: string, expectedDappDetails: ExpectedDAppDetails) { - const currentDAppUrl = new URL(expectedDappDetails.url); - const commonDappPageElements = new CommonDappPageElements(); - await commonDappPageElements.pageTitle.waitForDisplayed(); - expect(await commonDappPageElements.pageTitle.getText()).to.equal(await t(expectedTitleKey)); - await commonDappPageElements.dAppLogo.waitForDisplayed({ reverse: !expectedDappDetails.hasLogo }); - await commonDappPageElements.dAppName.waitForDisplayed(); - expect(await commonDappPageElements.dAppName.getText()).to.equal(expectedDappDetails.name); - await commonDappPageElements.dAppUrl.waitForDisplayed(); - const expectedUrl = `${currentDAppUrl.protocol}//${currentDAppUrl.host}`; - expect(await commonDappPageElements.dAppUrl.getText()).to.equal(expectedUrl); - } - - async assertSeeAuthorizeDAppPage(expectedDappDetails: ExpectedDAppDetails) { + async assertSeeAuthorizeDAppPage() { await this.assertSeeHeader(); - await this.assertSeeTitleAndDappDetails('dapp.connect.header', expectedDappDetails); - await AuthorizeDAppPage.banner.container.waitForDisplayed(); await AuthorizeDAppPage.banner.icon.waitForDisplayed(); await AuthorizeDAppPage.banner.description.waitForDisplayed(); @@ -72,7 +57,6 @@ class DAppConnectorAssert { async assertSeeCollateralDAppPage(expectedDappDetails: ExpectedDAppDetails) { await this.assertSeeHeader(); - await this.assertSeeTitleAndDappDetails('dapp.collateral.set.header', expectedDappDetails); await CollateralDAppPage.modalDescription.waitForDisplayed(); const currentDAppUrl = new URL(expectedDappDetails.url); @@ -275,24 +259,15 @@ class DAppConnectorAssert { } } - async assertSeeConfirmTransactionPage( - expectedDApp: ExpectedDAppDetails, - expectedTransactionData: ExpectedTransactionData - ) { + async assertSeeConfirmTransactionPage(expectedTransactionData: ExpectedTransactionData) { await this.assertSeeHeader(); - await this.assertSeeTitleAndDappDetails('dapp.confirm.header', expectedDApp); await ConfirmTransactionPage.transactionTypeTitle.waitForDisplayed(); expect(await ConfirmTransactionPage.transactionTypeTitle.getText()).to.equal( - await t('dapp.confirm.details.header') + await t('package.core.dappTransaction.transaction') ); await ConfirmTransactionPage.transactionType.waitForDisplayed(); expect(await ConfirmTransactionPage.transactionType.getText()).to.equal(expectedTransactionData.typeOfTransaction); - await ConfirmTransactionPage.transactionAmountTitle.waitForDisplayed(); - expect(await ConfirmTransactionPage.transactionAmountTitle.getText()).to.equal( - await t('package.core.dappTransaction.sending', 'core') - ); - await ConfirmTransactionPage.transactionAmountValue.waitForDisplayed(); expect(await ConfirmTransactionPage.transactionAmountValue.getText()).to.equal(expectedTransactionData.amountADA); @@ -300,24 +275,7 @@ class DAppConnectorAssert { expect(await ConfirmTransactionPage.transactionFeeTitle.getText()).to.equal( await t('package.core.activityDetails.transactionFee', 'core') ); - await ConfirmTransactionPage.transactionFeeTooltipIcon.waitForDisplayed(); await ConfirmTransactionPage.transactionFeeValueAda.waitForDisplayed(); - await ConfirmTransactionPage.transactionFeeValueFiat.waitForDisplayed(); - - if (expectedTransactionData.amountAsset && expectedTransactionData.amountAsset !== '0') { - await ConfirmTransactionPage.transactionAmountAsset.waitForDisplayed(); - expect(await ConfirmTransactionPage.transactionAmountAsset.getText()).to.equal( - expectedTransactionData.amountAsset - ); - } - - await ConfirmTransactionPage.transactionRecipientTitle.waitForDisplayed(); - expect(await ConfirmTransactionPage.transactionRecipientTitle.getText()).to.equal( - await t('dapp.confirm.details.recepient') - ); - expect(await ConfirmTransactionPage.transactionRecipientAddress.getText()).to.contain( - expectedTransactionData.recipientAddress.slice(-10) - ); await ConfirmTransactionPage.confirmButton.waitForDisplayed(); expect(await ConfirmTransactionPage.confirmButton.getText()).to.equal(await t('dapp.confirm.btn.confirm')); @@ -335,22 +293,6 @@ class DAppConnectorAssert { expect(await SignTransactionPage.cancelButton.getText()).to.equal(await t('dapp.confirm.btn.cancel')); } - async assertSeeSignDataConfirmTransactionPage( - expectedDApp: ExpectedDAppDetails, - expectedTransactionRecipientAddress: string - ) { - await this.assertSeeHeader(); - await this.assertSeeTitleAndDappDetails('dapp.confirm.header', expectedDApp); - - expect(await ConfirmTransactionPage.transactionRecipientAddressTitle.getText()).to.equal('Address:'); - expect(await ConfirmTransactionPage.transactionRecipientAddress.getText()).to.equal( - expectedTransactionRecipientAddress - ); - - expect(await ConfirmTransactionPage.transactionDataTitle.getText()).to.equal('Data:'); - expect(await ConfirmTransactionPage.transactionData.getText()).to.equal('fixed the bug'); - } - async assertSeeSomethingWentWrongPage() { await this.assertSeeHeader(); await ErrorDAppModal.image.waitForDisplayed(); diff --git a/packages/e2e-tests/src/assert/tokensPageAssert.ts b/packages/e2e-tests/src/assert/tokensPageAssert.ts index 2e57677cd..962c29fa2 100644 --- a/packages/e2e-tests/src/assert/tokensPageAssert.ts +++ b/packages/e2e-tests/src/assert/tokensPageAssert.ts @@ -140,15 +140,19 @@ class TokensPageAssert { async assertSeeValueSubtractedAda(tokenName: string, subtractedAmount: string, fee: string) { const expectedValue = Number.parseFloat(await testContext.load(`${Asset.getByName(tokenName)?.ticker}tokenBalance`)) - - Number.parseFloat(subtractedAmount) - + Number.parseFloat(subtractedAmount) + Number.parseFloat(fee); const expectedValueRounded = Number.parseFloat(expectedValue.toFixed(2)); Logger.log(`waiting for token: ${tokenName} with value: ${expectedValueRounded}`); await browser.waitUntil( - async () => - (await TokensPage.getTokenBalanceAsFloatByName(tokenName)) === expectedValueRounded + 0.01 || - (await TokensPage.getTokenBalanceAsFloatByName(tokenName)) === expectedValueRounded - 0.01 || - (await TokensPage.getTokenBalanceAsFloatByName(tokenName)) === expectedValueRounded, + async () => { + const tokenValueAsFloat = await TokensPage.getTokenBalanceAsFloatByName(tokenName); + return ( + tokenValueAsFloat === expectedValueRounded + 0.01 || + tokenValueAsFloat === expectedValueRounded - 0.01 || + tokenValueAsFloat === expectedValueRounded + ); + }, { timeout: 120_000, interval: 3000, diff --git a/packages/e2e-tests/src/elements/dappConnector/confirmTransactionPage.ts b/packages/e2e-tests/src/elements/dappConnector/confirmTransactionPage.ts index 772693953..e5ddbe951 100644 --- a/packages/e2e-tests/src/elements/dappConnector/confirmTransactionPage.ts +++ b/packages/e2e-tests/src/elements/dappConnector/confirmTransactionPage.ts @@ -7,72 +7,107 @@ class ConfirmTransactionPage extends CommonDappPageElements { private TRANSACTION_TYPE = '[data-testid="dapp-transaction-type"]'; private TRANSACTION_AMOUNT_TITLE = '[data-testid="dapp-transaction-amount-title"]'; private TRANSACTION_AMOUNT_VALUE = '[data-testid="dapp-transaction-amount-value"]'; - private TRANSACTION_AMOUNT_FEE_TITLE = '[data-testid="tx-amount-fee-label"]'; - private TRANSACTION_AMOUNT_FEE_TITLE_TOOLTIP_ICON = '[data-testid="tx-amount-fee-tooltip-icon"]'; - private TRANSACTION_AMOUNT_FEE_VALUE_ADA = '[data-testid="tx-amount-fee-amount"]'; - private TRANSACTION_AMOUNT_FEE_VALUE_FIAT = '[data-testid="tx-amount-fee-fiat"]'; - private TRANSACTION_AMOUNT_ASSET = '[data-testid="dapp-transaction-asset"]'; - private TRANSACTION_RECIPIENT_TITLE = '[data-testid="dapp-transaction-recipient-title"]'; - private TRANSACTION_RECIPIENT_ADDRESS_TITLE = '[data-testid="dapp-transaction-recipient-address-title"]'; - private TRANSACTION_RECIPIENT_ADDRESS = '[data-testid="dapp-transaction-recipient-address"]'; - private TRANSACTION_DATA_TITLE = '[data-testid="dapp-transaction-data-title"]'; - private TRANSACTION_DATA = '[data-testid="dapp-transaction-data"]'; + + private TRANSACTION_ORIGIN = '[data-testid="dapp-transaction-origin"]'; + + private TRANSACTION_RETURNED_DEPOSIT_TITLE = '[data-testid="tx-amount-returned-deposit-label"]'; + private TRANSACTION_RETURNED_DEPOSIT_ADA = '[data-testid="tx-amount-returned-deposit-amount"]'; + + private TRANSACTION_DEPOSIT_TITLE = '[data-testid="tx-amount-deposit-label"]'; + private TRANSACTION_DEPOSIT_ADA = '[data-testid="tx-amount-deposit-amount"]'; + + private TRANSACTION_FEE_TITLE = '[data-testid="tx-amount-fee-label"]'; + private TRANSACTION_FEE_ADA = '[data-testid="tx-amount-fee-amount"]'; + + private TRANSACTION_TO_ADDRESS_TITLE = '[data-testid="dapp-transaction-to-address-title"]'; + private TRANSACTION_TO_ADDRESS = '[data-testid="dapp-transaction-to-address-address"]'; + + private TRANSACTION_FROM_ADDRESS_TITLE = '[data-testid="dapp-transaction-from-address-title"]'; + private TRANSACTION_FROM_ADDRESS_ADDRESS = '[data-testid="dapp-transaction-from-address-address"]'; + + private TRANSACTION_AMOUNT_NFTS_TITLE = '[data-testid="dapp-transaction-nfts-title"]'; + private TRANSACTION_AMOUNT_NFTS_CONTAINER = '[data-testid="dapp-transaction-nfts-container"]'; + + private TRANSACTION_AMOUNT_TOKENS_TITLE = '[data-testid="dapp-transaction-tokens-title"]'; + private TRANSACTION_AMOUNT_TOKEN_CONTAINER = '[data-testid="dapp-transaction-token-container"]'; + private CONFIRM_BUTTON = '[data-testid="dapp-transaction-confirm"]'; private CANCEL_BUTTON = '[data-testid="dapp-transaction-cancel"]'; - get transactionTypeTitle(): ChainablePromiseElement { - return $(this.TRANSACTION_TYPE_TITLE); + get transactionOrigin(): ChainablePromiseElement { + return $(this.TRANSACTION_ORIGIN); } - get transactionType(): ChainablePromiseElement { - return $(this.TRANSACTION_TYPE); + get transactionFeeTitle(): ChainablePromiseElement { + return $(this.TRANSACTION_FEE_TITLE); } - get transactionAmountTitle(): ChainablePromiseElement { - return $(this.TRANSACTION_AMOUNT_TITLE); + get transactionFeeValueAda(): ChainablePromiseElement { + return $(this.TRANSACTION_FEE_ADA); } - get transactionAmountValue(): ChainablePromiseElement { - return $(this.TRANSACTION_AMOUNT_VALUE); + get transactionDepositTitle(): ChainablePromiseElement { + return $(this.TRANSACTION_DEPOSIT_TITLE); } - get transactionFeeTitle(): ChainablePromiseElement { - return $(this.TRANSACTION_AMOUNT_FEE_TITLE); + + get transactionDepositValueAda(): ChainablePromiseElement { + return $(this.TRANSACTION_DEPOSIT_ADA); } - get transactionFeeTooltipIcon(): ChainablePromiseElement { - return $(this.TRANSACTION_AMOUNT_FEE_TITLE_TOOLTIP_ICON); + get transactionReturnedDepositValueAda(): ChainablePromiseElement { + return $(this.TRANSACTION_RETURNED_DEPOSIT_ADA); } - get transactionFeeValueAda(): ChainablePromiseElement { - return $(this.TRANSACTION_AMOUNT_FEE_VALUE_ADA); + get transactionReturnedDepositTitle(): ChainablePromiseElement { + return $(this.TRANSACTION_RETURNED_DEPOSIT_TITLE); + } + + get transactionAmountNftsTitle(): ChainablePromiseElement { + return $(this.TRANSACTION_AMOUNT_NFTS_TITLE); + } + + get transactionAmountNftsContainer(): ChainablePromiseElement { + return $(this.TRANSACTION_AMOUNT_NFTS_CONTAINER); + } + + get transactionAmountTokensTitle(): ChainablePromiseElement { + return $(this.TRANSACTION_AMOUNT_TOKENS_TITLE); + } + + get transactionAmountTokensContainer(): ChainablePromiseElement { + return $(this.TRANSACTION_AMOUNT_TOKEN_CONTAINER); } - get transactionFeeValueFiat(): ChainablePromiseElement { - return $(this.TRANSACTION_AMOUNT_FEE_VALUE_FIAT); + get transactionToAddressTitle(): ChainablePromiseElement { + return $(this.TRANSACTION_TO_ADDRESS_TITLE); } - get transactionAmountAsset(): ChainablePromiseElement { - return $(this.TRANSACTION_AMOUNT_ASSET); + get transactionToAddress(): ChainablePromiseElement { + return $(this.TRANSACTION_TO_ADDRESS); } - get transactionRecipientTitle(): ChainablePromiseElement { - return $(this.TRANSACTION_RECIPIENT_TITLE); + get transactionFromAddressTitle(): ChainablePromiseElement { + return $(this.TRANSACTION_FROM_ADDRESS_TITLE); } - get transactionRecipientAddressTitle(): ChainablePromiseElement { - return $(this.TRANSACTION_RECIPIENT_ADDRESS_TITLE); + get transactionFromAddress(): ChainablePromiseElement { + return $(this.TRANSACTION_FROM_ADDRESS_ADDRESS); } - get transactionRecipientAddress(): ChainablePromiseElement { - return $(this.TRANSACTION_RECIPIENT_ADDRESS); + get transactionTypeTitle(): ChainablePromiseElement { + return $(this.TRANSACTION_TYPE_TITLE); + } + + get transactionType(): ChainablePromiseElement { + return $(this.TRANSACTION_TYPE); } - get transactionDataTitle(): ChainablePromiseElement { - return $(this.TRANSACTION_DATA_TITLE); + get transactionAmountTitle(): ChainablePromiseElement { + return $(this.TRANSACTION_AMOUNT_TITLE); } - get transactionData(): ChainablePromiseElement { - return $(this.TRANSACTION_DATA); + get transactionAmountValue(): ChainablePromiseElement { + return $(this.TRANSACTION_AMOUNT_VALUE); } get confirmButton(): ChainablePromiseElement { diff --git a/packages/e2e-tests/src/features/e2e/SendTransactionDappE2E.feature b/packages/e2e-tests/src/features/e2e/SendTransactionDappE2E.feature index 2b72cdbb8..d47d0a531 100644 --- a/packages/e2e-tests/src/features/e2e/SendTransactionDappE2E.feature +++ b/packages/e2e-tests/src/features/e2e/SendTransactionDappE2E.feature @@ -10,7 +10,7 @@ Feature: Send Transactions from Dapp - E2E And I open and authorize test DApp with "Only once" setting And I set send to wallet address to: "WalletReceiveDappTransactionE2E" in test DApp When I click "Send ADA" "Run" button in test DApp - Then I see DApp connector "Confirm transaction" page with: "3.00 ADA", "0" assets and receiving wallet "WalletReceiveDappTransactionE2E" + Then I see DApp connector "Confirm transaction" page with: "-3.17 tADA", "0" assets and receiving wallet "WalletReceiveDappTransactionE2E" And I save fee value on DApp "Confirm transaction" page And I click "Confirm" button on "Confirm transaction" page And I see DApp connector "Sign transaction" page diff --git a/packages/e2e-tests/src/pageobject/dAppConnectorPageObject.ts b/packages/e2e-tests/src/pageobject/dAppConnectorPageObject.ts index 3724d937d..78fa883d6 100644 --- a/packages/e2e-tests/src/pageobject/dAppConnectorPageObject.ts +++ b/packages/e2e-tests/src/pageobject/dAppConnectorPageObject.ts @@ -102,8 +102,7 @@ class DAppConnectorPageObject { } async saveDappTransactionFeeValue() { - let feeValue = await ConfirmTransactionPage.transactionFeeValueAda.getText(); - feeValue = feeValue.replace(' ADA', '').replace('Fee: ', ''); + const feeValue = await ConfirmTransactionPage.transactionFeeValueAda.getText(); await testContext.save('feeValueDAppTx', feeValue); } diff --git a/packages/e2e-tests/src/steps/dAppConnectorSteps.ts b/packages/e2e-tests/src/steps/dAppConnectorSteps.ts index 0812d96ac..12db3704e 100755 --- a/packages/e2e-tests/src/steps/dAppConnectorSteps.ts +++ b/packages/e2e-tests/src/steps/dAppConnectorSteps.ts @@ -66,10 +66,6 @@ Then(/^I see DApp connector "Confirm transaction" page in (dark|light) mode$/, a Then(/^I see DApp connector Sign data "Confirm transaction" page$/, async () => { await DAppConnectorPageObject.waitAndSwitchToDAppConnectorWindow(3); - await DAppConnectorAssert.assertSeeSignDataConfirmTransactionPage( - testDAppDetails, - String(getTestWallet('TestAutomationWallet').address) - ); }); Then( @@ -83,7 +79,7 @@ Then( amountAsset: assetValue, recipientAddress: String(getTestWallet(walletName).address) }; - await DAppConnectorAssert.assertSeeConfirmTransactionPage(testDAppDetails, expectedTransactionData); + await DAppConnectorAssert.assertSeeConfirmTransactionPage(expectedTransactionData); } ); @@ -101,7 +97,7 @@ Then( switch (expectedPage) { case 'Confirm transaction': - await DAppConnectorAssert.assertSeeConfirmTransactionPage(testDAppDetails, defaultDAppTransactionData); + await DAppConnectorAssert.assertSeeConfirmTransactionPage(defaultDAppTransactionData); break; case 'Something went wrong': await DAppConnectorAssert.assertSeeSomethingWentWrongPage(); diff --git a/packages/icons/raw/ada.component.svg b/packages/icons/raw/ada.component.svg new file mode 100644 index 000000000..6d1927e11 --- /dev/null +++ b/packages/icons/raw/ada.component.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/packages/icons/src/AdaComponent.tsx b/packages/icons/src/AdaComponent.tsx new file mode 100644 index 000000000..9ef4abae3 --- /dev/null +++ b/packages/icons/src/AdaComponent.tsx @@ -0,0 +1,28 @@ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +import * as React from 'react'; +import type { SVGProps } from 'react'; +const SvgAdaComponent = (props: SVGProps) => ( + + + + + + + + + +); +export { SvgAdaComponent as ReactComponent }; diff --git a/packages/ui/src/assets/images/dark-mode-fallback.png b/packages/ui/src/assets/images/dark-mode-fallback.png new file mode 100644 index 000000000..c44198169 Binary files /dev/null and b/packages/ui/src/assets/images/dark-mode-fallback.png differ diff --git a/packages/ui/src/assets/images/light-mode-fallback.png b/packages/ui/src/assets/images/light-mode-fallback.png new file mode 100644 index 000000000..e61e29ff3 Binary files /dev/null and b/packages/ui/src/assets/images/light-mode-fallback.png differ diff --git a/packages/ui/src/assets/images/token-1.png b/packages/ui/src/assets/images/token-1.png new file mode 100644 index 000000000..159c0e974 Binary files /dev/null and b/packages/ui/src/assets/images/token-1.png differ diff --git a/packages/ui/src/assets/images/token-2.png b/packages/ui/src/assets/images/token-2.png new file mode 100644 index 000000000..2be908e47 Binary files /dev/null and b/packages/ui/src/assets/images/token-2.png differ diff --git a/packages/ui/src/assets/images/token-3.png b/packages/ui/src/assets/images/token-3.png new file mode 100644 index 000000000..c0ab58f04 Binary files /dev/null and b/packages/ui/src/assets/images/token-3.png differ diff --git a/packages/ui/src/design-system/dapp-transaction-summary/dapp-transaction-assets.component.tsx b/packages/ui/src/design-system/dapp-transaction-summary/dapp-transaction-assets.component.tsx new file mode 100644 index 000000000..4c6ca2394 --- /dev/null +++ b/packages/ui/src/design-system/dapp-transaction-summary/dapp-transaction-assets.component.tsx @@ -0,0 +1,96 @@ +import React from 'react'; + +import classNames from 'classnames'; + +import DarkFallBack from '../../assets/images/dark-mode-fallback.png'; +import LightFallBack from '../../assets/images/light-mode-fallback.png'; +import { ThemeColorScheme } from '../../design-tokens'; +import { useThemeVariant } from '../../design-tokens/theme/hooks/use-theme-variant'; +import { Flex } from '../flex'; +import { Grid, Cell } from '../grid'; +import { UserProfile } from '../profile-picture'; +import * as Typography from '../typography'; + +import * as styles from './dapp-transaction-summary.css'; + +import type { OmitClassName } from '../../types'; + +type Props = OmitClassName<'div'> & { + imageSrc: string | undefined; + balance: string; + tokenName?: string; + coins?: string; + testId?: string; + showImageBackground?: boolean; +}; + +const isImageBase64Encoded = (image: string): boolean => { + try { + atob(image); + + return true; + } catch { + return false; + } +}; + +export const TransactionAssets = ({ + imageSrc, + balance, + tokenName, + testId, + showImageBackground = true, + ...props +}: Readonly): JSX.Element => { + const { theme } = useThemeVariant(); + const isNegativeBalance = balance.includes('-'); + + const setThemeFallbackImagine = + theme === ThemeColorScheme.Dark ? DarkFallBack : LightFallBack; + + const getImageSource = (value: string | undefined): string => { + if (value === '' || value === undefined) { + return setThemeFallbackImagine; + } else if (value.startsWith('ipfs')) { + return value.replace('ipfs://', 'https://ipfs.io/ipfs/'); + } else if (isImageBase64Encoded(value)) { + return `data:image/png;base64,${value}`; + } else { + return value; + } + }; + + return ( +
+ + + + + + + + + {balance} {tokenName} + + + + + +
+ ); +}; diff --git a/packages/ui/src/design-system/dapp-transaction-summary/dapp-transaction-summary.component.tsx b/packages/ui/src/design-system/dapp-transaction-summary/dapp-transaction-summary.component.tsx new file mode 100644 index 000000000..40429f2b9 --- /dev/null +++ b/packages/ui/src/design-system/dapp-transaction-summary/dapp-transaction-summary.component.tsx @@ -0,0 +1,56 @@ +/* eslint-disable @typescript-eslint/prefer-optional-chain */ +import React from 'react'; + +import { ReactComponent as AdaComponent } from '@lace/icons/dist/AdaComponent'; +import classNames from 'classnames'; + +import { Flex } from '../flex'; +import { Grid, Cell } from '../grid'; +import * as Typography from '../typography'; + +import * as styles from './dapp-transaction-summary.css'; + +import type { OmitClassName } from '../../types'; + +type Props = OmitClassName<'div'> & { + transactionAmount: string; + title?: string; + cardanoSymbol?: string; +}; + +export const TransactionSummary = ({ + transactionAmount, + title, + cardanoSymbol, + ...props +}: Readonly): JSX.Element => ( +
+ {title !== undefined && ( + + + {title} + + + )} +
+ + + + + + + + {transactionAmount} {cardanoSymbol} + + + + +
+
+); diff --git a/packages/ui/src/design-system/dapp-transaction-summary/dapp-transaction-summary.css.ts b/packages/ui/src/design-system/dapp-transaction-summary/dapp-transaction-summary.css.ts new file mode 100644 index 000000000..70e09012c --- /dev/null +++ b/packages/ui/src/design-system/dapp-transaction-summary/dapp-transaction-summary.css.ts @@ -0,0 +1,102 @@ +import { sx, style } from '../../design-tokens'; + +export const transactionTypeContainer = style({ + padding: '20px 0', +}); + +export const label = sx({ + fontWeight: '$semibold', +}); + +export const positiveBalance = sx({ + color: '$dapp_transaction_summary_positive_balance_label_color', +}); + +export const negativeBalance = sx({ + color: '$dapp_transaction_summary_label', +}); + +export const txSummaryTitle = style([ + sx({ + color: '$transaction_summary_label_color', + fontWeight: '$bold', + }), + { + paddingBottom: '18px', + }, +]); + +export const coloredText = sx({ + color: '$dapp_transaction_summary_type_label_color', + fontWeight: '$bold', +}); + +export const text = style([ + sx({ + color: '$transaction_summary_label_color', + fontWeight: '$medium', + }), + { + wordBreak: 'break-all', + }, +]); + +export const secondaryText = style([ + sx({ + color: '$transaction_summary_secondary_label_color', + fontWeight: '$medium', + }), + { + wordBreak: 'break-all', + }, +]); + +export const adaIcon = style([ + sx({ + display: 'flex', + }), + { + width: '34px', + height: '26px', + }, +]); + +export const avatarRoot = style([ + sx({ + borderRadius: '$small', + }), + { + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + verticalAlign: 'middle', + overflow: 'hidden', + userSelect: 'none', + width: '45px', + height: '45px', + }, +]); + +export const avatarImage = style({ + width: '70%', + height: '70%', + objectFit: 'cover', + borderRadius: 'inherit', +}); + +export const txAmountContainer = style({ + padding: '10px 0px', +}); + +export const balanceDetailContainer = style({ + height: '100%', + paddingRight: '0', +}); + +export const assetsContainer = style({ + padding: '10px 0px', +}); + +export const txSummaryContainer = style({ + paddingTop: '20px', +}); diff --git a/packages/ui/src/design-system/dapp-transaction-summary/dapp-transaction-summary.stories.tsx b/packages/ui/src/design-system/dapp-transaction-summary/dapp-transaction-summary.stories.tsx new file mode 100644 index 000000000..d5a01c37a --- /dev/null +++ b/packages/ui/src/design-system/dapp-transaction-summary/dapp-transaction-summary.stories.tsx @@ -0,0 +1,131 @@ +import React from 'react'; + +import type { Meta } from '@storybook/react'; + +import token1 from '../../assets/images/token-1.png'; +import token2 from '../../assets/images/token-2.png'; +import token3 from '../../assets/images/token-3.png'; +import { ThemeColorScheme, LocalThemeProvider } from '../../design-tokens'; +import { Box } from '../box'; +import { page, Variants, Section } from '../decorators'; +import { Flex } from '../flex'; +import { Grid, Cell } from '../grid'; + +import { TransactionAssets } from './dapp-transaction-assets.component'; +import { TransactionSummary } from './dapp-transaction-summary.component'; +import { TransactionType } from './dapp-transaction-type.component'; + +const subtitle = `Control that displays data items in rows.`; + +export default { + title: 'List & tables/DApp transaction summary', + subcomponents: { TransactionSummary, TransactionType }, + decorators: [page({ title: 'Dapp transaction summary', subtitle })], +} as Meta; + +const Layout = ({ + children, +}: Readonly<{ children: React.ReactNode }>): JSX.Element => ( + + + {children} + + +); + +const items = [ + { + imageSrc: token1, + balance: '-200.00', + tokenName: 'Maui', + recipient: '', + }, + { + imageSrc: '', + balance: '-10.00', + tokenName: 'HairMaui', + recipient: '', + }, + { + imageSrc: token2, + balance: '1000.00', + tokenName: 'Lapisluzzz', + recipient: '', + }, + { + imageSrc: token3, + balance: '-1078.00', + tokenName: 'HawaiSand', + metadataHash: '3430008', + recipient: '', + }, + { + imageSrc: '', + balance: '-20780.00', + tokenName: 'HelloSand', + recipient: '', + }, +]; + +const Example = (): JSX.Element => ( + + + + {items.map(value => ( + + ))} + +); + +const MainComponents = (): JSX.Element => ( + + + + + + <> + {items.map(value => ( + + ))} + + + + +); + +export const Overview = (): JSX.Element => ( + + +
+ +
+ +
+ + + + + + + + +
+
+
+); diff --git a/packages/ui/src/design-system/dapp-transaction-summary/dapp-transaction-type.component.tsx b/packages/ui/src/design-system/dapp-transaction-summary/dapp-transaction-type.component.tsx new file mode 100644 index 000000000..b43204b3f --- /dev/null +++ b/packages/ui/src/design-system/dapp-transaction-summary/dapp-transaction-type.component.tsx @@ -0,0 +1,57 @@ +import React from 'react'; + +import { Flex } from '../flex'; +import { Grid, Cell } from '../grid'; +import * as Typography from '../typography'; + +import * as cx from './dapp-transaction-summary.css'; + +import type { OmitClassName } from '../../types'; + +export enum TransactionTypes { + Withdrawal = 'withdrawal', + Receive = 'receive', + Sent = 'sent', + Send = 'send', + Sending = 'sending', + Mint = 'mint', + 'Self Transaction' = 'self', +} + +type TransactionType = keyof typeof TransactionTypes; + +type Props = OmitClassName<'div'> & { + label: string; + transactionType: TransactionType; +}; + +export const TransactionType = ({ + label, + transactionType, + ...props +}: Readonly): JSX.Element => { + return ( +
+ + + + {label} + + + + + + {transactionType} + + + + +
+ ); +}; diff --git a/packages/ui/src/design-system/dapp-transaction-summary/index.ts b/packages/ui/src/design-system/dapp-transaction-summary/index.ts new file mode 100644 index 000000000..24037ad5d --- /dev/null +++ b/packages/ui/src/design-system/dapp-transaction-summary/index.ts @@ -0,0 +1,3 @@ +export { TransactionType } from './dapp-transaction-type.component'; +export { TransactionSummary } from './dapp-transaction-summary.component'; +export { TransactionAssets } from './dapp-transaction-assets.component'; diff --git a/packages/ui/src/design-system/grid/grid.component.tsx b/packages/ui/src/design-system/grid/grid.component.tsx index 55691561f..960008b55 100644 --- a/packages/ui/src/design-system/grid/grid.component.tsx +++ b/packages/ui/src/design-system/grid/grid.component.tsx @@ -9,13 +9,15 @@ export type Props = PropsWithChildren<{ columns?: GridVariants['columns']; rows?: GridVariants['rows']; gutters?: GridVariants['gutters']; + alignItems?: GridVariants['alignItems']; }>; export const Grid = ({ columns = '$none', children, rows = '$none', + alignItems, gutters = '$16', }: Readonly): JSX.Element => ( -
{children}
+
{children}
); diff --git a/packages/ui/src/design-system/grid/grid.css.ts b/packages/ui/src/design-system/grid/grid.css.ts index a9f236f8f..e9c1c7e6e 100644 --- a/packages/ui/src/design-system/grid/grid.css.ts +++ b/packages/ui/src/design-system/grid/grid.css.ts @@ -83,6 +83,23 @@ export const grid = recipe({ $20: sx({ gap: '$20' }), $32: sx({ gap: '$32' }), }, + alignItems: { + $center: { + alignItems: 'center', + }, + $fleStart: { + alignItems: 'flex-start', + }, + $flexEnd: { + alignItems: 'flex-end', + }, + $baseline: { + alignItems: 'baseline', + }, + $stretch: { + alignItems: 'stretch', + }, + }, }, defaultVariants: { diff --git a/packages/ui/src/design-system/index.ts b/packages/ui/src/design-system/index.ts index 5498d4b91..dcc11a288 100644 --- a/packages/ui/src/design-system/index.ts +++ b/packages/ui/src/design-system/index.ts @@ -46,6 +46,10 @@ export type { export { SelectGroup } from './select'; export { ActionCard } from './action-card'; export { Loader } from './loader'; +export { TransactionType } from './dapp-transaction-summary'; +export { TransactionSummary as DappTransactionSummary } from './dapp-transaction-summary'; +export { TransactionAssets } from './dapp-transaction-summary'; +export { SummaryExpander } from './summary-expander'; export * from './auto-suggest-box'; export * from './table'; export { InfoBar } from './info-bar'; diff --git a/packages/ui/src/design-system/profile-picture/user-profile.component.tsx b/packages/ui/src/design-system/profile-picture/user-profile.component.tsx index 1980ab990..9d858ad1f 100644 --- a/packages/ui/src/design-system/profile-picture/user-profile.component.tsx +++ b/packages/ui/src/design-system/profile-picture/user-profile.component.tsx @@ -13,6 +13,7 @@ interface Props { alt?: string; delayMs?: number; radius?: 'circle' | 'rounded'; + background?: 'none'; } export const UserProfile = ({ @@ -21,11 +22,13 @@ export const UserProfile = ({ alt, delayMs = 600, radius = 'circle', + background, }: Readonly): JSX.Element => ( diff --git a/packages/ui/src/design-system/profile-picture/user-profile.css.ts b/packages/ui/src/design-system/profile-picture/user-profile.css.ts index 51511fcf6..70355d535 100644 --- a/packages/ui/src/design-system/profile-picture/user-profile.css.ts +++ b/packages/ui/src/design-system/profile-picture/user-profile.css.ts @@ -25,6 +25,10 @@ export const circle = sx({ borderRadius: '$circle', }); +export const noBackground = style({ + background: 'none', +}); + export const image = style({ width: '100%', height: '100%', diff --git a/packages/ui/src/design-system/summary-expander/index.ts b/packages/ui/src/design-system/summary-expander/index.ts new file mode 100644 index 000000000..24df5614c --- /dev/null +++ b/packages/ui/src/design-system/summary-expander/index.ts @@ -0,0 +1 @@ +export { SummaryExpander } from './summary-expander.component'; diff --git a/packages/ui/src/design-system/transaction-summary/transaction-summary-amount.component.tsx b/packages/ui/src/design-system/transaction-summary/transaction-summary-amount.component.tsx index d6bc56c3d..cccad9650 100644 --- a/packages/ui/src/design-system/transaction-summary/transaction-summary-amount.component.tsx +++ b/packages/ui/src/design-system/transaction-summary/transaction-summary-amount.component.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { ReactComponent as InfoIcon } from '@lace/icons/dist/InfoComponent'; +import classNames from 'classnames'; import { Box } from '../box'; import { Flex } from '../flex'; @@ -16,8 +17,11 @@ type Props = OmitClassName<'div'> & { label?: string; tooltip?: string; amount: string; - fiatPrice: string; + fiatPrice?: string; 'data-testid'?: string; + className?: string; + displayFiat?: boolean; + highlightPositiveAmount?: boolean; }; const makeTestId = (namespace = '', path = ''): string | undefined => { @@ -29,50 +33,61 @@ export const Amount = ({ amount, fiatPrice, tooltip, + className, + displayFiat = true, + highlightPositiveAmount, ...props }: Readonly): JSX.Element => { const testId = props['data-testid']; - + const shouldHighlightPositiveAmount = + highlightPositiveAmount === true && !amount.includes('-'); return ( - - - - - {label} - - {tooltip !== undefined && ( - - -
- -
-
-
- )} -
-
- - - - {amount} - - - {fiatPrice} - - - -
+
+ + + + + {label} + + {tooltip !== undefined && ( + + +
+ +
+
+
+ )} +
+
+ + + + {amount} + + {displayFiat && ( + + {fiatPrice} + + )} + + +
+
); }; diff --git a/packages/ui/src/design-system/transaction-summary/transaction-summary.css.ts b/packages/ui/src/design-system/transaction-summary/transaction-summary.css.ts index 995421544..93b18eb18 100644 --- a/packages/ui/src/design-system/transaction-summary/transaction-summary.css.ts +++ b/packages/ui/src/design-system/transaction-summary/transaction-summary.css.ts @@ -2,13 +2,13 @@ import { sx, style } from '../../design-tokens'; export const label = sx({ color: '$transaction_summary_label_color', - fontWeight: '$semibold', + fontWeight: '$bold', }); export const text = style([ sx({ color: '$transaction_summary_label_color', - fontWeight: '$medium', + fontWeight: '$semibold', }), { wordBreak: 'break-all', @@ -18,7 +18,7 @@ export const text = style([ export const secondaryText = style([ sx({ color: '$transaction_summary_secondary_label_color', - fontWeight: '$medium', + fontWeight: '$semibold', }), { wordBreak: 'break-all', @@ -39,3 +39,15 @@ export const tooltipText = style([ display: 'flex', }), ]); + +export const normalAmount = style([ + sx({ + color: '$transaction_summary_amount_color', + }), +]); + +export const highlightedAmount = style([ + sx({ + color: '$transaction_summary_highlighted_amount_color', + }), +]); diff --git a/packages/ui/src/design-system/transaction-summary/transaction-summary.stories.tsx b/packages/ui/src/design-system/transaction-summary/transaction-summary.stories.tsx index 46b4c49d8..33f5e137b 100644 --- a/packages/ui/src/design-system/transaction-summary/transaction-summary.stories.tsx +++ b/packages/ui/src/design-system/transaction-summary/transaction-summary.stories.tsx @@ -55,9 +55,6 @@ const Example = (): JSX.Element => ( - - - ( data-testid="sample" /> + + + diff --git a/packages/ui/src/design-tokens/colors.data.ts b/packages/ui/src/design-tokens/colors.data.ts index a77f4e07c..4c9ef372b 100644 --- a/packages/ui/src/design-tokens/colors.data.ts +++ b/packages/ui/src/design-tokens/colors.data.ts @@ -161,7 +161,13 @@ export const colors = { $summary_expander_trigger_label_color_pressed: '', $transaction_summary_label_color: '', + $transaction_summary_amount_color: '', + $transaction_summary_highlighted_amount_color: '', $transaction_summary_secondary_label_color: '', + $dapp_transaction_summary_positive_balance_label_color: '', + + $dapp_transaction_summary_type_label_color: '', + $dapp_transaction_summary_label: '', $toast_bar_container_bgColor: '', $toast_bar_label_color: '', 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 65017e510..159d6e1b5 100644 --- a/packages/ui/src/design-tokens/theme/dark-theme.css.ts +++ b/packages/ui/src/design-tokens/theme/dark-theme.css.ts @@ -232,8 +232,17 @@ const colors: Colors = { darkColorScheme.$primary_mid_grey, $transaction_summary_label_color: darkColorScheme.$primary_white, + $transaction_summary_amount_color: darkColorScheme.$primary_white, + $transaction_summary_highlighted_amount_color: + darkColorScheme.$secondary_data_green, $transaction_summary_secondary_label_color: darkColorScheme.$primary_light_grey, + $dapp_transaction_summary_positive_balance_label_color: + darkColorScheme.$secondary_data_green, + + $dapp_transaction_summary_type_label_color: + darkColorScheme.$primary_accent_purple, + $dapp_transaction_summary_label: darkColorScheme.$primary_white, $toast_bar_container_bgColor: darkColorScheme.$primary_dark_grey, $toast_bar_label_color: darkColorScheme.$primary_white, diff --git a/packages/ui/src/design-tokens/theme/hooks/use-theme-variant.tsx b/packages/ui/src/design-tokens/theme/hooks/use-theme-variant.tsx new file mode 100644 index 000000000..9dfa42797 --- /dev/null +++ b/packages/ui/src/design-tokens/theme/hooks/use-theme-variant.tsx @@ -0,0 +1,11 @@ +import { useContext } from 'react'; + +import { ThemeContext } from '../theme.context'; + +import type { ThemeColorScheme } from '../theme.context'; + +export const useThemeVariant = (): { theme: ThemeColorScheme } => { + const themeContext = useContext(ThemeContext); + + return { theme: themeContext.colorScheme }; +}; 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 50f553764..8ba4cc1f0 100644 --- a/packages/ui/src/design-tokens/theme/light-theme.css.ts +++ b/packages/ui/src/design-tokens/theme/light-theme.css.ts @@ -252,8 +252,17 @@ const colors: Colors = { lightColorScheme.$primary_light_grey_plus, $transaction_summary_label_color: lightColorScheme.$primary_black, + $transaction_summary_amount_color: lightColorScheme.$primary_black, + $transaction_summary_highlighted_amount_color: + lightColorScheme.$secondary_data_green, $transaction_summary_secondary_label_color: lightColorScheme.$primary_dark_grey, + $dapp_transaction_summary_positive_balance_label_color: + lightColorScheme.$secondary_data_green, + + $dapp_transaction_summary_type_label_color: + lightColorScheme.$primary_accent_purple, + $dapp_transaction_summary_label: lightColorScheme.$primary_dark_grey, $toast_bar_container_bgColor: lightColorScheme.$primary_white, $toast_bar_label_color: lightColorScheme.$primary_black,