expo / expo

An open-source framework for making universal native apps with React. Expo runs on Android, iOS, and the web.
https://docs.expo.dev
MIT License
32.73k stars 5.21k forks source link

Images won't be found if Expo-Updates implemented on iOS release build (in TestFlight versions) #22656

Closed efkan closed 1 year ago

efkan commented 1 year ago

Minimal reproducible example

https://github.com/efkan/ExpoUpdatesExample.git

Summary

While images are displayed as expected on a newly created React-Native app, they could not be found after I implemented the Expo-Updates module.

On the other hand, the images are shown as expected on Android for both Dev and Prod environments.

We cannot continue testing & development due to bumping this issue. I've searched on the Internet but couldn't find any solution.

Ps: sorry if I'd overlooked something on the Expo documentation

image

Environment

expo-env-info 1.0.5 environment info: System: OS: macOS 13.3.1 Shell: 5.9 - /bin/zsh Binaries: Node: 16.16.0 - ~/.nvm/versions/node/v16.16.0/bin/node Yarn: 1.22.19 - /usr/local/bin/yarn npm: 8.16.0 - ~/.nvm/versions/node/v16.16.0/bin/npm Managers: CocoaPods: 1.12.1 - /Users/efkan/.rvm/gems/ruby-3.0.0/bin/pod SDKs: iOS SDK: Platforms: DriverKit 22.4, iOS 16.4, macOS 13.3, tvOS 16.4, watchOS 9.4 Android SDK: API Levels: 30, 31, 33 Build Tools: 30.0.2, 30.0.3, 31.0.0, 33.0.0, 33.0.1, 34.0.0 System Images: android-28 | Google ARM64-V8a Play ARM 64 v8a, android-30 | Google APIs ARM 64 v8a, android-30 | Google Play ARM 64 v8a, android-31 | ARM 64 v8a, android-31 | Intel x86 Atom_64, android-31 | Google APIs ARM 64 v8a, android-31 | Google APIs Intel x86 Atom_64, android-31 | Google Play ARM 64 v8a, android-33 | Google APIs ARM 64 v8a IDEs: Android Studio: 2022.1 AI-221.6008.13.2211.9619390 Xcode: 14.3/14E222b - /usr/bin/xcodebuild npmPackages: expo: ^48.0.0 => 48.0.17 react: 18.2.0 => 18.2.0 react-native: 0.71.8 => 0.71.8 npmGlobalPackages: eas-cli: 3.13.1 Expo Workflow: bare

efkan commented 1 year ago

Additional info: on our app's release version, the images became visible after getting an OTA update.

efkan commented 1 year ago

Found another clue; I didn't run across the asset issue on SDK v47.0.14.

However, it persists on v48.

douglowder commented 1 year ago

The example repo has a local update server as the URL, but an EAS project ID. Where are the updates served from?

Please add instructions to your README explaining how you set up the project before building the app. Did you use EAS build, or a local build?

efkan commented 1 year ago

The repo might be dysfunctional. But we encountered this issue while we’re implementing expo-updates module.

Yes I used “eas build” command to check it if it works as expected.

Please note my comment: https://github.com/expo/expo/issues/22656#issuecomment-1571311387

asphyxiar commented 1 year ago

Hello, we encountered the same issue, while adding sentry into our project, in the docs theres an instruction to install also expo updates module. If we remove the expo-updates, images are shown on release build, and if we leave the expo-updates in package.json the images are not displayed. We are not using EAS build.

chochihim commented 1 year ago

Same issue here. We are attempting to add expo-updates in our bare react-native app. We can reproduce in release build on simulator. We don't even need to add and remove the package like @asphyxiar to reproduce it. Just enable and disable the flag in Expo.plist can see the issue: Disabled:

Screenshot 2023-06-23 at 11 49 45 AM

Enabled:

Screenshot 2023-06-23 at 11 51 43 AM

Note that the custom update server http://localhost:3000/api/manifest does not exist yet. We are in the beginning of the process of integrating expo-updates. Anyway, if the url call fails, I think expo should just use bundled javascript and assets, right? Now it seems that no image is bundled and found.

Libraries versions we are using:

"expo": "^48.0.19",
"expo-updates": "~0.16.4"
expo-bot commented 1 year ago

Thank you for filing this issue! This comment acknowledges we believe this may be a bug and there’s enough information to investigate it. However, we can’t promise any sort of timeline for resolution. We prioritize issues based on severity, breadth of impact, and alignment with our roadmap. If you’d like to help move it more quickly, you can continue to investigate it more deeply and/or you can open a pull request that fixes the cause.

douglowder commented 1 year ago

I can reproduce the issue, and we will take a look.

douglowder commented 1 year ago

ENG-9073

sregg commented 1 year ago

We're seeing a similar issue but only image assets from node_modules like the flags in react-native-country-flag

image
douglowder commented 1 year ago

I was not reproducing this issue in current main Expo code. Now that the SDK 49 beta is out, I will test to see if the issue still shows up there.

sregg commented 1 year ago

I forgot to mention we're using Expo Updates on a vanilla RN app.

    "expo": "^48.0.0",
    "expo-updates": "~0.16.4",
douglowder commented 1 year ago

@sregg @efkan @asphyxiar I did some more investigation, and now this appears to be fixable with some changes to the way you are calling expo-asset APIs. useAssets() returns an array, so I import all the assets with a single call to that hook, and then each image component references the localUri property of one of the assets in the array.

It may be that the vanilla React Native Image component can be used too, but the API for expo-image is very similar.

With the following changes to the example app, the images do show up in a release build as expected.

diff --git a/package.json b/package.json
index 67c9d98..7471922 100644
--- a/package.json
+++ b/package.json
@@ -10,9 +10,10 @@
     "test": "jest"
   },
   "dependencies": {
-    "expo": "^48.0.0",
+    "expo": "~48.0.18",
     "expo-asset": "~8.9.1",
     "expo-constants": "~14.2.1",
+    "expo-image": "^1.3.1",
     "expo-updates": "~0.16.4",
     "react": "18.2.0",
     "react-native": "0.71.8"
diff --git a/App.tsx b/App.tsx
index 29a3539..4090290 100644
--- a/App.tsx
+++ b/App.tsx
@@ -5,10 +5,10 @@
  * @format
  */

+import { Image } from 'expo-image';
 import React from 'react';
-import type {PropsWithChildren} from 'react';
+import type { PropsWithChildren } from 'react';
 import {
-  Image,
   SafeAreaView,
   ScrollView,
   StatusBar,
@@ -18,7 +18,7 @@ import {
   View,
 } from 'react-native';

-import {Asset, useAssets} from 'expo-asset';
+import { Asset, useAssets } from 'expo-asset';

 import {
   Colors,
@@ -32,7 +32,7 @@ type SectionProps = PropsWithChildren<{
   title: string;
 }>;

-function Section({children, title}: SectionProps): JSX.Element {
+function Section({ children, title }: SectionProps): JSX.Element {
   const isDarkMode = useColorScheme() === 'dark';
   return (
     <View style={styles.sectionContainer}>
@@ -42,7 +42,8 @@ function Section({children, title}: SectionProps): JSX.Element {
           {
             color: isDarkMode ? Colors.white : Colors.black,
           },
-        ]}>
+        ]}
+      >
         {title}
       </Text>
       <Text
@@ -51,7 +52,8 @@ function Section({children, title}: SectionProps): JSX.Element {
           {
             color: isDarkMode ? Colors.light : Colors.dark,
           },
-        ]}>
+        ]}
+      >
         {children}
       </Text>
     </View>
@@ -61,8 +63,9 @@ function Section({children, title}: SectionProps): JSX.Element {
 function App(): JSX.Element {
   const isDarkMode = useColorScheme() === 'dark';

-  const [img] = useAssets([
+  const [img, error] = useAssets([
     require('./assets/images/category/_gamingNotebooks.png'),
+    require('./assets/GradientTop.png'),
   ]);

   // console.log(img);
@@ -79,30 +82,30 @@ function App(): JSX.Element {
       />
       <ScrollView
         contentInsetAdjustmentBehavior="automatic"
-        style={backgroundStyle}>
+        style={backgroundStyle}
+      >
         <Header />
-        <Image
-          style={$SLIDE_LIST_IMAGE}
-          source={img || {}}
-          alt={'Gaming Notebooks'}
-          resizeMode="cover"
-        />
-        <Image
-          style={$SLIDE_LIST_IMAGE}
-          source={require('./assets/images/category/_gamingNotebooks.png')}
-          alt={'Gaming Notebooks'}
-          resizeMode="cover"
-        />
-        <Image
-          style={$SLIDE_LIST_IMAGE}
-          source={require('./GradientTop.png')}
-          alt={'Gaming Notebooks'}
-          resizeMode="cover"
-        />
+        {img && img.length > 0 ? (
+          <Image
+            style={$SLIDE_LIST_IMAGE}
+            source={img[0].localUri}
+            alt={'Gaming Notebooks'}
+            contentFit="cover"
+          />
+        ) : null}
+        {img && img.length > 1 ? (
+          <Image
+            style={$SLIDE_LIST_IMAGE}
+            source={img[1].localUri}
+            alt={'GradientTop'}
+            contentFit="cover"
+          />
+        ) : null}
         <View
           style={{
             backgroundColor: isDarkMode ? Colors.black : Colors.white,
-          }}>
+          }}
+        >
           <Section title="Step One">
             Edit <Text style={styles.highlight}>App.tsx</Text> to change this
             screen and then come back to see your edits.
sregg commented 1 year ago

We're not using expo-image nor expo-assets. Moreover, the issue comes from assets in node_modules so we don't have access to that code. Is there a way to disable assets in updates altogether?

Bengejd commented 1 year ago

I was also experiencing this issue when I added expo-updates to our project. Didn't realize that's what was causing it for about a week and a half, after trying every conceivable solution I could find to fix the problem. Turned out, once I uninstalled expo-updates, the issue went away and my images went back to rendering properly on IOS & Android.

I also did not use expo-assets, or expo-image in my project before (when this worked flawlessly) or after (when I fixed it).

Was occurring consistently on IOS builds, but occasionally would happen on Android builds when I tried pushing out an expo-update call. Seems to me that the expo-updates app is messing with the hashes of the images & causing them to not be found.

douglowder commented 1 year ago

Thanks @sregg -- I'll take a look specifically at what is happening with images that are coming from node_modules paths. Once I have a repro, I'll create a separate issue to track that problem.

@Bengejd is your issue also with images coming from node_modules paths? If you could describe specifically what you did when you were "pushing out an expo-update call" (or even provide a sample app that shows the problem), that would be great!

Bengejd commented 1 year ago

My issue is coming from local assets, e.g: assets/logo-small.jpg when I have the expo-updates package installed, and then using the local assets like so:

assets/images.tsx

Generically require them in an object, so that they can be passed around throughout the application via reference.

export const IMAGES: {
    LOGO_SMALL: number;
    ...
} = {
    LOGO_SMALL: require("./logo-small.jpg"),
    ...
}

components/logo-small.tsx

Logo Small Component consumes that required image reference

import {IMAGES} from "@assets";
import {Image} from "react-native-ui-lib";
...
export const LogoSmall = (...) => {

  const [assets, error] = useAssets([IMAGES.LOGO_SMALL]);

  const logo = !error ? assets?.[0] ?? null : null;

  return (
    <Image style={styles.imageLogoSmall} source={logo} />
  );
};

This method works great when used during development (utilizing Expo-Go for example), the images show up and everything is peachy keen. However, once I have built the application using eas build --profile testing, the application no longer loads the images. All that is shown is a blank grey square in their place.

This issue persisted for several builds & several OTA updates via expo-update with the command: eas update --branch testing. However, the issue was always present in these builds, whether I used the OTA update feature or not.

However, after stumbling across this thread, I decided to uninstall expo-updates & run a new build (with the build command above), and the issue completely resolved itself. Images show up, leading me to believe that OTA updates were causing the issue (as others have listed in the original thread).

At the moment I don't have time to replicate the environment while stripping away the functionality & confidential information (crunch week 😄), but I can attempt to sometime this weekend, maybe.

douglowder commented 1 year ago

@Bengejd thanks very much, this is definitely enough to get started creating a repro.

sregg commented 1 year ago

Same for us. It's not just image assets from node_modules but also from our own assets folder that are imported with require:

const heroImagePlaceholder = require('~/assets/images/imageBanner.jpg');
...
<Image source={heroImagePlaceholder} />
douglowder commented 1 year ago

Hmm I did a simple test.

yarn create expo-app AssetTest --template blank-typescript@48
cd AssetTest
npx expo install expo-updates
import { StatusBar } from 'expo-status-bar';
import { Image, StyleSheet, Text, View } from 'react-native';

const image = require('./assets/dougheadshot.jpg');

export default function App() {
  return (
    <View style={styles.container}>
      <Text>Open up App.tsx to start working on your app!</Text>
      <Image source={image} />
      <StatusBar style="auto" />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
});
npx expo run:ios --configuration Release

And the image appears.

Simulator Screenshot - iPhone 14 - 2023-07-06 at 12 43 19

douglowder commented 1 year ago

I see the same thing after running eas init and eas update:configure.

@sregg should not your image import be

const heroImagePlaceholder = require('./assets/images/imageBanner.jpg');

???

douglowder commented 1 year ago

Ok I finally was able to reproduce the bug, by copying the metro.config.js from @efkan 's project into my test project.

The root cause seems to be that the affected projects are using the plain vanilla Metro config from RN, and not importing Expo's config.

Your metro.config.js should look like this:

// Learn more https://docs.expo.io/guides/customizing-metro
const { getDefaultConfig } = require('expo/metro-config');

module.exports = getDefaultConfig(__dirname);
Bengejd commented 1 year ago

@douglowder Interesting. I wonder why metro config would break with expo-updates specifically then, and not just be broken the whole time?

For reference, here is my metro.config, which does import expo's config to extend it:

const { withNxMetro } = require("@nrwl/expo");
const path = require('path');
const workspaceRoot = path.resolve(__dirname, '../../');
const projectRoot = __dirname;

const { getDefaultConfig } = require("@expo/metro-config");

module.exports = (() => {
  const config = getDefaultConfig(projectRoot);

  const {transformer, resolver} = config;

  config.transformer = {
    ...transformer,
    babelTransformerPath: require.resolve("react-native-svg-transformer"),
    assetPlugins: ["expo-asset/tools/hashAssetFiles"],
  }

  config.resolver = {
    ...resolver,
    assetExts: resolver.assetExts.filter((ext) => ext !== "svg"),
    sourceExts: [...resolver.sourceExts, "svg"],
  }

  config.watchFolders = [workspaceRoot];

  config.resolver.nodeModulesPaths = [
    path.resolve(projectRoot, 'node_modules'),
    path.resolve(workspaceRoot, 'node_modules'),
  ];

  return withNxMetro(config, {
    debug: false,
    extensions: [],
    projectRoot: __dirname,
    watchFolders: [],
  })
})();
douglowder commented 1 year ago

@Bengejd I think not having expo/metro-config breaks expo-updates specifically because it is adjusting asset paths. Ultimately the source for our Metro config is at

https://github.com/expo/expo/blob/main/packages/%40expo/metro-config/src/ExpoMetroConfig.ts

If you are on SDK 48, you should use

https://github.com/expo/expo/blob/sdk-48/packages/%40expo/metro-config/src/ExpoMetroConfig.ts

sregg commented 1 year ago

Thanks @douglowder I'll try this. Maybe this should be explained in https://docs.expo.dev/bare/installing-updates/ Is there really no way to disable this asset thing and only use expo-updates to update the JS bundle?

sregg commented 1 year ago

Using the expo metro config

const { getDefaultConfig } = require('expo/metro-config');

module.exports = getDefaultConfig(__dirname);

didn't fix the issue for me.

The following images don't show up:

sregg commented 1 year ago

Here's our metro.config.js

const { getDefaultConfig } = require('expo/metro-config');

module.exports = (async () => {
  const {
    resolver: { sourceExts, assetExts, resolverMainFields },
  } = await getDefaultConfig(__dirname);
  return {
    transformer: {
      getTransformOptions: async () => ({
        transform: {
          experimentalImportSupport: false,
          inlineRequires: true,
        },
      }),
      babelTransformerPath: require.resolve('react-native-svg-transformer'),
    },
    resolver: {
      assetExts: assetExts.filter((ext) => ext !== 'svg'),
      sourceExts: [...sourceExts, 'svg'],
      // this is needed for Storybook (see https://github.com/storybookjs/react-native/issues/405#issuecomment-1436683333)
      resolverMainFields: ['sbmodern', ...resolverMainFields],
    },
  };
})();
sregg commented 1 year ago

I also wanted to mention that we're using using expo-asset at all. It's only referenced in our yarn.lock as it's a dependency of expo.

sregg commented 1 year ago

I updated my config file but it's still not working:


const { getDefaultConfig } = require('expo/metro-config');

module.exports = (async () => {
  const config = await getDefaultConfig(__dirname);
  return {
    ...config,
    transformer: {
      ...config.transformer,
      getTransformOptions: async () => ({
        transform: {
          experimentalImportSupport: false,
          inlineRequires: true,
        },
      }),
      babelTransformerPath: require.resolve('react-native-svg-transformer'),
    },
    resolver: {
      ...config.resolver,
      assetExts: config.resolver.assetExts.filter((ext) => ext !== 'svg'),
      sourceExts: [...config.resolver.sourceExts, 'svg'],
      // this is needed for Storybook (see https://github.com/storybookjs/react-native/issues/405#issuecomment-1436683333)
      resolverMainFields: ['sbmodern', ...config.resolver.resolverMainFields],
    },
  };
})();
sregg commented 1 year ago

Should the assets be renamed inside the .app?

image

Also here's the error in the log when loading the image

Task <AC8A67BB-3EAA-4541-81CC-D94C56BE0E77>.<3> finished with error [-1100] Error Domain=NSURLErrorDomain Code=-1100 "The requested URL was not found on this server." UserInfo={NSLocalizedDescription=The requested URL was not found on this server., NSErrorFailingURLStringKey=file:///Users/simonreggiani/Library/Developer/CoreSimulator/Devices/AFA69A09-627D-41A3-AEA4-3B7F6FD92C35/data/Containers/Data/Application/9A151CC4-2691-4225-9614-CC3442EB5F76/Library/Application<decode: mismatch for [%20S] got [OBJECT public sz:48]>upport/.expo-internal/assets/src/assets/images/banner-placeholder.png, NSErrorFailingURLKey=file:///Users/simonreggiani/Library/Developer/CoreSimulator/Devices/AFA69A09-627D-41A3-AEA4-3B7F6FD92C35/data/Containers/Data/Application/9A151CC4-2691-4225-9614-CC3442EB5F76/Library/Application<decode: mismatch for [%20S] got [SCALAR sz:8]>upport/.expo-internal/assets/src/assets/images/banner-placeholder.png, _NSURLErrorRelatedURLSessionTaskErrorKey=(
    "LocalDataTask <AC8A67BB-3EAA-4541-81CC-D94C56BE0E77>.<3>"
), _NSURLErrorFailingURLSessionTaskErrorKey=LocalDataTask <AC8A67BB-3EAA-4541-81CC-D94

Here's what's in /Users/simonreggiani/Library/Developer/CoreSimulator/Devices/AFA69A09-627D-41A3-AEA4-3B7F6FD92C35/data/Containers/Data/Application/9A151CC4-2691-4225-9614-CC3442EB5F76/Library/

image

No .expo-internal. Also here's the code in the JS bundle where the image is loaded

__d(function (global, _$$_REQUIRE, _$$_IMPORT_DEFAULT, _$$_IMPORT_ALL, module, exports, _dependencyMap) {
  module.exports = _$$_REQUIRE(_dependencyMap[0]).registerAsset({
    "__packager_asset": true,
    "httpServerLocation": "/assets/src/assets/images",
    "width": 375,
    "height": 300,
    "scales": [1],
    "hash": "31f4b797520b24f058bfbdf1b9ce45aa",
    "name": "banner-placeholder",
    "type": "png",
    "fileHashes": ["31f4b797520b24f058bfbdf1b9ce45aa"]
  });
},3093,[1216]);
chochihim commented 1 year ago

I finally resolve this problem. About my app:

  1. A bare react native app
  2. I am following doc to integrate expo-updates
  3. I am not using expo-image, expo-asset etc. I just wanna add expo-updates to my app.
  4. I have checked all babel/metro/whatever-you-name-it configs. They are all using expo's recommended one.

With above setting and expo-updates enabled, I build the app with "Release" config on iOS simulator. And my images are missing.

Turns out you NEED to use expo-asset but just need to add import 'expo-asset'; at the very beginning of your js bundle (probably index.js at root). Inside expo-asset, it would call setCustomSourceTransformer from react-native's Image to tell it how to resolve image assets from correct location (the hidden .expo-internal). After adding the expo-asset line, my images finally appear.

@douglowder Can you confirm if my finding is correct?

sregg commented 1 year ago

Thank you so much @chochihim this was it! I was missing import 'expo-asset'; too. This definitely should be explained in the Expo Updates docs.

asphyxiar commented 1 year ago

@chochihim have u also added expo-asset into the package.json and actually installed it? or just adding the line? Its not working for me

chochihim commented 1 year ago

@asphyxiar I didn't install it. It is one of the dependencies of expo so I just import it.

asphyxiar commented 1 year ago

still not seeing the images, could it be connected to the fact that iam running expo 47 not 48?