Skip to content

Commit

Permalink
Merge branch 'brian/network-controller-v20-merging-in-v21' of github.…
Browse files Browse the repository at this point in the history
…com:MetaMask/metamask-extension into brian/network-controller-v20-merging-in-v21
  • Loading branch information
bergeron committed Sep 12, 2024
2 parents affb750 + 943be09 commit d69e1b8
Show file tree
Hide file tree
Showing 16 changed files with 416 additions and 8 deletions.
27 changes: 27 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.

29 changes: 29 additions & 0 deletions ui/components/app/assets/asset-list/asset-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,17 @@ import {
DetectedTokensBanner,
TokenListItem,
ImportTokenLink,
ReceiveModal,
} from '../../../multichain';
import { useAccountTotalFiatBalance } from '../../../../hooks/useAccountTotalFiatBalance';
import { useIsOriginalNativeTokenSymbol } from '../../../../hooks/useIsOriginalNativeTokenSymbol';
import { useI18nContext } from '../../../../hooks/useI18nContext';
import {
showPrimaryCurrency,
showSecondaryCurrency,
} from '../../../../../shared/modules/currency-display.utils';
import { roundToDecimalPlacesRemovingExtraZeroes } from '../../../../helpers/utils/util';
import { FundingMethodModal } from '../../../multichain/funding-method-modal/funding-method-modal';
///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask)
import {
RAMPS_CARD_VARIANT_TYPES,
Expand All @@ -66,6 +69,7 @@ const AssetList = ({ onClickAsset, showTokensLinks }) => {
type,
rpcUrl,
);
const t = useI18nContext();
const trackEvent = useContext(MetaMetricsContext);
const balance = useSelector(getMultichainSelectedAccountCachedBalance);
const balanceIsLoading = !balance;
Expand Down Expand Up @@ -101,6 +105,14 @@ const AssetList = ({ onClickAsset, showTokensLinks }) => {
getIstokenDetectionInactiveOnNonMainnetSupportedNetwork,
);

const [showFundingMethodModal, setShowFundingMethodModal] = useState(false);
const [showReceiveModal, setShowReceiveModal] = useState(false);

const onClickReceive = () => {
setShowFundingMethodModal(false);
setShowReceiveModal(true);
};

const { tokensWithBalances, loading } = useAccountTotalFiatBalance(
selectedAccount,
shouldHideZeroBalanceTokens,
Expand Down Expand Up @@ -151,6 +163,9 @@ const AssetList = ({ onClickAsset, showTokensLinks }) => {
? RAMPS_CARD_VARIANT_TYPES.BTC
: RAMPS_CARD_VARIANT_TYPES.TOKEN
}
handleOnClick={
isBtc ? undefined : () => setShowFundingMethodModal(true)
}
/>
) : null
///: END:ONLY_INCLUDE_IF
Expand Down Expand Up @@ -213,6 +228,20 @@ const AssetList = ({ onClickAsset, showTokensLinks }) => {
{showDetectedTokens && (
<DetectedToken setShowDetectedTokens={setShowDetectedTokens} />
)}
{showReceiveModal && selectedAccount?.address && (
<ReceiveModal
address={selectedAccount.address}
onClose={() => setShowReceiveModal(false)}
/>
)}
{showFundingMethodModal && (
<FundingMethodModal
isOpen={showFundingMethodModal}
onClose={() => setShowFundingMethodModal(false)}
title={t('selectFundingMethod')}
onClickReceive={onClickReceive}
/>
)}
</>
);
};
Expand Down
4 changes: 3 additions & 1 deletion ui/components/app/currency-input/currency-input.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ export default function CurrencyInput({
isSkeleton,
isMatchingUpstream,
}) {
const assetDecimals = Number(asset?.decimals) || NATIVE_CURRENCY_DECIMALS;
const assetDecimals = isNaN(Number(asset?.decimals))
? NATIVE_CURRENCY_DECIMALS
: Number(asset?.decimals);

const preferredCurrency = useSelector(getNativeCurrency);
const secondaryCurrency = useSelector(getCurrentCurrency);
Expand Down
5 changes: 3 additions & 2 deletions ui/components/app/modals/qr-scanner/qr-scanner.component.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import Spinner from '../../../ui/spinner';

import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../../../shared/constants/app';
import { SECOND } from '../../../../../shared/constants/time';
import { parseScanContent } from './scan-util';

const READY_STATE = {
ACCESSING_CAMERA: 'ACCESSING_CAMERA',
Expand All @@ -30,8 +31,8 @@ const parseContent = (content) => {
// Ethereum address links - fox ex. ethereum:0x.....1111
if (content.split('ethereum:').length > 1) {
type = 'address';
values = { address: content.split('ethereum:')[1] };

// uses regex capture groups to match and extract address while ignoring everything else
values = { address: parseScanContent(content) };
// Regular ethereum addresses - fox ex. 0x.....1111
} else if (content.substring(0, 2).toLowerCase() === '0x') {
type = 'address';
Expand Down
24 changes: 24 additions & 0 deletions ui/components/app/modals/qr-scanner/qr-scanner.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { parseScanContent } from './scan-util';

describe('Extract Address from string', () => {
it('should correctly extract the address from the string', () => {
const result = parseScanContent(
'ethereum:0xCf464B40cb2419944138F24514C9aE4D1889ccC1',
);
expect(result).toStrictEqual('0xCf464B40cb2419944138F24514C9aE4D1889ccC1');
});

it('should correctly extract the address from the string when there is a 0x appended', () => {
const result = parseScanContent(
'ethereum:0xCf464B40cb2419944138F24514C9aE4D1889ccC1@0x1',
);
expect(result).toStrictEqual('0xCf464B40cb2419944138F24514C9aE4D1889ccC1');
});

it('should return null if there is no address to extract that matches the pattern', () => {
const result = parseScanContent(
'ethereum:0xCf464B40cb2419944138F24514C9aE4D1',
);
expect(result).toStrictEqual(null);
});
});
7 changes: 7 additions & 0 deletions ui/components/app/modals/qr-scanner/scan-util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export function parseScanContent(content: string): string | null {
const matches = content.match(/^[a-zA-Z]+:(0x[0-9a-fA-F]{40})(?:@.*)?/u);
if (!matches) {
return null;
}
return matches[1];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from 'react';
import { Box, Text, Icon, IconName } from '../../component-library';
import {
Display,
FlexDirection,
TextVariant,
TextColor,
AlignItems,
} from '../../../helpers/constants/design-system';

type FundingMethodItemProps = {
icon: IconName;
title: string;
description: string;
onClick: () => void;
};

const FundingMethodItem: React.FC<FundingMethodItemProps> = ({
icon,
title,
description,
onClick,
}) => (
<Box
display={[Display.Flex]}
gap={2}
alignItems={AlignItems.center}
onClick={onClick}
className="funding-method-item"
padding={4}
>
<Icon name={icon} />
<Box display={[Display.Flex]} flexDirection={FlexDirection.Column}>
<Text variant={TextVariant.bodyMdMedium}>{title}</Text>
<Text variant={TextVariant.bodySm} color={TextColor.textAlternative}>
{description}
</Text>
</Box>
</Box>
);

export default FundingMethodItem;
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import React from 'react';
import { fireEvent } from '@testing-library/react';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { renderWithProvider } from '../../../../test/jest/rendering';
import mockState from '../../../../test/data/mock-state.json';
import useRamps from '../../../hooks/ramps/useRamps/useRamps';
import { FundingMethodModal } from './funding-method-modal';

jest.mock('../../../hooks/ramps/useRamps/useRamps', () => ({
__esModule: true,
default: jest.fn(),
}));

const mockStore = configureMockStore([thunk]);

describe('FundingMethodModal', () => {
let store = configureMockStore([thunk])(mockState);
let openBuyCryptoInPdapp: jest.Mock<() => void>;

beforeEach(() => {
store = mockStore(mockState);
openBuyCryptoInPdapp = jest.fn();
(useRamps as jest.Mock).mockReturnValue({ openBuyCryptoInPdapp });
});

afterEach(() => {
jest.clearAllMocks();
});

it('should render the modal when isOpen is true', () => {
const { getByTestId, getByText } = renderWithProvider(
<FundingMethodModal
isOpen={true}
onClose={jest.fn()}
title="Test Modal"
onClickReceive={jest.fn()}
/>,
store,
);

expect(getByTestId('funding-method-modal')).toBeInTheDocument();
expect(getByText('Test Modal')).toBeInTheDocument();
});

it('should not render the modal when isOpen is false', () => {
const { queryByTestId } = renderWithProvider(
<FundingMethodModal
isOpen={false}
onClose={jest.fn()}
title="Test Modal"
onClickReceive={jest.fn()}
/>,
store,
);

expect(queryByTestId('funding-method-modal')).toBeNull();
});

it('should call openBuyCryptoInPdapp when the Buy Crypto item is clicked', () => {
const { getByText } = renderWithProvider(
<FundingMethodModal
isOpen={true}
onClose={jest.fn()}
title="Test Modal"
onClickReceive={jest.fn()}
/>,
store,
);

fireEvent.click(getByText('Buy crypto'));
expect(openBuyCryptoInPdapp).toHaveBeenCalled();
});

it('should call onClickReceive when the Receive Crypto item is clicked', () => {
const onClickReceive = jest.fn();
const { getByText } = renderWithProvider(
<FundingMethodModal
isOpen={true}
onClose={jest.fn()}
title="Test Modal"
onClickReceive={onClickReceive}
/>,
store,
);

fireEvent.click(getByText('Receive crypto'));
expect(onClickReceive).toHaveBeenCalled();
});

it('should open a new tab with the correct URL when Transfer Crypto item is clicked', () => {
global.platform.openTab = jest.fn();

const { getByText } = renderWithProvider(
<FundingMethodModal
isOpen={true}
onClose={jest.fn()}
title="Test Modal"
onClickReceive={jest.fn()}
/>,
store,
);

fireEvent.click(getByText('Transfer crypto'));
expect(global.platform.openTab).toHaveBeenCalledWith({
url: expect.stringContaining('transfer'),
});
});
});
Loading

0 comments on commit d69e1b8

Please sign in to comment.