diff --git a/app/scripts/constants/snaps.ts b/app/scripts/constants/snaps.ts index 121817438688..1421b87cceef 100644 --- a/app/scripts/constants/snaps.ts +++ b/app/scripts/constants/snaps.ts @@ -53,11 +53,6 @@ export const PREINSTALLED_SNAPS_URLS = [ // @ts-expect-error TS1470: 'import.meta' is not allowed in CommonJS import.meta.url, ), - new URL( - '@metamask/solana-wallet-snap/dist/preinstalled-snap.json', - // @ts-expect-error TS1470: 'import.meta' is not allowed in CommonJS - import.meta.url, - ), new URL( '@metamask/tron-wallet-snap/dist/preinstalled-snap.json', // @ts-expect-error TS1470: 'import.meta' is not allowed in CommonJS diff --git a/app/scripts/controllers/permissions/snaps/specifications.ts b/app/scripts/controllers/permissions/snaps/specifications.ts index ee067a005e07..4e32c7963c5e 100644 --- a/app/scripts/controllers/permissions/snaps/specifications.ts +++ b/app/scripts/controllers/permissions/snaps/specifications.ts @@ -1,12 +1,17 @@ import { buildSnapEndowmentSpecifications, buildSnapRestrictedMethodSpecifications, + type ConfirmTransactionParams, RestrictedMethodMessenger, + type UpdateConfirmTransactionParams, } from '@metamask/snaps-rpc-methods'; import { ControllerGetStateAction } from '@metamask/base-controller'; +import { ApprovalType } from '@metamask/controller-utils'; import { CurrencyRateController } from '@metamask/assets-controllers'; import type { AssetsControllerGetStateAction } from '@metamask/assets-controller'; +import type { MultichainTransactionsControllerActions } from '@metamask/multichain-transactions-controller'; import type { RemoteFeatureFlagControllerGetStateAction } from '@metamask/remote-feature-flag-controller'; +import { rpcErrors } from '@metamask/rpc-errors'; import { SnapControllerClearSnapStateAction, SnapControllerGetSnapAction, @@ -17,6 +22,7 @@ import { SnapInterfaceControllerGetInterfaceAction, SnapInterfaceControllerSetInterfaceDisplayedAction, } from '@metamask/snaps-controllers'; +import { nanoid } from 'nanoid'; import { KeyringControllerGetKeyringsByTypeAction, KeyringControllerWithKeyringAction, @@ -32,6 +38,10 @@ import { ExcludedSnapEndowments, ExcludedSnapPermissions, } from '../../../../../shared/constants/snaps/permissions'; +import { + UNIVERSAL_TRANSACTION_APPROVAL_TYPE, + USE_UNIVERSAL_MULTICHAIN_CONFIRMATION, +} from '../../../../../shared/constants/confirmations'; import { PreferencesControllerGetStateAction } from '../../preferences-controller'; import { KeyringType } from '../../../../../shared/constants/keyring'; import { AppStateControllerGetUnlockPromiseAction } from '../../app-state-controller-method-action-types'; @@ -43,6 +53,12 @@ import { } from '../../../../../shared/lib/assets-unify-state/remote-feature-flag'; import { getIsAssetsUnifiedStateIncludedInBuild } from '../../../../../shared/lib/environment'; +function getUniversalTransactionConfirmationKey(snapId: string, id: string) { + const encodedId = encodeURIComponent(`${snapId}:${id}`).replace(/%/gu, '.'); + + return `snap-confirm-tx:${encodedId}`; +} + export type SnapPermissionSpecificationsActions = | AppStateControllerGetUnlockPromiseAction | SnapControllerClearSnapStateAction @@ -66,7 +82,8 @@ export type SnapPermissionSpecificationsActions = | SnapInterfaceControllerSetInterfaceDisplayedAction | TestOrigin | SnapControllerUpdateSnapStateAction - | ApprovalControllerAddRequestAction; + | ApprovalControllerAddRequestAction + | MultichainTransactionsControllerActions; /** * Get the permission specifications for Snaps. @@ -180,6 +197,91 @@ export function getSnapPermissionSpecifications( */ getClientCryptography: () => ({}), + showUniversalTransactionConfirmation: async ( + snapId: string, + params: ConfirmTransactionParams, + ) => { + if (!USE_UNIVERSAL_MULTICHAIN_CONFIRMATION) { + throw rpcErrors.methodNotSupported(); + } + + const approvalId = params.id + ? getUniversalTransactionConfirmationKey(snapId, params.id) + : nanoid(); + + messenger.call( + 'MultichainTransactionsController:addPendingTransaction', + { + approvalId, + chainId: params.chainId, + accountId: params.accountId, + to: params.to, + amount: params.amount, + assetId: params.assetId, + custom: params.custom, + fee: params.fee, + origin: snapId, + createdAt: Date.now(), + }, + ); + + const addAndShowApprovalRequest = async () => + messenger.call( + 'ApprovalController:addRequest', + { + id: approvalId, + origin: snapId, + type: UNIVERSAL_TRANSACTION_APPROVAL_TYPE as ApprovalType, + requestData: { + approvalId, + chainId: params.chainId, + txParams: { + to: params.to, + value: params.amount, + }, + }, + }, + true, + ); + + try { + await addAndShowApprovalRequest(); + return true; + } catch (_err) { + return false; + } finally { + messenger.call( + 'MultichainTransactionsController:removePendingTransaction', + approvalId, + ); + } + }, + + updateUniversalTransactionConfirmation: async ( + snapId: string, + params: UpdateConfirmTransactionParams, + ) => { + if (!USE_UNIVERSAL_MULTICHAIN_CONFIRMATION) { + throw rpcErrors.methodNotSupported(); + } + + const approvalId = getUniversalTransactionConfirmationKey( + snapId, + params.id, + ); + + const patch = { + ...(params.custom === undefined ? {} : { custom: params.custom }), + ...(params.fee === undefined ? {} : { fee: params.fee }), + }; + + messenger.call( + 'MultichainTransactionsController:updatePendingTransaction', + approvalId, + patch, + ); + }, + getSnapKeyring: async () => { // TODO: Use `withKeyring` instead. const [snapKeyring] = messenger.call( diff --git a/app/scripts/messenger-client-init/messengers/permission-controller-messenger.ts b/app/scripts/messenger-client-init/messengers/permission-controller-messenger.ts index b3a257f1d32d..61ab04cabd9a 100644 --- a/app/scripts/messenger-client-init/messengers/permission-controller-messenger.ts +++ b/app/scripts/messenger-client-init/messengers/permission-controller-messenger.ts @@ -6,6 +6,7 @@ import type { ApprovalControllerAcceptRequestAction, ApprovalControllerRejectRequestAction, } from '@metamask/approval-controller'; +import type { MultichainTransactionsControllerActions } from '@metamask/multichain-transactions-controller'; import type { GetSubjectMetadata } from '@metamask/permission-controller'; import { AccountsControllerListAccountsAction } from '@metamask/accounts-controller'; import { @@ -68,6 +69,7 @@ type AllowedInitializationActions = | AccountsControllerListAccountsAction | MultichainRoutingServiceGetSupportedAccountsAction | MultichainRoutingServiceIsSupportedScopeAction + | MultichainTransactionsControllerActions | NetworkControllerFindNetworkClientIdByChainIdAction | SnapPermissionSpecificationsActions | ApprovalControllerAddRequestAction; @@ -107,6 +109,9 @@ export function getPermissionControllerInitMessenger( 'KeyringController:addNewKeyring', 'MultichainRoutingService:isSupportedScope', 'MultichainRoutingService:getSupportedAccounts', + 'MultichainTransactionsController:addPendingTransaction', + 'MultichainTransactionsController:removePendingTransaction', + 'MultichainTransactionsController:updatePendingTransaction', 'NetworkController:findNetworkClientIdByChainId', 'PhishingController:maybeUpdateState', 'PhishingController:testOrigin', diff --git a/package.json b/package.json index 91ef598b08cd..a9d448bf76d2 100644 --- a/package.json +++ b/package.json @@ -393,7 +393,7 @@ "@metamask/multichain-api-client": "^0.10.1", "@metamask/multichain-api-middleware": "^3.1.0", "@metamask/multichain-network-controller": "^3.1.0", - "@metamask/multichain-transactions-controller": "^7.1.0", + "@metamask/multichain-transactions-controller": "file:.yalc/@metamask/multichain-transactions-controller", "@metamask/name-controller": "^9.1.0", "@metamask/network-controller": "^32.0.0", "@metamask/network-enablement-controller": "^5.1.0", @@ -424,11 +424,11 @@ "@metamask/shield-controller": "^5.0.1", "@metamask/signature-controller": "^39.2.0", "@metamask/smart-transactions-controller": "^24.1.0", - "@metamask/snaps-controllers": "^20.0.5", + "@metamask/snaps-controllers": "file:.yalc/@metamask/snaps-controllers", "@metamask/snaps-execution-environments": "^11.0.2", - "@metamask/snaps-rpc-methods": "^16.0.0", - "@metamask/snaps-sdk": "^11.1.1", - "@metamask/snaps-utils": "^12.2.0", + "@metamask/snaps-rpc-methods": "file:.yalc/@metamask/snaps-rpc-methods", + "@metamask/snaps-sdk": "file:.yalc/@metamask/snaps-sdk", + "@metamask/snaps-utils": "file:.yalc/@metamask/snaps-utils", "@metamask/solana-wallet-snap": "^2.8.0", "@metamask/solana-wallet-standard": "^1.0.0", "@metamask/storage-service": "^1.0.0", diff --git a/shared/constants/confirmations.ts b/shared/constants/confirmations.ts new file mode 100644 index 000000000000..4c3c6a499cc7 --- /dev/null +++ b/shared/constants/confirmations.ts @@ -0,0 +1,13 @@ +/** + * POC flag. When true, non-EVM send confirmations triggered via + * `snap_confirmTransaction` render the new universal multichain + * confirmation page instead of the legacy snap-templated dialog. + */ +export const USE_UNIVERSAL_MULTICHAIN_CONFIRMATION = true; + +/** + * Approval type used by `snap_confirmTransaction` flows. Cast to + * {@link ApprovalType} at call sites until the canonical enum in + * `@metamask/controller-utils` is updated. + */ +export const UNIVERSAL_TRANSACTION_APPROVAL_TYPE = 'universalTransaction'; diff --git a/shared/lib/accounts/solana-wallet-snap.ts b/shared/lib/accounts/solana-wallet-snap.ts index 579a9958e171..3feff60c2a03 100644 --- a/shared/lib/accounts/solana-wallet-snap.ts +++ b/shared/lib/accounts/solana-wallet-snap.ts @@ -1,5 +1,11 @@ import { SnapId } from '@metamask/snaps-sdk'; +// This dependency is still installed as part of the `package.json`, however +// the Snap is being pre-installed only for Flask build (for the moment). +import SolanaWalletSnap from '@metamask/solana-wallet-snap/dist/preinstalled-snap.json'; +// export const SOLANA_WALLET_SNAP_ID: SnapId = SolanaWalletSnap.snapId as SnapId; export const SOLANA_WALLET_SNAP_ID: SnapId = - 'npm:@metamask/solana-wallet-snap' as SnapId; -export const SOLANA_WALLET_NAME: string = 'Solana'; + 'local:http://localhost:8080' as SnapId; + +export const SOLANA_WALLET_NAME: string = + SolanaWalletSnap.manifest.proposedName; diff --git a/ui/pages/confirmations/components/confirm/footer/footer.tsx b/ui/pages/confirmations/components/confirm/footer/footer.tsx index fc98394623b0..b04e0ff9a23d 100644 --- a/ui/pages/confirmations/components/confirm/footer/footer.tsx +++ b/ui/pages/confirmations/components/confirm/footer/footer.tsx @@ -34,6 +34,7 @@ import { useEnableShieldCoverageChecks } from '../../../hooks/transactions/useEn import { useTransactionConfirm } from '../../../hooks/transactions/useTransactionConfirm'; import { useConfirmActions } from '../../../hooks/useConfirmActions'; import { useDappSwapActions } from '../../../hooks/transactions/dapp-swap-comparison/useDappSwapActions'; +import { useUniversalTransactionBalanceAlert } from '../../../hooks/alerts/transactions/useUniversalTransactionBalanceAlert'; import { useOriginThrottling } from '../../../hooks/useOriginThrottling'; import { isAddEthereumChainType, @@ -245,6 +246,8 @@ const Footer = () => { const currentConfirmationId = currentConfirmation?.id; const t = useI18nContext(); const { isGaslessLoading } = useIsGaslessLoading(); + const { isLoading: isUniversalTransactionBalanceLoading } = + useUniversalTransactionBalanceAlert(); const { from: fromAddress } = getConfirmationSender(currentConfirmation); const { shouldThrottleOrigin } = useOriginThrottling(); @@ -304,7 +307,9 @@ const Footer = () => { ]); const isConfirmDisabled = - (!isScrollToBottomCompleted && !isSignature) || isGaslessLoading; + (!isScrollToBottomCompleted && !isSignature) || + isGaslessLoading || + isUniversalTransactionBalanceLoading; const shouldShowReconnectButton = shouldRunHardwareWalletPreflight && diff --git a/ui/pages/confirmations/components/confirm/header/header.tsx b/ui/pages/confirmations/components/confirm/header/header.tsx index b48627a64381..5e669486eb1b 100644 --- a/ui/pages/confirmations/components/confirm/header/header.tsx +++ b/ui/pages/confirmations/components/confirm/header/header.tsx @@ -4,6 +4,7 @@ import { } from '@metamask/transaction-controller'; import React from 'react'; import { ORIGIN_METAMASK } from '../../../../../../shared/constants/app'; +import { UNIVERSAL_TRANSACTION_APPROVAL_TYPE } from '../../../../../../shared/constants/confirmations'; import { Box, Text } from '../../../../../components/component-library'; import { PreferredAvatar } from '../../../../../components/app/preferred-avatar'; import { @@ -90,10 +91,15 @@ const Header = () => { // back button if it's a wallet initiated confirmation. The default header is // the original header for the redesigns and includes the sender and recipient // addresses as well. + const isUniversalTransaction = + (currentConfirmation?.type as string | undefined) === + UNIVERSAL_TRANSACTION_APPROVAL_TYPE; const isConfirmationWithNewHeader = - currentConfirmation?.type && - CONFIRMATIONS_WITH_ALT_HEADER.includes(currentConfirmation.type); + isUniversalTransaction || + (currentConfirmation?.type && + CONFIRMATIONS_WITH_ALT_HEADER.includes(currentConfirmation.type)); const isWalletInitiated = + isUniversalTransaction || (currentConfirmation as TransactionMeta)?.origin === ORIGIN_METAMASK; const isSimpleHeader = diff --git a/ui/pages/confirmations/components/confirm/header/wallet-initiated-header.tsx b/ui/pages/confirmations/components/confirm/header/wallet-initiated-header.tsx index 57c0fac58ed4..5deb8d22340d 100644 --- a/ui/pages/confirmations/components/confirm/header/wallet-initiated-header.tsx +++ b/ui/pages/confirmations/components/confirm/header/wallet-initiated-header.tsx @@ -5,6 +5,7 @@ import { import React, { useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import { MetaMetricsEventLocation } from '../../../../../../shared/constants/metametrics'; +import { UNIVERSAL_TRANSACTION_APPROVAL_TYPE } from '../../../../../../shared/constants/confirmations'; import { Box, ButtonIcon, @@ -35,11 +36,23 @@ export const WalletInitiatedHeader = () => { const { currentConfirmation } = useConfirmContext(); const navigate = useNavigate(); + const isUniversalTransaction = + currentConfirmation?.type === UNIVERSAL_TRANSACTION_APPROVAL_TYPE; + const isSendTransaction = - currentConfirmation?.type && - SEND_TRANSACTION_TYPES.includes(currentConfirmation.type); + isUniversalTransaction || + (currentConfirmation?.type && + SEND_TRANSACTION_TYPES.includes(currentConfirmation.type)); const handleBackButtonClick = useCallback(() => { + if (isUniversalTransaction) { + onCancel({ + location: MetaMetricsEventLocation.Confirmation, + navigateBackForSend: true, + }); + return; + } + if ( currentConfirmation.type === TransactionType.shieldSubscriptionApprove ) { @@ -74,7 +87,7 @@ export const WalletInitiatedHeader = () => { navigateBackForSend: true, }); } - }, [currentConfirmation, navigate, onCancel]); + }, [currentConfirmation, isUniversalTransaction, navigate, onCancel]); const getHeaderTitle = () => { if (isSendTransaction) { diff --git a/ui/pages/confirmations/components/confirm/info/info.tsx b/ui/pages/confirmations/components/confirm/info/info.tsx index df953c8642ae..67f27f3e3c4d 100644 --- a/ui/pages/confirmations/components/confirm/info/info.tsx +++ b/ui/pages/confirmations/components/confirm/info/info.tsx @@ -1,6 +1,7 @@ import { TransactionType } from '@metamask/transaction-controller'; import { ApprovalType } from '@metamask/controller-utils'; import React, { useMemo } from 'react'; +import { UNIVERSAL_TRANSACTION_APPROVAL_TYPE } from '../../../../../../shared/constants/confirmations'; import { useEnabledAdvancedPermissions } from '../../../../../hooks/gator-permissions/useEnabledAdvancedPermissions'; import { useTrustSignalMetrics } from '../../../../trust-signals/hooks/useTrustSignalMetrics'; import { useConfirmContext } from '../../../context/confirm'; @@ -29,6 +30,7 @@ import TokenTransferInfo from './token-transfer/token-transfer'; import TypedSignV1Info from './typed-sign-v1/typed-sign-v1'; import TypedSignInfo from './typed-sign/typed-sign'; import TypedSignPermissionInfo from './typed-sign/typed-sign-permission'; +import UniversalTransactionInfo from './universal-transaction/universal-transaction-info'; // TODO: Fix in https://github.com/MetaMask/metamask-extension/issues/31860 // eslint-disable-next-line @typescript-eslint/naming-convention @@ -159,6 +161,8 @@ const Info = () => { [ApprovalType.AddEthereumChain]: () => AddEthereumChain, + [UNIVERSAL_TRANSACTION_APPROVAL_TYPE]: () => UniversalTransactionInfo, + [TransactionType.musdClaim]: () => MusdClaimInfo, [TransactionType.musdConversion]: () => MusdConversionInfo, [TransactionType.perpsDeposit]: () => PerpsDepositInfo, diff --git a/ui/pages/confirmations/components/confirm/info/universal-transaction/universal-transaction-info.tsx b/ui/pages/confirmations/components/confirm/info/universal-transaction/universal-transaction-info.tsx new file mode 100644 index 000000000000..396123c90610 --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/universal-transaction/universal-transaction-info.tsx @@ -0,0 +1,62 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { Box } from '@metamask/design-system-react'; +import React from 'react'; + +import { ConfirmInfoSection } from '../../../../../../components/app/confirm/info/row/section'; +import { useSolanaTransactionData } from '../../../../hooks/transactions/useSolanaTransactionData'; +import { useUniversalTransactionDataOptional } from '../../../../hooks/transactions/useUniversalTransactionData'; +import { SolanaTestCountRow } from '../../../rows/solana/solana-test-count-row'; +import { UniversalTransactionFeeRow } from '../../../rows/universal/universal-transaction-fee-row'; +import { UniversalTransactionFromRow } from '../../../rows/universal/universal-transaction-from-row'; +import { UniversalTransactionHeadingRow } from '../../../rows/universal/universal-transaction-heading-row'; +import { UniversalTransactionNetworkRow } from '../../../rows/universal/universal-transaction-network-row'; +import { UniversalTransactionToRow } from '../../../rows/universal/universal-transaction-to-row'; +import { ConfirmLoader } from '../shared/confirm-loader/confirm-loader'; + +function UniversalTransactionInfo() { + const data = useUniversalTransactionDataOptional(); + + if (!data) { + return ; + } + + return ( + <> + + + + + + + + + + + + + + + ); +} + +function SolanaSection() { + const data = useSolanaTransactionData(); + + if (!data) { + return null; + } + + return ( + + + + ); +} + +export default UniversalTransactionInfo; diff --git a/ui/pages/confirmations/components/rows/solana/solana-test-count-row.tsx b/ui/pages/confirmations/components/rows/solana/solana-test-count-row.tsx new file mode 100644 index 000000000000..f3a6a5611e44 --- /dev/null +++ b/ui/pages/confirmations/components/rows/solana/solana-test-count-row.tsx @@ -0,0 +1,19 @@ +import React from 'react'; + +import { ConfirmInfoRow } from '../../../../../components/app/confirm/info/row/row'; +import { useSolanaTransactionData } from '../../../hooks/transactions/useSolanaTransactionData'; + +export function SolanaTestCountRow() { + const data = useSolanaTransactionData(); + const count = data?.custom?.count; + + if (count === undefined) { + return null; + } + + return ( + + {String(count)} + + ); +} diff --git a/ui/pages/confirmations/components/rows/universal/truncate-universal-address.ts b/ui/pages/confirmations/components/rows/universal/truncate-universal-address.ts new file mode 100644 index 000000000000..23ab34e9c127 --- /dev/null +++ b/ui/pages/confirmations/components/rows/universal/truncate-universal-address.ts @@ -0,0 +1,24 @@ +const ELLIPSIS = '\u2026'; +const PREFIX_LENGTH = 6; +const SUFFIX_LENGTH = 4; + +/** + * Middle-truncate a non-EVM (e.g. Solana base58) address for display. + * + * Mirrors the visual style of the EVM AccountFlowRow truncation, without + * applying any EVM-specific normalization (no checksumming, no lowercasing). + * + * @param address - Raw address string. + * @returns Truncated address, e.g. `C9qwTy...44v`. + */ +export function truncateUniversalAddress(address: string): string { + if (!address) { + return ''; + } + + if (address.length <= PREFIX_LENGTH + SUFFIX_LENGTH + 1) { + return address; + } + + return `${address.slice(0, PREFIX_LENGTH)}${ELLIPSIS}${address.slice(-SUFFIX_LENGTH)}`; +} diff --git a/ui/pages/confirmations/components/rows/universal/universal-transaction-fee-row.tsx b/ui/pages/confirmations/components/rows/universal/universal-transaction-fee-row.tsx new file mode 100644 index 000000000000..3570c2545414 --- /dev/null +++ b/ui/pages/confirmations/components/rows/universal/universal-transaction-fee-row.tsx @@ -0,0 +1,86 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; + +import { ConfirmInfoAlertRow } from '../../../../../components/app/confirm/info/row/alert-row/alert-row'; +import { RowAlertKey } from '../../../../../components/app/confirm/info/row/constants'; +import { + AvatarToken, + AvatarTokenSize, + Box, + Text, +} from '../../../../../components/component-library'; +import { + AlignItems, + Display, + FlexDirection, + JustifyContent, + TextAlign, + TextColor, + TextVariant, +} from '../../../../../helpers/constants/design-system'; +import { useI18nContext } from '../../../../../hooks/useI18nContext'; +import { useConfirmationId } from '../../../hooks/useConfirmationId'; +import { useUniversalTransactionDataOptional } from '../../../hooks/transactions/useUniversalTransactionData'; +import { selectConfirmationAdvancedDetailsOpen } from '../../../selectors/preferences'; + +export function UniversalTransactionFeeRow() { + const t = useI18nContext(); + const data = useUniversalTransactionDataOptional(); + const confirmationId = useConfirmationId(); + const showAdvancedDetails = useSelector( + selectConfirmationAdvancedDetailsOpen, + ); + + if (!data?.formattedFee || !data.feeAssetSymbol) { + return null; + } + + const collapsedFee = data.formattedFeeFiat ?? data.formattedFee; + + const amountWithIcon = (amountText: string) => ( + + {amountText} + + {data.feeAssetSymbol} + + ); + + return ( + + {showAdvancedDetails ? ( + + {amountWithIcon(data.formattedFee)} + {data.formattedFeeFiat ? ( + + {data.formattedFeeFiat} + + ) : null} + + ) : ( + amountWithIcon(collapsedFee) + )} + + ); +} diff --git a/ui/pages/confirmations/components/rows/universal/universal-transaction-from-row.tsx b/ui/pages/confirmations/components/rows/universal/universal-transaction-from-row.tsx new file mode 100644 index 000000000000..25820ec7fe92 --- /dev/null +++ b/ui/pages/confirmations/components/rows/universal/universal-transaction-from-row.tsx @@ -0,0 +1,67 @@ +import { + AvatarAccountSize, + Box, + BoxAlignItems, + BoxFlexDirection, + FontWeight, + Text, + TextColor, + TextVariant, +} from '@metamask/design-system-react'; +import React from 'react'; +import { useSelector } from 'react-redux'; + +import { ConfirmInfoRow } from '../../../../../components/app/confirm/info/row/row'; +import { PreferredAvatar } from '../../../../../components/app/preferred-avatar'; +import { FlexDirection } from '../../../../../helpers/constants/design-system'; +import { useI18nContext } from '../../../../../hooks/useI18nContext'; +import { useUniversalTransactionDataOptional } from '../../../hooks/transactions/useUniversalTransactionData'; +import { selectAccountGroupNameByInternalAccount } from '../../../selectors/accounts'; +import { truncateUniversalAddress } from './truncate-universal-address'; + +export function UniversalTransactionFromRow() { + const t = useI18nContext(); + const data = useUniversalTransactionDataOptional(); + const accountGroupName = useSelector((state) => + selectAccountGroupNameByInternalAccount( + state as Parameters[0], + data?.from, + ), + ); + + if (!data) { + return null; + } + + const display = accountGroupName ?? truncateUniversalAddress(data.from); + + return ( + + + + + {display} + + + + + + ); +} diff --git a/ui/pages/confirmations/components/rows/universal/universal-transaction-heading-row.tsx b/ui/pages/confirmations/components/rows/universal/universal-transaction-heading-row.tsx new file mode 100644 index 000000000000..525685bc799f --- /dev/null +++ b/ui/pages/confirmations/components/rows/universal/universal-transaction-heading-row.tsx @@ -0,0 +1,41 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import React from 'react'; + +import { + AvatarToken, + AvatarTokenSize, + Box, + Text, +} from '../../../../../components/component-library'; +import { + TextColor, + TextVariant, +} from '../../../../../helpers/constants/design-system'; +import SendHeadingLayout from '../../confirm/info/shared/send-heading-layout/send-heading-layout'; +import { useUniversalTransactionDataOptional } from '../../../hooks/transactions/useUniversalTransactionData'; + +export function UniversalTransactionHeadingRow() { + const data = useUniversalTransactionDataOptional(); + + if (!data) { + return null; + } + + return ( + + } + > + + + {`${data.formattedAmount} ${data.assetSymbol}`} + + + + ); +} diff --git a/ui/pages/confirmations/components/rows/universal/universal-transaction-network-row.tsx b/ui/pages/confirmations/components/rows/universal/universal-transaction-network-row.tsx new file mode 100644 index 000000000000..ea1849453944 --- /dev/null +++ b/ui/pages/confirmations/components/rows/universal/universal-transaction-network-row.tsx @@ -0,0 +1,50 @@ +import React from 'react'; + +import { ConfirmInfoRow } from '../../../../../components/app/confirm/info/row/row'; +import { + AvatarNetwork, + AvatarNetworkSize, + Box, + Text, +} from '../../../../../components/component-library'; +import { + AlignItems, + BlockSize, + Display, + FlexWrap, + TextColor, + TextVariant, +} from '../../../../../helpers/constants/design-system'; +import { useI18nContext } from '../../../../../hooks/useI18nContext'; +import { useUniversalTransactionDataOptional } from '../../../hooks/transactions/useUniversalTransactionData'; + +export function UniversalTransactionNetworkRow() { + const t = useI18nContext(); + const data = useUniversalTransactionDataOptional(); + + if (!data) { + return null; + } + + return ( + + + + + {data.networkLabel} + + + + ); +} diff --git a/ui/pages/confirmations/components/rows/universal/universal-transaction-to-row.tsx b/ui/pages/confirmations/components/rows/universal/universal-transaction-to-row.tsx new file mode 100644 index 000000000000..83b105fa36e0 --- /dev/null +++ b/ui/pages/confirmations/components/rows/universal/universal-transaction-to-row.tsx @@ -0,0 +1,67 @@ +import { + AvatarAccountSize, + Box, + BoxAlignItems, + BoxFlexDirection, + FontWeight, + Text, + TextColor, + TextVariant, +} from '@metamask/design-system-react'; +import React from 'react'; +import { useSelector } from 'react-redux'; + +import { ConfirmInfoRow } from '../../../../../components/app/confirm/info/row/row'; +import { PreferredAvatar } from '../../../../../components/app/preferred-avatar'; +import { FlexDirection } from '../../../../../helpers/constants/design-system'; +import { useI18nContext } from '../../../../../hooks/useI18nContext'; +import { useUniversalTransactionDataOptional } from '../../../hooks/transactions/useUniversalTransactionData'; +import { selectAccountGroupNameByInternalAccount } from '../../../selectors/accounts'; +import { truncateUniversalAddress } from './truncate-universal-address'; + +export function UniversalTransactionToRow() { + const t = useI18nContext(); + const data = useUniversalTransactionDataOptional(); + const accountGroupName = useSelector((state) => + selectAccountGroupNameByInternalAccount( + state as Parameters[0], + data?.to, + ), + ); + + if (!data) { + return null; + } + + const display = accountGroupName ?? truncateUniversalAddress(data.to); + + return ( + + + + + {display} + + + + + + ); +} diff --git a/ui/pages/confirmations/components/send/amount-recipient/amount-recipient.tsx b/ui/pages/confirmations/components/send/amount-recipient/amount-recipient.tsx index eeaf88d91b61..518832a59b73 100644 --- a/ui/pages/confirmations/components/send/amount-recipient/amount-recipient.tsx +++ b/ui/pages/confirmations/components/send/amount-recipient/amount-recipient.tsx @@ -32,7 +32,8 @@ export const AmountRecipient = () => { const [shouldSubmitOnAcknowledge, setShouldSubmitOnAcknowledge] = useState(false); const { asset, toResolved, nonEVMSubmitError } = useSendContext(); - const { amountError, validateNonEvmAmountAsync } = useAmountValidation(); + const { amountError, isBalanceLoading, validateNonEvmAmountAsync } = + useAmountValidation(); const { isNonEvmSendType } = useSendType(); const { handleSubmit } = useSendActions(); const { captureAmountSelected } = useAmountSelectionMetrics(); @@ -50,6 +51,7 @@ export const AmountRecipient = () => { Boolean(hexDataError) || Boolean(nonEVMSubmitError); const isDisabled = hasBlockingError || !toResolved || isNetworkUnreliable; + const isSubmitDisabled = isDisabled || isBalanceLoading; const proceedWithSubmit = useCallback(async () => { if (isNonEvmSendType) { @@ -119,7 +121,7 @@ export const AmountRecipient = () => {