Deploying React Native to Bitrise, Fabric, CircleCI

Table of Contents

This post is about implementing QR code scanning, fixing building issues, continuous integration with Bitrise, crash reporting with Fabric and how to tame React Native projects into source control. React Native is hard, but dealing with dependencies is harder. Just after a short while I collect tons of issues, most of them are well known issues, but some take a while to figure out. Let’s start with a simple QR scanner feature.

QR code scanner with react-native-camera

After integrating Facebook and Firebase, my app needs QR code scanner feature. A quick search shows several libraries, but all of them seem to be just a wrapper around react-native-camera.

In an ideal world, we can just run npm install react-native-camera --save and react-native link react-native-camera to have a camera feature. We of course need to tweak a bit for each platform. Firstly with permission <uses-permission android:name=”android.permission.CAMERA” /> on Android, and NSCameraUsageDescription on iOS. Then there are some issues. The version I use is react-native-camera 1.2.0 . Future version of the library will hopefully fix the below bugs, but we all have to be ready for other bugs.

Duplicate module name: react-native

Loading dependency graph...(node:15556) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 2): Error: jest-haste-map: @providesModule naming collision:
Duplicate module name: react-native
Paths: /Users/khoa/XcodeProject2/MyApp/node_modules/react-native/package.json collides with /Users/khoa/XcodeProject2/MyApp/ios/Pods/React/package.json
This error is caused by a @providesModule declaration with the same name across two different files.
(node:34363) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

This clearly states that there are 2 module with the same name react-native . Because react-native-camera use CocoaPods to install a project called RNCamera , somehow they brings with a pod React , which is why there is Users/khoa/XcodeProject2/MyApp/ios/Pods/React/package.json , which contains the name of react-native .

A quick fix is to change the name to something else, for example

"name": "react-native-avoid-collision"

Argument list too long

Argument list too long: recursive header expansion failed at /Users/khoa/XcodeProject2/MyApp/node_modules/react-native-camera/ios/../../../ios/build

This is because of recursive search paths in framework search path. If you open RNCamera.xcodeproj at node_modules/react-native-camera/ios/RNCamera.xcodeproj/project.pbxproj and look for Framework search path under Build settings, you can see that it is currently recursive . You should change $(SRCROOT)/../../ios/to non-recursive for both Debug and Release configuration.

The library com.google.android.gms:play-services-basement is being requested by various other libraries

org.gradle.api.GradleException: The library com.google.android.gms:play-services-basement is being requested by various other libraries at [[15.0.1,15.0.1]], but resolves to 12.0.1. Disable the plugin and check your dependencies tree using ./gradlew :app:dependencies.

Because I use Firebase, it has conflict with react-native-camera. Simply because it needs Firebase for face and text detection. In my opinion this is nice to have, but break the single responsibility principle. A camera library should have camera feature only !!!

GMV (Google Mobile Vision) is used for Face detection/Text recognition by the iOS RNCamera. You have to link the google frameworks to your project to successfully compile the RNCamera project.

A quick fix is to specify ext { googlePlayServicesVersion = "15.0.1" } to the project build.gradle

The usage is quite straightforward

render() {
return (
<View style={styles.container}>
<Camera
style={styles.camera}
aspect={Camera.constants.Aspect.fill}
onBarCodeRead={this.onBarCodeRead}
ref={cam => this.camera = cam}
/>
{this.makeOverlayIfAny()}
</View>
)
}

The onBarCodeRead is where we handle callback for QR code detection.

OK, all the issues seem to be solved. Let’s see how it builds on CI.

Running React Native apps on Bitrise

I had good experience with Bitrise for Android and iOS app, and since it also supports React Native, there’s no double in using it. In theory, we can just add add a React Native app and Bitrise can build iOS and Android apps. But in practice, there’s always issues.

Cannot find module ‘jest-haste-map’

My app runs perfectly locally, but fails on Bitrise. I have contacted support but clueless. Since Bitrise supports Workflow with lots of steps, I can choose Run npm command to install missing module

After running npm install jest-haste-map , I get

Cannot find module 'jest-worker'

Then I run npm install jest-worker , I get

Cannot find module 'jest-serializer'

Then I run npm install jest-serializer , I get

Loading dependency graph...(node:74975) UnhandledPromiseRejectionWarning: Error: fseventsunavailable (this watcher can only be used on Darwin)
at new FSEventsWatcher

main.jsbundle does not exist

The other issue I get is

main.jsbundle does not exist. This must be a bug with + echo 'React Native.

This mean there’s an error during bundling, so the main.jsbundle could not be found. Since it runs fine locally, I think I should manually generate bundle and track that to Git, since react-native init by default adds jsbundle in .gitignore . Let’s add the following to package.json , remember that the name field must be all lowercase and not contains spaces.

"build:ios": "react-native bundle --entry-file ./index.js --platform ios --bundle-output ios/main.jsbundle"

It still fails on Bitrise. And with newer version of React, this is not needed, as React Native a bundling script in Xcode Build Phrase that can detect configuration and whether we use index.js or index.ios.js

# Define entry file
if [[ -s "index.ios.js" ]]; then
ENTRY_FILE=${1:-index.ios.js}
else
ENTRY_FILE=${1:-index.js}
fi
if [[ "$CONFIGURATION" = "Debug" && ! "$PLATFORM_NAME" == *simulator ]]; then
IP=$(ipconfig getifaddr en0)
if [ -z "$IP" ]; then
IP=$(ifconfig | grep 'inet ' | grep -v ' 127.' | cut -d\ -f2 | awk 'NR==1{print $1}')
fi
echo "$IP" > "$DEST/ip.txt"
fi
BUNDLE_FILE="$DEST/main.jsbundle"$NODE_BINARY "$CLI_PATH" $BUNDLE_COMMAND \
$CONFIG_ARG \
--entry-file "$ENTRY_FILE" \
--platform ios \
--dev $DEV \
--reset-cache \
--bundle-output "$BUNDLE_FILE" \
--assets-dest "$DEST" \
$EXTRA_PACKAGER_ARGS

For Android, before React Native 0.19.0 we needed to manually run below to generate bundle.

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/

Now we don’t need to manually run React Native bundle command, because there is react.gradle as part of the build step. If you look at the app build.gradle, you will see

The react.gradle file registers a task for each build variant (e.g. bundleDebugJsAndAssets
and bundleReleaseJsAndAssets).
These basically call `react-native bundle` with the correct arguments during the Android build
cycle. By default, bundleDebugJsAndAssets is skipped, as in debug/dev mode we prefer to load the
bundle directly from the development server. Below you can see all the possible configurations
and their defaults. If you decide to add a configuration block, make sure to add it before the
`apply from: “../../node_modules/react-native/react.gradle”` line.

Nothing look suspicious here. I also tried clearing cache, closing debugger, deleting app but it still failed on Bitrise. The thing that it runs perfectly fine locally make me thing there’s mismatch between tooling and environments between local and Bitrise.

Matching tooling versions

Run react-native info to get versions of all the tools we use. Here I have

React Native Environment Info:
System:
OS: macOS High Sierra 10.13.6
CPU: x64 Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz
Memory: 94.97 MB / 16.00 GB
Shell: 5.3 - /bin/zsh
Binaries:
Node: 8.11.4 - ~/.nodenv/versions/8.11.4/bin/node
npm: 5.6.0 - ~/.nodenv/versions/8.11.4/bin/npm
Watchman: 4.9.0 - /usr/local/bin/watchman
SDKs:
iOS SDK:
Platforms: iOS 11.4, macOS 10.13, tvOS 11.4, watchOS 4.3
IDEs:
Android Studio: 3.1 AI-173.4907809
Xcode: 9.4.1/9F2000 - /usr/bin/xcodebuild
npmPackages:
react: 16.4.1 => 16.4.1
react-native: ^0.56.0 => 0.56.0
npmGlobalPackages:
react-native-cli: 2.0.1

So let’s make sure that we have the same node, npm, react-native versions. Luckily Bitrise has custom steps to change

But even with same versions and configurations, it still fails on Bitrise 😕

Trying with a clean git clone

The next thing that I should do is to delete the local repo and clone from remote again. And surprise, I actually get the same errors on Bitrise. It’s time to dig into the problems.

Remember the workarounds I did to make react-native-camera works? To keep those changes, I track node_modules and Pods to git, as I don’t trust the npm system and also I want to see the changes to any of my dependencies. This way the CI can just build my apps without npm install or pod install

The thing is, with this, we have to understand .gitignore extremely well, as it is based on patterns. Some files are tracked, and some files are not, and it can gives room for inconsistence between local and CI. So the logical move is to ignore node_modules and Pods and somehow still keep the workaround changes. Tracking dependencies are useless as when it changes, there’s bunch of changes in git diff and we probably don’t know (and don’t want to) what’s happening.

The lesson that I learn is to never track generated or downloadable stuff, like node_modules, jsbundle, Pods as they are not our concern.

We have package-lock.json and Podfile.lock so we should be on the same versions. The package manager should handle the rest. Yeah, hopefully.

So, how to keep the workaround changes? I see there are several ways

Fork modules

React Native has a bunch of dependencies, and any of them can have bugs, especially during version change, for example this schedule 0.5.0 in React Native 0.57.0 . We can ope issues, wait, pray and hope that someone will come and fix. That is where we become dependants, we have no control over the things that we do. One way is to maintain your our own fork with the changes we need

Here is the proper way to do this while using npm to manage your forked version of the module:

- Fork the project on GitHub

- Clone the fork to your machine

- Fix the bug or add the feature you want

- Push your commits up to your fork on GitHub

- Open your fork on GitHub, and click on the latest commit you made

- On the page of that commit, click on the “Downloads” button

- Right click on the “Download .tar.gz” button inside the popup, and copy the link (“Copy Link Address” in Chrome)

- Open up your package.json file, and replace the version number of the module with the url you just copied

- Send a pull request upstream (Optional, but this way you will avoid having to maintain that patch of yours against newer versions of the module you forked)

Or if we want to say goodbye to the old fashioned forks, we can use patch-package to apply patches

Track inside ignored folders

Another way is to track just the files that we change. The .gitignore file generated by react-native init contains node_modules , which means that React Native team recommend not tracking node modules. But from git 2.8 , we can track files inside ignore folders. Here is how I keep my changes to RNCamera.xcodeproj inside node_modules . Use ! in .gitignore

!node_modules/react-native-camera/ios/RNCamera.xcodeproj

This is fine, but too manual for a lazy programmer. Furthermore, it can be easily overwritten by the next npm install or pod install

Shell scripting

Lazy programmers should automate boring tasks. Did you know that npm can run lot of scripts in many events.

npm supports the “scripts” property of the package.json file, for the following script

- preinstall: Run BEFORE the package is installed

- install, postinstall: Run AFTER the package is installed.

- preuninstall, uninstall: Run BEFORE the package is uninstalled.

- postuninstall: Run AFTER the package is uninstalled.

We can use shell script. For example to change to name in Pods/React/package.json to avoid the issue Duplicate module name: react-native, we can create a file call postinstall.sh , the name is arbitrary

#!/bin/bash
set -e
# Fix Duplicate module name: react-nativePODS_REACT_PACKAGE_JSON = 'ios/Pods/React/package.json'if [ -e PODS_REACT_PACKAGE_JSON ]
then
sed -i -e 's/"name": "react-native"/"name": "react-native-avoid-collision"/g' PODS_REACT_PACKAGE_JSON
fi
# Fix react-native-camera Argument list too longREACT_NATIVE_CAMERA_PROJECT = ./node_modules/react-native-camera/ios/RNCamera.xcodeproj/project.pbxprojif [ -e PODS_REACT_PACKAGE_JSON ]
then
sed -i -e 's/ios\/\*\*/ios\/\*/g' REACT_NATIVE_CAMERA_PROJECT
fi

And add postinstall inside scripts in the root package.json

"postinstall": "./postinstall.sh"

Node scripting

There’s lot of code with shell scripting. We have node environment, let’s utilise that together with lots of packages like fs , so create a new file called postinstall.js . We can create function to encapsulate common functionalities.

const fs = require('fs')function replaceJson(path, process) {
if (fs.existsSync(path)) {
let json = JSON.parse(fs.readFileSync(path, 'utf8'))
json = process(json)
fs.writeFileSync(path, JSON.stringify(json, null, 2))
}
}
function replaceString(path, oldString, newString) {
if (fs.existsSync(path)) {
let string = fs.readFileSync(path, 'utf8')
string = string.replace(oldString, newString)
fs.writeFileSync(path, string, 'utf8')
}
}
function write(path, string) {
fs.writeFileSync(path, string, 'utf8')
}
// Fix Duplicate module name: react-native
PODS_REACT_PACKAGE_JSON='ios/Pods/React/package.json'
replaceJson(PODS_REACT_PACKAGE_JSON, function(json) {
json.name = 'react-native-avoid-collision'
return json
})
// Fix react-native-camera Argument list too long
REACT_NATIVE_CAMERA_PROJECT='./node_modules/react-native-camera/ios/RNCamera.xcodeproj/project.pbxproj'
replaceString(REACT_NATIVE_CAMERA_PROJECT,
'$(SRCROOT)/../../ios/**',
'$(SRCROOT)/../../ios/'
)
replaceString(REACT_NATIVE_CAMERA_PROJECT,
'$(SRCROOT)/../../../ios/**',
'$(SRCROOT)/../../../ios/'
)
// Android sdk
const sdkDir = "sdk.dir = /Users/khoa/Library/Android/sdk"
LOCAL_PROPERTIES = "android/local.properties"
write(LOCAL_PROPERTIES, sdkDir)

Now every time after we run npm install , the workaround we need has been reapplied for us. This helps in other situations like the issue

Error: SDK location not found. Define location with sdk.dir in the local.properties file or with an ANDROID_HOME environment variable.

To solve this, we need to create local.properties with the SDK path. Mine is sdk.dir = /Users/khoa/Library/Android/sdk . This has my username in the path, which is very different than the username vagrant in Bitrise. I can’t seem to use ${HOME} or $HOME variable, so this change should not be tracked into git. You can see above that the script auto generates this file for us. So nifty.

Now everything works fine, and the build works fine too in Bitrise.

Deploy to Fabric with Crashlytics reporting

To be honest, I almost ended up building locally and uploading the IPA/APK to Fabric since I couldn’t find a way to solve building errors on Bitrise. But right now we have to go with Fabric any way, as it has good installation page for testers, and also its Crashlytics reporting is so good. The good (or annoying) thing about Fabric is that we need to successfully build and run an app before it is registered into Fabric.

For iOS, we need to install the Fabric macOS app and follow the instruction. I prefer dragging Fabric and Crashlytics frameworks into our Xcode projects more than using CocoaPods. Further more, if you archive project in Xcode, then you can manually choose archive and upload to Fabric. Simply choose the Archive tab in Fabric app.

For Android, we need to install Fabric for Android Studio. After installing, it is available in View -> Tool Widows -> Fabric

The tool only sets up simple things. We still need to configure gradle, follow the instructions on Install Crashlytics via Gradle

Deploy to CircleCI 2.0

CircleCI has a lot of configurations, and it takes time to learn. Here is a simple configuration to check if the React Native project can build well for both iOS and Android. Create a file called config.yml inside .circleci folder.

version: 2
jobs:
node:
docker:
- image: circleci/node:8
steps:
- checkout
- run: npm install
android:
docker:
- image: circleci/android:api-27-node8-alpha
steps:
- checkout
- run: npm install
- run: cd android && ./gradlew assembleRelease
ios:
macos:
xcode: "9.4.1"
# use a --login shell so our "set Ruby version" command gets picked up for later steps
shell: /bin/bash --login -o pipefail
steps:
- checkout
- run: npm install
- run: xcodebuild -project ios/MyApp.xcodeproj -scheme "MyApp" -sdk iphonesimulator -destination 'platform=iOS Simulator,OS=11.4,name=iPhone X'
workflows:
version: 2
node-android-ios:
jobs:
- node
- android:
requires:
- node
- ios:
requires:
- node

React Native in Xcode 10

For now with React Native 0.57.0 , it does not fully work in Xcode 10 yet. I often get this error error: Multiple commands produce . In that case we need to append -UseModernBuildSystem=N to xcodebuild command. Or in Xcode, we need to use the old legacy build system. Go to File -> Project Setting and set Build System to Legacy Build System

Where to go from here

In this post, we go through implementing QR code scanner feature with react-native-camera , how to debug issues with inconsistent between CI and local environment, how to clone repository to start from fresh, and how to deploy to Bitrise, Fabric and CircleCI. The key lesson for me is to track problem one after another, and never track generated or downloadable content in git, let the package manager handle that. We should also use more scripting to automate boring tasks and utilise node packages in scripting.

Stories and tutorials for developers interested in React Native

Recommended from Medium

Load Local JSON With HttpClient on Stackblitz

https://www.instagram.com/tv/CXmm_nNDkHw/?utm_medium=copy_link

Creating a Bug Report Form in GitHub

10 Tricky Item That Every Web Developer must know…

How to develop and distribute iOS apps without Mac with React Native & Codemagic

https://twitter.com/runner_click/status/821079732506787840

Netlify — Client Side Routing

React Modal Component Implementation

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Khoa Pham

Khoa Pham

Check out my apps https://onmyway133.com/apps

More from Medium

Hybrid Application: Native-like Experience with WebView

WebView

Authenticated requests with Vite + React 18 + Hummingbird Swift API

React-Native Push Notifications with Amazon SNS

Archiving iOS React Native app in an NRWL monorepo: debugging guide