Skip to content

Commit 23df428

Browse files
authored
Merge branch 'main' into jc/WAPI-728
2 parents 1574d60 + e4220c6 commit 23df428

File tree

9 files changed

+472
-84
lines changed

9 files changed

+472
-84
lines changed

ui/components/multichain-accounts/address-qr-code-modal/address-qr-code-modal.test.tsx

Lines changed: 78 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,43 @@
11
import React from 'react';
2-
import { fireEvent, screen, waitFor } from '@testing-library/react';
2+
import { fireEvent, screen } from '@testing-library/react';
33
import { renderWithProvider } from '../../../../test/jest';
44
import { useCopyToClipboard } from '../../../hooks/useCopyToClipboard';
55
import { openBlockExplorer } from '../../multichain/menu-items/view-explorer-menu-item';
6+
import { getBlockExplorerInfo } from '../../../helpers/utils/multichain/getBlockExplorerInfo';
67
import { AddressQRCodeModal } from './address-qr-code-modal';
78

8-
// Mock copy to clipboard hook
9+
// Import the mocked function
10+
11+
// Mock only the essential dependencies that the component actually uses
912
jest.mock('../../../hooks/useCopyToClipboard', () => ({
1013
useCopyToClipboard: jest.fn(),
1114
}));
1215

13-
// Mock the openBlockExplorer function
1416
jest.mock(
1517
'../../../components/multichain/menu-items/view-explorer-menu-item',
1618
() => ({
1719
openBlockExplorer: jest.fn(),
1820
}),
1921
);
2022

23+
jest.mock('../../../helpers/utils/multichain/getBlockExplorerInfo', () => ({
24+
getBlockExplorerInfo: jest.fn(),
25+
}));
26+
2127
const mockUseCopyToClipboard = useCopyToClipboard as jest.MockedFunction<
2228
typeof useCopyToClipboard
2329
>;
2430
const mockOpenBlockExplorer = openBlockExplorer as jest.Mock;
2531

32+
const mockGetBlockExplorerInfo = getBlockExplorerInfo as jest.Mock;
33+
2634
describe('AddressQRCodeModal', () => {
2735
beforeEach(() => {
2836
jest.clearAllMocks();
2937
mockUseCopyToClipboard.mockReturnValue([false, jest.fn(), jest.fn()]);
38+
39+
// Set up default mock return values
40+
mockGetBlockExplorerInfo.mockReturnValue(null);
3041
});
3142

3243
it('should render the modal when isOpen is true', () => {
@@ -37,6 +48,7 @@ describe('AddressQRCodeModal', () => {
3748
address="0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc"
3849
accountName="Test Account"
3950
networkName="Ethereum"
51+
chainId="eip155:1"
4052
networkImageSrc="./images/eth_logo.svg"
4153
/>,
4254
);
@@ -58,6 +70,7 @@ describe('AddressQRCodeModal', () => {
5870
address="0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc"
5971
accountName="Test Account"
6072
networkName="Ethereum"
73+
chainId="eip155:1"
6174
networkImageSrc="./images/eth_logo.svg"
6275
/>,
6376
);
@@ -75,28 +88,33 @@ describe('AddressQRCodeModal', () => {
7588
address="0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc"
7689
accountName="Test Account"
7790
networkName="Ethereum"
91+
chainId="eip155:1"
7892
networkImageSrc="./images/eth_logo.svg"
7993
/>,
8094
);
8195

82-
// The address is displayed in segments: start + middle + end (0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc)
83-
// Start: first 6 chars, End: last 5 chars
84-
expect(screen.getByText('0x0dcd')).toBeInTheDocument(); // First 6 chars
85-
expect(
86-
screen.getByText('5d886577d5081b0c52e242ef29e70be'),
87-
).toBeInTheDocument();
96+
// The address is displayed in segments, so we check for the last 5 characters
8897
expect(screen.getByText('3e7bc')).toBeInTheDocument(); // Last 5 chars
8998
expect(screen.getByText('Copy address')).toBeInTheDocument();
9099
});
91100

92101
it('should render the view on explorer button for Ethereum', () => {
102+
// Mock the getBlockExplorerInfo to return Ethereum explorer info
103+
mockGetBlockExplorerInfo.mockReturnValue({
104+
addressUrl:
105+
'https://etherscan.io/address/0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc',
106+
name: 'Etherscan',
107+
buttonText: 'View on Etherscan',
108+
});
109+
93110
renderWithProvider(
94111
<AddressQRCodeModal
95112
isOpen={true}
96113
onClose={jest.fn()}
97114
address="0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc"
98115
accountName="Test Account"
99116
networkName="Ethereum"
117+
chainId="eip155:1"
100118
networkImageSrc="./images/eth_logo.svg"
101119
/>,
102120
);
@@ -115,22 +133,20 @@ describe('AddressQRCodeModal', () => {
115133
address="0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc"
116134
accountName="Test Account"
117135
networkName="Ethereum"
136+
chainId="eip155:1"
118137
networkImageSrc="./images/eth_logo.svg"
119138
/>,
120139
);
121140

122141
const copyButton = screen.getByText('Copy address');
123142
fireEvent.click(copyButton);
124143

125-
await waitFor(() => {
126-
expect(mockHandleCopy).toHaveBeenCalledWith(
127-
'0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc',
128-
);
129-
});
144+
expect(mockHandleCopy).toHaveBeenCalledTimes(1);
130145
});
131146

132-
it('should show copy success state when copy is successful', () => {
133-
mockUseCopyToClipboard.mockReturnValue([true, jest.fn(), jest.fn()]);
147+
it('should show copy success state when copy is successful', async () => {
148+
const mockHandleCopy = jest.fn();
149+
mockUseCopyToClipboard.mockReturnValue([true, mockHandleCopy, jest.fn()]);
134150

135151
renderWithProvider(
136152
<AddressQRCodeModal
@@ -139,6 +155,7 @@ describe('AddressQRCodeModal', () => {
139155
address="0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc"
140156
accountName="Test Account"
141157
networkName="Ethereum"
158+
chainId="eip155:1"
142159
networkImageSrc="./images/eth_logo.svg"
143160
/>,
144161
);
@@ -151,13 +168,21 @@ describe('AddressQRCodeModal', () => {
151168
it('should navigate to the correct URL for Ethereum explorer when button is clicked', () => {
152169
const address = '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc';
153170

171+
// Mock the getBlockExplorerInfo to return Ethereum explorer info
172+
mockGetBlockExplorerInfo.mockReturnValue({
173+
addressUrl: `https://etherscan.io/address/${address}`,
174+
name: 'Etherscan',
175+
buttonText: 'View on Etherscan',
176+
});
177+
154178
renderWithProvider(
155179
<AddressQRCodeModal
156180
isOpen={true}
157181
onClose={jest.fn()}
158182
address={address}
159183
accountName="Test Account"
160184
networkName="Ethereum"
185+
chainId="eip155:1"
161186
networkImageSrc="./images/eth_logo.svg"
162187
/>,
163188
);
@@ -183,97 +208,104 @@ describe('AddressQRCodeModal', () => {
183208
address="0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc"
184209
accountName="Test Account"
185210
networkName="Ethereum"
211+
chainId="eip155:1"
186212
networkImageSrc="./images/eth_logo.svg"
187213
/>,
188214
);
189215

190-
const closeButton = screen.getByRole('button', { name: 'Close' });
216+
const closeButton = screen.getByLabelText('Close');
191217
fireEvent.click(closeButton);
192218

193-
expect(onClose).toHaveBeenCalled();
219+
expect(onClose).toHaveBeenCalledTimes(1);
194220
});
195221

196222
it('should handle different network types and navigate to Solana explorer correctly', () => {
197-
// Test Solana
198-
const solanaAddress = 'Dh9ZYBBCdD5FjjgKpAi9w9GQvK4f8k3b8a8HHKhz7kLa';
223+
const address = '9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM';
224+
225+
// Mock the getBlockExplorerInfo to return Solana explorer info
226+
mockGetBlockExplorerInfo.mockReturnValue({
227+
addressUrl: `https://solscan.io/account/${address}`,
228+
name: 'Solscan',
229+
buttonText: 'View on Solscan',
230+
});
231+
199232
renderWithProvider(
200233
<AddressQRCodeModal
201234
isOpen={true}
202235
onClose={jest.fn()}
203-
address={solanaAddress}
204-
accountName="Solana Account"
236+
address={address}
237+
accountName="Test Account"
205238
networkName="Solana"
239+
chainId="solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"
206240
networkImageSrc="./images/sol_logo.svg"
207241
/>,
208242
);
209243

210-
expect(screen.getByText('Solana Account / Solana')).toBeInTheDocument();
211-
expect(screen.getByText('Solana Address')).toBeInTheDocument();
212-
expect(screen.getByText('Dh9ZYB')).toBeInTheDocument(); // First 6 chars
213-
expect(screen.getByText('z7kLa')).toBeInTheDocument(); // Last 5 chars
214-
215244
const explorerButton = screen.getByRole('button', {
216245
name: 'View on Solscan',
217246
});
218-
expect(explorerButton).toBeInTheDocument();
219247

220248
fireEvent.click(explorerButton);
221249

222250
expect(mockOpenBlockExplorer).toHaveBeenCalledTimes(1);
223251
expect(mockOpenBlockExplorer.mock.calls[0][0]).toBe(
224-
`https://solscan.io/address/${solanaAddress}`,
252+
`https://solscan.io/account/${address}`,
225253
);
226254
});
227255

228256
it('should handle Bitcoin network and navigate to Bitcoin explorer correctly', () => {
229-
const bitcoinAddress = 'bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh';
257+
const address = '1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa';
258+
259+
// Mock the getBlockExplorerInfo to return Bitcoin explorer info
260+
mockGetBlockExplorerInfo.mockReturnValue({
261+
addressUrl: `https://blockstream.info/address/${address}`,
262+
name: 'Blockstream',
263+
buttonText: 'View on Blockstream',
264+
});
265+
230266
renderWithProvider(
231267
<AddressQRCodeModal
232268
isOpen={true}
233269
onClose={jest.fn()}
234-
address={bitcoinAddress}
235-
accountName="Bitcoin Account"
270+
address={address}
271+
accountName="Test Account"
236272
networkName="Bitcoin"
273+
chainId="bitcoin:0"
237274
networkImageSrc="./images/btc_logo.svg"
238275
/>,
239276
);
240277

241-
expect(screen.getByText('Bitcoin Account / Bitcoin')).toBeInTheDocument();
242-
expect(screen.getByText('Bitcoin Address')).toBeInTheDocument();
243-
244278
const explorerButton = screen.getByRole('button', {
245279
name: 'View on Blockstream',
246280
});
247-
expect(explorerButton).toBeInTheDocument();
248281

249282
fireEvent.click(explorerButton);
250283

251284
expect(mockOpenBlockExplorer).toHaveBeenCalledTimes(1);
252285
expect(mockOpenBlockExplorer.mock.calls[0][0]).toBe(
253-
`https://blockstream.info/address/${bitcoinAddress}`,
286+
`https://blockstream.info/address/${address}`,
254287
);
255288
});
256289

257290
it('should handle unknown network gracefully', () => {
291+
// Mock the getBlockExplorerInfo to return null for unknown network
292+
mockGetBlockExplorerInfo.mockReturnValue(null);
293+
258294
renderWithProvider(
259295
<AddressQRCodeModal
260296
isOpen={true}
261297
onClose={jest.fn()}
262-
address="unknown_address_format"
298+
address="0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc"
263299
accountName="Test Account"
264300
networkName="Unknown Network"
301+
chainId="unknown:123"
302+
networkImageSrc="./images/unknown_logo.svg"
265303
/>,
266304
);
267305

306+
// Should not render explorer button for unknown network
268307
expect(
269-
screen.getByText('Test Account / Unknown Network'),
270-
).toBeInTheDocument();
271-
expect(screen.getByText('Unknown Network Address')).toBeInTheDocument();
272-
// Explorer button should not be rendered for unknown networks
273-
expect(
274-
screen.queryByRole('button', { name: /view.*explorer/iu }),
308+
screen.queryByRole('button', { name: /View on/u }),
275309
).not.toBeInTheDocument();
276-
// Make sure openBlockExplorer was not called
277-
expect(mockOpenBlockExplorer).not.toHaveBeenCalled();
278310
});
279311
});

ui/components/multichain-accounts/address-qr-code-modal/address-qr-code-modal.tsx

Lines changed: 16 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React, { useCallback, useContext, useMemo } from 'react';
22
import qrCode from 'qrcode-generator';
3+
import { CaipChainId } from '@metamask/utils';
34
import {
45
Text,
56
TextVariant,
@@ -28,6 +29,7 @@ import { useI18nContext } from '../../../hooks/useI18nContext';
2829
import { useCopyToClipboard } from '../../../hooks/useCopyToClipboard';
2930
import { openBlockExplorer } from '../../multichain/menu-items/view-explorer-menu-item';
3031
import { MetaMetricsContext } from '../../../contexts/metametrics';
32+
import { getBlockExplorerInfo } from '../../../helpers/utils/multichain/getBlockExplorerInfo';
3133

3234
// Constants for QR code generation
3335
const QR_CODE_TYPE_NUMBER = 4;
@@ -48,6 +50,7 @@ export type AddressQRCodeModalProps = Omit<
4850
address: string;
4951
accountName: string;
5052
networkName: string;
53+
chainId: CaipChainId;
5154
networkImageSrc?: string | undefined;
5255
};
5356

@@ -57,6 +60,7 @@ export const AddressQRCodeModal: React.FC<AddressQRCodeModalProps> = ({
5760
address,
5861
accountName,
5962
networkName,
63+
chainId,
6064
networkImageSrc,
6165
}) => {
6266
const t = useI18nContext();
@@ -88,47 +92,24 @@ export const AddressQRCodeModal: React.FC<AddressQRCodeModalProps> = ({
8892
handleCopy(address);
8993
}, [address, handleCopy]);
9094

91-
// TODO: Move this out into a utility or selector
92-
// Centralized explorer configuration
93-
const explorerInfo = useMemo(() => {
94-
const networkNameLower = networkName.toLowerCase();
95-
96-
if (networkNameLower.includes('ethereum')) {
97-
return {
98-
url: 'https://etherscan.io',
99-
name: 'Etherscan',
100-
buttonText: t('viewAddressOnExplorer', ['Etherscan']),
101-
};
102-
}
103-
104-
if (networkNameLower.includes('solana')) {
105-
return {
106-
url: 'https://solscan.io',
107-
name: 'Solscan',
108-
buttonText: t('viewAddressOnExplorer', ['Solscan']),
109-
};
110-
}
111-
112-
if (networkNameLower.includes('bitcoin')) {
113-
return {
114-
url: 'https://blockstream.info',
115-
name: 'Blockstream',
116-
buttonText: t('viewAddressOnExplorer', ['Blockstream']),
117-
};
118-
}
119-
120-
// Return null if no valid explorer found - button won't be shown
121-
return null;
122-
}, [networkName, t]);
95+
// Get block explorer info from network configuration
96+
const explorerInfo = getBlockExplorerInfo(
97+
t as (key: string, ...args: string[]) => string,
98+
address,
99+
{ networkName, chainId },
100+
);
123101

124102
const handleExplorerNavigation = useCallback(() => {
125103
if (!explorerInfo) {
126104
return;
127105
}
128106

129-
const addressLink = `${explorerInfo.url}/address/${address}`;
130-
openBlockExplorer(addressLink, 'Address QR Code Modal', trackEvent);
131-
}, [address, explorerInfo, trackEvent]);
107+
openBlockExplorer(
108+
explorerInfo.addressUrl,
109+
'Address QR Code Modal',
110+
trackEvent,
111+
);
112+
}, [explorerInfo, trackEvent]);
132113

133114
return (
134115
<Modal isOpen={isOpen} onClose={onClose}>

0 commit comments

Comments
 (0)