Skip to content

Commit 0671d38

Browse files
[DSRN] Add BadgeNetwork (#470)
<!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** This PR adds the `BadgeNetwork` component to the `@metamask/design-system-react-native` package <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> ## **Related issues** Fixes: #396 ## **Manual testing steps** 1. Run `yarn storybook:ios` from root 2. Go to Components > BadgeNetwork 3. ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** https://github.com/user-attachments/assets/d812887d-68f5-4f9b-85c6-60a658db30b6 <!-- [screenshots/recordings] --> ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) - [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-extension/blob/develop/.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.
1 parent 6a76e9a commit 0671d38

File tree

11 files changed

+292
-8
lines changed

11 files changed

+292
-8
lines changed

apps/storybook-react-native/.storybook/storybook.requires.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ const getStories = () => {
5353
"./../../packages/design-system-react-native/src/components/AvatarIcon/AvatarIcon.stories.tsx": require("../../../packages/design-system-react-native/src/components/AvatarIcon/AvatarIcon.stories.tsx"),
5454
"./../../packages/design-system-react-native/src/components/AvatarNetwork/AvatarNetwork.stories.tsx": require("../../../packages/design-system-react-native/src/components/AvatarNetwork/AvatarNetwork.stories.tsx"),
5555
"./../../packages/design-system-react-native/src/components/AvatarToken/AvatarToken.stories.tsx": require("../../../packages/design-system-react-native/src/components/AvatarToken/AvatarToken.stories.tsx"),
56+
"./../../packages/design-system-react-native/src/components/BadgeNetwork/BadgeNetwork.stories.tsx": require("../../../packages/design-system-react-native/src/components/BadgeNetwork/BadgeNetwork.stories.tsx"),
5657
"./../../packages/design-system-react-native/src/components/Button/Button.stories.tsx": require("../../../packages/design-system-react-native/src/components/Button/Button.stories.tsx"),
5758
"./../../packages/design-system-react-native/src/components/Button/variants/ButtonPrimary/ButtonPrimary.stories.tsx": require("../../../packages/design-system-react-native/src/components/Button/variants/ButtonPrimary/ButtonPrimary.stories.tsx"),
5859
"./../../packages/design-system-react-native/src/components/Button/variants/ButtonSecondary/ButtonSecondary.stories.tsx": require("../../../packages/design-system-react-native/src/components/Button/variants/ButtonSecondary/ButtonSecondary.stories.tsx"),
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import type { Meta, StoryObj } from '@storybook/react-native';
2+
import { ImageSourcePropType, View } from 'react-native';
3+
4+
import { SAMPLE_AVATARNETWORK_URIS } from '../AvatarNetwork/AvatarNetwork.constants';
5+
import BadgeNetwork from './BadgeNetwork';
6+
import type { BadgeNetworkProps } from './BadgeNetwork.types';
7+
8+
const meta: Meta<BadgeNetworkProps> = {
9+
title: 'Components/BadgeNetwork',
10+
component: BadgeNetwork,
11+
argTypes: {
12+
twClassName: {
13+
control: 'text',
14+
},
15+
},
16+
};
17+
18+
export default meta;
19+
20+
type Story = StoryObj<BadgeNetworkProps>;
21+
const storyImageSource: ImageSourcePropType = {
22+
uri: 'https://cryptologos.cc/logos/ethereum-eth-logo.svg',
23+
};
24+
25+
export const Default: Story = {
26+
args: {
27+
twClassName: '',
28+
},
29+
render: (args) => <BadgeNetwork {...args} src={storyImageSource} />,
30+
};
31+
32+
export const SampleNetworks: Story = {
33+
render: () => (
34+
<View style={{ gap: 16 }}>
35+
{SAMPLE_AVATARNETWORK_URIS.map((networkUri) => (
36+
<BadgeNetwork
37+
src={{
38+
uri: networkUri,
39+
}}
40+
key={networkUri}
41+
/>
42+
))}
43+
</View>
44+
),
45+
};
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// BadgeNetwork.test.tsx
2+
3+
import React from 'react';
4+
import { render } from '@testing-library/react-native';
5+
import BadgeNetwork from './BadgeNetwork';
6+
import { AvatarNetworkSize } from '../AvatarNetwork';
7+
8+
const remoteImageSrc = { uri: 'https://example.com/photo.png' };
9+
describe('BadgeNetwork', () => {
10+
it('renders an AvatarNetwork with size forced to Xs and forwards additional props', () => {
11+
const { getByTestId } = render(
12+
<BadgeNetwork
13+
src={remoteImageSrc}
14+
testID="badge-network"
15+
imageProps={{ testID: 'image-or-svg' }}
16+
/>,
17+
);
18+
const renderedComponent = getByTestId('badge-network');
19+
expect(renderedComponent).toBeTruthy();
20+
expect(renderedComponent.props.style[0].height.toString()).toStrictEqual(
21+
(Number(AvatarNetworkSize.Xs) + 2).toString(),
22+
);
23+
24+
expect(renderedComponent.props.children[1].props.src).toStrictEqual(
25+
remoteImageSrc,
26+
);
27+
});
28+
});
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
2+
import React from 'react';
3+
4+
import AvatarNetwork, { AvatarNetworkSize } from '../AvatarNetwork';
5+
import type { BadgeNetworkProps } from './BadgeNetwork.types';
6+
7+
const BadgeNetwork = ({
8+
name,
9+
fallbackText,
10+
fallbackTextProps,
11+
...props
12+
}: BadgeNetworkProps) => (
13+
<AvatarNetwork
14+
name={name}
15+
fallbackText={fallbackText}
16+
fallbackTextProps={fallbackTextProps}
17+
{...props}
18+
size={AvatarNetworkSize.Xs}
19+
hasSolidBackgroundColor
20+
hasBorder
21+
/>
22+
);
23+
24+
export default BadgeNetwork;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { AvatarNetworkProps } from '../AvatarNetwork';
2+
3+
export type BadgeNetworkProps = Omit<AvatarNetworkProps, 'size' | 'shape'>;
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
# BadgeNetwork
2+
3+
The `BadgeNetwork` component is reserved for badged representing networks. It provides a fallback mechanism in case of an image load failure, ensuring a graceful degradation by displaying either an alternative text or a fallback icon.
4+
5+
---
6+
7+
## Props
8+
9+
The `BadgeNetwork` component accepts the following props:
10+
11+
### `src` (Required)
12+
13+
The source of the image or SVG. It determines whether a local image, a local SVG component, or a remote image/SVG (via URI) is rendered.
14+
15+
| TYPE | REQUIRED | DEFAULT |
16+
| :------------------------------------------------------ | :------- | :------ |
17+
| `number \| ComponentType<SvgProps> \| { uri?: string }` | Yes | `N/A` |
18+
19+
---
20+
21+
### `name` (Optional)
22+
23+
Used to generate fallback text when the image or SVG fails to load.
24+
25+
| TYPE | REQUIRED | DEFAULT |
26+
| :------- | :------- | :---------- |
27+
| `string` | No | `undefined` |
28+
29+
---
30+
31+
### `fallbackText` (Optional)
32+
33+
Custom fallback text displayed when the image fails to load.
34+
35+
| TYPE | REQUIRED | DEFAULT |
36+
| :------- | :------- | :---------- |
37+
| `string` | No | `name?.[0]` |
38+
39+
---
40+
41+
### `fallbackTextProps` (Optional)
42+
43+
Additional props for customizing the fallback text.
44+
45+
| TYPE | REQUIRED | DEFAULT |
46+
| :------- | :------- | :------ |
47+
| `object` | No | `{}` |
48+
49+
---
50+
51+
### `imageProps` (Optional)
52+
53+
Additional properties for the image component.
54+
55+
| TYPE | REQUIRED | DEFAULT |
56+
| :----------- | :------- | :-------------------------- |
57+
| `ImageProps` | No | `{ resizeMode: 'contain' }` |
58+
59+
---
60+
61+
### `onImageError` (Optional)
62+
63+
Callback function triggered when the image fails to load.
64+
65+
| TYPE | REQUIRED | DEFAULT |
66+
| :------------------------------------------------------- | :------- | :---------- |
67+
| `(e: NativeSyntheticEvent<ImageErrorEventData>) => void` | No | `undefined` |
68+
69+
---
70+
71+
### `onSvgError` (Optional)
72+
73+
Callback function triggered when the SVG fails to load.
74+
75+
| TYPE | REQUIRED | DEFAULT |
76+
| :----------------- | :------- | :---------- |
77+
| `(e: any) => void` | No | `undefined` |
78+
79+
---
80+
81+
### Other Props
82+
83+
`BadgeNetwork` supports all other props from [`AvatarNetwork`](#) and [`ImageOrSvg`](#), such as:
84+
85+
- **`twClassName`** – Tailwind class names for styling.
86+
- **`testID`** – Identifier used for testing purposes.
87+
- **`style`** – Custom styles for the Badge container.
88+
89+
---
90+
91+
## Accessibility
92+
93+
To ensure proper accessibility, the following React Native accessibility props can be passed:
94+
95+
- **`accessibilityLabel`**: Describes the Badge for screen readers.
96+
- **`accessible`**: Set to `true` if the Badge represents meaningful content.
97+
98+
---
99+
100+
## Usage
101+
102+
### Basic Usage
103+
104+
```tsx
105+
import React from 'react';
106+
import BadgeNetwork from '@metamask/design-system-react-native/badge-network';
107+
108+
const App = () => (
109+
<BadgeNetwork
110+
name="MetaMask"
111+
source={{ uri: 'https://example.com/network.png' }}
112+
/>
113+
);
114+
115+
export default App;
116+
```
117+
118+
---
119+
120+
### Handling Image Errors
121+
122+
```tsx
123+
import React from 'react';
124+
import BadgeNetwork from '@metamask/design-system-react-native/badge-network';
125+
126+
const handleError = () => {
127+
console.log('Image failed to load');
128+
};
129+
130+
const App = () => (
131+
<BadgeNetwork
132+
name="ETH"
133+
source={{ uri: 'https://invalid-url.com' }}
134+
onImageError={handleError}
135+
/>
136+
);
137+
138+
export default App;
139+
```
140+
141+
---
142+
143+
## Notes
144+
145+
- **Fallback Mechanism:**
146+
If an image or SVG fails to load, the component falls back to displaying text derived from the `name` prop.
147+
- **Customization:**
148+
Supports various props for shape, size, and additional styling.
149+
150+
- **Extensibility:**
151+
Any additional `ImageOrSvg` props are forwarded for greater flexibility.
152+
153+
---
154+
155+
## Contributing
156+
157+
1. Add tests for any new features or modifications.
158+
2. Update this README to reflect any changes in the API.
159+
3. Follow the project's coding guidelines and best practices.
160+
161+
---
162+
163+
For further details, refer to the [React Native documentation](https://reactnative.dev/docs/image).
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { default } from './BadgeNetwork';
2+
export type { BadgeNetworkProps } from './BadgeNetwork.types';

packages/design-system-react-native/src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ import AvatarTokenComponent from './components/AvatarToken';
4646
export const AvatarToken = withThemeProvider(AvatarTokenComponent);
4747
export { AvatarTokenProps, AvatarTokenSize } from './components/AvatarToken';
4848

49+
import BadgeNetworkComponent from './components/BadgeNetwork';
50+
export const BadgeNetwork = withThemeProvider(BadgeNetworkComponent);
51+
export { BadgeNetworkProps } from './components/BadgeNetwork';
52+
4953
import BlockiesComponent from './primitives/Blockies';
5054
export const Blockies = BlockiesComponent;
5155
export { BlockiesProps } from './primitives/Blockies';

packages/design-system-react-native/src/primitives/AvatarBase/AvatarBase.test.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ describe('AvatarBase', () => {
1717
expect(classNames).toContain(
1818
'items-center justify-center overflow-hidden',
1919
);
20-
expect(classNames).toContain('bg-background-default');
20+
expect(classNames).toContain('bg-transparent'); // Default hasSolidBackgroundColor = false
2121
expect(classNames).toContain(`h-[${DEFAULT_AVATARBASE_PROPS.size}px]`);
2222
expect(classNames).toContain(`w-[${DEFAULT_AVATARBASE_PROPS.size}px]`);
2323
expect(classNames).toContain('rounded-full'); // Default shape
@@ -66,6 +66,13 @@ describe('AvatarBase', () => {
6666
});
6767
});
6868

69+
it('applies correct solid background color when hasSolidBackgroundColor is true', () => {
70+
const classNames = generateAvatarBaseContainerClassNames({
71+
hasSolidBackgroundColor: true,
72+
});
73+
expect(classNames).toContain('bg-background-default');
74+
});
75+
6976
it('appends additional Tailwind class names', () => {
7077
const classNames = generateAvatarBaseContainerClassNames({
7178
twClassName: 'shadow-lg ring-2',
@@ -81,12 +88,13 @@ describe('AvatarBase', () => {
8188
size,
8289
shape: AvatarBaseShape.Square,
8390
hasBorder: true,
91+
hasSolidBackgroundColor: true,
8492
twClassName: 'border border-blue-500',
8593
});
8694
expect(classNames).toContain(
8795
'items-center justify-center overflow-hidden',
8896
);
89-
expect(classNames).toContain('bg-background-default');
97+
expect(classNames).toContain('bg-background-default'); // Solid background enabled
9098
expect(classNames).toContain(`h-[${expectedSize}px]`);
9199
expect(classNames).toContain(`w-[${expectedSize}px]`);
92100
expect(classNames).toContain(TWCLASSMAP_AVATARBASE_SIZE_SHAPE[size]);

packages/design-system-react-native/src/primitives/AvatarBase/AvatarBase.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const AvatarBase = ({
2626
size,
2727
shape,
2828
hasBorder,
29+
hasSolidBackgroundColor,
2930
twClassName,
3031
});
3132
}, [size, shape, hasBorder, twClassName]);

0 commit comments

Comments
 (0)