{EP}

Edwin Perez

Deploying React Native App to App Store - 2020

IOS

July 16, 2020

Deploying IOS app

Overview

When trying to deploy an app to the app store is when things start to get complicated. There are certificates, provisioning profiles, bundle ids, team ids, developer accounts, etc. All of those can be pretty hard to understand and can be a real time sink when you just want to deploy an application. In the ideal case scenario, you would want to not even have to think about this, just run some command and 🚀 deploy!

Here is the git repo that contains all the code.

Prerequisites

  • Apple Dev Account
  • Mac laptop
  • Github account
  • Apple developer account
  • Ruby installed
  • Latest Xcode installed
  • Keychain password | Mac laptop login password
  • Apple App Specific Password
  • Icon file, must be 1024x1024

Step 1: Create react native app

# Creates the react native app, change the eprntest for the name of your app!
npx react-native init eprntest

cd eprntest

After app is created, open it up on Xcode and ensure everything loads properly. The project file to open on Xcode should be in PROJECT_DIR/ios/PROJECT_NAME.xcodeproj. There is one thing we should do and it is to update our bundle identifier on Xcode.

Xcode Bundle ID change

The App Icon

The app icon is a requirement in order to upload the app to the app store. Since, there are many sizes and different ways to do this. I have created a simple bash utility to make this process easier and can be run from the command line.

cd PROJECT_DIR
mkdir deploy-scripts
cd deploy-script
touch Content.json
touch generate-icons.sh

Here is the generate-icons.sh utility, feel free to adjust anything you would like here.

set -e

SRC_FILE="$1"
DST_PATH="$2"
CONTENTS_JSON_PATH="$3"

VERSION=1.0.0

info() {
  local green="\033[1;32m"
  local normal="\033[0m"
  echo -e "[${green}INFO${normal}] $1"
}

error() {
  local red="\033[1;31m"
  local normal="\033[0m"
  echo -e "[${red}ERROR${normal}] $1"
}

usage() {
  cat << EOF
VERSION: $VERSION
USAGE:
  $0 srcfile dstpath contentsjsonfile

DESCRIPTION:
  This script aim to generate ios app icons easier and simply.

  srcfile - The source png image. Preferably above 1024x1024
  dstpath - The destination path where the icons generate to.
  contentjsonfile - The Contents.json file to copy to ios location

  This script is depend on ImageMagick. So you must install ImageMagick first
  You can use 'sudo brew install ImageMagick' to install it

ORIGINAL AUTHOR:
  Pawpaw<lvyexuwenfa100@126.com>

AUTHOR:
  Edwin Perez<edwinperez539@gmail.com>

LICENSE:
  This script follow MIT license.

EXAMPLE:
  $0 Icon.png ios/myproject/Images.xcassets/AppIcon.appiconset deploy-scripts/Contents.json
EOF
}

# Check ImageMagick
command -v convert >/dev/null 2>&1 || { error >&2 "The ImageMagick is not installed. Please use brew to install it first."; exit -1; }

# Check param
if [ $# != 3 ];then
  usage
  exit -1
fi

# Check dst path whether exist.
if [ ! -d "$DST_PATH" ];then
  mkdir -p "$DST_PATH"
fi

# Generating Icons

convert "$SRC_FILE" -resize 20x20 "$DST_PATH/Icon-20.png"
convert "$SRC_FILE" -resize 29x29 "$DST_PATH/Icon-29.png"
convert "$SRC_FILE" -resize 40x40 "$DST_PATH/Icon-40.png"
convert "$SRC_FILE" -resize 58x58 "$DST_PATH/Icon-58.png"
convert "$SRC_FILE" -resize 60x60 "$DST_PATH/Icon-60.png"
convert "$SRC_FILE" -resize 76x76 "$DST_PATH/Icon-76.png"
convert "$SRC_FILE" -resize 80x80 "$DST_PATH/Icon-80.png"
convert "$SRC_FILE" -resize 87x87 "$DST_PATH/Icon-87.png"
convert "$SRC_FILE" -resize 120x120 "$DST_PATH/Icon-120.png"
convert "$SRC_FILE" -resize 152x152 "$DST_PATH/Icon-152.png"
convert "$SRC_FILE" -resize 167x167 "$DST_PATH/Icon-167.png"
convert "$SRC_FILE" -resize 180x180 "$DST_PATH/Icon-180.png"
convert "$SRC_FILE" -resize 1024x1024 "$DST_PATH/Icon-1024.png"

info 'Generated all icons'

cp $CONTENTS_JSON_PATH $DST_PATH

info 'Copied Content.json'

Next, here is what the Contents.json file looks like:

{
  "images": [
    {
      "size": "20x20",
      "idiom": "iphone",
      "filename": "Icon-40.png",
      "scale": "2x"
    },
    {
      "size": "20x20",
      "idiom": "iphone",
      "filename": "Icon-60.png",
      "scale": "3x"
    },
    {
      "size": "29x29",
      "idiom": "iphone",
      "filename": "Icon-58.png",
      "scale": "2x"
    },
    {
      "size": "29x29",
      "idiom": "iphone",
      "filename": "Icon-87.png",
      "scale": "3x"
    },
    {
      "size": "40x40",
      "idiom": "iphone",
      "filename": "Icon-80.png",
      "scale": "2x"
    },
    {
      "size": "40x40",
      "idiom": "iphone",
      "filename": "Icon-120.png",
      "scale": "3x"
    },
    {
      "size": "60x60",
      "idiom": "iphone",
      "filename": "Icon-120.png",
      "scale": "2x"
    },
    {
      "size": "60x60",
      "idiom": "iphone",
      "filename": "Icon-180.png",
      "scale": "3x"
    },
    {
      "size": "20x20",
      "idiom": "ipad",
      "filename": "Icon-20.png",
      "scale": "1x"
    },
    {
      "size": "20x20",
      "idiom": "ipad",
      "filename": "Icon-40.png",
      "scale": "2x"
    },
    {
      "size": "29x29",
      "idiom": "ipad",
      "filename": "Icon-29.png",
      "scale": "1x"
    },
    {
      "size": "29x29",
      "idiom": "ipad",
      "filename": "Icon-58.png",
      "scale": "2x"
    },
    {
      "size": "40x40",
      "idiom": "ipad",
      "filename": "Icon-40.png",
      "scale": "1x"
    },
    {
      "size": "40x40",
      "idiom": "ipad",
      "filename": "Icon-80.png",
      "scale": "2x"
    },
    {
      "size": "76x76",
      "idiom": "ipad",
      "filename": "Icon-76.png",
      "scale": "1x"
    },
    {
      "size": "76x76",
      "idiom": "ipad",
      "filename": "Icon-152.png",
      "scale": "2x"
    },
    {
      "size": "83.5x83.5",
      "idiom": "ipad",
      "filename": "Icon-167.png",
      "scale": "2x"
    },
    {
      "size": "1024x1024",
      "idiom": "ios-marketing",
      "filename": "Icon-1024.png",
      "scale": "1x"
    }
  ],
  "info": {
    "version": 1,
    "author": "xcode"
  }
}

Lastly, all we need to do now is run the bash command to generate the icons. This assumes you have created a Icon.png file at the root of your project directory. Size of Icon.png must be 1024x1024.

cd PROJECT_DIR
sh deploy-scripts/icon-generator.sh Icon.png ios/eprntest/Images.xcassets/AppIcon.appiconset deploy-scripts/Contents.json

Step 2: Install and configure Fastlane

The install

To install we'll need to create a Gemfile and run the bundle installer. This has to be done inside the following folder eprntest/ios.

# Go into your ios folder inside your app
cd ios

# Create Gemfile
touch Gemfile

Edit the Gemfile to have the following

source "https://rubygems.org"

gem "fastlane"
gem "dotenv"

Now we're ready to install fastlane.

# This install fastlane in your project
bundle install

# Initialize fastlane
# Select the options you would like to enable with fastlane.
# For this walkthrough, we'll select the manual option (4).
bundle exec fastlane init

We should see a new folder and files under the ios directory, this means we installed fastlane and initialized it properly.

Fastlane init

The Configuration

We'll be editing some Fastlane files and environment files to configure Fastlane. Let's create the required files:

# Go to fastlane folder
cd eprntest/ios/fastlane
touch .env
touch .env.ios
touch .env.secret # Do not commit this file to git, ensure it is in the .gitignore list

Configure the .env file, this file contains shared settings for ios / macOS / tvOS if your project had those.

#########################################
# Apple account
#########################################

FASTLANE_USER=edwinperez539@gmail.com # Apple login username
FASTLANE_TEAM_NAME=Edwin Perez # Apple team ID
FASTLANE_ITC_TEAM_NAME=Edwin Perez # Apple team ID

#########################################
# Produce
#########################################

PRODUCE_APP_IDENTIFIER=io.eperez.eprntest # Bundle identifier
PRODUCE_APP_NAME=React Native Test 2020
PRODUCE_APP_VERSION=0.1.0
PRODUCE_APP_SKU=io.eperez.eprntest # Usually same as bundle identifier, otherwise some unique id.
PRODUCE_APP_PLATFORMS=ios

#########################################
# Match
#########################################

MATCH_USERNAME=edwinperez539@gmail.com # Apple login username
MATCH_GIT_URL=https://github.com/trigun539/eprntest-certs.git # Git repo to store certs, ensure it is private!
MATCH_APP_IDENTIFIER=io.eperez.eprntest # Bundle identifier
MATCH_TYPE=appstore

#########################################
# Deliver
#########################################

DELIVER_APP_IDENTIFIER=io.eperez.fastlanetest # Bundle identifier
DELIVER_TEAM_NAME=Edwin Perez # Apple team ID
DELIVER_DEV_PORTAL_TEAM_NAME=Edwin Perez # Apple team ID
DELIVER_RUN_PRECHECK_BEFORE_SUBMIT=false
DELIVER_SKIP_SCREENSHOTS=true
DELIVER_SKIP_METADATA=true

# Deliver - Available options are: DAV, Aspera, Signiant
DELIVER_ITMSTRANSPORTER_ADDITIONAL_UPLOAD_PARAMETERS=-t Aspera

Next, we'll configure the .env.ios file. This file will contain most of the settings for ios.

MATCH_PLATFORM=ios

FL_VERSION_NUMBER_PROJECT=eprntest.xcodeproj # Location of xcode project
FL_BUILD_NUMBER_PROJECT=eprntest.xcodeproj # Location of xcode project

FL_PROJECT_SIGNING_PROJECT_PATH=eprntest.xcodeproj # Location of xcode project
FL_PROJECT_SIGNING_TARGETS=eprntest # Xcode project name
FL_PROJECT_SIGNING_BUILD_CONFIGURATIONS=Release
FL_PROJECT_USE_AUTOMATIC_SIGNING=false
FL_CODE_SIGN_IDENTITY=Apple Distribution

GYM_SCHEME=eprntest # Xcode project name
GYM_EXPORT_METHOD=app-store
GYM_OUTPUT_DIRECTORY=build/ios

DELIVER_PLATFORM=ios

Finally, we'll configure the .env.secret file which contains our app specific password and keychain password. Don't include this file in git repo 🤫!

MATCH_KEYCHAIN_PASSWORD="KEYCHAIN PASSWORD usually your Mac laptop login password"
FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD="APP SPECIFIC PASSWORD"

Next, we'll setup our Fastfile which will configure our lanes and deployment workflow.

fastlane_require "dotenv"

before_all do
  UI.important "Loading secret environment variables"
  Dotenv.load ".env.secret"
end

default_platform(:ios)

desc "Create on Developer portal and App Store Connect"
lane :create_app do
  produce
end

platform :ios do
  before_all do
    Dotenv.load ".env.ios"
  end

  desc "Sync signing"
  lane :signing do
    match

    mapping = Actions.lane_context[SharedValues::MATCH_PROVISIONING_PROFILE_MAPPING]
    update_code_signing_settings(
      profile_name: mapping[ENV['MATCH_APP_IDENTIFIER']]
    )
  end

  desc "Build the binary"
  lane :build do
    signing
    gym
  end

  desc "Deploy to app store"
  lane :release do
    build
    deliver
  end
end

Last but not least is the Appfile which was created when we run our bundle exec fastlane init command. There are a couple settings here that we need to configure.

app_identifier "io.eperez.eprntest"
apple_id "APPLE LOGIN USERNAME"
team_id "APPLE TEAM ID"
itc_team_id "APPLE TEAM ID"

With this we are ready to take our app through the different build workflow steps. This is a good way to check that every step is working before we start running all the steps at once.

To do this, follow the next commands. Ensure you are in the PROJECT_ROOT/ios directory. 💪

Testing produce step

# Create app on developer portal and app store connect
bundle exec fastlane ios create_app

If everything went well, we should see something like this:

Fastlane produce success

App Store Connect

Developer portal

Testing signing step

# You'll be asked for a passphrase for Match storage, make sure you save this in a password manager.
bundle exec fastlane ios signing

If everything went well, we should see following output:

Fastlane signing success

Testing build step

bundle exec fastlane ios build

This step could take a while, so it is the best time to get some ☕.

If everything went well, we should see following output:

Fastlane build success

Testing release step

This is our final step, remember that each step runs previous one as well.

bundle exec fastlane ios release

Here is what you should see if everything went well:

Fastlane release success

Apple Success Email

That's it! 🎆 I know it is a long process but once you go step by step it is a lot more manageable. There are many more things that can be configured such as screenshots, so I would recommend taking a look at fastlane actions so you know what is available.