React Native

Building a full-stack iOS app in one hour

The Story

Let me be honest for a second: I love giving tech talks, but my brain hates them. Every single time I sit down to prepare for a tech talk, I hit a writer’s block more substantial than The Wall in Game of Thrones. Writer’s block is not something I am used to dealing with. I can usually sit down at my keyboard and crank out elegant solutions to incredibly hairy distributed systems problems, but when it comes to speaking in front of a room full friends and strangers alike my brain seems to just shut itself off and give me the finger in the process. The tech talk I gave at NodeJS Austin this last week was no exception.

Unlike my past few talks, this tech talk was supposed to be super simple: a 15 minute lightning talk on Expo, an amazing development framework for building mobile apps with React Native without the native build tool overhead. The time came immediately following BoltSource’s executive staff meeting to start work on the presentation, but instead of jumping in I decided to once again procrastinate. My operations manager, Lauren, needed a new MacBook so I decided to take a “quick detour” to the Apple Store with her to pick one up. While waiting in line at the store, my eyes locked on to an iPad Mini and suddenly my creative gears started turning: “What if I buy an iPad mini right now, build an app for collecting leads at tech conferences using Expo, and then demo the app and the code at the lightning talk to demonstrate how insanely easy it is to build mobile apps with expo while simultaneously using it as a shameless plug to collect leads for business development?” With this kind of multiple-points-of-value idea, I knew I had to follow through.

The Constraints

Flash forward about an hour to 3pm, and I was finally at home in front of my computer ready to build the app. The tech talk was at 7pm, so I only had a couple of hours to build the app. I set a few constraints for myself to keep things simple:

  1. The entire build, including any backend code, should be less than 300 lines of code for the sake of effective demonstration.
  2. I needed to put together the presentation, shower, get dressed, Uber down town, and have dinner before the talk. This meant I had about an hour to build the app and get it working reliably on the new iPad.
  3. While it doesn’t have to be super secure, the app should interact with our CRM, Copper, in a way that limits blast radius. This means an API is necessary to avoid sticking Copper API keys in the mobile app.

At first glance, this seemed like quite a challenge. That said, I am a startup junkie. I do my best work under insane pressure. I embraced the thrill and accepted the challenge.

The Design

Like all programmers, I love white boards. I usually start projects by wire-framing UIs and/or backend components necessary to get the job done. From my point of view, there were 3 necessary components to this job in order to satisfy the scope, quality, and cost constraints I was imposing on myself:

  1. A mobile app built in Expo, duh
  2. A public API that accepts Lead objects via HTTP request, transforms the payload into the shape that Copper’s API expects, and loads the Lead into the CRM via a secure API call. For the sake of time and simplicity, running this API on Now.sh seemed like the best option.
  3. Copper CRM’s API, with a secure access key only accessible to the public API mentioned above.

I created a very sloppy whiteboard drawing to represent the entire end-to-end build, and snapped a picture of it complete with a terrible glare from my home office window.

board

Now that the design was out of the way, it was time to rock-n-roll.

The API

This entire project hinged my the ability to easily submit leads via Copper’s well-documented API. On paper this seemed easy enough, but I decided to start with the API to remove the biggest piece of perceived risk from the project.

I am a bit of a dinosaur when it comes to RESTful API development. A lot of the “cool kids” have moved on to Koa, Hapi, and several other API frameworks for NodeJS. Rather than switch to new tools for building RESTful APIs, I‘ve stuck with the reliable and battle hardened ExpressJS. For making HTTP requests to other systems such as Copper’s API, I use Axios. Building the integration with Copper using these two tools + Now.sh was as painless as I’d hoped:

const express = require('express');
const helmet = require('helmet');
const axios = require('axios');
const bodyParser = require('body-parser')

const client = axios.create({
  baseURL: 'https://api.prosperworks.com/developer_api',
  headers: {
    'X-PW-AccessToken': process.env.COPPER_CRM_API_KEY,
    'X-PW-Application': 'developer_api',
    'X-PW-UserEmail': process.env.COPPER_CRM_USEREMAIL,
    'Content-Type': 'application/json'
  }
})

const app = express();

app.use(helmet());

app.get('/ping', (req, res) => res.status(200).send('pong'))

app.post('/submit_lead', bodyParser(), async (req, res, next) => {
  try {
    const { name, email, companyName: company_name, title, tags } = req.body

    console.log('creating lead in copper', JSON.stringify({
      name,
      email: {
        email,
        category: 'work'
      },
      company_name,
      title,
      tags,
      date_created: Date.now()
    }))

    await client.post('/v1/leads', {
      name,
      email: {
        email,
        category: 'work'
      },
      company_name,
      title,
      tags,
      date_created: Date.now()
    })

    console.log('lead created in copper')

    return res.status(201).send({
      success: true
    })
  } catch (err) {
    console.log('error creating lead in copper')
    console.error(err)
    next(err)
  }
})

module.exports = app;

A single command, and the API was live and available to the world:

command

After a couple of minutes of testing, I was confident enough to move forward and build the actual iPad app.

The App

If you haven’t tried Expo before, you are really missing out. You know the feeling you get when you buy a brand new MacBook, open the package, and set it up for the first time? For me, it feels like simplicity made manifest in a physical expression, a lot like the developer experience when building websites with GatsbyJS. When it comes to building native applications, Expo feels the same way. All of the rough edges and decisions that you usually have to make when working with React Native are automated away. Expo handles building the app into binaries on a free remote service, push notification delivery, optimization of assets, social authentication, native graphics via WebGL, and more. You don’t even have to run native build tools, everything is done in the cloud for you and for free. The entire app build was relatively simple, requiring just around 200 lines of code:

import React, { useState, useRef } from 'react';
import { StyleSheet, View, KeyboardAvoidingView } from 'react-native';
import { Image, ThemeProvider, Input, Button } from 'react-native-elements';
import validator from 'validator'
import axios from 'axios'

import constants from './constants'

const { colors } = constants

const theme = {
  Input: {
    style: {
      color: colors.secondary
    },
    labelStyle: {
      marginTop: 16,
      color: colors.primary
    },
    placeholderTextColor: colors.secondaryFaded
  },
  Button: {
    buttonStyle: {
      backgroundColor: colors.primary
    },
    disabledStyle: {
      backgroundColor: colors.primaryFaded
    },
    disabledTitleStyle: {
      color: 'white'
    }
  }
};

const client = axios.create({
  baseURL: 'https://overload.andrew41.now.sh'
})

export default function App () {

  const nameEl = useRef(null)
  const [ name, setName ] = useState('')
  const [ email, setEmail ] = useState('')
  const [ emailError, setEmailError ] = useState(null)
  const [ title, setTitle ] = useState('')
  const [ companyName, setCompanyName ] = useState('')
  const [ isSubmitting, setSubmitting ] = useState(false)

  const handleSubmit = async () => {
    setSubmitting(true)
    try {
      let payload = {
        name,
        email,
        tags: ['event_ipad']
      }
      if (title) {
        payload.title = title
      }
      if (companyName) {
        payload.companyName = companyName
      }
      await client.post('/submit_lead', {
        name,
        email,
        title,
        companyName
      })
    } catch (err) {
      console.error(err)
    }

    setName('')
    setEmail('')
    setEmailError(null)
    setTitle('')
    setCompanyName('')

    setSubmitting(false)

    nameEl.current.focus()
    nameEl.current.blur()
  }

  return (
    <ThemeProvider theme={theme}>
      <View style={styles.rootView}>
        <KeyboardAvoidingView behavior='height' style={{ flex: 1 }}>
          <View style={styles.bannerView}>
            <Image
              source={require('./assets/boltsource-banner-logo.png')}
              style={styles.banner}
              resizeMode='contain'
            />
          </View>
          <View style={styles.inputView}>
            <Input
              containerStyle={{ width: '90%' }}
              placeholder="John Doe"
              placeholderTextColor={colors.secondaryFaded}
              label="Full Name"
              autoCapitalize='words'
              ref={nameEl}
              value={name}
              autoCorrect={false}
              disabled={isSubmitting}
              onChangeText={name => setName(name)}
            />
            <Input
              containerStyle={{ width: '90%' }}
              placeholder="john.doe@acme.co"
              placeholderTextColor={colors.secondaryFaded}
              errorMessage={emailError}
              label="Email"
              type='email'
              autoCorrect={false}
              autoCapitalize='none'
              autoCompleteType='email'
              value={email}
              keyboardType='email-address'
              disabled={isSubmitting}
              onBlur={(e) => {
                if (email && !validator.isEmail(email)) {
                  setEmailError('Invalid email address')
                }
              }}
              onChangeText={email => {
                setEmailError(null)
                setEmail(email)
              }}
              onFocus={() => setEmailError(null)}
            />
            <Input
              containerStyle={{ width: '90%' }}
              placeholder="Software Engineer"
              placeholderTextColor={colors.secondaryFaded}
              label="Work Title"
              autoCapitalize='words'
              value={title}
              autoCorrect={false}
              disabled={isSubmitting}
              onChangeText={title => setTitle(title)}
            />
            <Input
              containerStyle={{ width: '90%' }}
              autoCapitalize='words'
              placeholder="Acme Co"
              placeholderTextColor={colors.secondaryFaded}
              label="Company Name"
              value={companyName}
              autoCorrect={false}
              disabled={isSubmitting}
              onChangeText={companyName => setCompanyName(companyName)}
            />
          </View>
          <View style={styles.buttonView}>
            <Button
              onPress={handleSubmit}
              containerStyle={{ width: '90%' }}
              title="Submit"
              type="solid"
              raised={true}
              loading={isSubmitting}
              disabled={isSubmitting || !email || !name || emailError}
            />
          </View>
        </KeyboardAvoidingView>
      </View>
    </ThemeProvider>
  );
}

const styles = StyleSheet.create({
  rootView: {
    flex: 1,
  },
  bannerView: {
    flex: 1,
    marginTop: 100,
    alignItems: 'center',
    justifyContent: 'flex-start'
  },
  banner: {
    flex: 1
  },
  inputView: {
    marginTop: 100,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center'
  },
  buttonView: {
    flex: 1,
    justifyContent: 'flex-end',
    alignItems: 'center',
    marginBottom: 36
  }
});

One thing to note here is how insanely easy React’s new hooks API makes it to manage state. Where you once needed redux or class components, you can now call a function to manage component state. It’s an absolutely brilliant feature that should significantly cut down on the amount of required code in most React-based product codebases.

The Result

After a bit of UI tweaking a few minutes of testing, I was pretty happy with the end result:

result

All in all, the entire build took just about an hour of total time, giving me plenty of breathing room to finish up the presentation and head into down town Austin to meet a friend for dinner before the tech talk.

One thing I could have done a bit differently here is add authentication, requiring BoltSource employees to log in via Google with their BoltSource email account before allowing access. I could have then secured the API via a JWT or something like it, as well as associated each submitted lead with the employee’s name to track which leads are generated by which employees. This is something I’ll probably end up adding to the app before the next event that our team attends.

The entire build is completely open source. Feel free to fork it and make it your own. The repo is here: https://github.com/boltsource/overload. Enjoy!

Partner with us

You have the vision for a gorgeous product experience. We are the software design and engineering team that can bring it to life. Learn more

Have questions about our content?

Join our developer community and ask a BoltSource Engineer today