1

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

react native 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

0 Answers0