Search code examples
react-nativeexpodotenv

react-native expo prebuild command with node_env


Background Info

We are not using eas or expo go, instead we are using our own provisioning profiles and a combination of app.json and babel.config.js.

We want to transition from dev environment to qa/staging environments. We are using azure pipelines to run expo's prebuild command for ios and android builds. For example: npx expo prebuild --platform ios --clean --npm

I was successful in setting up the dev environment to run with the following command... npx expo run dev-android (which points to the following script in package.json "dev-android": "cross-env NODE_ENV=development npx expo run:android --port 8082"

The following successfully prints out in the development environment...

enter image description here

The Structure

enter image description here

The Code...

App.tsx

import React, { useEffect } from 'react';
import * as SplashScreen from 'expo-splash-screen';
import '@app/utils/IgnoreWarnings';
import Toast from 'react-native-toast-message';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { GlobalContextProvider, useLoadingStore } from '@app/stores';
import { ToastInit } from '@app/components';
import { NavigationConductor } from '@app/navigation';
import { toastFailedProps } from '@app/utils';
import { RANDOM_ENV_DEV, RANDOM_ENV_QA } from '@env';
import type { LoadingStore } from '@app/types';

// Keep the splash screen visible while we fetch resources
SplashScreen.preventAutoHideAsync();

export default function App() {
  // variables
  const { loadInitialAppData } = useLoadingStore((store: LoadingStore) => store);

  // setup
  useEffect(() => {
    console.log('NODE_ENV:', process.env.NODE_ENV);
    console.log('RANDOM_ENV_DEV:', RANDOM_ENV_DEV);
    console.log('RANDOM_ENV_QA:', RANDOM_ENV_QA);
    const loadApp = async () => {
      try {
        await loadInitialAppData();
      } catch (error: any) {
        Toast.show(toastFailedProps(error.message));
      } finally {
        await SplashScreen.hideAsync();
      }
    };
    loadApp();
  }, [loadInitialAppData]);

  // render
  return (
    <GlobalContextProvider>
      <SafeAreaProvider>
        <NavigationConductor />
        <ToastInit />
      </SafeAreaProvider>
    </GlobalContextProvider>
  );
}

.env

RANDOM_ENV_DEV=HelloWorldDev
RANDOM_ENV_QA=HelloWorldQa

babel.config.js

module.exports = function (api) {
  api.cache(true);
  return {
    presets: ['module:metro-react-native-babel-preset', 'babel-preset-expo'],
    plugins: [
      [
        require.resolve('babel-plugin-module-resolver'),
        {
          extensions: ['.js', '.jsx', '.ts', '.tsx', '.android.js', '.android.tsx', '.ios.js', '.ios.tsx'],
          alias: {
            '@': './',
            '@app': './app'
          }
        }
      ],
      [
        'module:react-native-dotenv',
        {
          envName: 'APP_ENV',
          moduleName: '@env',
          path: '.env',
          blocklist: null,
          allowlist: null,
          blacklist: null, // DEPRECATED
          whitelist: null, // DEPRECATED
          safe: false,
          allowUndefined: true,
          verbose: false
        }
      ]
    ]
  };
};

app.json (config file)

{
  "expo": {
    "name": "PROJECTNAME",
    "slug": "PROJECTSLUG",
    "version": "1.0.0",
    "scheme": "msauth",
    "orientation": "portrait",
    "icon": "./assets/images/qa-icon-1024.png",
    "userInterfaceStyle": "light",
    "backgroundColor": "#001689",
    "splash": {
      "image": "./assets/images/qa-splash.png",
      "resizeMode": "contain",
      "backgroundColor": "#001689"
    },
    "assetBundlePatterns": ["**/*"],
    "ios": {
      "supportsTablet": true,
      "bundleIdentifier": "com.example.qa",
      "buildNumber": "1.0.0"
    },
    "android": {
      "adaptiveIcon": {
        "foregroundImage": "./assets/images/qa-icon-1024.png",
        "backgroundColor": "#001689"
      },
      "package": "com.example.qa",
      "versionCode": 1
    },
    "web": {
      "favicon": "./assets/favicon.png"
    },
    "plugins": [
      [
        "expo-build-properties",
        {
          "android": {
            "minSdkVersion": 23
          }
        }
      ],
      [
        "expo-image-picker",
        {
          "photosPermission": "Allow $(PRODUCT_NAME) to access your photos",
          "cameraPermissions": "Allow $(PRODUCT_NAME) to access your camera"
        }
      ]
    ]
  }
}

package.json

{
  "name": "Project-Name",
  "version": "1.0.0",
  "main": "node_modules/expo/AppEntry.js",
  "scripts": {
    "ts:check": "tsc",
    "dev-android": "cross-env NODE_ENV=development npx expo run:android --port 8082",
    "dev-ios": "cross-env NODE_ENV=development npx expo run:ios --port 8082",
    "android": "expo run:android",
    "ios": "expo run:ios"
  },
  "dependencies": {
    "@expo/vector-icons": "^13.0.0",
    "@react-native-async-storage/async-storage": "1.18.2",
    "@react-native-community/netinfo": "9.3.10",
    "@react-navigation/native": "^6.1.9",
    "@react-navigation/stack": "^6.3.20",
    "axios": "^1.6.2",
    "core-js": "^3.35.0",
    "expo": "~49.0.15",
    "expo-auth-session": "~5.0.2",
    "expo-build-properties": "^0.8.3",
    "expo-constants": "~14.4.2",
    "expo-file-system": "~15.4.5",
    "expo-image-manipulator": "~11.3.0",
    "expo-image-picker": "~14.3.2",
    "expo-secure-store": "~12.3.1",
    "expo-splash-screen": "~0.20.5",
    "expo-status-bar": "~1.6.0",
    "expo-system-ui": "~2.4.0",
    "expo-web-browser": "~12.3.2",
    "formik": "^2.4.5",
    "jwt-decode": "^4.0.0",
    "lodash": "^4.17.21",
    "react": "18.2.0",
    "react-native": "0.72.10",
    "react-native-device-info": "^11.1.0",
    "react-native-element-dropdown": "^2.10.1",
    "react-native-gesture-handler": "~2.12.0",
    "react-native-get-random-values": "~1.9.0",
    "react-native-safe-area-context": "4.6.3",
    "react-native-screens": "~3.22.0",
    "react-native-toast-message": "^2.1.9",
    "react-query": "^3.39.3",
    "scandit-react-native-datacapture-barcode": "^6.21.3",
    "scandit-react-native-datacapture-core": "^6.21.3",
    "uri-scheme": "^1.1.0",
    "uuid": "^9.0.1",
    "yup": "^1.3.2",
    "zustand": "^4.4.7"
  },
  "devDependencies": {
    "@babel/core": "^7.20.0",
    "@types/lodash": "^4.14.202",
    "@types/react": "~18.2.14",
    "@types/uuid": "^9.0.7",
    "cross-env": "^7.0.3",
    "react-native-dotenv": "^3.4.10",
    "typescript": "^5.1.3"
  },
  "private": true
}

Env.ts

declare module '@env' {
  export const RANDOM_ENV_DEV: string;
  export const RANDOM_ENV_QA: string;
}
declare var process: {
  env: {
    NODE_ENV: string;
  };
};

Results

This is working in the development environment where process.env.NODE_ENV reads as development.

The Question

Now how do we make it where when we use expo prebuild command to package a build that would include process.env.NODE_ENV will read as uat or qa?


Solution

  • The only solution I could find for this was still a bit hacky, but at least it is in one place.

    babel.config.js

    change to...

    module.exports = function (api) {
      api.cache(false);
      return {
        ...,
        plugins: [
          ...
          [
            ...
            {
              ...
              path: '.env.dev', // .env.qa | .env.uat | ect...
              ...
            }
          ]
        ]
      };
    };
    

    Just avoid doing any rebase between the branches and when you upload the changes per branch in the right order then future merges will not overwrite this value.

    It's still not an ideal solution, but at least the value is changed in only one location.