Skip to content

Commit

Permalink
feat(3417): sensitive text component (#11965)
Browse files Browse the repository at this point in the history
## **Description**

### Overview
This PR introduces a new `SensitiveText` component to our component
library. The `SensitiveText` component extends our existing `Text`
component to handle sensitive information, providing the ability to hide
or show text content as needed.

### Features
- Extends the existing `Text` component functionality
- Allows toggling between visible and hidden states for sensitive
information
- Supports different lengths of hidden text (Short, Medium, Long)
- Maintains all styling capabilities of the `Text` component (variants,
colors, etc)

## **Related issues**

Feature:
[#3417](MetaMask/MetaMask-planning#3417)

## **Manual testing steps**

1. Follow instructions on getting started with storybook 
2. Play with new component there

## **Screenshots/Recordings**

![sensitive_text](https://github.com/user-attachments/assets/f737d1d3-f0d7-4202-b5cd-9bbefb430ffe)

### **Before**

NA

### **After**

NA

## **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
- [x] 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**

- [x] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [x] 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
vinnyhoward authored Oct 24, 2024
1 parent 6b2cf82 commit def6f50
Show file tree
Hide file tree
Showing 8 changed files with 370 additions and 1 deletion.
2 changes: 1 addition & 1 deletion .storybook/storybook.requires.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
};
};

Expand Down
57 changes: 57 additions & 0 deletions app/component-library/components/Texts/SensitiveText/README.md
Original file line number Diff line number Diff line change
@@ -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.

| <span style="color:gray;font-size:14px">TYPE</span> | <span style="color:gray;font-size:14px">REQUIRED</span> | <span style="color:gray;font-size:14px">DEFAULT</span> |
| :-------------------------------------------------- | :------------------------------------------------------ | :----------------------------------------------------- |
| boolean | Yes | false |

### `length`

Determines the length of the hidden text (number of dots). Can be a predefined SensitiveTextLength or a custom string number.

| <span style="color:gray;font-size:14px">TYPE</span> | <span style="color:gray;font-size:14px">REQUIRED</span> | <span style="color:gray;font-size:14px">DEFAULT</span> |
| :-------------------------------------------------- | :------------------------------------------------------ | :----------------------------------------------------- |
| [SensitiveTextLengthType](./SensitiveText.types.ts#L14) \| [CustomLength](./SensitiveText.types.ts#L19) | No | SensitiveTextLength.Short |

### `children`

The text content to be displayed or hidden.

| <span style="color:gray;font-size:14px">TYPE</span> | <span style="color:gray;font-size:14px">REQUIRED</span> | <span style="color:gray;font-size:14px">DEFAULT</span> |
| :-------------------------------------------------- | :------------------------------------------------------ | :----------------------------------------------------- |
| 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';

<SensitiveText
isHidden={true}
length={SensitiveTextLength.Medium}
variant={TextVariant.BodyMD}
>
Sensitive Information
</SensitiveText>

<SensitiveText
isHidden={true}
length="15"
variant={TextVariant.BodyMD}
>
Custom Length Hidden Text
</SensitiveText>
```

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.
Original file line number Diff line number Diff line change
@@ -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 },
) => (
<>
<SensitiveText
{...args}
isHidden={false}
length={SensitiveTextLength.Short}
variant={TextVariant.DisplayMD}
color={TextColor.Alternative}
>
Visible Sensitive Text
</SensitiveText>
{Object.values(SensitiveTextLength).map((length) => (
<SensitiveText
key={length}
{...args}
length={
length as (typeof SensitiveTextLength)[keyof typeof SensitiveTextLength]
}
isHidden
>
{`Hidden (${length})`}
</SensitiveText>
))}
</>
);
SensitiveTextVariants.argTypes = {
isHidden: { control: false },
length: { control: false },
children: { control: false },
};
SensitiveTextVariants.args = {
variant: TextVariant.BodyMD,
color: TextColor.Default,
};
Original file line number Diff line number Diff line change
@@ -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(<SensitiveText {...testProps} />);
expect(wrapper).toMatchSnapshot();
});

it('should display the text when isHidden is false', () => {
const { getByText } = render(<SensitiveText {...testProps} />);
expect(getByText('Sensitive Information')).toBeTruthy();
});

it('should hide the text when isHidden is true', () => {
const { queryByText, getByText } = render(
<SensitiveText {...testProps} isHidden />,
);
expect(queryByText('Sensitive Information')).toBeNull();
expect(getByText('••••••')).toBeTruthy();
});

it('should render the correct number of asterisks for different lengths', () => {
const { getByText: getShort } = render(
<SensitiveText
{...testProps}
isHidden
length={SensitiveTextLength.Short}
/>,
);
expect(getShort('••••••')).toBeTruthy();

const { getByText: getMedium } = render(
<SensitiveText
{...testProps}
isHidden
length={SensitiveTextLength.Medium}
/>,
);
expect(getMedium('•••••••••')).toBeTruthy();

const { getByText: getLong } = render(
<SensitiveText
{...testProps}
isHidden
length={SensitiveTextLength.Long}
/>,
);
expect(getLong('••••••••••••')).toBeTruthy();

const { getByText: getExtraLong } = render(
<SensitiveText
{...testProps}
isHidden
length={SensitiveTextLength.ExtraLong}
/>,
);
expect(getExtraLong('••••••••••••••••••••')).toBeTruthy();
});

it('should apply the correct text color', () => {
const { getByText } = render(
<SensitiveText {...testProps} color={TextColor.Default} />,
);
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(
<SensitiveText {...testProps} isHidden length={value} />,
);
expect(getByText('•'.repeat(Number(value)))).toBeTruthy();
});
});

it('should handle custom length as a string', () => {
const { getByText } = render(
<SensitiveText {...testProps} isHidden length="15" />,
);
expect(getByText('•••••••••••••••')).toBeTruthy();
});

it('should fall back to Short length for invalid custom length', () => {
const { getByText } = render(
<SensitiveText {...testProps} isHidden length="invalid" />,
);
expect(getByText('••••••')).toBeTruthy();
});

it('should log a warning for invalid custom length', () => {
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
render(<SensitiveText {...testProps} isHidden length="abc" />);
expect(consoleSpy).toHaveBeenCalledWith(
'Invalid length provided: abc. Falling back to Short.',
);
consoleSpy.mockRestore();
});
});
Original file line number Diff line number Diff line change
@@ -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<SensitiveTextProps> = ({
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 <Text {...props}>{isHidden ? fallback : children}</Text>;
};

export default SensitiveText;
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`SensitiveText should render correctly 1`] = `
<Text
accessibilityRole="text"
style={
{
"color": "#141618",
"fontFamily": "EuclidCircularB-Regular",
"fontSize": 14,
"fontWeight": "400",
"letterSpacing": 0,
"lineHeight": 22,
}
}
>
Sensitive Information
</Text>
`;
3 changes: 3 additions & 0 deletions app/component-library/components/Texts/SensitiveText/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { default } from './SensitiveText';
export { SensitiveTextLength } from './SensitiveText.types';
export type { SensitiveTextProps } from './SensitiveText.types';

0 comments on commit def6f50

Please sign in to comment.