diff --git a/.storybook/storybook.requires.js b/.storybook/storybook.requires.js index a1ca7774ac8..c08c085d9a9 100644 --- a/.storybook/storybook.requires.js +++ b/.storybook/storybook.requires.js @@ -126,7 +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"), + "./app/component-library/components/Texts/SensitiveText/SensitiveText.stories.tsx": require("../app/component-library/components/Texts/SensitiveText/SensitiveText.stories.tsx"), }; }; diff --git a/app/component-library/components/Texts/SensitiveText/README.md b/app/component-library/components/Texts/SensitiveText/README.md new file mode 100644 index 00000000000..dd81879e398 --- /dev/null +++ b/app/component-library/components/Texts/SensitiveText/README.md @@ -0,0 +1,57 @@ +# SensitiveText + +SensitiveText is a component that extends the Text component to handle sensitive information. It provides the ability to hide or show the text content, replacing it with dots when hidden. + +## Props + +This component extends all props from the [Text](../Text/README.md) component and adds the following: + +### `isHidden` + +Boolean to determine whether the text should be hidden or visible. + +| TYPE | REQUIRED | DEFAULT | +| :-------------------------------------------------- | :------------------------------------------------------ | :----------------------------------------------------- | +| boolean | Yes | false | + +### `length` + +Determines the length of the hidden text (number of dots). Can be a predefined SensitiveTextLength or a custom string number. + +| TYPE | REQUIRED | DEFAULT | +| :-------------------------------------------------- | :------------------------------------------------------ | :----------------------------------------------------- | +| [SensitiveTextLengthType](./SensitiveText.types.ts#L14) \| [CustomLength](./SensitiveText.types.ts#L19) | No | SensitiveTextLength.Short | + +### `children` + +The text content to be displayed or hidden. + +| TYPE | REQUIRED | DEFAULT | +| :-------------------------------------------------- | :------------------------------------------------------ | :----------------------------------------------------- | +| string | Yes | - | + +## Usage + +```javascript +import SensitiveText from 'app/component-library/components/Texts/SensitiveText'; +import { TextVariant } from 'app/component-library/components/Texts/Text'; +import { SensitiveTextLength } from 'app/component-library/components/Texts/SensitiveText/SensitiveText.types'; + + + Sensitive Information + + + + Custom Length Hidden Text + +``` + +This will render a Text component with dots instead of the actual text when `isHidden` is true, and the original text when `isHidden` is false. The number of asterisks is determined by the `length` prop. diff --git a/app/component-library/components/Texts/SensitiveText/SensitiveText.stories.tsx b/app/component-library/components/Texts/SensitiveText/SensitiveText.stories.tsx new file mode 100644 index 00000000000..c881014ed43 --- /dev/null +++ b/app/component-library/components/Texts/SensitiveText/SensitiveText.stories.tsx @@ -0,0 +1,89 @@ +// Third party dependencies +import React from 'react'; + +// External dependencies +import { TextVariant, TextColor } from '../Text/Text.types'; + +// Internal dependencies +import SensitiveText from './SensitiveText'; +import { SensitiveTextProps, SensitiveTextLength } from './SensitiveText.types'; + +const SensitiveTextMeta = { + title: 'Component Library / Texts', + component: SensitiveText, + argTypes: { + isHidden: { + control: 'boolean', + }, + length: { + options: SensitiveTextLength, + control: { + type: 'select', + }, + }, + variant: { + options: TextVariant, + control: { + type: 'select', + }, + }, + color: { + options: TextColor, + control: { + type: 'select', + }, + }, + children: { + control: { type: 'text' }, + }, + }, +}; +export default SensitiveTextMeta; + +export const SensitiveTextExample = { + args: { + isHidden: false, + length: SensitiveTextLength.Short, + variant: TextVariant.BodyMD, + color: TextColor.Default, + children: 'Sensitive Information', + }, +}; + +export const SensitiveTextVariants = ( + args: React.JSX.IntrinsicAttributes & + SensitiveTextProps & { children?: React.ReactNode | undefined }, +) => ( + <> + + Visible Sensitive Text + + {Object.values(SensitiveTextLength).map((length) => ( + + {`Hidden (${length})`} + + ))} + +); +SensitiveTextVariants.argTypes = { + isHidden: { control: false }, + length: { control: false }, + children: { control: false }, +}; +SensitiveTextVariants.args = { + variant: TextVariant.BodyMD, + color: TextColor.Default, +}; diff --git a/app/component-library/components/Texts/SensitiveText/SensitiveText.test.tsx b/app/component-library/components/Texts/SensitiveText/SensitiveText.test.tsx new file mode 100644 index 00000000000..2e5b158263c --- /dev/null +++ b/app/component-library/components/Texts/SensitiveText/SensitiveText.test.tsx @@ -0,0 +1,116 @@ +// Third party dependencies +import React from 'react'; +import { render } from '@testing-library/react-native'; + +// External dependencies +import { mockTheme } from '../../../../util/theme'; + +// Internal dependencies +import SensitiveText from './SensitiveText'; +import { SensitiveTextLength } from './SensitiveText.types'; +import { TextVariant, TextColor } from '../Text/Text.types'; + +describe('SensitiveText', () => { + const testProps = { + isHidden: false, + length: SensitiveTextLength.Short, + variant: TextVariant.BodyMD, + color: TextColor.Default, + children: 'Sensitive Information', + }; + + it('should render correctly', () => { + const wrapper = render(); + expect(wrapper).toMatchSnapshot(); + }); + + it('should display the text when isHidden is false', () => { + const { getByText } = render(); + expect(getByText('Sensitive Information')).toBeTruthy(); + }); + + it('should hide the text when isHidden is true', () => { + const { queryByText, getByText } = render( + , + ); + expect(queryByText('Sensitive Information')).toBeNull(); + expect(getByText('••••••')).toBeTruthy(); + }); + + it('should render the correct number of asterisks for different lengths', () => { + const { getByText: getShort } = render( + , + ); + expect(getShort('••••••')).toBeTruthy(); + + const { getByText: getMedium } = render( + , + ); + expect(getMedium('•••••••••')).toBeTruthy(); + + const { getByText: getLong } = render( + , + ); + expect(getLong('••••••••••••')).toBeTruthy(); + + const { getByText: getExtraLong } = render( + , + ); + expect(getExtraLong('••••••••••••••••••••')).toBeTruthy(); + }); + + it('should apply the correct text color', () => { + const { getByText } = render( + , + ); + const textElement = getByText('Sensitive Information'); + expect(textElement.props.style.color).toBe(mockTheme.colors.text.default); + }); + it('should handle all predefined SensitiveTextLength values', () => { + Object.entries(SensitiveTextLength).forEach(([_, value]) => { + const { getByText } = render( + , + ); + expect(getByText('•'.repeat(Number(value)))).toBeTruthy(); + }); + }); + + it('should handle custom length as a string', () => { + const { getByText } = render( + , + ); + expect(getByText('•••••••••••••••')).toBeTruthy(); + }); + + it('should fall back to Short length for invalid custom length', () => { + const { getByText } = render( + , + ); + expect(getByText('••••••')).toBeTruthy(); + }); + + it('should log a warning for invalid custom length', () => { + const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(); + render(); + expect(consoleSpy).toHaveBeenCalledWith( + 'Invalid length provided: abc. Falling back to Short.', + ); + consoleSpy.mockRestore(); + }); +}); diff --git a/app/component-library/components/Texts/SensitiveText/SensitiveText.tsx b/app/component-library/components/Texts/SensitiveText/SensitiveText.tsx new file mode 100644 index 00000000000..6e1f512c35b --- /dev/null +++ b/app/component-library/components/Texts/SensitiveText/SensitiveText.tsx @@ -0,0 +1,39 @@ +// external dependencies +import React, { useMemo } from 'react'; +import Text from '../Text/Text'; + +// internal dependencies +import { SensitiveTextProps, SensitiveTextLength } from './SensitiveText.types'; + +const SensitiveText: React.FC = ({ + isHidden = false, + children = '', + length = SensitiveTextLength.Short, + ...props +}) => { + const getFallbackLength = useMemo( + () => (len: string) => { + const numLength = Number(len); + return Number.isNaN(numLength) ? 0 : numLength; + }, + [], + ); + + const isValidCustomLength = (value: string): boolean => { + const num = Number(value); + return !Number.isNaN(num) && num > 0; + }; + + if (!(length in SensitiveTextLength) && !isValidCustomLength(length)) { + console.warn(`Invalid length provided: ${length}. Falling back to Short.`); + length = SensitiveTextLength.Short; + } + + const fallback = useMemo( + () => '•'.repeat(getFallbackLength(length)), + [length, getFallbackLength], + ); + return {isHidden ? fallback : children}; +}; + +export default SensitiveText; diff --git a/app/component-library/components/Texts/SensitiveText/SensitiveText.types.ts b/app/component-library/components/Texts/SensitiveText/SensitiveText.types.ts new file mode 100644 index 00000000000..1c6f4688b78 --- /dev/null +++ b/app/component-library/components/Texts/SensitiveText/SensitiveText.types.ts @@ -0,0 +1,46 @@ +// External dependencies. +import { TextProps } from '../Text/Text.types'; + +/** + * SensitiveText length options. + */ +export const SensitiveTextLength = { + Short: '6', + Medium: '9', + Long: '12', + ExtraLong: '20', +} as const; + +/** + * Type for SensitiveTextLength values. + */ +export type SensitiveTextLengthType = + (typeof SensitiveTextLength)[keyof typeof SensitiveTextLength]; + +/** + * Type for custom length values. + */ +export type CustomLength = string; + +/** + * SensitiveText component props. + */ +export interface SensitiveTextProps extends TextProps { + /** + * Boolean to determine whether the text should be hidden or visible. + * + * @default false + */ + isHidden?: boolean; + /** + * Determines the length of the hidden text (number of asterisks). + * Can be a predefined SensitiveTextLength or a custom string number. + * + * @default SensitiveTextLength.Short + */ + length?: SensitiveTextLengthType | CustomLength; + /** + * The text content to be displayed or hidden. + */ + children: string; +} diff --git a/app/component-library/components/Texts/SensitiveText/__snapshots__/SensitiveText.test.tsx.snap b/app/component-library/components/Texts/SensitiveText/__snapshots__/SensitiveText.test.tsx.snap new file mode 100644 index 00000000000..baa2e5148bf --- /dev/null +++ b/app/component-library/components/Texts/SensitiveText/__snapshots__/SensitiveText.test.tsx.snap @@ -0,0 +1,19 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SensitiveText should render correctly 1`] = ` + + Sensitive Information + +`; diff --git a/app/component-library/components/Texts/SensitiveText/index.ts b/app/component-library/components/Texts/SensitiveText/index.ts new file mode 100644 index 00000000000..4ea8f25dad4 --- /dev/null +++ b/app/component-library/components/Texts/SensitiveText/index.ts @@ -0,0 +1,3 @@ +export { default } from './SensitiveText'; +export { SensitiveTextLength } from './SensitiveText.types'; +export type { SensitiveTextProps } from './SensitiveText.types';