How we built self-healing kiosks
A look at how Local Kitchens' engineering team solved connectivity challenges with Stripe readers for in-store ordering
A running theme at Local Kitchens is our evergreen effort to improve the guest experience across both the physical and the digital (we believe that vocabulary is important, and call our customers “guests”). This thinking led us to choose a self-serve ordering model at our stores, which eliminates lines at the cashier, allows us to have a frequently changing digital menu display, and allows guests to discover new tasty options.
To that end, each Local Kitchens location has 2-3 iPads that run a react-native application. Our app connects to both our backend and to the Stripe terminals to process payments and ingest orders. Our kiosks can be the first or even the only point of contact guests have with us, so it’s important we get the experience right and minimize downtime.
We have network failovers to help minimize downtime, but disruptions still happen due to power outages, loose wiring, and the occasional multi-carrier outage. In the past, recovering from these downtimes required manually reconnecting our iPads to their corresponding Stripe terminal. This process was unintuitive for the kitchen team and required navigating unfamiliar network settings. It was also easy to ignore during peak hours, since staff are more focused on making great food instead of managing IT.
To solve this issue, we built a system that allows our iPads to automatically reconnect to the Stripe readers as soon as they’re back online. Since our application is built in React Native, we wrote a new react context to handle all the connection logic whilst exposing the connection status for any other part of the code to consume. Let’s dive into the code!
Code
Our app saves the connected stripe reader ID to async storage upon initial connection and uses Stripe's react native library (which is currently in early beta) to check the network status of the reader every 10 seconds. This way, if a reader goes offline, we’re able to reconnect within 10 seconds of the reader coming back online.
An outline of the code in our context is seen below. In practice there is additional logic for e.g. managing errors, but the below should convey the core idea.
Step 1: First, we need to track various states exposed by Stripe’s react-native library, such as whether we’re actively searching for readers on the network or actively connecting to a reader. This is useful for knowing if it’s safe to call some reader APIs. We’re working with a lot of asynchronous calls here, and in practice we’ve found it’s good to be conservative when making requests to your reader. Stripe’s RN API can be unfortunately quite error-prone otherwise.
const [isDiscovering, setIsDiscovering] = useState(false);
const [knownReaderId, setKnownReaderId] = useState("");
const [connectingReader, setConnectingReader] = useState<Reader.Type>();
const {
cancelDiscovering,
discoverReaders,
discoveredReaders,
connectedReader,
isStripeInitialized
} = useStripeTerminal({
onFinishDiscoveringReaders: (finishError) => {
if (connectingReader?.id) {
setStorageKnownReaderId(connectingReader?.id);
}
setIsDiscovering(false);
setConnectingReader(undefined);
},
});
/**
* Sets the known reader ID from local storage
*/
const setStorageKnownReaderId = useCallback(
async (id: string): Promise<void> => {
await SecureStore.setItemAsync(TERMINAL_STORAGE_KEY, id);
if (id !== knownReaderId) {
setKnownReaderId(id);
}
},
[knownReaderId, setKnownReaderId],
);
Step 2: Now that we have basic reader state, we want to make sure we keep a near-real time view of the reader’s availability. The below code polls for reader status every 10s when disconnected.
const isConnectedToReader = useMemo(() => {
return (
isStripeInitialized &&
connectedReader?.status === "online"
);
}, [isStripeInitialized, connectedReader?.status]);
/**
* Attempt to discover the last known reader every 10s
* when disconnected
*/
const shouldRefetchReaders = useMemo(
() => !isConnectedToReader && knownReaderId,
[isConnectedToReader, knownReaderId],
);
useInterval(async () => {
if (shouldRefetchReaders) {
console.log(
"[Stripe] Refetching readers to keep up to date on their network state",
);
// This in turn calls onFinishDiscoveringReaders() above
await handleDiscoverReaders();
}
}, 10 * 1000);
/**
* Discover readers via Internet.
*/
const handleDiscoverReaders = useCallback(async () => {
console.log("[Stripe] Discovering readers.");
setIsDiscovering(true);
const { error: discoverReadersError } = await discoverReaders({
discoveryMethod: "internet"
});
// … handle error
}, [isInitialized, discoverReaders]);
Step 3: Finally, we use the above state to determine when to reconnect to a reader. Again, you could try to reconnect every 10s in a while loop, but in practice we’ve found this code to be way less buggy.
/**
* Gracefully reconnect to a known reader when it comes back online
*/
const shouldReconnectToReader = (
knownReaderId &&
!connectingReader &&
!!discoveredReaders?.find(
(reader) => reader.id === knownReaderId && reader.status === "online",
),
)
useEffect(() => {
if (shouldReconnectToReader) {
console.log(
"[Stripe] is attempting to reconnect to the previously seen reader",
);
const matchingReader =
discoveredReaders.find(
(reader) => reader.id === knownReaderId && reader.status === "online",
);
if (matchingReader) {
console.log(
"[Stripe] Connecting previously connected terminal",
knownReaderId,
);
handleConnectReader(matchingReader);
}
}
}, [
shouldReconnectToReader,
handleConnectReader,
discoveredReaders,
knownReaderId
]);
/**
* Connect reader. Also set the device storage terminal ID for future connections
*/
const handleConnectReader = useCallback(
async (availableReader: Reader.Type) => {
if (availableReader.status !== "online") {
console.error(
"[Stripe] Connect internet reader error: ",
`Reader is ${availableReader.status}.`,
);
return;
}
setConnectingReader(availableReader);
},
[],
);
Conclusion
This system has been a huge success for us. Across our 10 locations we’ve historically seen a Stripe reader briefly lose connectivity about once every 4.5 days (these are almost always small blips). This system has eliminated the need to manually reconnect to our readers for each of those cases and greatly improved device uptime. It also freed up resources for our kitchen, engineering, IT, and guest success teams. It’s super gratifying to deploy code that eliminates an entire class of issues! Our ideal state for kitchen infra is to automate the tedium away and allow our kitchen staff to focus on making incredible food for our guests.
In a future post we’ll detail how we used Segment functions to build realtime observability for the fleet.
Also, if you find this sort of work interesting, we’re hiring! Check us out at localkitchens.com/careers