Search code examples
react-nativeexpo

Expo AppLoading hiding SplashScreen immediately on app load in TestFlight


I have an Expo Managed React Native application. I have some async functions that are called before the main app screen renders. It works perfectly fine on dev mode, production dev mode and Expo Go, but on Test Flight the app immediately hides the Splash Screen.

The long and short of it is that I have a state hook that changes when the AppLoading async method completes. During that async period I update my Context API to include the fetched data from an SQLite database. Here is my code:

import React, { useContext, useEffect, useState } from 'react';
import AppLoading from 'expo-app-loading';
import {
  StyleSheet,
  FlatList,
  View,
  Text,
} from 'react-native';
import { Icon } from 'react-native-elements';
import Card from '../components/Card';
import Colors from '../constants/Colors';
import DatabaseService from '../services/database.service';

import { AppContext } from '../contexts/appContext';

export default () => {
  /**
   * Vars
   */
  const databaseService = DatabaseService.getService();

  /**
   * State
   */
  const [appInitialized, setAppInitialized] = useState(false);
  const [appInitializedFail, setAppInitializedFail] = useState(false);

  /**
   * Contexts
   */
  const { cardsArray, dispatchEvent } = useContext(AppContext);

  /**
   * On mount initialize the database and query for all cards
   */
  useEffect(() => {
    initialize()
      .then(didInitialize => {
        if (didInitialize) {
          setAppInitialized(true);
        } else {
          setAppInitialized(true);
          setAppInitializedFail(true);
        }
      })
      .catch(err => {
        setAppInitialized(true);
        setAppInitializedFail(true);

        console.error(err);
      });
  }, []);

  /**
   * Prepare the app by loading async data
   *
   * @returns {Promise}
   */
  const initialize = async () => {
    const _initialTime = new Date();

    return databaseService.init()
      .then(async database => {
        return getAllCards(database)
          .then(async createdData => {
            const [createdArray, createdMap] = createdData;

            await dispatchEvent('UPDATE_CARDS_MAP', createdMap);
            await dispatchEvent('UPDATE_CARDS_ARRAY', createdArray);

            console.debug(
              `\nFetched all ${createdArray.length} results in ${(new Date() - _initialTime) / 1000}s.`
            );

            return true;
          })
          .catch(err => {
            console.error(err);

            return false;
          });
      })
      .catch(err => {
        console.error(err);

        return false;
      });
  };

  /**
   * Query the database for all cards
   *
   * @param {Class} database Service to utilize our database queries on
   * @returns {Promise}
   */
  const getAllCards = (database) => {
    return new Promise((resolve, reject) => {
      database.db.transaction(
        (tx) => {
          tx.executeSql(
            `SELECT cards.* FROM cards ORDER BY RANDOM()`,
            [],
            (_, res) => {
              const cards = createCardsArray(res.rows._array);

              resolve(cards);
            },
            (_, err) => {
              console.error(err);

              reject(err);
            }
          );
        },
        (err) => {
          console.error(err);

          reject(err);
        }
      );
    });
  };

  /**
   * Create cards array based on a set of cards passed through
   *
   * @param {Object} cards Raw cards array before we manipulate the data
   * @returns {Array}
   */
  const createCardsArray = (cards) => {
    const createdMap = {};
    const createdArray = cards.map(card => {
      const { uuid, otherFaceIds, scryfallId, layout, side } = card;
      const url = `https://api.scryfall.com/cards/${scryfallId}?format=image&version=`;

      let isTransform = undefined;
      let isFlip = undefined;
      let isMeld = undefined;
      let isSplit = undefined;
      let isDual = undefined;
      let imageUrl = `${url}normal`;
      let cropUrl = `${url}art_crop`;
      let thumbUrl = `${url}small`;

      if (otherFaceIds) {
        isDual = true;

        // Cards with two faces
        if (layout.includes('transform') || layout.includes('modal_dfc')) {
          isTransform = true;

          if (side === 'b') {
            imageUrl = imageUrl + `&face=back`;
            cropUrl = cropUrl + `&face=back`;
          }
          // Cards with two sets of data but one side of a card
        } else if (layout.includes('flip')) {
          isFlip = true;
        } else if (layout.includes('meld')) {
          isMeld = true;
        } else if (layout.includes('split')) {
          isSplit = true;
        }
      }

      const newCard = {
        ...card,
        imageUrl,
        cropUrl,
        thumbUrl,
        isDual,
        isTransform,
        isSplit,
        isFlip,
        isMeld,
      };

      createdMap[uuid] = newCard;

      return newCard;
    });

    return [createdArray, createdMap];
  };

  /**
   * Render the loading view
   *
   * @returns {JSX}
   */
  if (!appInitialized) {
    return (
      <AppLoading />
    );
  }

  /**
   * Render the error view
   *
   * @returns {JSX}
   */
  if (appInitializedFail) {
    return (
      <View style={styles.screenView}>
        <Icon color={Colors.redColor} size={40} name="warning-outline" type="ionicon" />
        <Text style={styles.errorText}>The database failed to load!</Text>
        <Text style={[styles.errorText, styles.errorTextSmall]}>You can try restarting the application, restarting your device or reinstalling the application to resolve this issue.</Text>
      </View>
    );
  }

  /**
   * Render the successful view
   *
   * @returns {JSX}
   */
  return (
    <View style={styles.screenView}>
      <FlatList
        data={cardsArray}
        renderItem={({ item: card }) => <Card
          card={card}
          hideFavoriteButton={false}
        />}
        keyExtractor={item => item.id}
      />
    </View>
  );
};

/**
 * Styles
 */
const styles = StyleSheet.create({
  screenView: {
    ...StyleSheet.absoluteFill,
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: Colors.darkColor,
    paddingLeft: 20,
    paddingRight: 20,
  },
  errorText: {
    textAlign: 'center',
    color: Colors.redColor,
    fontSize: 20,
    fontWeight: 'bold',
    marginBottom: 20,
  },
  errorTextSmall: {
    fontSize: 16,
    color: Colors.lightColor,
  }
});

Can anyone decipher whats going on here to cause TestFlight to not respect the AppLoading component?


Solution

  • Turns out the issue was that my promise setup was not returning correctly, the proper way to return the promises to not hide the AppLoading component was to change my code to this using Expo Splash Screen and changing my promises like so:

    import React, { useContext, useEffect, useState } from 'react';
    import * as SplashScreen from 'expo-splash-screen';
    import {
      StyleSheet,
      FlatList,
      View,
      Text,
    } from 'react-native';
    import { Icon } from 'react-native-elements';
    import * as Analytics from 'expo-firebase-analytics';
    import Card from '../components/Card';
    import Colors from '../constants/Colors';
    import DatabaseService from '../services/database.service';
    
    import { AppContext } from '../contexts/appContext';
    
    SplashScreen.preventAutoHideAsync();
    
    export default () => {
      /**
       * Vars
       */
      const databaseService = DatabaseService.getService();
    
      /**
       * State
       */
      const [appInitializedFail, setAppInitializedFail] = useState(false);
      const [errorHasBeenSet, setErrorHasBeenSet] = useState(false);
      const [errorCode, setErrorCode] = useState(null);
    
      /**
       * Contexts
       */
      const { cardsArray, dispatchEvent } = useContext(AppContext);
    
      /**
       * On mount initialize the database and query for all cards
       */
      useEffect(() => {
        initialize()
          .then(didInitialize => {
            if (!!didInitialize) {
              SplashScreen.hideAsync();
            } else {
              setErrorViewState('Error: App initialized with no data.');
            }
          })
          .catch(err => {
            setErrorViewState('Error: Failed to initialize application.');
    
            console.error(err);
          });
      }, []);
    
      /**
       * Prepare the app by loading async data
       *
       * @returns {Promise}
       */
      const initialize = async () => {
        const _initialTime = new Date();
    
        return databaseService.init(setErrorViewState)
          .then(async database => {
            return await getAllCards(database)
              .then(results => {
                if(results){
                  const [createdArray, createdMap] = results;
    
                  dispatchEvent('UPDATE_CARDS_MAP', createdMap);
                  dispatchEvent('UPDATE_CARDS_ARRAY', createdArray);
    
                  console.debug(
                    `\nFetched all ${createdArray.length} results in ${(new Date() - _initialTime) / 1000}s.`
                  );
    
                  return true;
                } else {
                  setErrorViewState('Error: No database data.');
    
                  return false;
                }
              })
              .catch(err => {
                setErrorViewState('Error: Failed to get database data.');
    
                return err;
              });
          })
          .catch(err => {
            setErrorViewState('Error: Failed to initialize the database.');
    
            return err;
          });
      };
    
      /**
       * Query the database for all cards
       *
       * @param {Class} database Service to utilize our database queries on
       * @returns {Promise}
       */
      const getAllCards = (database) => {
        return new Promise(async (resolve, reject) => {
          return await database.db.transaction(
            (tx) => {
              tx.executeSql(
                `SELECT cards.* FROM cards ORDER BY RANDOM()`,
                [],
                (_, res) => {
                  const cards = createCardsArray(res.rows._array);
    
                  resolve(cards);
                },
                (_, err) => {
                  setErrorViewState('Error: Failed to execute query.');
    
                  reject(err);
                }
              );
            },
            (err) => {
              setErrorViewState('Error: Failed to execute transaction.');
    
              reject(err);
            }
          );
        });
      };
    
      /**
       * Create cards array based on a set of cards passed through
       *
       * @param {Object} cards Raw cards array before we manipulate the data
       * @returns {Array}
       */
      const createCardsArray = (cards) => {
        const createdMap = {};
        const createdArray = cards
          .map(card => {
            const { uuid, otherFaceIds, scryfallId, layout, side } = card;
            const url = `https://api.scryfall.com/cards/${scryfallId}?format=image&version=`;
    
            let isTransform = undefined;
            let isFlip = undefined;
            let isMeld = undefined;
            let isSplit = undefined;
            let isDual = undefined;
            let imageUrl = `${url}normal`;
            let cropUrl = `${url}art_crop`;
            let thumbUrl = `${url}small`;
    
            if (otherFaceIds) {
              isDual = true;
    
              // Cards with two faces
              if (layout.includes('transform') || layout.includes('modal_dfc')) {
                isTransform = true;
    
                if (side === 'b') {
                  imageUrl = imageUrl + `&face=back`;
                  cropUrl = cropUrl + `&face=back`;
                }
                // Cards with two sets of data but one side of a card
              } else if (layout.includes('flip')) {
                isFlip = true;
              } else if (layout.includes('meld')) {
                isMeld = true;
              } else if (layout.includes('split')) {
                isSplit = true;
              }
            }
    
            const newCard = {
              ...card,
              imageUrl,
              cropUrl,
              thumbUrl,
              isDual,
              isTransform,
              isSplit,
              isFlip,
              isMeld,
            };
    
            createdMap[uuid] = newCard;
    
            return newCard;
          })
          // Filter out other sides so we can see the main face first and swap them later
          .filter(card => card.side === 'a' || !card.side);
    
        return [createdArray, createdMap];
      };
    
      /**
       * Set state to show our error message
       */
      const setErrorViewState = (errorCode = null) => {
        // Ensure we establish the first caught error
        if(!errorHasBeenSet){
          setErrorHasBeenSet(true);
          setAppInitializedFail(true);
          setErrorCode(errorCode);
    
          SplashScreen.hideAsync();
    
          Analytics.logEvent('app_failed_initialization', {
            contentType: 'text',
            itemId: errorCode || 'Error: Unknown',
            method: 'app load'
          });
        }
      }
    
      /**
       * Render the error view
       *
       * @returns {JSX}
       */
      if (appInitializedFail) {
        return (
          <View style={styles.screenView}>
            <Icon color={Colors.redColor} size={40} name="warning-outline" type="ionicon" />
            <Text style={styles.errorText}>There was a problem</Text>
            {errorCode && <Text style={[styles.errorText, styles.errorCode]}>{errorCode}</Text>}
            <Text style={[styles.errorText, styles.errorTextSmall]}>You can try restarting the application, restarting your device and reinstalling the application to resolve this issue.</Text>
            <Text style={[styles.errorText, styles.errorTextSmall]}>If that does not work, please take a screenshot and report this to the developer.</Text>
          </View>
        );
      }
    
      /**
       * Render the successful view
       *
       * @returns {JSX}
       */
      return (
        <View style={styles.screenView}>
          <FlatList
            data={cardsArray}
            keyExtractor={item => item.id}
            showsVerticalScrollIndicator={false}
            renderItem={({ item: card }) => <Card
              card={card}
              hideFavoriteButton={false}
            />}
          />
        </View>
      );
    };
    
    /**
     * Styles
     */
    const styles = StyleSheet.create({
      screenView: {
        ...StyleSheet.absoluteFill,
        flex: 1,
        justifyContent: 'center',
        alignItems: 'center',
        backgroundColor: Colors.darkColor,
      },
      errorText: {
        textAlign: 'center',
        color: Colors.redColor,
        fontSize: 20,
        fontWeight: 'bold',
        marginBottom: 20,
      },
      errorCode: {
        fontSize: 18,
      },
      errorTextSmall: {
        fontSize: 16,
        color: Colors.lightColor,
      }
    });