From ac9071a43cceb6299e1605d796908ea8dddf7855 Mon Sep 17 00:00:00 2001 From: Omri Dan <61094771+omridan159@users.noreply.github.com> Date: Thu, 21 Mar 2024 17:39:38 +0000 Subject: [PATCH] fix: improve SVG Validation and Error Handling in AvatarFavicon Component (#9023) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **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. ## **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 ### **Before** ### **After** ## **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. --- .../AvatarFavicon/AvatarFavicon.test.tsx | 3 +- .../variants/AvatarFavicon/AvatarFavicon.tsx | 40 ++++++++++++++----- .../__snapshots__/AvatarFavicon.test.tsx.snap | 30 ++++++++++---- 3 files changed, 54 insertions(+), 19 deletions(-) diff --git a/app/component-library/components/Avatars/Avatar/variants/AvatarFavicon/AvatarFavicon.test.tsx b/app/component-library/components/Avatars/Avatar/variants/AvatarFavicon/AvatarFavicon.test.tsx index 6e2acfcc44e..19c10cd8c34 100644 --- a/app/component-library/components/Avatars/Avatar/variants/AvatarFavicon/AvatarFavicon.test.tsx +++ b/app/component-library/components/Avatars/Avatar/variants/AvatarFavicon/AvatarFavicon.test.tsx @@ -45,6 +45,7 @@ describe('AvatarFavicon', () => { imageSource={SAMPLE_AVATARFAVICON_SVGIMAGESOURCE_REMOTE} />, ); + expect(wrapper).toMatchSnapshot(); }); @@ -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(); }); }); diff --git a/app/component-library/components/Avatars/Avatar/variants/AvatarFavicon/AvatarFavicon.tsx b/app/component-library/components/Avatars/Avatar/variants/AvatarFavicon/AvatarFavicon.tsx index b7be4664060..a2ef84cd507 100644 --- a/app/component-library/components/Avatars/Avatar/variants/AvatarFavicon/AvatarFavicon.tsx +++ b/app/component-library/components/Avatars/Avatar/variants/AvatarFavicon/AvatarFavicon.tsx @@ -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, @@ -29,11 +29,12 @@ const AvatarFavicon = ({ ...props }: AvatarFaviconProps) => { const [error, setError] = useState(undefined); + const [svgSource, setSvgSource] = useState(''); const { styles } = useStyles(stylesheet, { style }); const onError = useCallback( (e: NativeSyntheticEvent) => - setError(e.nativeEvent.error), + setError(e.nativeEvent?.error), [setError], ); @@ -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]); @@ -62,7 +80,7 @@ const AvatarFavicon = ({ height="100%" uri={svgSource} style={styles.image} - onError={onSvgError} + onError={(e: any) => onSvgError(e)} /> ) : null; diff --git a/app/component-library/components/Avatars/Avatar/variants/AvatarFavicon/__snapshots__/AvatarFavicon.test.tsx.snap b/app/component-library/components/Avatars/Avatar/variants/AvatarFavicon/__snapshots__/AvatarFavicon.test.tsx.snap index fa74012b10c..53cba6c3525 100644 --- a/app/component-library/components/Avatars/Avatar/variants/AvatarFavicon/__snapshots__/AvatarFavicon.test.tsx.snap +++ b/app/component-library/components/Avatars/Avatar/variants/AvatarFavicon/__snapshots__/AvatarFavicon.test.tsx.snap @@ -30,9 +30,14 @@ exports[`AvatarFavicon should render SVG 1`] = ` size="32" style={Object {}} > - `; @@ -52,9 +55,22 @@ exports[`AvatarFavicon should render fallback when svg has error 1`] = ` size="32" style={Object {}} > - `;