Skip to content

Commit

Permalink
fix: improve SVG Validation and Error Handling in AvatarFavicon Compo…
Browse files Browse the repository at this point in the history
…nent (#9023)

## **Description**

This PR enhances the AvatarFavicon component by implementing an SVG
validation step using a HEAD request to ensure URIs point to valid SVG
files. It improves error handling by setting up a reliable fallback
mechanism that activates if the SVG fails to load or doesn't pass
validation. The introduction of state management for SVG source
validation ensures that only verified SVGs are rendered, thereby
minimizing potential rendering issues and improving overall component
stability. This streamlined approach enhances user experience by
providing a more resilient and error-tolerant AvatarFavicon component.

<!--
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:

## **Manual testing steps**

1. Go to this page...
2.
3.

## **Screenshots/Recordings**


https://github.com/MetaMask/metamask-mobile/assets/61094771/61423bae-12b2-4d20-bb55-efeb216d7518



https://github.com/MetaMask/metamask-mobile/assets/61094771/33b13f8a-a1aa-403b-a4fa-a979c36d4909


<!-- If applicable, add screenshots and/or recordings to visualize the
before and after of your change. -->

### **Before**

<!-- [screenshots/recordings] -->

### **After**

<!-- [screenshots/recordings] -->

## **Pre-merge author checklist**

- [x] I’ve followed [MetaMask Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've clearly explained what problem this PR is solving and how it
is solved.
- [x] I've linked related issues
- [x] I've included manual testing steps
- [x] I've included screenshots/recordings if applicable
- [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.
- [x] I’ve properly set the pull request status:
  - [x] In case it's not yet "ready for review", I've set it to "draft".
- [x] In case it's "ready for review", I've changed it from "draft" to
"non-draft".

## **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
omridan159 authored and metamaskbot committed Mar 21, 2024
1 parent 6725a14 commit ac9071a
Show file tree
Hide file tree
Showing 3 changed files with 54 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ describe('AvatarFavicon', () => {
imageSource={SAMPLE_AVATARFAVICON_SVGIMAGESOURCE_REMOTE}
/>,
);

expect(wrapper).toMatchSnapshot();
});

Expand Down Expand Up @@ -76,7 +77,7 @@ describe('AvatarFavicon', () => {
const currentImageComponent = wrapper.findWhere(
(node) => node.prop('testID') === AVATARFAVICON_IMAGE_TESTID,
);
expect(currentImageComponent.exists()).toBe(false);
expect(currentImageComponent.exists()).toBe(true);
expect(wrapper).toMatchSnapshot();
});
});
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
/* eslint-disable react/prop-types */

// Third party dependencies.
import React, { useCallback, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { Image, ImageErrorEventData, NativeSyntheticEvent } from 'react-native';
import { SvgUri } from 'react-native-svg';

// External dependencies.
import AvatarBase from '../../foundation/AvatarBase';
import { useStyles } from '../../../../../hooks';
import Icon from '../../../../Icons/Icon';
import { ICONSIZE_BY_AVATARSIZE } from '../../Avatar.constants';
import AvatarBase from '../../foundation/AvatarBase';

// Internal dependencies.
import { AvatarFaviconProps } from './AvatarFavicon.types';
import { isNumber } from 'lodash';
import { isFaviconSVG } from '../../../../../../util/favicon';
import {
DEFAULT_AVATARFAVICON_SIZE,
DEFAULT_AVATARFAVICON_ERROR_ICON,
AVATARFAVICON_IMAGE_TESTID,
DEFAULT_AVATARFAVICON_ERROR_ICON,
DEFAULT_AVATARFAVICON_SIZE,
} from './AvatarFavicon.constants';
import stylesheet from './AvatarFavicon.styles';
import { isNumber } from 'lodash';
import { isFaviconSVG } from '../../../../../../util/favicon';
import { AvatarFaviconProps } from './AvatarFavicon.types';

const AvatarFavicon = ({
imageSource,
Expand All @@ -29,11 +29,12 @@ const AvatarFavicon = ({
...props
}: AvatarFaviconProps) => {
const [error, setError] = useState<any>(undefined);
const [svgSource, setSvgSource] = useState<string>('');
const { styles } = useStyles(stylesheet, { style });

const onError = useCallback(
(e: NativeSyntheticEvent<ImageErrorEventData>) =>
setError(e.nativeEvent.error),
setError(e.nativeEvent?.error),
[setError],
);

Expand All @@ -48,9 +49,26 @@ const AvatarFavicon = ({
/>
);

const svgSource = useMemo(() => {
useEffect(() => {
const checkSvgContentType = async (uri: string) => {
try {
const response = await fetch(uri, { method: 'HEAD' });
const contentType = response.headers.get('Content-Type');
return contentType?.includes('image/svg+xml');
} catch (err: any) {
return false;
}
};

if (imageSource && !isNumber(imageSource) && 'uri' in imageSource) {
return isFaviconSVG(imageSource);
const svg = isFaviconSVG(imageSource);
if (svg) {
checkSvgContentType(svg).then((isSvg) => {
if (isSvg) {
setSvgSource(svg);
}
});
}
}
}, [imageSource]);

Expand All @@ -62,7 +80,7 @@ const AvatarFavicon = ({
height="100%"
uri={svgSource}
style={styles.image}
onError={onSvgError}
onError={(e: any) => onSvgError(e)}
/>
) : null;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,14 @@ exports[`AvatarFavicon should render SVG 1`] = `
size="32"
style={Object {}}
>
<SvgUri
height="100%"
<Image
onError={[Function]}
resizeMode="contain"
source={
Object {
"uri": "https://metamask.github.io/test-dapp/metamask-fox.svg",
}
}
style={
Object {
"flex": 1,
Expand All @@ -41,8 +46,6 @@ exports[`AvatarFavicon should render SVG 1`] = `
}
}
testID="favicon-avatar-image"
uri="https://metamask.github.io/test-dapp/metamask-fox.svg"
width="100%"
/>
</AvatarBase>
`;
Expand All @@ -52,9 +55,22 @@ exports[`AvatarFavicon should render fallback when svg has error 1`] = `
size="32"
style={Object {}}
>
<Icon
name="Global"
size="20"
<Image
onError={[Function]}
resizeMode="contain"
source={
Object {
"uri": "https://metamask.github.io/test-dapp/metamask-fox.svg",
}
}
style={
Object {
"flex": 1,
"height": undefined,
"width": undefined,
}
}
testID="favicon-avatar-image"
/>
</AvatarBase>
`;

0 comments on commit ac9071a

Please sign in to comment.