Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions app/_locales/en/messages.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 36 additions & 0 deletions app/_locales/en_GB/messages.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 28 additions & 2 deletions shared/lib/activity/types.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,34 @@
import type {
ActivityItem,
ActivityKind as ClientActivityKind,
} from '@metamask/client-utils';

export type {
ActivityItem as ActivityListItem,
ActivityKind,
Status,
TokenAmount,
FiatAmount,
Fee as ActivityFee,
} from '@metamask/client-utils';

/**
* Money-account deposits and withdrawals are extension-local activity kinds:
* `@metamask/client-utils` has no money-account mapping (the transactions are
* EIP-7702 batches it classifies as `contractInteraction`), so
* `enrichLocalActivity` re-types them. They reuse the perps MM Pay data shape
* (`fiat` + `token`) because the rows render the same way: a fiat amount
* against a product balance.
*/
export type MoneyAccountActivityKind =
| 'moneyAccountDeposit'
| 'moneyAccountWithdraw';

export type MoneyAccountActivityItem = Omit<
Extract<ActivityItem, { type: 'perpsAddFunds' | 'perpsWithdraw' }>,
'type'
> & {
type: MoneyAccountActivityKind;
};

export type ActivityKind = ClientActivityKind | MoneyAccountActivityKind;

export type ActivityListItem = ActivityItem | MoneyAccountActivityItem;
28 changes: 28 additions & 0 deletions ui/pages/activity/rows/useActivityRowContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ import type { ActivityRowProps } from '../types';
import { useFormatAsFiat } from '../../../hooks/useFormatAsFiat';
import { useFormatTokenAmount } from './useFormatTokenAmount';

// mUSD is pegged 1:1 to USD, so money-account rows show USD like perps rows.
const MONEY_ACCOUNT_FIAT_CURRENCY = 'usd';

type ActivityContent = {
title: string;
subtitle?: string;
Expand Down Expand Up @@ -250,6 +253,31 @@ export function useActivityRowContent(activity: ActivityRowProps['data']) {
activity.type === 'perpsAddFunds' ? 'in' : undefined,
};
}
// Fiat amount against the Money account, like the other MM Pay rows
case 'moneyAccountDeposit':
case 'moneyAccountWithdraw': {
const { fiat, token } = activity.data;
const fiatAmount = fiat ? Number(fiat.amount) : undefined;
const signedFiatAmount =
activity.type === 'moneyAccountWithdraw' && fiatAmount !== undefined
? -fiatAmount
: fiatAmount;

return {
avatarTokens: [token?.assetId],
title: t(labelKeys.title.key),
subtitle: t(labelKeys.description.key),
primaryAmount:
signedFiatAmount !== undefined && Number.isFinite(signedFiatAmount)
? formatCurrencyWithMinThreshold(
signedFiatAmount,
MONEY_ACCOUNT_FIAT_CURRENCY,
)
: undefined,
primaryDirection:
activity.type === 'moneyAccountDeposit' ? 'in' : undefined,
};
}
case 'nftBuy':
case 'nftSell': {
const { token, paymentToken } = activity.data;
Expand Down
139 changes: 139 additions & 0 deletions ui/pages/details/templates/money-account-details.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import React from 'react';
import {
Button,
ButtonSize,
ButtonVariant,
Text,
} from '@metamask/design-system-react';
import { TransactionStatus as TransactionMetaStatus } from '@metamask/transaction-controller';
import type { MoneyAccountActivityItem } from '../../../../shared/lib/activity/types';
import { ActivityAvatar } from '../../../components/app/activity-list-item-avatar';
import { TransactionStatus } from '../../../components/app/transaction/transaction-status';
import { useLocalTransactionMeta } from '../../../hooks/activity/useLocalTransactionMeta';
import { useMoneyAccountDeposit } from '../../../hooks/money/useMoneyAccountDeposit';
import { useMoneyAccountInfo } from '../../../hooks/money/useMoneyAccountInfo';
import { useFormatters } from '../../../hooks/useFormatters';
import { useI18nContext } from '../../../hooks/useI18nContext';
// eslint-disable-next-line import-x/no-restricted-paths
import { TransactionDetailsProvider } from '../../confirmations/components/activity/transaction-details-context';
// eslint-disable-next-line import-x/no-restricted-paths
import { TransactionDetailsSummary } from '../../confirmations/components/activity/transaction-details-summary';
import { BlockExplorerButton } from '../components/block-explorer-button';
import { Footer, Row, Section } from '../components/shared';

// mUSD is pegged 1:1 to USD, so money-account details show USD like perps.
const MONEY_ACCOUNT_FIAT_CURRENCY = 'usd';

type Props = {
item: MoneyAccountActivityItem;
};

/**
* Details for money-account deposits and withdrawals, laid out like the
* other MM Pay details (perps): fiat hero, status and date, MM Pay fee
* breakdown, and the per-transaction summary.
*
* @param props - Component props.
* @param props.item - The money-account activity item to render.
*/
export function MoneyAccountDetails({ item }: Readonly<Props>) {
const t = useI18nContext();
const { formatDateTime, formatCurrencyWithMinThreshold } = useFormatters();
const { hasMoneyAccount } = useMoneyAccountInfo();
const { initiateDeposit, isLoading: isDepositLoading } =
useMoneyAccountDeposit();
const transactionMeta = useLocalTransactionMeta(item.hash);
const { metamaskPay } = transactionMeta ?? {};
const { networkFeeFiat, bridgeFeeFiat, totalFiat } = metamaskPay || {};

const isDeposit = item.type === 'moneyAccountDeposit';

const formatFiat = (value?: string) =>
value
? formatCurrencyWithMinThreshold(
Number(value),
MONEY_ACCOUNT_FIAT_CURRENCY,
)
: null;

const formattedAmount = formatFiat(item.data.fiat?.amount);
const hasFees = Boolean(networkFeeFiat || bridgeFeeFiat || totalFiat);

return (
<div className="flex grow flex-col">
<div className="divide-y divide-border-muted">
<div
className="flex items-center gap-2 pb-4"
data-testid="transaction-details-hero"
>
<ActivityAvatar tokens={[item.data.token?.assetId]} />
<Text
variant="heading-lg"
color={isDeposit ? 'text-success-default' : 'text-default'}
>
{formattedAmount
? `${isDeposit ? '+' : '-'}${formattedAmount}`
: null}
</Text>
</div>

<Section>
<Row
label={t('status')}
value={<TransactionStatus status={item.status} hash={item.hash} />}
/>

<Row label={t('date')} value={formatDateTime(item.timestamp)} />
</Section>

{hasFees ? (
<Section>
<Row
label={t('networkFee')}
testId="transaction-base-fee"
value={formatFiat(networkFeeFiat)}
/>
{bridgeFeeFiat ? (
<Row
label={t('providerFee')}
testId="transaction-bridge-fee"
value={formatFiat(bridgeFeeFiat)}
/>
) : null}
<Row
label={t('total')}
testId="transaction-breakdown-value-amount"
value={formatFiat(totalFiat)}
/>
</Section>
) : null}

{transactionMeta ? (
<Section>
<TransactionDetailsProvider transactionMeta={transactionMeta}>
<TransactionDetailsSummary />
</TransactionDetailsProvider>
</Section>
) : null}
</div>

<Footer>
{isDeposit &&
hasMoneyAccount &&
transactionMeta?.status === TransactionMetaStatus.confirmed ? (
<Button
className="w-full"
size={ButtonSize.Lg}
variant={ButtonVariant.Primary}
isLoading={isDepositLoading}
onClick={() => initiateDeposit()}
>
{t('addFunds')}
</Button>
) : (
<BlockExplorerButton chainId={item.chainId} txHash={item.hash} />
)}
</Footer>
</div>
);
}
4 changes: 4 additions & 0 deletions ui/pages/details/templates/template-loader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ApprovalDetails } from './approval-details';
import { BridgeDetails } from './bridge-details/bridge-details';
import { ConvertDetails } from './convert-details';
import { DefaultDetails } from './default-details';
import { MoneyAccountDetails } from './money-account-details';
import { NftDetails } from './nft-details';
import { PerpsDepositDetails } from './perps-deposit-details';
import { PerpsDetails } from './perps-details';
Expand Down Expand Up @@ -43,6 +44,9 @@ export function TemplateLoader({ item }: Props) {
case 'revokeSpendingCap':
case 'increaseSpendingCap':
return <ApprovalDetails item={item} />;
case 'moneyAccountDeposit':
case 'moneyAccountWithdraw':
return <MoneyAccountDetails item={item} />;
case 'perpsAddFunds':
return <PerpsDepositDetails item={item} />;
case 'perpsWithdraw':
Expand Down
Loading
Loading