Skip to content

Commit

Permalink
feat: add copy button component (#11736)
Browse files Browse the repository at this point in the history
## **Description**

Adding re-usable copy button component and some code cleanup.

## **Related issues**

Fixes: #11735

## **Manual testing steps**

1. Run storybook locally 
2. Check copy button component

## **Screenshots/Recordings**
<img width="382" alt="Screenshot 2024-10-10 at 4 29 36 PM"
src="https://github.com/user-attachments/assets/7d5e08ef-37b8-4236-9d6c-19a2afc3881f">

## **Pre-merge author checklist**

- [X] I’ve followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile
Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [X] I've completed the PR template to the best of my ability
- [X] I’ve included tests if applicable
- [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [X] I’ve applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.

## **Pre-merge reviewer checklist**

- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.
  • Loading branch information
jpuri authored Oct 10, 2024
1 parent 678d468 commit a133d1c
Show file tree
Hide file tree
Showing 12 changed files with 135 additions and 42 deletions.
1 change: 1 addition & 0 deletions .storybook/storybook.requires.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ const getStories = () => {
"./app/components/Views/confirmations/components/UI/InfoRow/InfoRow.stories.tsx": require("../app/components/Views/confirmations/components/UI/InfoRow/InfoRow.stories.tsx"),
"./app/components/Views/confirmations/components/UI/ExpandableSection/ExpandableSection.stories.tsx": require("../app/components/Views/confirmations/components/UI/ExpandableSection/ExpandableSection.stories.tsx"),
"./app/components/Views/confirmations/components/UI/Tooltip/Tooltip.stories.tsx": require("../app/components/Views/confirmations/components/UI/Tooltip/Tooltip.stories.tsx"),
"./app/components/Views/confirmations/components/UI/CopyButton/CopyButton.stories.tsx": require("../app/components/Views/confirmations/components/UI/CopyButton/CopyButton.stories.tsx"),
};
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { View } from 'react-native';
import { useSelector } from 'react-redux';

import { strings } from '../../../../../../../../locales/i18n';
import { selectRpcUrl } from '../../../../../../../selectors/networkController';
import { selectProviderConfig } from '../../../../../../../selectors/networkController';
import { selectNetworkName } from '../../../../../../../selectors/networkInfos';
import useAccountInfo from '../../../../hooks/useAccountInfo';
import useApprovalRequest from '../../../../hooks/useApprovalRequest';
Expand All @@ -15,7 +15,8 @@ import InfoURL from '../../../UI/InfoRow/InfoValue/InfoURL';
const AccountNetworkInfoExpanded = () => {
const { approvalRequest } = useApprovalRequest();
const networkName = useSelector(selectNetworkName);
const networkRpcUrl = useSelector(selectRpcUrl);
const { rpcUrl: networkRpcUrl, type: networkType } =
useSelector(selectProviderConfig);
const fromAddress = approvalRequest?.requestData?.from;
const { accountAddress, accountBalance } = useAccountInfo(fromAddress);

Expand All @@ -34,7 +35,9 @@ const AccountNetworkInfoExpanded = () => {
{networkName}
</InfoRow>
<InfoRow label={strings('confirm.rpc_url')}>
<InfoURL url={networkRpcUrl} />
<InfoURL
url={networkRpcUrl ?? `https://${networkType}.infura.io/v3/`}
/>
</InfoRow>
</InfoSection>
</View>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,9 @@ exports[`AccountNetworkInfoExpanded should match snapshot for personal sign 1`]
"marginTop": 8,
}
}
/>
>
mainnet.infura.io/v3/
</Text>
</View>
</View>
</View>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const styleSheet = (params: { theme: Theme }) => {
fontSize: 14,
fontWeight: '400',
},
copyButton: {
copyButtonContainer: {
position: 'absolute',
top: -40,
right: 0,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,37 +1,24 @@
import React, { useCallback, useMemo, useState } from 'react';
import React, { useMemo } from 'react';
import { Text, View } from 'react-native';
import { hexToText } from '@metamask/controller-utils';

import ButtonIcon, {
ButtonIconSizes,
} from '../../../../../../../../component-library/components/Buttons/ButtonIcon';
import ClipboardManager from '../../../../../../../../core/ClipboardManager';
import {
IconColor,
IconName,
} from '../../../../../../../../component-library/components/Icons/Icon';
import { sanitizeString } from '../../../../../../../../util/string';
import { strings } from '../../../../../../../../../locales/i18n';
import { useStyles } from '../../../../../../../../component-library/hooks';
import useApprovalRequest from '../../../../../hooks/useApprovalRequest';
import ExpandableSection from '../../../../UI/ExpandableSection';
import styleSheet from './Message.styles';
import CopyButton from '../../../../UI/CopyButton';

const Message = () => {
const { approvalRequest } = useApprovalRequest();
const [copied, setCopied] = useState(false);
const { styles } = useStyles(styleSheet, {});

const message = useMemo(
() => sanitizeString(hexToText(approvalRequest?.requestData?.data)),
[approvalRequest?.requestData?.data],
);

const copyMessage = useCallback(async () => {
await ClipboardManager.setString(message);
setCopied(true);
}, [message, setCopied]);

return (
<ExpandableSection
collapsedContent={
Expand All @@ -44,14 +31,9 @@ const Message = () => {
}
expandedContent={
<View style={styles.messageContainer}>
<ButtonIcon
iconColor={IconColor.Muted}
size={ButtonIconSizes.Sm}
onPress={copyMessage}
iconName={copied ? IconName.CopySuccess : IconName.Copy}
style={styles.copyButton}
testID="copyButtonTestId"
/>
<View style={styles.copyButtonContainer}>
<CopyButton copyText={message} />
</View>
<Text style={styles.messageExpanded}>{message}</Text>
</View>
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import React from 'react';
import { storiesOf } from '@storybook/react-native';

import CopyButton from './CopyButton';

storiesOf('Confirmations / CopyButton', module)
.addDecorator((getStory) => getStory())
.add('Default', () => <CopyButton copyText="DUMMY" />);
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React from 'react';
import { fireEvent, render } from '@testing-library/react-native';

import ClipboardManager from '../../../../../../core/ClipboardManager';
import CopyButton from './CopyButton';

jest.mock('../../../../../../core/ClipboardManager');

describe('CopyButton', () => {
it('should match snapshot', async () => {
const container = render(<CopyButton copyText={'DUMMY'} />);
expect(container).toMatchSnapshot();
});

it('should copy text to clipboard when pressed', async () => {
const { getByTestId } = render(<CopyButton copyText={'DUMMY'} />);
fireEvent.press(getByTestId('copyButtonTestId'));
expect(ClipboardManager.setString).toHaveBeenCalledTimes(1);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React, { useCallback, useState } from 'react';

import ButtonIcon, {
ButtonIconSizes,
} from '../../../../../../component-library/components/Buttons/ButtonIcon';
import ClipboardManager from '../../../../../../core/ClipboardManager';
import {
IconColor,
IconName,
} from '../../../../../../component-library/components/Icons/Icon';

interface CopyButtonProps {
copyText: string;
testID?: string;
}

const CopyButton = ({ copyText, testID }: CopyButtonProps) => {
const [copied, setCopied] = useState(false);

const copyMessage = useCallback(async () => {
await ClipboardManager.setString(copyText);
setCopied(true);
}, [copyText, setCopied]);

return (
<ButtonIcon
iconColor={IconColor.Alternative}
size={ButtonIconSizes.Sm}
onPress={copyMessage}
iconName={copied ? IconName.CopySuccess : IconName.Copy}
testID={testID ?? 'copyButtonTestId'}
/>
);
};

export default CopyButton;
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`CopyButton should match snapshot 1`] = `
<TouchableOpacity
accessible={true}
activeOpacity={1}
disabled={false}
onPress={[Function]}
onPressIn={[Function]}
onPressOut={[Function]}
style={
{
"alignItems": "center",
"borderRadius": 8,
"height": 24,
"justifyContent": "center",
"opacity": 1,
"width": 24,
}
}
testID="copyButtonTestId"
>
<SvgMock
color="#6a737d"
height={16}
name="Copy"
style={
{
"height": 16,
"width": 16,
}
}
width={16}
/>
</TouchableOpacity>
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './CopyButton';
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
import { StyleSheet } from 'react-native';

import { Colors, Theme } from '../../../../../../util/theme/models';
import { Theme } from '../../../../../../util/theme/models';
import { fontStyles } from '../../../../../../styles/common';

const createStyles = (colors: Colors, shadows: Theme['shadows']) =>
StyleSheet.create({
const styleSheet = (params: { theme: Theme }) => {
const { theme } = params;

return StyleSheet.create({
modal: {
margin: 0,
},
modalView: {
backgroundColor: colors.background.default,
backgroundColor: theme.colors.background.default,
justifyContent: 'center',
alignItems: 'center',
marginHorizontal: 16,
borderRadius: 8,
...shadows.size.sm,
...theme.shadows.size.sm,
elevation: 11,
paddingHorizontal: 16,
paddingVertical: 24,
Expand All @@ -25,17 +27,18 @@ const createStyles = (colors: Colors, shadows: Theme['shadows']) =>
right: 10,
},
modalTitle: {
color: colors.text.default,
color: theme.colors.text.default,
...fontStyles.bold,
fontSize: 16,
fontWeight: '700',
marginBottom: 16,
},
modalContent: {
color: colors.text.default,
color: theme.colors.text.default,
...fontStyles.normal,
fontSize: 14,
}
},
});
};

export default createStyles;
export default styleSheet;
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ import {
IconColor,
IconName,
} from '../../../../../../component-library/components/Icons/Icon';
import { useStyles } from '../../../../../../component-library/hooks';
import { useTheme } from '../../../../../../util/theme';
import createStyles from './style';
import styleSheet from './Tooltip.styles';

interface TooltipProps {
content: ReactNode;
Expand All @@ -20,8 +21,8 @@ interface TooltipProps {

const Tooltip = ({ content, title, tooltipTestId }: TooltipProps) => {
const [open, setOpen] = useState(false);
const { colors, shadows } = useTheme();
const styles = createStyles(colors, shadows);
const { colors } = useTheme();
const { styles } = useStyles(styleSheet, {});

return (
<View>
Expand Down Expand Up @@ -52,8 +53,8 @@ const Tooltip = ({ content, title, tooltipTestId }: TooltipProps) => {
style={styles.closeModalBtn}
testID={tooltipTestId ?? 'tooltipTestId'}
/>
{title && <Text style={styles.modalTitle}>{title}</Text>}
<Text>{content}</Text>
{title && <Text style={styles.modalTitle}>{title}</Text>}
<Text>{content}</Text>
</View>
</Modal>
</View>
Expand Down

0 comments on commit a133d1c

Please sign in to comment.