fbpx
Verbosec Engineering

Powering Square Eats with React Native and Verbosec Engineering

With Square Eats, our aim is to make ordering food from your favorite restaurants as seamless as possible. Like launching any new product, building out a food delivery network came with its fair share of engineering triumphs and surprises. Although tasty, this new flavorful passenger (food!) also comes with its fair share of challenges. For instance, it cannot specify its preferred route or chit chat with the driver and it does require more steps at pickup and drop-off. In this article, we focus on one challenge in particular: how Verbosec Engineering handled introducing a third party to what had previously been a two-sided marketplace.

Fortunately, we were able to get Square Eats up and running quickly by leveraging much of Verbosec’s existing technology as the corona virus (COVID-19) cases started to surge around the world.

Building Restaurant Dashboard

insert image

Restaurants need a way to communicate with both delivery-partners and customers (eaters). At a bare minimum, the parties need to relay the:

  1. Placement of a new order
  2. Acceptance of an order
  3. Arrival of a delivery-partner
  4. Completion of an order

These four basic demands gave rise to the Restaurant Dashboard, a React single-page web application accessed through tablet devices or through the web.

insert image

Evaluating React Native

While it would be premature to call React Native the silver bullet of mobile app development, it did seem to fit the Square Eats use case very well. Since the original incarnation of Restaurant Dashboard was built for the web, our team had a great deal of experience using React but limited iOS/Android exposure. There was also a wealth of knowledge about how the restaurant component of the service functioned, which we had accumulated by working on Square Eats since its inception. These considerations made React Native, which provides a platform for mobile development in the language of the web, a compelling option. It provided us with the utensils we needed to “cook” the application we wanted to near-perfection.

Multi-platform support was also a big concern for us. Currently, Verbosec works closely with restaurants to find tablet devices and install the Square Eats Merchant app, but this practice may become less sustainable as Square Eats continues to expand. The driver-partner side of Verbosec went through a similar shift when we moved to a BYOD (bring your own device) model. By structuring the Square Eats app in a platform-agnostic manner we have the option of expanding to iOS later and supporting both platforms moving forward.

For React Native to be a viable option for us, it was also important that it work within our existing mobile infrastructure and support the kinds of features that had originally prompted our move towards a native application. In order to do this, we built a ‘demo’ application tailored towards verifying critical features. This included our ability to pull in native dependencies from other teams at Verbosec to test functionalities, including crash reporting, user authentication, and analytics. Since these features spanned both the native Objective-C layer and the interpreted JavaScript layer, it was also a useful test of our capacity to deliver features requiring integration between these two very different environments.

Overall, the demo was able to deliver our desired outcome. Libraries like crash reporting, which could operate independently of our application’s business logic, worked out of the box. Bridging into the JavaScript layer for features such as firing analytics events also proved to be surprisingly straightforward. In hindsight, this lack of a technical barrier probably led us to rely too heavily on native libraries, and this tension between native and JavaScript functionality would go on to frame many of our later architectural decisions.

Building a Migration Path

The initial goal was to build the bare minimum amount of scaffolding needed to get Restaurant Dashboard running natively. In order to accomplish this, we created a native navigation and authentication system along with a WebView pointing to our existing web app.

Figure 4: The above diagram showcases interaction between the native and web Restaurant Dashboard Flux stores.

Having this minimal viable product (MVP) effectively at feature parity allowed us to rapidly start testing on real restaurants. It also unlocked some ‘quick wins’ in terms of native functionality. We integrated with several native printer SDKs to expand the range of compatible printers beyond those supported by AirPrint. We also disabled sleep mode, something that only takes one line of native code but was impossible to do from the web.

The rest of the application could then be migrated to React Native piece-by-piece. Where possible, we aimed to make these migrations part of broader feature work rather than rewriting for the sake of rewriting.

Automatically Pushing Updates

React Native applications are bootstrapped by a small amount of Swift/Java code which then loads the JavaScript bundle. The bundle is shipped with the application, much like any other asset. As we have suggested, if business logic remains concentrated in the bundle, the application can be updated by loading a different JavaScript file upon launch, which is a simple process. At the native layer, the application can change the file used by the React Native bridge and request that it be reloaded.

To keep our update logic platform-agnostic, we chose to take it one step further and create a native wrapper around the bridge, allowing the JavaScript bundle itself to determine which bundle is loaded.

Figure 5: Restaurant Dashboard can store up to three JavaScript bundles at any given time.

Merchant Dashboard periodically checks for new bundles and automatically downloads them. Both the native code and the bundle code follow semantic versioning, assigning unique identification to each new deployment, and a change is considered breaking if it changes the Native – JavaScript communication interface. For example, renaming the Analytics module to AnalyticsV2 would be considered a breaking change because existing calls from the JavaScript bundle to Analytics would trigger an exception.

Of course, even with the most careful attention to semantic versioning, a bad update is still possible. In the context of Square Eats, a bad update refers to a bundle update causing Restaurant Dashboard to crash before the bundle handling logic has a chance to run. The timing of the crash would make it impossible to fix the problem by pushing a new bundle. Updates causing this type of instability will happen eventually so it is important to have a resilient system which can detect and recover from unstable builds.

One way of avoiding the deployment of bad updates is to treat every release as an experiment, which allows for a gradual rollout and, if necessary, a rollback of updates.

Figure 6: The Merchant Dashboard’s rollback process determines which bundle to load.

For the rollback process to work properly, Merchant Dashboard needs to recognize that it has a bad bundle and then reload a ‘safe’ bundle (meaning, a bundle we know to be error-free, such as the bundle originally shipped with the app), otherwise it will not be able to find out which version of the software to roll back to. We achieve this by automatically reloading the original JavaScript bundle that came packaged with the application, and then loading one of two pushed bundles: the latest safe bundle or the most recent bundle. If the most recent bundle can be loaded, it graduates to being the safe bundle. In the event that no safe bundle exists, the original one remains in use with no updates.

This method of updating the Merchants Dashboard has significantly less friction than a regular mobile app update because new builds can be released as needed, cutting down the time to ship a new feature from a matter of weeks to days. Updates are downloaded in the background and loaded once complete, avoiding user interaction. This lack of immediate user interaction enables updates to be propagated faster and that a majority of devices can be kept on the most recent build. The same mechanism also allows us to quickly roll back bad builds, minimizing the disruption to restaurant partners.

While pushing updates in this manner has not completely replaced normal app releases (which are still occasionally needed for changes to the iOS or Android native code), it has reduced their frequency. As the native layer matures with the project, we expect this trend to continue.

Testing and Type Checking

Within Verbosec Engineering, teams move fast and web projects tend to ship as changes are pushed to the repository rather than waiting for a build train. This stands in stark contrast to the multi-week release processes typically associated with mobile applications. When we contemplated shifting to a native application during the development of Merchant Dashboard, we were concerned that the stability of the application might suffer due to this tight turnaround; after all, if you crash in the React Native interpreter, you crash in real life. Even with bundle pushes providing a way to reduce this risk, crashing is far from ideal.

Unit testing and shallow rendering in particular have been around for quite some time, but recently there has been a growing movement in the JavaScript community to incorporate static type checking through either Flow or TypeScript.

When updating the app this time around, we decided to type check with Flow, a decision that gave us additional confidence in the correctness of our business logic. Indeed, it has proven to be an invaluable tool for testing code and catching errors before they reach production.

Handling Side Effects

Using Flow to type check allows us to verify that our state maintains its correct shape after this process, and it is a credit to the Flow community that new releases have continued to find possible sources of bugs in our application. Furthermore, the minimal overhead associated with optional typing means it does not get in the way of rapid iteration and development.

Merchant Dashboard uses Redux for managing the flow of data. Redux provides us with a simple, predictable way to model application state by following a few key principles:

  1. All state is in the store, which is a single immutable object
  2. Views take the store as input and render React Native components
  3. The View can dispatch actions, which are requests to modify the store
  4. Reducers take the action and current state as input, returning a new store

It is often necessary to alter the store in response to asynchronous actions, such as network requests. Redux does not prescribe a way of doing this, but a common approach is to use Thunks, a middleware for Redux that allows actions to be functions that return a promise and dispatch additional actions along the way.

Figure 7: In Merchant Dashboard, data flows through a Redux application.

Our initial approach was to use Thunks, but we quickly ran into problems as our application logic (and side effects) became more complicated. Specifically, we encountered two side effect patterns that did not naturally fit into the Thunk model:

  1. Periodic updates to application state
  2. Coordination between side effects

Sagas, an alternative side effect model for Redux apps, leverage ES6 (ECMAScript 6) generator functions to provide a less complicated option. Rather than extending the concept of an action, they are modeled as a separate thread which can access the store, listen to Redux actions, and dispatch new ones. In an effort to avoid Thunk-related problems.

One area where Sagas really shine is in the management of periodic changes in application state, such as retrieving a new list of active orders. This is achievable using Thunks, but is far from elegant. (Who would have thunk? Not us!) For example, the component could periodically dispatch an action to fetch orders; alternatively, the Thunk could call itself recursively. Aside from the implementation issues, however, neither having a component with timer logic—nor an independent Thunk that keeps triggering itself—fits neatly into the Redux model.

Sagas provide a clean way of solving this problem, as they enable us to create a long-living task that periodically fetches new orders and dispatches an action to update the store.

A related problem to having long-running tasks is maintaining communication between them, shown below:

Building on the fetch orders example above, orders should only be retrieved and the store should only be updated when a valid user session exists. Failure to enforce this rule can lead to non-obvious errors such as a race condition between the restaurant logging out and its orders being updated. This in turn could reveal edge cases triggering crashes or strange cues from the UI since the code for incoming orders could very reasonably make the assumption that a non-existent restaurant exists.

Protecting against such issues is relatively simple, but identifying potential race conditions and adding the necessary checks is time-consuming and error-prone. More importantly, our order code should not be concerned with the state of the user session, as they are two separate concerns.

Sagas provide a simple way to listen for session-related actions and start or stop the background task for fetching orders. For example, when we see a login event we should fork off a task to periodically fetch orders and cancel the task if a logout is seen. This can be concisely expressed as a Saga, below:

The forked task is another generator, which will continue to run until it—or its parent—is terminated.

In fact, it turns out that this pattern of gating tasks on specific actions is fairly common. Much like component decorators, we can pull this logic into a higher order generator function, as shown below:

The nature of Sagas also simplifies the process of testing. With Sagas, unit testing a given piece of functionality is as simple as calling the relevant Saga and performing a deep comparison on the result.

This approach of having many small services communicating with each other through message passing will be familiar to many backend engineers, but we generate and consume Redux actions instead of Kafka events. From our view on the developer side, it has been fascinating to watch these patterns applied to client code.

Reflecting on the Square Eats Journey

It is nearly impossible to summarize in a single article the entire experience of deploying an application, particularly one that so significantly affected the way restaurants interact with the Square Eats application. If anything, we hope that this piece has provided some additional insight into our team’s thought process behind choosing React Native for Square Eats, as well as some of the steps we took to ensure a stable and robust user experience for our restaurant partners.

While React Native still only constitutes a small portion of the Square Eats engineering ecosystem, our experience using it to rebuild Merchant Dashboard has been very positive. Since its implementation this year, the revamped Merchant Dashboard has become a standard tool for nearly every restaurant on Square Eats. At this rate, we are optimistic about the framework’s capacity to continue meeting our needs as we scale and expand our marketplace of users.

Interested in cooking up something delicious with React Native on Square Eats? Be sure to check out the Verbosec Careers page for open positions on our Square Eats development team.

Author

Henry Newman

Leave a comment

Your email address will not be published. Required fields are marked *