
OnePay’s Mobile Platform Evolution: One Source, Omni-Channel

Share
In this Article
Share
Introduction: The Native World
Three years ago, our mobile engineering team was organized around two platform-specific tracks: iOS development in Swift and Android development in Kotlin. This structure meant that some of our work (design reviews, testing, and bug fixes) was duplicated across platforms. While the products shared the same goals and brand, they often required parallel effort to deliver each release.
Feature parity became a perpetual game of catch-up. A feature that shipped on iOS in one sprint might not reach Android for another two weeks or longer. Product managers maintained separate tracking for each platform. We weren't moving as quickly as we wanted to.
Beyond the obvious duplication of effort, subtler problems emerged. Design inconsistencies crept in as each platform interpreted shared specifications slightly differently. The user experience diverged. Onboarding a new mobile engineer meant choosing a platform first, effectively limiting their scope to half the product. Having two separate codebases doubled the opportunities for subtle bugs hiding in platform-specific implementations.
The solution was a coordinated move from Native to React Native. Now, we’re sharing how we evolved, the wins, the challenges and lessons learned along the way.
What is React Native, and why?
React Native is an open-source UI framework originally created by Meta. It allows developers to build cross-platform mobile applications using JavaScript and React, a popular JavaScript library for building user interfaces.
When we began evaluating cross-platform solutions, React Native wasn't an automatic choice. We considered three paths: staying native with improved processes and tooling, exploring Flutter, or adopting React Native. Each had compelling arguments.
The Business Imperative
The business case for consolidation was straightforward. Our roadmap had increased significantly in ambition while our engineering team had grown only modestly. Building features twice meant we could only ship half as many features, or we'd need to double our team size. Neither option was viable.
We needed to:
Reduce time-to-market significantly. Launching a new feature required coordinated releases across all platforms simultaneously.
Eliminate duplicate engineering effort. Every hour spent implementing the same feature twice was an hour not spent on innovation or quality improvements.
Enable faster experimentation. The ability to test ideas quickly across all users and platforms was a strategic advantage.
Simplify our technology stack. Two platforms meant two sets of dependencies, two sets of build pipelines, two sets of everything. The operational overhead was becoming unsustainable.
The Technical Drivers
The technical case for React Native was equally compelling, particularly given our existing strengths:
TypeScript Everywhere. Our team already had deep TypeScript expertise. React Native's first-class TypeScript support meant we could leverage that knowledge across mobile development. Type safety at the scale of a financial application is essential.
React Ecosystem. We weren't just building a mobile app in isolation. We had React web applications, and the promise of sharing not just patterns, but code, state management approaches, and even some components, was tantalizing. The React ecosystem's maturity, particularly around state management (Redux Toolkit), form handling, and UI component libraries, gave us a head start.
JavaScript Talent Pool. Hiring was always a constraint. The pool of experienced iOS or Android engineers is relatively small. JavaScript/TypeScript engineers are far more abundant, and the learning curve from React Web to React Native is gentler than from backend engineering to native mobile development.
The Three-Platform Vision
Here's where our story diverges from typical React Native adoption: we didn't just want to consolidate iOS and Android. We had a web application that needed to share the same UI components, business logic, and user experience. React Native Web emerged as the dark horse solution that let us write once and deploy to three platforms: iOS, Android, and Web.
Architecture: Building for Scale and Maintainability
The architectural decisions we made early in our React Native adoption proved important to our long-term evolution. While React Native provided the framework, we still needed to build the castle.
These were our principles:
Module-Based Architecture: Each feature lives in its own self-contained module with everything it needs: screens, components, hooks, utilities, and business logic. This enables parallel development, clear module ownership, easier testing and maintenance over time.
Shared Design System: Before building a single screen, we invested heavily in a custom design system. This became the foundation that made three-platform development viable. It provides reusable components that adapt to each platform, a type-safe theming system, a set of responsive layout primitives, and consistent typography, spacing and color schemes.
Type Safety Across the Board: TypeScript strict mode is non-negotiable in our codebase, but we took it further with platform-specific TypeScript configurations for iOS, Android and Web specific type checking. This lets us catch platform-specific type errors earlier, at compile time.
Native Modules When JavaScript Isn't Enough: React Native's JavaScript bridge is powerful, but some operations demand native code.
Predictable State Management with Redux Toolkit: For an application with complex state requirements, we chose Redux Toolkit with RTK Query for server state management. It provides predictable state updates, time-travel debugging, strong TypeScript integration and comes with great developer tools.
The Third Platform: Web
React Native Web required a significant investment to get right. The promise is compelling: to provide a truly omnichannel experience, write React Native code and run it in web browsers just as well as you do for iOS and Android. We used strategies, such as having different Webpack Configuration per platform, Code Splitting for performance, and Multiple Entry Points for different flows.The reality is far more nuanced.
React Native Web isn't magic and we faced challenges: growing bundle sizes, lacking web support in some libraries, CSS-in-JS overhead, and SEO limitations.
Despite these challenges, the ability to share ~85% of our code between mobile and web made React Native Web worthwhile. The alternative was to maintain a completely separate React web
The Migration: A Complete Rebuild in 3 Months
Our goal was to rebuild the entire application in React Native from scratch in just three months, achieving 100% feature parity with our native apps plus a complete design refresh. No incremental migration, no hybrid apps but a full, focused, uninterrupted rewrite.
During the rollout, we watched several metrics obsessively in real-time, including crash rates, startup times, render times, memory usage, error rates and above all, app store ratings and reviews.
Challenges and Solutions
React Native delivered on its promise, but not without obstacles.
Performance: We experienced performance issues both with List rendering and animations. We had screens displaying hundreds of transactions, contacts, or investment positions. React Native's standard FlatList struggled with smooth scrolling, particularly on mid-range Android devices. We quickly shifted to Shopify's FlashList, which uses a different rendering strategy that avoids most layout thrashing. To improve animation performance, we adopted react-native-reanimated, which runs animations on the native UI thread and delivers superior performance.
Native Module Gaps: Some functionality simply didn't have React Native libraries, particularly around security and payments. We wrote our own bridges. While this requires native development knowledge, it lets us integrate any native SDK without compromise.
Bundle Size: Our React Native bundle size increased quickly in the beginning, and it impacted startup time, particularly on Android. We opted in for Code Splitting (Web), Hermes Engine (Android), and strict dependency auditing and native module lazy loading.
Debugging Across Platforms: Debugging issues that only appeared on one platform was initially frustrating. Different JavaScript engines (JavaScriptCore on iOS, V8/Hermes on Android, V8 on Web) occasionally revealed subtle bugs. We implemented comprehensive instrumentation and monitoring for performance, errors, and enabled distributed tracing throughout the system.
Training and Organizational Change: Our iOS and Android engineers needed to learn React Native. We started with pairing, internal workshops and documentation, and gradual feature ownership transition.
Managing Quality Across Platforms
Testing three platforms from one codebase required a sophisticated, multi-layered approach.
We started with unit and component testing, using Jest with React Native Testing Library. Our strict requirement: every component and utility function must have tests. TypeScript catches type errors, but tests catch logic errors.
We then moved to the next quality layer, end-to-end testing. We use Detox for iOS and Android. It provides real-device testing using simulators, native-level synchronization, and cross-platform test scripts with platform-specific overrides.
We additionally use Playwright for Web. While we tried to share test code between Detox and Playwright, the APIs are too different. Instead, we share test scenarios and data, but implement them separately for each platform.
Final Thoughts
The migration from Native to React Native was a sound technical decision as it improved our development velocity. The unified codebase, shared design system, and ability to deploy to three platforms from one source improved our ability to offer a true omni-channel experience to our customers. Looking ahead, React Native’s new Architecture, featuring Fabric rendering and Turbo modules bring even stronger performance improvements and native integration capabilities, and we are looking forward to bringing these enhancements to our users fingertips.