Fixing React Native issues and happy deploy to Bitrise, Fabric, CircleCI
Fixing React Native issues and happy deploy to Bitrise, Fabric, CircleCI
![](https://cdn-images-1.medium.com/freeze/max/60/0*7vKg_mdUkSLfvPVz.jpg?q=20)
![](https://cdn-images-1.medium.com/max/1600/0*7vKg_mdUkSLfvPVz.jpg)
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
![](https://cdn-images-1.medium.com/freeze/max/60/1*8hINdX6Zsomwy8u3e6QrQg.png?q=20)
![](https://cdn-images-1.medium.com/max/1600/1*8hINdX6Zsomwy8u3e6QrQg.png)
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
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” />
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.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: 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.
![](https://cdn-images-1.medium.com/freeze/max/60/1*KFKLuzMYTibCkehW6vAsWA.png?q=20)
![](https://cdn-images-1.medium.com/max/1600/1*KFKLuzMYTibCkehW6vAsWA.png)
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.
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
![](https://cdn-images-1.medium.com/freeze/max/60/1*sdO5TWK3dStoeDRTFz6ZnQ.png?q=20)
![](https://cdn-images-1.medium.com/max/1600/1*sdO5TWK3dStoeDRTFz6ZnQ.png)
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: fsevents
unavailable (this watcher can only be used on Darwin)at new FSEventsWatcher
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 fileif [[ -s "index.ios.js" ]]; then ENTRY_FILE=${1:-index.ios.js}else ENTRY_FILE=${1:-index.js}fi
![](https://cdn-images-1.medium.com/freeze/max/60/1*FP3yKJ4h1AurMGNfwICo5w.png?q=20)
![](https://cdn-images-1.medium.com/max/1600/1*FP3yKJ4h1AurMGNfwICo5w.png)
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. bundleDebugJsAndAssetsand bundleReleaseJsAndAssets).These basically call `react-native bundle` with the correct arguments during the Android buildcycle. By default, bundleDebugJsAndAssets is skipped, as in debug/dev mode we prefer to load thebundle directly from the development server. Below you can see all the possible configurationsand 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
![](https://cdn-images-1.medium.com/freeze/max/60/1*Kh-FrpeUh5UnH-H6vG8Avg.png?q=20)
![](https://cdn-images-1.medium.com/max/1600/1*Kh-FrpeUh5UnH-H6vG8Avg.png)
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.
![](https://cdn-images-1.medium.com/freeze/max/60/0*Y_2csNNYoXnezSgI.png?q=20)
![](https://cdn-images-1.medium.com/max/1600/0*Y_2csNNYoXnezSgI.png)
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/bashset -e
# Fix Duplicate module name: react-native
PODS_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_JSONfi
# Fix react-native-camera Argument list too long
REACT_NATIVE_CAMERA_PROJECT = ./node_modules/react-native-camera/ios/RNCamera.xcodeproj/project.pbxproj
if [ -e PODS_REACT_PACKAGE_JSON ]then sed -i -e 's/ios\/\*\*/ios\/\*/g' REACT_NATIVE_CAMERA_PROJECTfi
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-nativePODS_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 longREACT_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 sdkconst 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 withsdk.dir
in the local.properties file or with anANDROID_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.
![](https://cdn-images-1.medium.com/freeze/max/60/1*jYjFvs1hCaAQUjTH6eIzpQ.png?q=20)
![](https://cdn-images-1.medium.com/max/1600/1*jYjFvs1hCaAQUjTH6eIzpQ.png)
For Android, we need to install Fabric for Android Studio. After installing, it is available in View -> Tool Widows -> Fabric
![](https://cdn-images-1.medium.com/freeze/max/60/1*3eP-zzqkK2VM6731n2404Q.png?q=20)
![](https://cdn-images-1.medium.com/max/1600/1*3eP-zzqkK2VM6731n2404Q.png)
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: 2jobs: 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
![](https://cdn-images-1.medium.com/freeze/max/60/1*n_Ydz7oLbwieJb5Ko-msRw.png?q=20)
![](https://cdn-images-1.medium.com/max/1600/1*n_Ydz7oLbwieJb5Ko-msRw.png)
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.