React Native CodePush

CodePush is a magical service that lets you update your app on the fly! It means that when you find a bug in your app you can push the code instantly to your users and update their apps / release new features.

Why should I care?

With Native apps you have to go through app review process every time you release a new version. Sometimes its fast, sometimes its not. But with CodePush you can update JS part of the app bypassing the review!

Me wants!

Setup is actually quite easy and documentation pretty good. You  need to:

  1. Install CLI tools
  2. Register your app
  3. Install CodePush in your app
    npm install --save react-native-code-push
    react-native link react-native-code-push
    
  4. Wrap your top-level component in a codepush wrapper
    import codePush from "react-native-code-push";
    class TeamParrot extends Component {
    ...
    }
    export default codePush( TeamParrot );
    
  5. Add deployment keys to your project
    1. iOS: add to info.plist:
      
      CodePushDeploymentKey
      your key
  6. To update and rollback your app:
    code-push release-react TeamParrot-ios ios
    code-push promote TeamParrot-ios Staging Production
    code-push rollback TeamParrot-ios Production
    
  7. Profit

React Native Isomorphic app over the weekend

This article is part of my “React Native Isomorphic app over the weekend” series. It shares the problems I encountered during development of TeamParrot and Headstart Journal.

React Native: Rename my app

You started building your app before you knew what it was, didn’t you? Well, that is certainly what I did.

My app started as “SlackBird”, then I realized that i may be violating some trademark there ( hint: probably not “bird” ), so I went and changed the name to “TLDR“. But after that I realized my addiction to Party Parrot so “TeamParrot” it is!

So what files should I actually change?

IOS

  • When you go to project details in Xcode, you can edit name. When you edit it, Xcode “Rename files” tool opens.Screen Shot 2016-10-19 at 22.37.32 PM.png
  • In ios/PROJECT/info.plist change CFBundleURLName
  • In ios/PROJECT.xcodeproj/xcshareddata/xcschemes/PROJECT.xcschemechangeBuildableName`
  • Rename file ios/PROJECT.xcodeproj/xcshareddata/xcschemes/PROJECT.xcscheme itself to NEWPROJECT.xcsccheme

Android

I just grepped for files containing my old name “TLDR” in this instance. These variables needed to be changed:

  • package in android/app/BUCK
  • applicationId in android/app/build.gradle
  • package in android/app/src/main/AndroidManifest.xml
  • return value of getMainComponentName in android/app/src/main/java/com/slackbird/MainActivity.java . That is the name of your main component in JavaScript portion.
  • package in android/app/src/main/java/com/PROJECT/MainApplication.java
  • Application name in android/app/src/main/res/values/strings.xml

React part

Now you renamed your min entry point to your application. So don’t forget to rename your main component name:

AppRegistry.registerComponent( 'TeamParrot', () => Login );

If you renamed everything, some directories will still have old name of your app. I decided I’m fine with that.

React Native Isomorphic app over the weekend

This article is part of my “React Native Isomorphic app over the weekend” series. It shares the problems I encountered during development of TeamParrot and Headstart Journal.

React Native: Vector Icons – FontAwesome and others

You want some nice icons in your app, don’t you? In my laziness I tried using emojis instead, but it turned out to be sub-par.

So I conceded and installed an icon package called

React Native Vector Icons

npm install react-native-vector-icons --save to install

How this works

This package provides fonts and some wrappers. Since SVG is still not working 100% on RN, introducing vector icons via font seems like a reasonable approach. In mobile apps you want to bundle the font with the app itself.

IOS

Theoretically, you should be able to rnpm link. Unfortunately, that didn’t work for me, so I had to install font manually:

  • Drag “Fonts” folder from node_modules/react-native-vector-icons to your project
  • You have to add fonts in info.plist This is how I added FontAwesome only:

UIAppFonts : Fonts/FontAwesome.ttf

ANDROID

In android/app/build.gradle

project.ext.vectoricons = [
    iconFontNames: [ 'FontAwesome.ttf' ] // Name of the font files you want to copy
]

apply from: "../../node_modules/react-native-vector-icons/fonts.gradle"

In android/settings.gradle :

include ':react-native-vector-icons'

project(':react-native-vector-icons').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-vector-icons/android')

Using

Now you want to plaster icons all over your application, don’t you? I do too!

If you are using Font Awesome like me, you would go to http://fontawesome.io/icons/ and pick your icon. You need the name without “fa-” at the beginning, and then you drop icon in your project like so:

import Icon from 'react-native-vector-icons/FontAwesome';

WEB

Because my project is isomorphic, this caused some problems in the web version. react-native-web is not yet compatible with react-native-vector-icons. To make the project even compile on the web, I made 3 files that abstracted the icon enough:

icon.web.js

import React from 'react';
import { Text } from 'react-native';
export default Text;

index.android.js and index.ios.js

import Icon from 'react-native-vector-icons/FontAwesome';
export default Icon;

Because isomorphism works in react native by picking specific files, my project now builds under all 3 environments. Granted, I would prefer it to build for web with working icons, which it does not do currently, but thats a battle for another day.

React Native Isomorphic app over the weekend

This article is part of my “React Native Isomorphic app over the weekend” series. It shares the problems I encountered during development of TeamParrot and Headstart Journal.

React Native Web & Isomorphic magic

One of the most appealing qualities of React Native is the idea of isomorphic code. Code, that can run on both iOS and Android, enabling you to write 2 apps at the same time.

But, there is another benefit to isomorphic code. You can also speed up writing your code just for one app.

Since react Native is using React code, I initially wanted to develop parts of the application “the React way”, using hot reloading, React Devtools and the fast feedback cycle that web development permits. That would let me work on the app without emulators and building the code.

It’s possible and much more.

React-Native-Web

‘react-native-web’ builts your react native app as a web app. Not pieces and snippets, but the whole app. Granted, there is a lot of functionality missing, so it’s almost certain that you will have to modify your source a bit to even make the build work, but there is one use-case that it’s perfect for:

UI components.

In TeamParrot I have separated all UI components as “dumb components” without much logic nor dependencies. That way, I can develop UI separately from the whole app. I have made specific container-component for web that holds all of these dumb components and allows me to see all of them in one place, iterate and work on in my web browser.

Let’s do this!

If you want this also for your app, here is how I’ve done it for TeamParrot:

React-Native-Web-Starter

I used react native web boilerplate called react-native-web-starter to get started. I downloaded the latest master, dumped the files into root of TeamParrot project and… Build failed. Now web, ios, and android build stopped working.

Now, let’s fix it!

First need proper libraries in your package.json devDependencies:

"react-native-web": "^0.0.39",
"babel-core": "6.9.1",
"babel-loader": "6.2.4",
"babel-plugin-transform-decorators-legacy": "1.3.4",
"babel-preset-es2015": "6.9.0",
"babel-preset-react": "6.5.0",
"babel-preset-stage-1": "6.5.0",
"babel-runtime": "6.9.2",
"webpack": "1.13.1",
"webpack-dev-server": "1.14.1"

You need to remove .babelrc provided by react-native-web-starter, since that will interfere with RN babel build process. But the configuration from .babelrc needs to end up somewhere, so you need to modify your package.json loader to include:

{
    test: /\.js$/,
    exclude: /node_modules/,
    loader: 'babel-loader',
    query: {
        cacheDirectory: true,
        "presets": [
            "es2015",
            "stage-1",
            "react"
        ],
        "plugins": [
            "transform-decorators-legacy"
        ]
    },
}

Everything seems ok at this moment, but the app still does not want to load. After debugging for a while it turned out that these components didnt want to play nice with React Native Web:

  • Switch used for toggles
  • Icon from `react-native-vector-icons`
  • RefreshControl used for “Pull to refresh” on iOS.

I decided on a n ugly solution that I would just render “Text” instead of Icon and not render Switch or RefreshControl at all by passing “webDevelopment” prop:


export default function MainView( { queue, playing, channels, stop, pause, play, refresh, loading, header, openDrawer, webDevelopment = false } ) {
	const msg = get( queue, [ 0, 'items', 0 ] );
	const scrollProps = { style: { flex: 4 } };
	if ( ! webDevelopment ) {
		//When building this component inside react-native-web, RefreshControl does not work correctly
		scrollProps[ 'refreshControl' ] = ;
	}

The last problem I needed to solve was loading images. React Native has a an Image component, to which you pass local images by require prop:

To make this work on web, with webpack, you need to install file-loader:

npm install –save-dev file-loader

Webpackconfig

it’s located under /web/webpack.config.dev.js :


const path = require('path')
const webpack = require('webpack')

const DIRECTORY = path.join(__dirname)

module.exports = {
    devServer: {
        contentBase: path.join(__dirname, 'src')
    },
    entry: [
        path.join(__dirname, '../index.web.js')
    ],
    module: {
        loaders: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                loader: 'babel-loader',
                query: {
                    cacheDirectory: true,
                    "presets": [
                        "es2015",
                        "stage-1",
                        "react"
                    ],
                    "plugins": [
                        "transform-decorators-legacy"
                    ]
                }
            },
            {
                test: /\.(png|jpg|gif)$/,
                loader: "file-loader?name=img/img-[hash:6].[ext]"
            }
        ]
    },
    output: {
        filename: 'bundle.js'
    },
    plugins: [
        new webpack.DefinePlugin({
            'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development')
        }),
        new webpack.optimize.DedupePlugin(),
        new webpack.optimize.OccurenceOrderPlugin()
    ],
    resolve: {
        alias: {
            'react-native': 'react-native-web'
        }
    }
}

Actual web code

This is my index.web.js:


import { AppRegistry } from 'react-native'; import Screens from './web/screens';

AppRegistry.registerComponent('App', () => Screens); AppRegistry.runApplication('App', { rootTag: document.getElementById('react-root') } );

Let’s roll!

And this is how I run my webpack dev server:

webpack-dev-server --port 3000 --config web/webpack.config.dev.js --inline --hot --colors --quiet

Zrzut ekranu 2016-11-12 o 19.37.20.png
Beauty of developing React Native app in devtools

Try it! It really speeds up the whole process!

React Native Isomorphic app over the weekend

This article is part of my “React Native Isomorphic app over the weekend” series. It shares the problems I encountered during development of TeamParrot and Headstart Journal.

React Native: Oauth

If you want to use any self-respecting API on the web, you are going to need Oauth authentication. If you need an explanation what oauth is, there are plenty better ones. These are the steps your app needs to go through:

  • Send user to special URL in the browser that lets them log in to the service
  • Handle callback URL from this service
  • Exchange keys with the service to make sure callback was legit
  • Store the token.

Since I used Slack API from the slack npm package, I had the “Exchanging the keys” parts ready.

0. Preparing the app

You thought we were going to start from 1, didn’t you? Well, you you must be new to this “programming” stuff. 😃  On the more serious note, you need to register your app in the API provider to get special keys for the step 1 and fill in URL for the step 2.

For TeamParrot, I set up following OAuth redirect URL:

Zrzut ekranu 2016-10-30 o 16.29.31.png

1.Send a user to the special URL

You will need to use “Linking” library. DO NOT confuse Library called “Linking” with the process of “Linking libraries”, which is tying native libraries to JS code. I just send my users to 'https://slack.com/oauth/authorize?client_id=' + client_id + '&scope=channels:read%20channels:history%20users:read%20channels:write' and that opens web browser with appropriate “back” button.

2. Handling callback URL

If all goes good and your users give you appropriate permissions, they will be sent to the URL you specified in step 0. You may notice that this step has a weird protocol (team-parrot://). It actually can have any protocol you want that’s not used by an other app. This is enabled by “Deep linking” and “Intents” On android.

iOS

First, you need to configure your app to handle this specific link you want to capture. That tells the operating system to route those links to your beautiful and shiny app. As per usual, you have to do that in info.plist :

<string>com.piszek.teamparrot</string>
<key>CFBundleURLSchemes</key>
<array>
<string>team-parrot</string>
</array>

For a reason I completely do not understand, you also have to mess with a bit of Native code. More info in the docs

So, in file ios/SlackBird/AppDelegate.m :

@implementation AppDelegate
+- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation {
+  return [RCTLinkingManager application:application openURL:url sourceApplication:sourceApplication annotation:annotation];
+}

Android

Deep linking is called “intents” in Android and much of the same concept applies. You have to tell the OS that you want to open certain protocol inside your app. So:

In android/app/src/main/AndroidManifest.xml

<activity android:name=".MainActivity"
+        android:launchMode="singleTask"
...
>
+        <intent-filter>
+          <action android:name="android.intent.action.VIEW" />
+          <category android:name="android.intent.category.DEFAULT" />
+          <category android:name="android.intent.category.BROWSABLE" />
+          <data android:scheme="team-parrot"/>
+      </intent-filter>

OAuth walkthrough

Now you are ready to become a Oauth Sith and step to the dark side. This is how you oauth:

Linking.openURL( 'https://slack.com/oauth/authorize?client_id=' + client_id + '&scope=channels:read%20channels:history%20users:read%20channels:write' );
Linking.addEventListener( 'url', handleUrl );

And now your function “handleUrl” will be responsible for confirming “CODE” and exchanging it for a token. Usually, that will be handled by your API library of choice. Here is how I did it for TeamParrot:

import { client_id, client_secret } from '../config/slack';
import exchangeCode from 'slack/methods/oauth.access';
import testAuth from 'slack/methods/auth.test';
import { Linking } from 'react-native';

export function login() {
    return new Promise( ( resolve, reject ) => {
        Linking.openURL( 'https://slack.com/oauth/authorize?client_id=' + client_id + '&scope=channels:read%20channels:history%20users:read%20channels:write' );
        Linking.addEventListener( 'url', handleUrl );
        function handleUrl( event ) {
            const error = event.url.toString().match( /error=([^&]+)/ );
            const code = event.url.toString().match( /code=([^&]+)/ );
            if ( error ) {
                reject( error[ 1 ] );
            } else if ( code ) {
                exchangeCode( { client_id, client_secret, code: code[ 1 ] }, ( err, data ) => err ? reject( err ) : resolve( data ) );
                Linking.removeEventListener( 'url', handleUrl );
            } else {
                reject( 'params' );
            }
        }
    } );
}

export function testLogin( account ) {
    return new Promise( ( resolve, reject ) => testAuth( { token: account.access_token }, ( err, data ) => {
        if ( err || data.user_id !== account.user_id ) {
            reject( err, data );
        } else {
            resolve( Object.assign( {}, account, data ) );
        }
    } ) );
}

So there you have it. Now, you should be authenticated over Oauth2 with the slack API and able to make calls.

React Native Isomorphic app over the weekend

This article is part of my “React Native Isomorphic app over the weekend” series. It shares the problems I encountered during development of TeamParrot and Headstart Journal.

  • Oauth
  • Vector Icons – FontAwesome
  • CodePush
  • Renaming an App
  • Web & Isomorphic code development in Chrome devTools
  • Whole story