Waddup salty members of stackoverflow.
I have a react-native app with expo and have been developing on IOS for some time. When I finnaly got around to getting an android phone to test on for android. Everything in the app runs apart from the SVGS.
I'm running into an error on Android (works fine on IOS) when trying to render svgs within my app.
The Error:
java.lang.Double cannot be cast to abi48_0_0.com.facebook.react.bridge.ReadableMap
I've done a bunch of digging down rabbit holes but all to no avail. Obvously in the native code for android it it trying to cast a double to the type "ReadableMap". Looking into the source code for react native, it appears to be used for the fill and the stroke.
Here is my package.json
{
"name": "fruitminder",
"version": "1.22.2",
"main": "node_modules/expo/AppEntry.js",
"scripts": {
"start": "expo start",
"android": "expo start --android",
"ios": "expo start --ios",
"web": "expo start --web",
"test": "jest",
"test:watch": "jest --watch",
"lint": "eslint",
"ensure": "node_modules/.bin/tsc --noEmit && yarn lint . && yarn test",
"eject": "expo eject",
"generate-graphql": "graphql-codegen --config codegen.yml",
"postinstall": "patch-package"
},
"dependencies": {
"@expo-google-fonts/work-sans": "^0.2.2",
"@expo/config-plugins": "^6.0.1",
"@gorhom/bottom-sheet": "^4.4.0",
"@graphql-codegen/typescript-urql": "^3.4.2",
"@react-native-async-storage/async-storage": "~1.17.3",
"@react-native-community/datetimepicker": "6.7.3",
"@react-navigation/bottom-tabs": "^6.3.1",
"@react-navigation/drawer": "^6.3.1",
"@react-navigation/native": "^6.0.6",
"@react-navigation/native-stack": "^6.2.5",
"@react-navigation/stack": "^6.1.1",
"@sentry/react-native": "4.13.0",
"@shopify/react-native-skia": "0.1.172",
"@types/uuid": "^8.3.4",
"@urql/exchange-auth": "^0.1.7",
"@urql/exchange-graphcache": "^4.3.6",
"date-fns": "^2.29.1",
"dotenv": "^14.3.2",
"expo": "^48.0.0",
"expo-application": "~5.1.1",
"expo-barcode-scanner": "~12.3.2",
"expo-camera": "~13.2.1",
"expo-clipboard": "~4.1.2",
"expo-constants": "~14.2.1",
"expo-device": "~5.2.1",
"expo-file-system": "~15.2.2",
"expo-font": "~11.1.1",
"expo-haptics": "~12.2.1",
"expo-image-manipulator": "^11.1.1",
"expo-image-picker": "~14.1.1",
"expo-linking": "~4.0.1",
"expo-localization": "~14.1.1",
"expo-location": "~15.1.1",
"expo-notifications": "~0.18.1",
"expo-splash-screen": "~0.18.1",
"expo-status-bar": "~1.4.4",
"expo-tracking-transparency": "~3.0.3",
"expo-updates": "~0.16.3",
"formik": "^2.2.9",
"graphql": "^16.3.0",
"graphql-tag": "^2.12.6",
"i18n-js": "^3.8.0",
"intl": "^1.2.5",
"jest": "^29.2.1",
"jest-expo": "^46.0.0",
"patch-package": "^6.4.7",
"postinstall-postinstall": "^2.1.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-native": "0.71.3",
"react-native-country-picker-modal": "^2.0.0",
"react-native-dropdown-picker": "^5.4.0",
"react-native-gesture-handler": "~2.9.0",
"react-native-get-random-values": "~1.8.0",
"react-native-graph": "^0.1.1",
"react-native-iconly": "^1.0.12",
"react-native-keyboard-aware-scroll-view": "^0.9.5",
"react-native-maps": "1.3.2",
"react-native-qrcode-svg": "^6.1.2",
"react-native-reanimated": "~2.14.4",
"react-native-safe-area-context": "4.5.0",
"react-native-screens": "~3.20.0",
"react-native-svg": "12.3.0",
"react-native-web": "~0.18.7",
"sentry-expo": "~6.1.0",
"urql": "^2.0.6",
"uuid": "^8.3.2",
"wonka": "^4.0.15",
"yup": "^1.0.0-beta.3"
},
"devDependencies": {
"@babel/core": "^7.18.6",
"@graphql-codegen/cli": "2.4.0",
"@graphql-codegen/introspection": "2.1.1",
"@graphql-codegen/typescript": "2.4.2",
"@graphql-codegen/typescript-document-nodes": "2.2.2",
"@graphql-codegen/typescript-graphql-files-modules": "2.1.1",
"@graphql-codegen/typescript-operations": "2.2.2",
"@graphql-codegen/typescript-urql-graphcache": "^2.3.3",
"@graphql-codegen/urql-introspection": "2.1.1",
"@graphql-eslint/eslint-plugin": "^3.9.1",
"@testing-library/jest-native": "^4.0.5",
"@testing-library/react-native": "^11.0.0",
"@types/i18n-js": "^3.8.2",
"@types/jest": "^26.0.24",
"@types/react": "^18.0.0",
"@types/react-native": "~0.69.5",
"@typescript-eslint/eslint-plugin": "^5.10.1",
"@typescript-eslint/parser": "^5.10.1",
"eslint": "^8.7.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-jsx-expressions": "^1.3.1",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react": "^7.28.0",
"eslint-plugin-react-hooks": "^4.3.0",
"eslint-plugin-testing-library": "^5.0.6",
"prettier": "^2.5.1",
"react-native-svg-transformer": "^1.0.0",
"standard-version": "^7.1.0",
"standard-version-expo": "^1.0.3",
"ts-jest": "^27.1.3",
"typescript": "^4.6.3"
},
"resolutions": {
"@types/react": "^18.0.0"
},
"private": true
}
Here is my metro.config.js:
// Learn more https://docs.expo.io/guides/customizing-metro
const { getDefaultConfig } = require('expo/metro-config');
module.exports = (() => {
const config = getDefaultConfig(__dirname);
const { transformer, resolver } = config;
config.transformer = {
...transformer,
babelTransformerPath: require.resolve('react-native-svg-transformer'),
};
config.resolver = {
...resolver,
assetExts: resolver.assetExts.filter(ext => ext !== 'svg'),
sourceExts: [...resolver.sourceExts, 'svg'],
};
return config;
})();
And here is my profile screen
It has an assortmet of svgs being used (Sorry for not shortaning the code lol thought the real world example was better). Some from react-native-iconly, some that are built using react-native-svg and exported as a tsx element, and one that is set up like the prior mentioned one but also has animations.
I've gone through the app and tried to isolate if it has been any of the different types of svgs causing the errors, but it seems to be agnostic to the different types.
import * as ImagePicker from 'expo-image-picker';
import React, { useCallback, useEffect, useState } from 'react';
import { Alert, Image, TouchableOpacity, View, Linking } from 'react-native';
import * as Clipboard from 'expo-clipboard';
import { ScrollView } from 'react-native-gesture-handler';
import { Camera, Delete, Edit, Logout } from 'react-native-iconly';
import {
useSharedValue,
withRepeat,
withSpring,
} from 'react-native-reanimated';
import { useProfileScreenQuery } from '../../../../src/generated/graphql';
import AnimatedTree from '../../../components/animations/AnimatedTree';
import Button from '../../../components/Button';
import { ProfileTabStackScreen } from '../../../components/navigation/ParamList';
import Separator from '../../../components/Separator';
import Text from '../../../components/Text';
import TextField from '../../../components/TextField';
import { useAuth } from '../../../hooks/use-auth';
import useDirectUpload from '../../../hooks/use-direct-upload/use-direct-upload';
import useHeaderActivityIndicator from '../../../hooks/use-header-activity-indicator';
import Copy from '../../../icons/Copy';
import { SCREEN_GUTTER } from '../../../theme/constants';
import { spacing } from '../../../theme/spacing';
import useTheme from '../../../theme/use-theme';
import { formattedName } from '../../../utilities/helpers';
import useTranslations from '../../../utilities/use-translations';
export type ProfileScreenProps = { userId: string };
type Props = ProfileTabStackScreen<'Profile'>;
export default function ProfileScreen({
navigation,
route: {
params: { userId },
},
}: Props) {
const theme = useTheme();
const { t } = useTranslations({ scope: 'profileScreen' });
const [{ data, fetching, error }, refresh] = useProfileScreenQuery({
variables: { userId },
});
const { upload, uploading } = useDirectUpload();
const progress = useSharedValue(1);
const [image, setImage] =
useState<ImagePicker.ImagePickerSuccessResult | null>(null);
const { logout, deleteAccount } = useAuth();
const pickImage = useCallback(async () => {
// No permissions request is necessary for launching the image library
const res = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: true,
quality: 1,
});
if (!res.canceled && data?.user?.sgid) {
setImage(res);
await upload({
uri: res.assets?.[0]?.uri,
signedRecordId: data?.user?.sgid,
fieldName: 'avatar',
});
setImage(null);
refresh({ requestPolicy: 'network-only' });
}
}, [data?.user?.sgid, refresh, upload]);
const confirmDelete = useCallback(() => {
Alert.alert(t('delete.title'), t('delete.description'), [
{
text: t('cancel', { scope: 'common' }),
},
{
style: 'destructive',
text: t('delete.confirm'),
onPress: deleteAccount,
},
]);
}, [deleteAccount, t]);
useHeaderActivityIndicator({
loading: fetching,
title: t('profile', { scope: 'navigation' }),
navigation,
});
useEffect(() => {
if (uploading) {
progress.value = withRepeat(
withSpring(1, {
overshootClamping: true,
damping: 20,
stiffness: 90,
}),
-1,
true,
);
} else {
progress.value = 0;
}
}, [progress, uploading]);
const Missing = useCallback(() => {
if (data?.user || fetching) return null;
return (
<View style={{ marginTop: SCREEN_GUTTER }}>
<Text
style={{ color: theme.colours.error, textAlign: 'center' }}
size="xl"
>
{t('missing')}
</Text>
</View>
);
}, [data?.user, fetching, t, theme.colours.error]);
const Errored = useCallback(() => {
if (!error) return null;
return (
<View style={{ justifyContent: 'center', alignItems: 'center' }}>
<Text style={{ color: theme.colours.error }} size="xl">
{error.message}
</Text>
</View>
);
}, [error, theme.colours.error]);
const imageUri = image?.assets?.[0]?.uri || data?.user?.avatarUrl;
const Profile = useCallback(() => {
if (!data?.user) return null;
return (
<View>
<TouchableOpacity onPress={pickImage} style={{ position: 'relative' }}>
{imageUri ? (
<Image
source={{ uri: imageUri, cache: 'reload' }}
style={{
height: 282,
width: '100%',
resizeMode: 'cover',
opacity: uploading ? 0.5 : 1,
}}
/>
) : (
<View
style={{
height: 282,
width: '100%',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: theme.colours.backgroundLight,
}}
>
<Camera size={80} />
<Text size="lg">{t('uploadPhoto')}</Text>
</View>
)}
<View
style={{
position: 'absolute',
display: uploading ? 'flex' : 'none',
bottom: spacing[4],
right: spacing[3],
}}
>
<AnimatedTree
progress={progress}
size="large"
color={theme.colours.tint[0]}
/>
</View>
</TouchableOpacity>
<View
style={{
marginHorizontal: SCREEN_GUTTER,
marginTop: spacing[4],
marginBottom: -spacing[7],
}}
>
<Text size="2xl" weight="medium">
{formattedName(data?.user?.firstName, data?.user?.lastName)}
</Text>
<View style={{ marginVertical: spacing[2] }}>
<Text style={{ marginVertical: spacing[4] }} size="2xl">
{t('memberships')}
</Text>
{data.user.memberships?.map(membership => (
<View
style={{
flex: 1,
flexDirection: 'row',
justifyContent: 'space-between',
borderTopWidth: 1,
borderColor: theme.colours.separator,
marginHorizontal: -SCREEN_GUTTER,
paddingHorizontal: SCREEN_GUTTER,
}}
>
<Text weight="medium">{membership.organisation.name}</Text>
<Text>{membership.role}</Text>
</View>
))}
</View>
</View>
<Separator />
<View style={{ marginHorizontal: SCREEN_GUTTER }}>
<TextField
editable={false}
labelTextOptions={{ children: 'Email Address' }}
textInputOptions={{
value: data?.user?.email,
style: { backgroundColor: theme.colours.backgroundLight },
}}
/>
<Button
type="tertiary"
onPress={() => navigation.navigate('EditProfile', { userId })}
style={{ marginTop: spacing[3] }}
iconColor={theme.colours.settings}
icon={<Edit />}
>
{t('edit')}
</Button>
</View>
</View>
);
}, [
data?.user,
imageUri,
navigation,
pickImage,
progress,
t,
theme.colours.backgroundLight,
theme.colours.separator,
theme.colours.settings,
theme.colours.tint,
uploading,
userId,
]);
return (
<ScrollView>
<Errored />
<Missing />
<Profile />
<View style={{ flex: 1 }} />
<Button
type="tertiary"
onPress={logout}
style={{ margin: SCREEN_GUTTER }}
icon={<Logout />}
iconColor={theme.colours.danger}
>
{t('logout')}
</Button>
<Button
type="tertiary"
onPress={confirmDelete}
style={{ margin: SCREEN_GUTTER }}
icon={<Delete />}
iconColor={theme.colours.danger}
>
{t('deleteAccount')}
</Button>
<View
style={{
padding: SCREEN_GUTTER,
flex: 1,
flexDirection: 'column',
justifyContent: 'space-between',
}}
>
<Text>{t('betaContactDetails')}</Text>
<TouchableOpacity
style={{ marginTop: spacing[2] }}
onPress={() => Linking.openURL(`tel:${'+64 27 408 7233'}`)}
>
<Text
style={{
textDecorationLine: 'underline',
color: theme.colours.info,
}}
>
{t('betaContactPhoneNumber')}
</Text>
</TouchableOpacity>
<Button
icon={<Copy />}
type="tertiary"
style={{ marginTop: spacing[2] }}
onPress={() => {
Clipboard.setStringAsync(t('betaContactEmail'));
Alert.alert(t('copiedToClipboard'));
}}
>
{t('betaContactEmail')}
</Button>
</View>
</ScrollView>
);
}
Curious as to if anyone has run into this error or may be able to point me in the right direction. It feels as if something is not set up correctly as all the examples i've come across follow the same processes to set up as I have gone through, but get a different result - so makes me think there may be some other setting messing with things.
Things I have tried:
- Isolating individual SVG's to see if its a bug with any of them
- Changing package versions of react-native-svg
- Running the app with, and without react-native-svg-transformer
- Insuring all the props passed into the svgs are of the correct type
- running yarn doctor & yarn upgrade
- Scouring the internet for similar problems