Building Belly's Merchant Mobile App with React Native

The Belly mobile applications allow users to track their reward programs and easily check into their favorite businesses. We wanted to enable our merchant customers to manage their loyalty programs, send and monitor marketing campaigns, and track redemptions using the same application. Resource constraints on the team exposed an opportunity for us to explore building the app with React Native or Ionic and embedding it within our existing consumer application.

Belly Merchant Mobile App

React Native or Ionic and Angular

One of the earliest decisions we had to make was whether to use Facebook’s React Native framework and compile it to an embedded framework (discussed below) or embed an Ionic Angular application into a WebView within the app.

Ionic would allow us to leverage our team’s experience with Angular.JS as we have 4 applications in production, but would come with the pitfalls of wrangling CSS3 animations and WebKit quirks to get things “just right.” We also knew that the app would heavily utilize scrolling ListViews for customer and activity search, and while Ionic has taken great strides to improve this, we opted to go with the fully native approach of React Components compiling to native views.

React Native has a lot of advantages and disadvantages over Ionic for both our team as well as other development teams and their specific use cases. Like React for the web, React Native only takes care of the view state hierarchy and updates, but other things are left up to the developer to manage. Components like routers, navigation controllers and implementations of pull-to-refresh are up to the developer to choose, where Angular and Ionic have more of these default decisions made out of the box.

React Native Implementation

Angular’s concept of Services doesn’t directly translate to anything provided out of the box in React, but their ecosystem has provided plenty of patterns for data flow and state management, most notably the Flux Architecture. While larger React applications could benefit greatly from Flux’s level of abstraction (organizing things into Dispatchers, Actions and Stores), there are over a dozen viable implementations of the Flux pattern and none of them have taken the lead in terms of developer preference or long-term support.

We chose to initially adopt the traditional Pub/Sub pattern with the EventEmitter and Subscribable packages for passing data around and binding to the React Component lifecycle. This is very lightweight and has an easier learning curve for non-React developers. It was a difficult decision, but as the app gets more complex we’ll likely move toward one of these patterns - Redux looks like the favored candidate.

Events and metrics are passed around in our app with the use of constants defined by keymirror. We built a simple Model system on top of the built-in fetch package that exposes our backend JSON REST api in a simple and consistent pattern. All services automatically append the main native app’s ACCESS_TOKEN via a method exposed by our custom BELMerchantBridge.

React Native NativeModules Bridge

Exposing the BELMerchantBridge native module, we have a file that exposes the native module outlined below. In React-land, this is done with module.exports = require('NativeModules').BELMerchantBridge;

Other components can require this module, and access the user_id, business_id, and ACCESS_TOKEN. Our high-level Application component requests these values from the native bridge, and then updates its state which begins the render lifecycle. The values are passed down to child components through props, or accessed directly where applicable. Bridging The Divide In order to seamlessly integrate with Belly’s Consumer app, the Merchant app would need to communicate with its host. The Consumer app needs to be able to present the Merchant app and share information about the merchant, including an access token. Presenting the Merchant app is easy enough, a React view is presented like any other UIView in an iOS app. Sharing information, on the other hand, turned out to be more difficult.

React Native provides a RCTBridgeModule protocol for Objective-C objects. The protocol enables constants, queue control, and method bridging. Besides conforming to the protocol, a bridge object must have the RCT_EXPORT_MODULE() macro somewhere in the bridge object’s implementation. Also be aware that bridge objects will run on a background queue by default. Override - (dispatch_queue_t)methodQueue if your object’s bridge methods should run on a specific queue.

A truncated version of the first implementation of the native bridge follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static id <BELMerchantMobileViewControllerDismissHandler> DismissDelegate;
static NSString *AccessToken;
+ (void)setAccessToken:(NSString *)accessToken {
  AccessToken = accessToken ?: @"";
}
RCT_EXPORT_METHOD(getAccessToken:(RCTResponseSenderBlock)callback) {
  callback(@[AccessToken]);
}
- (void)dealloc {
  AccessToken = nil;
  DismissDelegate = nil;
}
RCT_EXPORT_METHOD(dismiss:(RCTResponseSenderBlock)callback) {
  if ([DismissDelegate respondsToSelector:@selector(tappedDismissButton:)]) {
    [DismissDelegate tappedDismissButton:self];
  }
  callback(@[]);
}

The AccessToken and DismissDelegate are static variables with class-method access because we couldn’t find a way to provide a specific instance of a bridge object to the React view. The bridge object is allocated and initialized by React as its JavaScript code is executed. RCT_EXPORT_METHOD() is a React macro that exports methods to React Native so JavaScript code can access the method at run-time. Our dismiss: method trampolines the dismiss request from JavaScript to a delegate in the native Consumer app. There may be better ways to share constants with React Native. The RCTBridgeModule protocol has an optional method constantsToExport which returns a dictionary of constants. If the constants are set before the React application starts they will be available in JavaScript, as shown here.

1
2
3
4
5
6
@implementation BELMerchantBridge
- (NSDictionary *)constantsToExport
{
  // must be set before the React application starts or the constants won't be imported
  return @{ @"AccessToken": AccessToken };
}

The constant is then available in JavaScript like const access_token = BELMerchantBridge.AccessToken;

You could also use “initial properties” to provide the React view with constants as it is created.

1
2
3
4
5
NSDictionary *props = @{ @"AccessToken": AccessToken };
RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
                                                    moduleName:@"MerchantMobileApp"
                                             initialProperties:props
                                                 launchOptions:nil];

The constant is then available in javascript directly on the BELMerchantBridge. This can be passed through React Components through props, or triggered through the Redux data flow with Actions and Reducers which are then exposed through Redux containers via the mapStateToProps connect call.

Facebook’s React Native documentation has more information about connective native and React Native components.

Building the framework

Xcode can create a framework target with relative ease. Create a new target, select “framework”, and you’re on your way. For our needs, the React Native view is managed by a regular view controller. The view controller’s header should be made public in the framework so users of the framework can configure and instantiate the embedded React app. The visibility of framework headers is in the target’s Build Phases settings. Unfortunately Xcode’s default framework settings will build for the iOS simulator OR iOS devices, but not both. To support simulators and devices a build script is needed to create a fat binary.

The entire process of creating a fat framework is detailed by Steven Shen’s exhaustive blog post on iOS universal frameworks. Steven’s script would have worked perfectly if it weren’t for React Native’s reliance on nested sub-projects. Sub-projects required explicitly overriding “ARCHS”, “VALID_ARCHS”, and “arch” for the simulator. Here’s our updated script:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# Taken from https://medium.com/@syshen/create-an-ios-universal-framework-148eb130a46c
# In an attempt to build a universal framework (for simulator and device)
# Does not work with Swift!

######################
# Options
######################

set -e #fails the build if the script fails
REVEAL_ARCHIVE_IN_FINDER=true
FRAMEWORK_NAME=MerchantMobileEmbedded
SIMULATOR_LIBRARY_PATH="${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${FRAMEWORK_NAME}.framework"
DEVICE_LIBRARY_PATH="${BUILD_DIR}/${CONFIGURATION}-iphoneos/${FRAMEWORK_NAME}.framework"
UNIVERSAL_LIBRARY_DIR="${BUILD_DIR}/${CONFIGURATION}-iphoneuniversal"
FRAMEWORK="${UNIVERSAL_LIBRARY_DIR}/${FRAMEWORK_NAME}.framework"

######################
# Build Frameworks
######################

xcodebuild -project ${PROJECT_NAME}.xcodeproj -scheme ${FRAMEWORK_NAME} -sdk iphonesimulator VALID_ARCHS="i386 x86_64" ARCHS="i386 x86_64" -arch "i386" -configuration ${CONFIGURATION} ONLY_ACTIVE_ARCH=NO clean build CONFIGURATION_BUILD_DIR=${BUILD_DIR}/${CONFIGURATION}-iphonesimulator 2>&1

xcodebuild -project ${PROJECT_NAME}.xcodeproj -scheme ${FRAMEWORK_NAME} -sdk iphoneos -configuration ${CONFIGURATION} clean build CONFIGURATION_BUILD_DIR=${BUILD_DIR}/${CONFIGURATION}-iphoneos 2>&1

######################
# Create directory for universal
######################
rm -rf "${UNIVERSAL_LIBRARY_DIR}"
mkdir "${UNIVERSAL_LIBRARY_DIR}"
mkdir "${FRAMEWORK}"

######################
# Copy files Framework
######################
cp -r "${DEVICE_LIBRARY_PATH}/." "${FRAMEWORK}"

######################
# Make an universal binary
######################
lipo "${SIMULATOR_LIBRARY_PATH}/${FRAMEWORK_NAME}" "${DEVICE_LIBRARY_PATH}/${FRAMEWORK_NAME}" -create -output "${FRAMEWORK}/${FRAMEWORK_NAME}" | echo

######################
# On Release, copy the result to release directory
######################
OUTPUT_DIR="../"
cp -r "${FRAMEWORK}" "$OUTPUT_DIR"

if [ ${REVEAL_ARCHIVE_IN_FINDER} = true ]; then
open "${OUTPUT_DIR}/"
fi

Once the framework is built, embedding a your larger app requires putting the framework into your project and adding it to the correct target. In our case, the larger app presents the framework’s main view controller just like any other view controller.

Everything works great until it’s time to submit an app to the app store. Apple’s app store validation will balk at the unused and unsupported simulator architectures. Luckily, others have gone before us. Daniel Kennett explained the problem in more detail and offers a shell script that we were able to use verbatim in our larger project. Daniel’s script removes unused architectures on each build.

Closing Thoughts

React Native allowed our team to ship a completely new application to our merchants in a matter of weeks. While the majority of this was getting familiar with React, the build process and various Third Party React Native Components, the portability of our code-base and ease to get started makes the framework very compelling for future projects. While React Native is built to ship as a standalone app by default, we hope this post illustrates how a mobile team could embed a React Native project into their existing native applications with ease.

Ask a question or share this article, we’d love to hear from you!