Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: add support for svg uris without viewbox #10247

Merged
merged 3 commits into from
Jul 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 17 additions & 7 deletions app/components/Base/RemoteImage/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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 <Identicon address={props.address} customStyle={props.style} />;
}

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) {
Expand All @@ -66,7 +70,13 @@ const RemoteImage = (props) => {
componentLabel="RemoteImage-SVG"
>
<View style={{ ...style, ...styles.svgContainer }}>
<SvgUri {...props} uri={uri} width={'100%'} height={'100%'} />
<SvgUri
{...props}
uri={uri}
width={'100%'}
height={'100%'}
viewBox={viewbox}
/>
</View>
</ComponentErrorBoundary>
);
Expand Down
42 changes: 42 additions & 0 deletions app/components/hooks/useSvgUriViewBox.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { renderHook, waitFor } from '@testing-library/react-native';
import useSvgUriViewBox from './useSvgUriViewBox';

describe('useSvgUriViewBox()', () => {
const MOCK_SVG_WITH_VIEWBOX = `<svg xmlns="http://www.w3.org/2000/svg" width="60" height="60" viewBox="0 0 60 60" fill="none"></svg>`;
const MOCK_SVG_WITHOUT_VIEWBOX = `<svg xmlns="http://www.w3.org/2000/svg" width="60" height="60" fill="none"></svg>`;

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();
});
});
49 changes: 49 additions & 0 deletions app/components/hooks/useSvgUriViewBox.ts
Original file line number Diff line number Diff line change
@@ -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<string | undefined>(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;
}
Loading