diff --git a/app/components/Base/RemoteImage/index.js b/app/components/Base/RemoteImage/index.js index 53aba49ae0d..f47269f60ce 100644 --- a/app/components/Base/RemoteImage/index.js +++ b/app/components/Base/RemoteImage/index.js @@ -10,6 +10,7 @@ import ComponentErrorBoundary from '../../UI/ComponentErrorBoundary'; import useIpfsGateway from '../../hooks/useIpfsGateway'; import { getFormattedIpfsUrl } from '@metamask/assets-controllers'; import Identicon from '../../UI/Identicon'; +import useSvgUriViewBox from '../../hooks/useSvgUriViewBox'; const createStyles = () => StyleSheet.create({ @@ -40,16 +41,19 @@ const RemoteImage = (props) => { const onError = ({ nativeEvent: { error } }) => setError(error); + const isSVG = + source && + source.uri && + source.uri.match('.svg') && + (isImageUrl || resolvedIpfsUrl); + + const viewbox = useSvgUriViewBox(uri, isSVG); + if (error && props.address) { return ; } - if ( - source && - source.uri && - source.uri.match('.svg') && - (isImageUrl || resolvedIpfsUrl) - ) { + if (isSVG) { const style = props.style || {}; if (source.__packager_asset && typeof style !== 'number') { if (!style.width) { @@ -66,7 +70,13 @@ const RemoteImage = (props) => { componentLabel="RemoteImage-SVG" > - + ); diff --git a/app/components/hooks/useSvgUriViewBox.test.ts b/app/components/hooks/useSvgUriViewBox.test.ts new file mode 100644 index 00000000000..30bf04a88f8 --- /dev/null +++ b/app/components/hooks/useSvgUriViewBox.test.ts @@ -0,0 +1,42 @@ +import { renderHook, waitFor } from '@testing-library/react-native'; +import useSvgUriViewBox from './useSvgUriViewBox'; + +describe('useSvgUriViewBox()', () => { + const MOCK_SVG_WITH_VIEWBOX = ``; + const MOCK_SVG_WITHOUT_VIEWBOX = ``; + + function arrangeMocks() { + const mockResponseTextFn = jest + .fn() + .mockResolvedValue(MOCK_SVG_WITHOUT_VIEWBOX); + jest + .spyOn(global, 'fetch') + .mockResolvedValue({ text: mockResponseTextFn } as unknown as Response); + + return { mockText: mockResponseTextFn }; + } + + it('should return view-box if svg if missing a view-box', async () => { + const { mockText } = arrangeMocks(); + mockText.mockResolvedValueOnce(MOCK_SVG_WITHOUT_VIEWBOX); + + const hook = renderHook(() => useSvgUriViewBox('URI', true)); + await waitFor(() => expect(hook.result.current).toBeDefined()); + }); + + it('should return view-box if svg already has view-box', async () => { + const { mockText } = arrangeMocks(); + mockText.mockResolvedValueOnce(MOCK_SVG_WITH_VIEWBOX); + + const hook = renderHook(() => useSvgUriViewBox('URI', true)); + await waitFor(() => expect(hook.result.current).toBeDefined()); + }); + + it('should not make async calls if image is not an svg', async () => { + const mocks = arrangeMocks(); + const hook = renderHook(() => useSvgUriViewBox('URI', false)); + + await waitFor(() => expect(hook.result.current).toBeUndefined()); + expect(mocks.mockText).not.toHaveBeenCalled(); + }); +}); diff --git a/app/components/hooks/useSvgUriViewBox.ts b/app/components/hooks/useSvgUriViewBox.ts new file mode 100644 index 00000000000..bbfb1ee2bc3 --- /dev/null +++ b/app/components/hooks/useSvgUriViewBox.ts @@ -0,0 +1,49 @@ +import { useEffect, useState } from 'react'; + +/** + * Support svg images urls that do not have a view box + * See: https://github.com/software-mansion/react-native-svg/issues/1202#issuecomment-1891110599 + * + * This will return the default SVG ViewBox from an SVG URI + * @param uri - uri to fetch + * @param isSVG - check to see if the uri is an svg + * @returns viewbox string + */ +export default function useSvgUriViewBox( + uri: string, + isSVG: boolean, +): string | undefined { + const [viewBox, setViewBox] = useState(undefined); + + useEffect(() => { + if (!isSVG) { + return; + } + + fetch(uri) + .then((response) => response.text()) + .then((svgContent) => { + const widthMatch = svgContent.match(/width="([^"]+)"/); + const heightMatch = svgContent.match(/height="([^"]+)"/); + const viewBoxMatch = svgContent.match(/viewBox="([^"]+)"/); + + if (viewBoxMatch?.[1]) { + setViewBox(viewBoxMatch[1]); + return; + } + + if (widthMatch?.[1] && heightMatch?.[1]) { + const width = widthMatch[1]; + const height = heightMatch[1]; + setViewBox(`0 0 ${width} ${height}`); + } + }) + .catch((error) => console.error('Error fetching SVG:', error)); + }, [isSVG, uri]); + + if (!viewBox) { + return undefined; + } + + return viewBox; +}