This is not an actual mobile application. This repository contains only React Native (RN) parts of the project. These parts are being used in the original (private) native code. However, you can still run this application to see these parts in the pure React Native.
Table of Contents
- Installation and Run
- Testing
- Building
- Environment
- Project structure
- Native modules
- Best practices
- e2e testing
- Upgrading react-native
Installation and Run
Note: we currently support only macOS. It is however possible to run the Android application under Linux by tweaking the file
android/app/build.gradle
like in this commit.
All necessary information are described in the official React Native documentation. Basically you need to install macOS dependencies:
brew install node watchman yarn
gem install cocoapods
Install Xcode and Android Studio. After that clone this repository and install all the necessary dependencies:
git clone [email protected]:kiwicom/mobile.git
cd mobile
yarn install
cd ios; pod install
And if you have Xcode already installed - just run yarn ios
. It should open iPhone emulator with our application.
It's also possible to open other devices from command line to test tablets for example:
xcrun simctl list devices
yarn ios --simulator="iPad Pro (10.5-inch)"
Similarly for Android (yarn android
) but you have to open Android emulator first.
You need to make sure that your Android emulator image supports Google Play Store. Look for this symbol:
Moreover, make sure you have a valid personal Gitlab token and set it as your environment variable:
export RNKIWIMOBILE_DEPLOYMENT_TOKEN="YOUR_TOKEN_HERE"
also you should add two files android/app/src/main/res/values/secrets.xml
<resources>
<string name="facebook_app_id">Your Facebook App ID Here</string>
<string name="google_maps_key">Your Google maps API Key Here</string>
</resources>
and android/playground/src/main/res/values/secrets.xml
<resources>
<string name="facebook_app_id">Your Facebook App ID Here</string>
<string name="google_maps_key">Your Google maps API Key Here</string>
</resources>
and you have to create the debug.keystore file:
- cd android/app
- keytool -genkeypair -v -keystore debug.keystore -alias androiddebugkey -keyalg RSA -keysize 2048 -validity 1000
-
- You will be asked for a password, use
android
, you will be prompted for another password, just hit enter to use same password.
- You will be asked for a password, use
Testing
You will usually need only this during development:
yarn test
yarn test --watch
You can also run tests without all the verbose messages produced by console.(log|warn|error) as such:
yarn test --silent
It's good idea to run the whole test-set using this command:
yarn test-ci
It basically consists of code linting, type checking, complete testing and GraphQL schema validation. You can find more possibilities by running yarn run
.
Building
We use Fastlane as a tool for building, codesigning and uploading to App Store, Google Play and beta testing environments. If you want to build just a JS bundle files simply run following command:
yarn build
You'll then find output files in the .build
directory (for Android and iOS).
Fastlane installation
You need to do several steps in order to be able to deploy and build this application:
- install Fastlane
- setup environment variables: create
ios/fastline/.env
file with[email protected]
- clone private
react-native-app-certificates
repository with all necessary keys (askTrond Bergquist
orMartin Zlamal
for access) - copy the
kiwi-rn-hotels.keystore
file intoandroid/app
directory - create a new password in the Keychain Access app (macOS) with name
android_keystore
and account namern_hotels
(the password is stored in the Vault - see Environment) - double click the
dist_cert.p12
file (password can be found in the Vault)
Fastlane run instructions
In order to build and deploy this project to the TestFlight just navigate to the ios
folder and run fastlane beta
. Alternatively from root directory:
( cd ios ; fastlane beta )
This new build has to be distributed to the (external) testers. To do so just go to iTunes Connect, select the right application > TestFlight > iOS builds > select build number > Groups (+) > select the group of testers > next, next, next...
On android, navigate to the android folder and run fastlane beta
. Alternatively from root directory:
( cd android ; fastlane beta )
The newly build apk will be found in android/app/build/outputs/apk/app-release.apk
Environment
All sensitive environment variables are stored in .env
file. You should setup these variables if you want 100% of all functions.
Information for Kiwi.com employees: all environment variables are shared using Vault. Ask your colleagues how to get them. You'll first need VPN, secret key (token) and Vault namespace.
Project structure
This project uses Yarn workspaces so the root directory is actually so called "workspace root". Workspace root is responsible for handling this repository (especially testing). You can find all sources inside of the app
directory. Each directory inside app
should be treated as separate NPM package and you should not reuse code from each other via import
statements. If you need to use one package inside of other package (this should be very common use-case) please require it in package.json
file (see app/core/package.json
).
.
├── .circleci/ - CI configuration
├── .github/ - GitHub templates (for PR, issues, contributing)
├── android/ - native code for Android
├── app/
│ │── core/ - core application (not distributed)
│ ├── hotels/ - Hotels application (distributed)
│ ├── MMB/ - Manage My Booking application (distributed)
├── ios/ - native code for iOS
├── packages/ - Yarn workspaces
│ ├── accessibility/ - @kiwicom/mobile-accessibility
│ ├── config/ - @kiwicom/mobile-config
│ ├── navigation/ - @kiwicom/mobile-navigation
│ ├── relay/ - @kiwicom/mobile-relay
│ ├── ...
│ └── shared/ - @kiwicom/mobile-shared
├── scripts/ - support scripts for the whole monorepo
└── schema.graphql - GraphQL schema of the backend server
In case you need additional dependency for the package, you should add it to the package.json
of the workspace (for example app/hotels/package.json
). Root package.json
is only for global dependencies related to the whole monorepo (testing tools, linters and so on).
Native modules
The native developers have prepared some native modules that we can use in our code. They are available through an npm package called @kiwicom/react-native-native-modules
Adding a new native module
- Navigate to
app/shared/package.json
and bump the version to latest version. - Navigate to
ios/Podfile
and add the new package like this:pod 'RNLogging', :path => '../node_modules/@kiwicom/react-native-native-modules'
- Run
yarn pod-install
- Navigate to
android/app/src/main/java/com/reactnativeapp/MainApplication.java
and add the new package to thegetPackages
method,new RNLoggingPackage()
Logging module
It exposes two methods
- ancillaryDisplayed
- ancillaryPurchased
And 4 types:
const Type = {
ANCILLARY_STEP_DETAILS,
ANCILLARY_STEP_PAYMENT,
ANCILLARY_STEP_RESULTS,
ANCILLARY_STEP_SEARCH_FORM,
};
Usage:
import { Logger } from '@kiwicom/mobile-shared';
Logger.ancillaryDisplayed(
Logger.type.ANCILLARY_STEP_DETAILS,
Logger.Provider.ANCILLARY_PROVIDER_BOOKINGCOM
);
Logger.ancillaryPurchased(
Logger.type.ANCILLARY_STEP_RESULTS,
Logger.Provider.ANCILLARY_PROVIDER_BOOKINGCOM
);
Translation module
It exposes one method
- translate
Usage:
import { Translate } from '@kiwicom/mobile-shared';
const someString = Translate('translation.key.to.translate');
Currency module
It exposes one method
- formatAmount
Usage:
import { CurrencyFormatter } from '@kiwicom/mobile-shared';
const priceInEuros = 500.34;
const currencyCode = 'NOK';
const priceInNOK = CurrencyFormatter(priceInEuros, currencyCode);
Note that in development, it uses a "dummy" currency formatter, so it is possible that some decimals appear as 12.99999999999999
. In production, however, the native module picks the formatter provided by the native host. (See the iOS code here for example)
Best practices
Accessing arbitrarily nested, possibly nullable properties on a JavaScript object
Sometimes (especially in GraphQL environment with nullable results) it's necessary to access deeply nested objects in order to get the value. However the path may contain nullable fields and therefore it's necessary to do checks like this:
props.user &&
props.user.friends &&
props.user.friends[0] &&
props.user.friends[0].friends
But that's not very friendly and this is why we have optional chaining
. You can use it like this:
const friends = props.user?.friends?.[0]?.friends;
Do not use _.get(...)
from Lodash!
Error handling
Error handling is complicated in general - especially in GraphQL environment. There are several scenarios that may occur:
- GraphQL API returns
data
field and noerrors
This should be considered as a valid full response and there should not be any errors. There may be nullable fields, however.
- GraphQL API returns
data = null
anderrors
field
This is fatal error. Server was not able to get data and it's probably not operating correctly. It's like equivalent of total GraphQL server error (500). We should display full page error (GeneralError
component).
- GraphQL API returns
data
but alsoerrors
field
Most common scenario (somewhere between). In this case we are able to fetch at least something but it failed partially so there are errors and we can expect some nullable fields. This may be just missing prices but also completely missing data. It's very different to point 2.
We are showing little warning in this case. How to handle nullable fields really depends on the situation. Sometimes it's OK to leave it empty instead of for example hotel rating (★★★), sometimes it's necessary to display error message or sad picture in case of completely missing hotels. It depends. We are always trying to render as much as possible.
Working with Playground
There is so called Playground for easier development. It's our custom WIP replacement for Storybook. The idea is to write regular component tests with ability to see them in the Playground. Therefore you need to write only the tests and you don't have to maintain additional stories. Example of simple test:
it('Works!', () => {
PlaygroundRenderer.render(<AdaptableBadge text="default badge" />);
PlaygroundRenderer.render(<AdaptableBadge text="badge with color" color="red" />);
});
The PlaygroundRenderer
allows you to see the tests in the Playground and it automatically creates shallow snapshots of the component.
To start the Playground, you need to switch which App
is imported in the index.js
file at the root of this repository. Comment the line importing the file app/App
and uncomment the following line, which imports packages/playground/src/Navigation
. That means you should now see
//import App from './app/App';
import App from './packages/playground/src/Navigation';
in index.js
. Start the app like you would normally do, i.e. by running yarn ios
, and you should see the following screen:
Working with GraphQL API
This application uses GraphQL API as a data source. You can find GraphQL schema in schema.graphql
file. This schema is used by Relay Compiler and Relay Babel plugin to generate and validate queries for backend server. GraphQL API itself is evolving over time so you may need to update our snapshot. You can do it easily using this command:
yarn graphql
Additional useful tools:
- https://kiwi-graphiql.now.sh/ (introspection and docs)
- https://kiwi-graphql-voyager.now.sh/ (graphical visualisation)
Working with translations
Current implementation is little bit dodgy because we have to use native code (requirement from native team). The underlying implementation is basically this:
- (NSString *)translate:(NSString *)key {
return key;
}
So it returns key back. However, this happens only in development environment and it should return real translation in production (we cannot test or use it in development). For this reason we have custom fallback vocabulary and we touch this repository if underlying code returns unchanged key.
You always have to use the following component:
<Translation id="Core.Authentication.Login" />
These components should be enforced everywhere we need to use translations (button titles, children of the Text
component). However, there are situations where we need to use the translation (because of the enforcement) but there is nothing to translate. In this case just use the passThrough
property instead of id
:
<Translation passThrough="★★★★★" />
It comply with the translations interface but it returns the property value directly back without even trying to translate it.
There are also situations where we need to return multiple translations but this is little bit more tricky because it's not possible to nest (or concat) multiple translations. You can use TranslationFragment
component for this:
<TranslationFragment>
<Translation passThrough="★★★★★" />
<Translation id="SingleHotel.Rating.Stars" />
</TranslationFragment>
This fragment also comply with Flow types and it has similar behaviour with React.Fragment
from React 16.2+...
PhraseApp
We use PhraseApp for managing translations. All keys and relevant screenshots are deployed automatically (the deployment script must be executed manually though):
yarn deploy-translations
PHRASE_APP_PROJECT_ID
and PHRASE_APP_TOKEN
should be added to ~/.bash_profile
(we need to improve it - this is just PoC)
How to create screenshots? Open iOS simulator and press Ctrl+S
(save). It will save the screenshot of current page so you can work with it later. It's usually good idea to mark where is the translated string on the screen. You should convert your new screenshots to JPG to make them smaller before commiting them to the Git (macOS):
mkdir jpegs
sips --resampleHeightWidthMax 640 --setProperty format jpeg ./*.png --out jpegs
Upgrading dependencies
Check all dependencies with outdated
Yarn command. This will tell you how behind we actually are. Try not to upgrade everything at once since there may be breaking changes (even though it's non-breaking upgrade). The second command will help you significantly with upgrading these dependencies across our workspace repository:
yarn outdated
yarn upgrade-interactive --latest
E2e testing
We use detox for e2e testing.
Note: One of our tests imageGallery.spec.js
might fail depending on device. See comment in that file.
Android
Detox is currently stable for android on RN-versions <=0.56. It runs on android, but there might be some quirks.
To run, first create an emulator in Android Studio with the name e2e_emulator
. Then you can run yarn e2e-aphone
.
Because we have an absolutely positioned close button on the bottom of the detail view, android will click
this button when trying to scroll. You might want to comment line 34 in BookingSummary.js
ios
ios should work just fine. Just do e2e-iphone
or e2e-ipad
to run the tests
Upgrade react-native
Since we upload our dependencies to maven, and these depends on the current version of react-native, there are no maven packages whenever we upgrade react-native version. In order to test it locally, you need to replace all occurrences of packageFromUnpkg(it, "react-native", reactNativeVersion)
with packageFromNodeModules(it, "react-native")
and you need to comment include ':rnkiwimobile'
and include ':playground'
in settings.gradle.
Remember to undo this changes before commiting. This is just to be able to test after upgrade