Preparing React Native App for Android Release on Mac

When your React Native Application is ready for release to the Android Platform, you need to perform the following steps to create the .aab file or Android App Bundle which contains your compiled code and resources and you can upload to the Play Store. The APK generation and signing is done by Google Play.

I’m using React Native 0.62.2 for this post in VS Code.

Open Terminal and check the Java Home Path:

/usr/libexec/java_home

cd to the path displayed as shown in the image.

Run the keytool command as shown using sudo to generate the keystore and alias as shown below:

sudo keytool -genkey -v -keystore some-helper-key.keystore -alias some-helper-key-alias -keyalg RSA -keysize 2048 -validity 10000

As shown in the screen-shot, answer the questions that pop and your keystore will be generated under the Java Home folder.

Copy that keystore file and paste it in your Android folder under app in your React Native Project.

Modify the android/gradle.properties file and add:

MYAPP_UPLOAD_STORE_FILE=some-helper-key.keystore
MYAPP_UPLOAD_KEY_ALIAS=some-helper-key-alias
MYAPP_UPLOAD_STORE_PASSWORD=***
MYAPP_UPLOAD_KEY_PASSWORD=***

You might not want to store the passwords directly in the above configuration and store them in the keychain Access App, then the last 2 password configs are not required.

Add the Release signingconfig section to the android/app/build.gradle file:

signingConfigs {
        debug {
            .....
        }
        release {
            if (project.hasProperty('MYAPP_UPLOAD_STORE_FILE')) {
                storeFile file(MYAPP_UPLOAD_STORE_FILE)
                storePassword MYAPP_UPLOAD_STORE_PASSWORD
                keyAlias MYAPP_UPLOAD_KEY_ALIAS
                keyPassword MYAPP_UPLOAD_KEY_PASSWORD
            }
        }
    }

cd to the Android folder and run:

./gradlew bundleRelease

The .aab file is generated as: android/app/build/outputs/bundle/release/app-release.aab

Now this is ready to be uploaded to the Play Store.

Issues Faced:

Resource and asset merger: Duplicate resources

The above error occurred for files under drawable folder, app.json, node_modules_reactnativevectoricons_glyphmaps_materialcommunityicons.json etc.

Go to android/app/src/main/res and delete all the directories starting with drawables and the above mentioned .json files. Do not delete the raw folder if it contains any resources.

If you want to directly test the Release version on your device, run the following command under Root folder of the Project:

react-native run-android --variant=release

You can install bundle tool on Mac:

brew install bundletool

To extract the APK directly from the .aab file, run the following commands:

bundletool build-apks --bundle=/MyApp/my_app.aab --output=/MyApp/my_app.apks

If you want to test it on a device, then also include the App signing information:

bundletool build-apks --bundle=/MyApp/apprelease.aab --output=/MyApp/my_app.apks
--ks=/MyApp/keystore.jks
--ks-pass=pass:***
--ks-key-alias=MyKeyAlias
--key-pass=pass:***

Please note that the .aab file should not contain hyphen else it’ll give file not found error.

If you want to generate a single Universal apks file, run the below command. Copy the .aab and .keystore files to the folder from where you’ll run bundle tool using terminal:

bundletool build-apks --bundle=MyApp.aab --output=MyApp.apks --mode=universal --ks=my-key.keystore --ks-pass=pass:**** --ks-key-alias=my-key-alias --key-pass=pass:****

Change the extension .apks to .zip and extract the universal.apk file. You can rename this file and transfer it to your Android device for installation.

You can also install the .apks directly to your connected device using bundle tool:

bundletool install-apks --apks=MyApp.apks

Create New Team Project TFS 2015

Most of the Admin features are available for TFS 2015 through the Browser Console. These can be managed using the URL:

http://servername:8080/tfs/_admin

If you need to create a new Team Project under an existing Collection, you need to be part of the Project Collection Administrators Group for that collection in TFS 2015.

The URL to access that is :

http://servername:8080/tfs/collectionname/_admin/_security/?_a=members

You can add Windows user or group or TFS group.

To create a New Team Project, go to the following URL:

http://servername:8080/tfs/collectionname/_admin

and click on New Team Project and fill out the required details and click on Create Project.

This will reflect in the Source Control Explorer in Visual Studio where you can then start creating branches.

Multiple Commands produce error React Native with XCode11

I had earlier installed react-native-vector-icons in my Project and used the below command to link it:

react-native link

The “multiple commands produce” error seems is caused by the new autolinking feature in React Native 0.60 or above – the line use_native_modules! in ios/Podfile means when you do pod install, any pods found in node_modules are automatically linked.

So when you do pod install, links to all font files are added to [CP] Copy Pods Resources. If you previously installed react-native-vector-icons manually by adding the font files to Copy Bundle Resources, you then get a fatal build error similar to this:

Build system information
error: Multiple commands produce '/Users/xxx/Library/Developer/Xcode/DerivedData/Project-dcurwlhxaqikupbgxeiimmauwnpw/Build/Products/Debug-iphonesimulator/MyApp.app/SimpleLineIcons.ttf':

To fix the problem, remove the font files from Copy Bundle Resources, so that they are only in [CP] Copy Pods Resources.

In your Xcode, Select your Project and go to Build Phases and remove the fonts shown below from Copy Bundle Resources:

Google AdMob with react-native-firebase

This post is to document the steps on how I integrated Google AdMob in my React Native Application and how the issues were resolved. My React Native version is 0.62.2.

I’m using react-native-firebase to integrate AdMob for displaying Advertisements. The steps to install react-native-firebase may be different for lower versions of react-native where manual install might be preferred for both iOS and Android builds.

Install the following packages:

npm install @react-native-firebase/app
nvm install @react-native-firebase/admob

Within the root of your React Native Project, add firebase.json file:

{
  "react-native": {
    "admob_android_app_id": "ca-app-pub-xxxxxxxx~xxxxxxxx",
    "admob_ios_app_id": "ca-app-pub-xxxxxxxx~xxxxxxxx"
  }
}

Before you build the App, you need to add the valid App Ids for both Android and iOS devices. The App Ids can be found in your Google AdMob account while registering the App for iOS/Android. You can create an account here.

Build the Android version with:

react-native run-android

For the iOS version, you need to run “pod install” first inside the iOS folder to install native modules: RNFBAdMob, RNFBApp, and RNVectorIcons and then run:

react-native run-ios

Even after adding the App Ids, my build was giving error for Android build as Execution failed for task ':app:mergeDexDebug' error.

To resolve this, I had to make changes to my build.gradle file under App Sub Folder under the Android folder.

defaultConfig {
        ....
        multiDexEnabled true
    }

dependencies {
    ...
    implementation 'com.android.support:multidex:1.0.3'
}

If your App is targeting children, make sure to set these configuration using setRequestConfiguration:

import admob, { MaxAdContentRating } from '@react-native-firebase/admob';

admob()
  .setRequestConfiguration({
    // Update all future requests suitable for parental guidance
    maxAdContentRating: MaxAdContentRating.PG,

    // Indicates that you want your content treated as child-directed for purposes of COPPA.
    tagForChildDirectedTreatment: true,

    // Indicates that you want the ad request to be handled in a
    // manner suitable for users under the age of consent.
    tagForUnderAgeOfConsent: true,
  })
  .then(() => {
    // Request config successfully set!
  });

There are different types of Ad Units, I’m using Banner Ads for my case. Also, note that you need to create an Ad Unit in Google AdMob account for the specific type of Ad e.g. Banner Ad and that Ad Unit Id is required to be used in your component. You should use the Test Id in your dev environment.

Below is the example Ad Component:

import React from 'react';
import { Platform, StyleSheet, Text, View } from 'react-native';
import { Colors } from 'react-native/Libraries/NewAppScreen';
import { BannerAd, BannerAdSize, TestIds } from '@react-native-firebase/admob';


const AdComp = (props) => {
    const adUnitId = __DEV__ ? TestIds.BANNER : Platform.OS === 'ios' ? 'ca-app-pub-xxxx/xxxx' : 'ca-app-pub-xxxx/xxxx';

    return(
        <View style={{height: '10%', paddingLeft: 20}}>
            <BannerAd 
            unitId={adUnitId}
            size={BannerAdSize.LARGE_BANNER}
            requestOptions={{
              requestNonPersonalizedAdsOnly: true,
            }} />
        </View>
    );
};

export default AdComp;

You May also face metro server related issues like 500 error for which you might need to restart the metro server which made it work.

If you face the error as:

Unable to load script from assets ‘index.android.bundle’

Follow the below steps:

  1. Go to your project directory and check if this folder exists android/app/src/main/assets

If it exists then delete two files viz index.android.bundle and index.android.bundle.meta

ii) If the folder assets doesn’t exist then create the assets directory there.

2. From your root project directory do

cd android
./gradlew clean

3. Finally, navigate back to the root directory and check

If there is only one file i.e. index.js then run following single command

native bundle --platform android --dev false --entry-file index.js --bundle-output android/app/src/main/assets/index.android.bundle --assets-dest android/app/src/main/res

If there are two files i.e index.android.js and index.ios.js then run this as a single command

react-native bundle --platform android --dev false --entry-file index.android.js --bundle-output android/app/src/main/assets/index.android.bundle --assets-dest android/app/src/main/res

How to give Use permissions for Build queues TFS 2015

If you have access to a Project Collection in TFS 2015 but still not able to queue builds, then you are required to be added to the Project Collection Build Administrators group.

The error message would be as below:

500: Access Denied: <username> needs Use permissions for queue <queuename> to perform this action.


To give access, when you’re in the Source Control Explorer in Visual Studio and connected to the required collection, click on the Team -> Team Project Collection settings -> Group Membership.

Under the Security Tab, add the User or group under the Project Collection Build Administrators group as shown below:

Set focus on tabbing ReactJS

I was working on Material-UI ExpansionPanel and had come across a task where I had to set focus on tabbing through them using keyboard. So when you press space on Focus, the Panel would open/close.

The ExpansionPanel sample implementation would be as below inside render:

return(
	<div>
	  <div className={classes.root}>
		{this.state.panelTypes.map((item) => (
		  <ExpansionPanel key={item.userType} expanded={this.state.expanded === item.userType} onChange={this.handleChange(item.userType)}>
			<ExpansionPanelSummary
			  expandIcon={<ExpandMoreIcon />}
			  aria-controls="panel1bh-content"
			  id="panel1bh-header"
			  style={{backgroundColor: "#e6e6e6", border: '1px solid silver'}}
			  className={classes.heading}
			>
			  <Typography className={classes.heading}>Heading</Typography>
			</ExpansionPanelSummary>
			<ExpansionPanelDetails>
			... display data here...
			</ExpansionPanelDetails>
		  </ExpansionPanel>
		))}
	  </div>
	</div>
  );

The above code is mapping through an array of PanelTypes for creating multiple ExpansionPanels.

The class would require the pseudo-class :focus to be used for tabbing through the Expansion Panel heading:

const styles = (theme) => ({
	//There might be other classes here..
  heading: {
    fontSize: theme.typography.pxToRem(15),
    flexBasis: '33.33%',
    flexShrink: 0,
    fontFamily: "Verdana,Arial,sans-serif",
    '&:focus': {
      outline: "-webkit-focus-ring-color auto 1px"
    }
  }
});

Similarly, other pseudo-classes can be used inside the styles e.g. ::after, ::before etc. as per their own usage.

Material-table example ReactJS

There are a lot of scenarios in Material-table that you might have difficulty in understanding from the documentation or may be scattered on different forums. I’ll try to cover up the scenarios which I faced while implementing a table in my Project with Actions like Add/Edit/Delete. Another scenario is to make it read-only i.e. remove all the Action buttons.

This is the sample courses React App over which I’ll develop further using Material-Table. You can get the code from the Github Project here. To get started, you’ll need the following packages:

npm i @material-ui/core
npm i @material-ui/icons
npm i @material-ui/lab
npm i material-table
npm i axios

The actions are managed through uncontrolled fields i.e. without managing through state directly. The API calls are as per my example and URL placeholder requires to be updated as per your API host name.

The columns and icons are set in the state. autoFocus property is set to the first TextField for edit mode.

The options prop which I’ve used has Paging, sorting and dragging of columns disabled. Sorting and draggable can simply be set to true and used.

To make the table read-only, the editable prop of Material Table can be set to null conditionally. I’ve used isReadOnly boolean to add the condition, you can have your own state here.

render() {
    const { classes } = this.props;
    return (
      <Fragment>
         <div
            id={this.props.id}
          >
            <MaterialTable
              components={{
                EditRow: (props) => {
                  return (
                    <MTableEditRow
                      {...props}
                      onEditingCanceled={(mode, rowData) => {
                        this.canceledClicked();
                        props.onEditingCanceled(mode);
                      }}
                    />
                  );
                },
              }}
              title=""
              columns={this.state.columns}
              data={this.state.StudentsEnrolled}
              icons={this.state.tableIcons}
              isLoading={this.state.showLoading}
              style={{
                border: "2px solid gray",
                maxWidth: "1450px",
                overflow: "scroll",
                marginTop: "10px",
                marginLeft: "20px",
              }}
              editable={
                isReadOnly
                ? null
                : 
                {
                onRowAdd: (newData) =>
                  new Promise((resolve, reject) => {
                    newData.studentName =
                      studentName_GRC === null ? "" : studentName_GRC;
                    
                    newData.grade = grade_GR === null ? "" : grade_GR;

                    newData.courseName = courseName_GR === null ? "" : courseName_GR;

                    newData.comments = comments_GR === null ? "" : comments_GR;

                    var errorMsg = " ";

                    if (newData.studentName === "") {
                      errorMsg = "Please insert Student Name.";
                    }
                    if (newData.grade === "") {
                      errorMsg = errorMsg + "\nPlease insert grade.";
                    }
                    if (newData.courseName === "") {
                      errorMsg = errorMsg + "\nPlease insert course Name.";
                    }

                    if (errorMsg !== " ") {
                      reject();
                      alert(errorMsg);
                    } else if (this.state.countGrid >= GridDataLimit) {
                      reject();
                      alert(
                        "You cannot add more than " +
                          GridDataLimit +
                          " Students to the list."
                      );
                    } else {
                      this.setState({ showLoading: true });
                      this.postStudentsEnrolledData(newData);
                      resolve();
                    }
                    // }, 600);
                  }),
                onRowUpdate: (newData, oldData) =>
                  new Promise((resolve, reject) => {
                    // setTimeout(() => {
                    //resolve();

                    if (oldData) {
                      newData.studentName =
                        studentName_GRC === null
                          ? oldData.studentName
                          : studentName_GRC === ""
                          ? ""
                          : studentName_GRC;

                      newData.grade =
                        grade_GR === null
                          ? oldData.grade
                          : grade_GR === ""
                          ? ""
                          : grade_GR;

                      newData.courseName =
                        courseName_GR === null
                          ? oldData.courseName
                          : courseName_GR === ""
                          ? ""
                          : courseName_GR;

                      newData.comments =
                        comments_GR === null
                          ? oldData.comments
                          : comments_GR === ""
                          ? ""
                          : comments_GR;

                      var errorMsg = " ";
                      if (newData.studentName === "") {
                        errorMsg = "Please insert Student Name.";
                      }
                      if (newData.grade === "") {
                        errorMsg = errorMsg + "\nPlease insert grade.";
                      }
                      if (newData.courseName === "") {
                        errorMsg = errorMsg + "\nPlease insert course Name.";
                      }
                      if (errorMsg !== " ") {
                        reject();
                        alert(errorMsg);
                      } else {
                        this.setState({ showLoading: true });
                        this.postStudentsEnrolledData(newData);
                        resolve();
                      }
                    }
                    // }, 600);
                  }),
                onRowDelete: (oldData) =>
                  new Promise((resolve) => {
                    // setTimeout(() => {
                    this.setState({ showLoading: true });
                    this.deleteStudentsEnrolledData(oldData);
                    resolve();
                    // }, 600);
                  }),
              }}
              options={{
                paging: false,
                sorting: false,
                draggable: false,
                addRowcourseName:"first",
                rowStyle: { backgroundColor: "#fff" },
              }}
            />
          </div>
      </Fragment>
    );
  }

The isLoading prop manages the hide/show of loader when an action is performed.

There is one more scenario where clicking outside the + icon, doesn’t trigger the onAddRowClick event. This is handled in componentDidMount method.

How to align array items in a row React Native

There could be a scenario where you’d want to display the elements in your array in a row spaced evenly.

I’ve imported the following object from another file that contains my Array:

export const keyArrays = {
    myArray : ["A", "B", "C", "D", "E", "F", "G"],
    ....
}

The following code snippet shows how the return method in the main App.js component uses a view. The rowContainer class used in the code below will align the array items in a row inside the view and will wrap the items if going out of View’s width.

The following is the stylesheet snippet used in the App.js component:

const styles = StyleSheet.create({
  rowContainer: {
    flexDirection: 'row',
    flexWrap: 'wrap',
    height: 30,
    paddingTop: 5,
  },
  sectionContainer: {
    marginTop: 32,
    paddingHorizontal: 24,
  },
  sectionTitle: {
    fontSize: 20,
    fontWeight: '600',
    fontFamily: 'AvenirNextCyr-Regular',
    color: Colors.white,
    backgroundColor: '#32879d',
  },
});

Render elements dynamically using map Reactjs

Here I’m using a component that needs to render multiple ExpansionPanel Material UI components based on data fetched in an array. The map is using Arrow function with () for an implicit return.

Below is the sample code for the Component:

import React, { Component } from "react";
import Header from "./Header";
import { withStyles } from '@material-ui/core/styles';
import ExpansionPanel from '@material-ui/core/ExpansionPanel';
import ExpansionPanelDetails from '@material-ui/core/ExpansionPanelDetails';
import ExpansionPanelSummary from '@material-ui/core/ExpansionPanelSummary';
import Typography from '@material-ui/core/Typography';
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
import loadingImg from "../images/LoadingImg_EZ.gif";

const styles = (theme) => ({
  root: {
    width: '100%',
  },
  heading: {
    fontSize: theme.typography.pxToRem(15),
    flexBasis: '33.33%',
    flexShrink: 0,
    fontWeight: 'bold',
  },
  secondaryHeading: {
    fontSize: theme.typography.pxToRem(15),
    color: theme.palette.text.secondary,
  },
  OfflineText: {
    fontSize: "20px",
    color: "red",
  },
});

class MyComponent extends Component {
  constructor(props) {
    super(props);
    this.state = {
      expanded: false,
      panelTypes: null,
      summary: null
    };
  }

  componentWillMount() {
    //get data here and set state.
  }

  handleChange = (panel) => (event, isExpanded) => {
    this.setState({ expanded: isExpanded? panel: false});
  };

  render() {
      const { classes } = this.props;
      
      return(
        <div>
        <Header />
        {this.state.panelTypes !=null ? (
        <>
          <h3 style={{fontSize: "15px", paddingLeft: "4px"}}>Summary</h3>
          <div className={classes.root}>
            {this.state.panelTypes.map((item) => (
              <ExpansionPanel expanded={this.state.expanded === item.types} onChange={this.handleChange(item.types)}>
                <ExpansionPanelSummary
                  expandIcon={<ExpandMoreIcon />}
                  aria-controls="panel1bh-content"
                  id="panel1bh-header"
                  style={{backgroundColor: "#e6e6e6"}}
                >
                  <Typography className={classes.heading}>{this.state.panelTypes !=null ? `${item.types} (Year ${item.year})`:''}</Typography>
                </ExpansionPanelSummary>
                <ExpansionPanelDetails>
                  <Typography>
                    Nulla facilisi. Phasellus sollicitudin nulla et quam mattis feugiat. Aliquam eget
                    maximus est, id dignissim quam.
                  </Typography>
                </ExpansionPanelDetails>
              </ExpansionPanel>
            ))}
          </div>
          </>) : (
          <div id="loading">
            <img id="loading-image" src={loadingImg} alt="Loading..." />
          </div>
        )}
        </div>
      );
  }

}

export default withStyles(styles, { withTheme: true })((MyComponent));
panelTypes will hold the array of items that will be used to create the same number of Expansion Panels using this.state.panelTypes.map() in the render method.

Check Application is Online ReactJS

It is a very common scenario to check whether the user is online while using your Web Application to make it robust. May be you need to just show a message to the user that the Internet connection is disrupted.

There is a very cool react package react-detect-offline available on npm that helps with this. It uses a default polling url to check whether you’re online. You can however change a few settings including the ping URL, enabled boolean and interval for ping duration. You can also change the timeout setting but I’ve not used it here in the polling object.

//say this code is inside constants.js file under common folder..
export const APIUrl = "https://testapi";
export const pingUrl = APIUrl + "/ping";
export const polling = {enabled: true, url: pingUrl, interval: 10000};
export const OfflineText = "You're Offline. Please check your Internet connection.";

In your component, import these objects and use it as below:

import { Offline, Online } from "react-detect-offline";
import { OfflineText, polling } from "../Common/Constants";

const styles = (theme) => ({
  OfflineText: {
    fontSize: "20px",
    color: "red",
  },
});

class MyComponent extends Component {
	render() {
		const { classes } = this.props;
		return (
		  <div id="loading">
            <Online polling={polling}>
              <img id="loading-image" src={loadingImg} alt="Loading..." />
            </Online>
            <Offline polling={polling}>
              <div className={classes.OfflineText}>{OfflineText}</div>
            </Offline>
		  </div>
		)
	}
}

The above example will ping every 10 seconds to check https://testapi for online status. This is called using http HEAD GET request. So please make sure both http verbs are allowed in your API.

For .net core users, the WebAPI should have the [HEAD] attribute set on the action and the method should be allowed in the Startup.cs file and Web.config.

Example of handlers in web.config as below:

<handlers>
            <remove name="ExtensionlessUrlHandler-ISAPI-4.0_64bit" />
            <remove name="ExtensionlessUrlHandler-ISAPI-4.0_32bit" />
            <add name="ExtensionlessUrlHandler-ISAPI-4.0_32bit" path="*." verb="GET,HEAD,POST,DEBUG,PUT,DELETE" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v4.0.30319\aspnet_isapi.dll" resourceType="Unspecified" requireAccess="Script" preCondition="classicMode,runtimeVersionv4.0,bitness32" responseBufferLimit="0" />
            <add name="ExtensionlessUrlHandler-ISAPI-4.0_64bit" path="*." verb="GET,HEAD,POST,DEBUG,PUT,DELETE" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_isapi.dll" resourceType="Unspecified" requireAccess="Script" preCondition="classicMode,runtimeVersionv4.0,bitness64" responseBufferLimit="0" />
        </handlers>