Skip to content

Commit ce94620

Browse files
committed
feat: add copy functionality for account address in gator permission item
1 parent 48847d4 commit ce94620

File tree

2 files changed

+142
-14
lines changed

2 files changed

+142
-14
lines changed

ui/components/multichain/pages/gator-permissions/components/review-gator-permission-item.test.tsx

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,33 @@ import configureStore from '../../../../../store/store';
1515
import mockState from '../../../../../../test/data/mock-state.json';
1616
import { ReviewGatorPermissionItem } from './review-gator-permission-item';
1717

18+
const mockAccountAddress = '0x4f71DA06987BfeDE90aF0b33E1e3e4ffDCEE7a63';
19+
const mockAccountName = 'Test Gator Account';
20+
1821
const store = configureStore({
1922
...mockState,
2023
metamask: {
2124
...mockState.metamask,
25+
internalAccounts: {
26+
...mockState.metamask.internalAccounts,
27+
accounts: {
28+
...mockState.metamask.internalAccounts.accounts,
29+
'test-account-id': {
30+
address: mockAccountAddress,
31+
id: 'test-account-id',
32+
metadata: {
33+
name: mockAccountName,
34+
importTime: 0,
35+
keyring: {
36+
type: 'HD Key Tree',
37+
},
38+
},
39+
options: {},
40+
methods: [],
41+
type: 'eip155:eoa',
42+
},
43+
},
44+
},
2245
},
2346
});
2447

@@ -43,8 +66,6 @@ describe('Permission List Item', () => {
4366
describe('render', () => {
4467
const mockOnClick = jest.fn();
4568
const mockNetworkName = 'Ethereum';
46-
const mockSelectedAccountAddress =
47-
'0x4f71DA06987BfeDE90aF0b33E1e3e4ffDCEE7a63';
4869
const mockStartTime = 1736271776; // January 7, 2025;
4970

5071
describe('NATIVE token permissions', () => {
@@ -54,7 +75,7 @@ describe('Permission List Item', () => {
5475
> = {
5576
permissionResponse: {
5677
chainId: '0x1',
57-
address: mockSelectedAccountAddress,
78+
address: mockAccountAddress,
5879
permission: {
5980
type: 'native-token-stream',
6081
isAdjustmentAllowed: false,
@@ -81,7 +102,7 @@ describe('Permission List Item', () => {
81102
> = {
82103
permissionResponse: {
83104
chainId: '0x1',
84-
address: mockSelectedAccountAddress,
105+
address: mockAccountAddress,
85106
permission: {
86107
type: 'native-token-periodic',
87108
isAdjustmentAllowed: false,
@@ -173,6 +194,32 @@ describe('Permission List Item', () => {
173194
);
174195
});
175196

197+
it('renders account name and copy button with visual feedback', () => {
198+
const { getByTestId } = renderWithProvider(
199+
<ReviewGatorPermissionItem
200+
networkName={mockNetworkName}
201+
gatorPermission={mockNativeTokenStreamPermission}
202+
onRevokeClick={() => mockOnClick()}
203+
/>,
204+
store,
205+
);
206+
207+
// Verify account name is displayed initially
208+
const accountText = getByTestId('review-gator-permission-account-name');
209+
expect(accountText).toBeInTheDocument();
210+
expect(accountText).toHaveTextContent(mockAccountName);
211+
212+
// Verify copy button is present
213+
const copyButton = getByTestId('review-gator-permission-copy-address');
214+
expect(copyButton).toBeInTheDocument();
215+
216+
// Click copy button to test functionality
217+
fireEvent.click(copyButton);
218+
219+
// After clicking, the text should change to "Address copied!"
220+
expect(accountText).toHaveTextContent('Address copied!');
221+
});
222+
176223
it('renders native token periodic permission correctly', () => {
177224
const { container, getByTestId } = renderWithProvider(
178225
<ReviewGatorPermissionItem
@@ -243,7 +290,7 @@ describe('Permission List Item', () => {
243290
> = {
244291
permissionResponse: {
245292
chainId: '0x5',
246-
address: mockSelectedAccountAddress,
293+
address: mockAccountAddress,
247294
permission: {
248295
type: 'erc20-token-periodic',
249296
isAdjustmentAllowed: false,
@@ -270,7 +317,7 @@ describe('Permission List Item', () => {
270317
> = {
271318
permissionResponse: {
272319
chainId: '0x5',
273-
address: mockSelectedAccountAddress,
320+
address: mockAccountAddress,
274321
permission: {
275322
type: 'erc20-token-stream',
276323
isAdjustmentAllowed: false,
@@ -408,7 +455,7 @@ describe('Permission List Item', () => {
408455
> = {
409456
permissionResponse: {
410457
chainId: '0x5',
411-
address: mockSelectedAccountAddress,
458+
address: mockAccountAddress,
412459
permission: {
413460
type: 'erc20-token-stream',
414461
isAdjustmentAllowed: false,

ui/components/multichain/pages/gator-permissions/components/review-gator-permission-item.tsx

Lines changed: 88 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import React, { useCallback, useMemo, useState } from 'react';
1+
import React, {
2+
useCallback,
3+
useEffect,
4+
useMemo,
5+
useRef,
6+
useState,
7+
} from 'react';
28
import { useSelector } from 'react-redux';
39
import { Hex } from '@metamask/utils';
410
import {
@@ -32,6 +38,12 @@ import { getImageForChainId } from '../../../../../selectors/multichain';
3238
import { getURLHost, shortenAddress } from '../../../../../helpers/utils/util';
3339
import Card from '../../../../ui/card';
3440
import { useI18nContext } from '../../../../../hooks/useI18nContext';
41+
import { useCopyToClipboard } from '../../../../../hooks/useCopyToClipboard';
42+
import {
43+
getInternalAccountByAddress,
44+
getNativeTokenInfo,
45+
selectERC20TokensByChain,
46+
} from '../../../../../selectors/selectors';
3547
import {
3648
convertTimestampToReadableDate,
3749
getPeriodFrequencyValueTranslationKey,
@@ -42,10 +54,6 @@ import {
4254
} from '../../../../../../shared/lib/gator-permissions';
4355
import { PreferredAvatar } from '../../../../app/preferred-avatar';
4456
import { BackgroundColor } from '../../../../../helpers/constants/design-system';
45-
import {
46-
getNativeTokenInfo,
47-
selectERC20TokensByChain,
48-
} from '../../../../../selectors/selectors';
4957
import { getTokenMetadata } from '../../../../../helpers/utils/token-util';
5058
import { getPendingRevocations } from '../../../../../selectors/gator-permissions/gator-permissions';
5159

@@ -122,6 +130,62 @@ export const ReviewGatorPermissionItem = ({
122130
getNativeTokenInfo(state, chainId),
123131
) as TokenMetadata;
124132
const pendingRevocations = useSelector(getPendingRevocations);
133+
const internalAccount = useSelector((state) =>
134+
getInternalAccountByAddress(state, permissionAccount),
135+
);
136+
137+
// Copy functionality with visual feedback
138+
const [accountText, setAccountText] = useState(
139+
shortenAddress(permissionAccount),
140+
);
141+
const [addressCopied, setAddressCopied] = useState(false);
142+
const [copyIcon, setCopyIcon] = useState(IconName.Copy);
143+
const [copyMessage, setCopyMessage] = useState(accountText);
144+
145+
const timeoutRef = useRef<number | null>(null);
146+
const [, handleCopy] = useCopyToClipboard();
147+
148+
// Cleanup timeout when component unmounts
149+
useEffect(() => {
150+
return () => {
151+
if (timeoutRef.current) {
152+
clearTimeout(timeoutRef.current);
153+
timeoutRef.current = null;
154+
}
155+
};
156+
}, []);
157+
158+
useEffect(() => {
159+
if (internalAccount?.metadata?.name) {
160+
setAccountText(internalAccount?.metadata?.name);
161+
}
162+
}, [internalAccount]);
163+
164+
// Update copy message when account changes
165+
useEffect(() => {
166+
setCopyMessage(accountText);
167+
}, [accountText]);
168+
169+
// Handle copy button click
170+
const handleCopyClick = useCallback(() => {
171+
// Clear existing timeout if clicking multiple times
172+
if (timeoutRef.current) {
173+
clearTimeout(timeoutRef.current);
174+
}
175+
176+
setAddressCopied(true);
177+
handleCopy(permissionAccount);
178+
setCopyMessage(t('addressCopied'));
179+
setCopyIcon(IconName.CopySuccess);
180+
181+
// Reset state after 1 second
182+
timeoutRef.current = window.setTimeout(() => {
183+
setCopyMessage(accountText);
184+
setCopyIcon(IconName.Copy);
185+
setAddressCopied(false);
186+
timeoutRef.current = null;
187+
}, 1000);
188+
}, [permissionAccount, accountText, handleCopy, t]);
125189

126190
const tokenMetadata: TokenMetadata = useMemo(() => {
127191
if (tokenAddress) {
@@ -483,10 +547,27 @@ export const ReviewGatorPermissionItem = ({
483547
<PreferredAvatar address={permissionAccount} />
484548
<Text
485549
variant={TextVariant.BodyMd}
486-
color={TextColor.TextAlternative}
550+
color={
551+
addressCopied
552+
? TextColor.SuccessDefault
553+
: TextColor.TextAlternative
554+
}
555+
data-testid="review-gator-permission-account-name"
487556
>
488-
{shortenAddress(permissionAccount)}
557+
{copyMessage}
489558
</Text>
559+
<ButtonIcon
560+
iconName={copyIcon}
561+
color={
562+
addressCopied ? IconColor.SuccessDefault : IconColor.IconMuted
563+
}
564+
size={ButtonIconSize.Sm}
565+
onClick={handleCopyClick}
566+
ariaLabel={
567+
addressCopied ? t('copiedExclamation') : t('copyToClipboard')
568+
}
569+
data-testid="review-gator-permission-copy-address"
570+
/>
490571
</Box>
491572
</Box>
492573
</Box>

0 commit comments

Comments
 (0)