Crypto Exchange Mobile App Development
A mobile app for an exchange is not an adapted web version. It's a separate product with different UX, different security requirements, and different technical constraints. A user on a phone trades differently: quick actions, smaller screen, biometrics instead of password.
Technology Selection
React Native vs Flutter vs Native
React Native (Meta) — our main choice for exchanges with existing web frontend on React. Code reuse, single team. Expo for quick start. Bare React Native for production with custom native modules.
Flutter (Google) — best choice with zero existing JS code. Dart, Skia rendering (looks identical on iOS and Android), good performance. Downside: fewer crypto-specific libraries.
Native (Swift/Kotlin) — maximum performance and API access, but two separate codebases. Justified only with very high performance requirements (HFT-level).
For most exchanges: React Native + Expo to start, migrate to bare workflow when custom native modules needed (secure enclave, biometrics, push notifications).
App Architecture
State Management
Exchange app state complexity is high: real-time tickers, order book, trade history, balances, active orders. All update via WebSocket.
// Zustand for local state + React Query for server state
import { create } from 'zustand';
import { subscribeWithSelector } from 'zustand/middleware';
interface MarketStore {
tickers: Record<string, Ticker>;
orderBook: OrderBook | null;
lastPrice: Record<string, string>;
updateTicker: (pair: string, ticker: Ticker) => void;
updateOrderBook: (book: OrderBook) => void;
}
const useMarketStore = create<MarketStore>()(
subscribeWithSelector((set) => ({
tickers: {},
orderBook: null,
lastPrice: {},
updateTicker: (pair, ticker) =>
set((state) => ({ tickers: { ...state.tickers, [pair]: ticker } })),
updateOrderBook: (book) => set({ orderBook: book }),
}))
);
WebSocket manager auto-reconnects on disconnect, deduplicates subscriptions, restores state:
class WSManager {
private ws: WebSocket | null = null;
private subscriptions = new Set<string>();
private reconnectTimer: NodeJS.Timeout | null = null;
connect(url: string) {
this.ws = new WebSocket(url);
this.ws.onopen = () => {
// Restore all subscriptions after reconnect
this.subscriptions.forEach(sub => this.ws!.send(sub));
};
this.ws.onclose = () => {
this.reconnectTimer = setTimeout(() => this.connect(url), 3000);
};
this.ws.onmessage = (e) => this.handleMessage(JSON.parse(e.data));
}
}
Navigation
React Navigation v6 — standard for React Native. Structure for exchange:
Root Navigator (Stack)
├── Auth Stack
│ ├── Login
│ ├── Register
│ └── 2FA Setup
└── Main Tabs
├── Home (Markets / Watchlist)
├── Trade
│ ├── Chart Screen
│ ├── Order Book
│ └── Place Order
├── Wallets
│ ├── Balance Overview
│ ├── Deposit
│ └── Withdraw
├── Orders (History + Active)
└── Profile
Deep linking for push notifications: exchange://trade/BTC-USDT opens trading screen directly.
Key Screens and Components
Trading Screen
Most complex screen. Includes:
-
TradingView chart via
react-native-webviewwith TradingView Lightweight Charts inside — industry standard for mobile exchanges - Order book with real-time updates — needs render optimization
- Order form with calculator, percentage input, validation
Order book render optimization:
// FlashList instead of FlatList — 5-10x faster for long lists
import { FlashList } from "@shopify/flash-list";
const OrderBookRow = React.memo(({ price, size, total, side }: OrderBookRowProps) => {
// Depth visualizer bar via absolute positioning
const depthWidth = `${(total / maxTotal) * 100}%`;
return (
<View style={styles.row}>
<View style={[styles.depthBar, { width: depthWidth,
backgroundColor: side === 'bid' ? '#0d2d1c' : '#2d0d0d' }]} />
<Text style={[styles.price, { color: side === 'bid' ? '#00b15e' : '#e84242' }]}>
{formatPrice(price)}
</Text>
<Text style={styles.size}>{formatSize(size)}</Text>
</View>
);
});
Deposit/Withdrawal Screen
QR code for deposit via react-native-qrcode-svg. QR scanning on withdrawal — expo-camera or react-native-vision-camera (more performant).
Adding new withdrawal address requires 2FA verification right in the app.
Security
Biometrics and Secure Storage
Biometric authentication — mandatory for exchange:
import * as LocalAuthentication from 'expo-local-authentication';
import * as SecureStore from 'expo-secure-store';
async function authenticateWithBiometrics(): Promise<boolean> {
const hasHardware = await LocalAuthentication.hasHardwareAsync();
const isEnrolled = await LocalAuthentication.isEnrolledAsync();
if (!hasHardware || !isEnrolled) {
return fallbackToPIN();
}
const result = await LocalAuthentication.authenticateAsync({
promptMessage: 'Confirm identity for logout',
disableDeviceFallback: false,
cancelLabel: 'Cancel',
});
return result.success;
}
// JWT token stored in SecureStore (iOS Keychain / Android Keystore)
async function saveToken(token: string) {
await SecureStore.setItemAsync('auth_token', token, {
keychainAccessible: SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
});
}
SecureStore uses iOS Keychain and Android Keystore — hardware encryption. Never store tokens in AsyncStorage — it's unencrypted.
Certificate Pinning
Prevents MITM attacks. For React Native via react-native-ssl-pinning:
fetch('https://api.exchange.com/v1/orders', {
sslPinning: {
certs: ['sha256/AAAA...'], // SHA-256 fingerprint of certificate
},
});
On certificate rotation, need to update the app — better to pin intermediate CA, not leaf certificate.
Jailbreak/Root Detection
Compromised device — elevated risk. Basic check:
import JailMonkey from 'jail-monkey';
if (JailMonkey.isJailBroken()) {
Alert.alert(
'Security Warning',
'Device is modified. Recommend using app only on standard devices.',
);
}
Solution: warn but don't block completely — otherwise legitimate developer users get blocked.
Push Notifications
Firebase Cloud Messaging (FCM) — standard for Android. APNs — for iOS. Expo Notifications abstracts both.
Notification types for exchange:
- Order filled / partially filled
- Watchlist price alert triggered
- Withdrawal confirmed
- New login from device
- Suspicious activity
// Register and get push token
const { status } = await Notifications.requestPermissionsAsync();
if (status === 'granted') {
const token = await Notifications.getExpoPushTokenAsync({
projectId: Constants.expoConfig.extra.eas.projectId,
});
await api.registerPushToken(token.data);
}
Server-side: on each event (fill, withdrawal) — enqueue to queue (Redis + Bull), worker sends via FCM/APNs API.
Performance Optimizations
Hermes engine — enabled by default in React Native 0.70+. Fast startup, less memory.
Image optimization: coin icons — expo-image with caching instead of standard <Image>. Lazy loading for long lists.
Worklets via Reanimated: price animations (flash green/red on update) — on UI thread via react-native-reanimated, without blocking JS thread:
const priceColor = useDerivedValue(() => {
return withTiming(
priceChange.value > 0 ? '#00b15e' : '#e84242',
{ duration: 300 }
);
});
Bundle splitting: Expo Router supports lazy loading screens. Trade screen loads only when Trade tab is opened.
Build and Distribution
EAS Build (Expo Application Services) — cloud build without local Xcode/Android Studio setup:
# iOS production build
eas build --platform ios --profile production
# Android AAB for Google Play
eas build --platform android --profile production
OTA Updates (Expo Updates) — update JS bundle without store release. Bug fixes — in minutes. New features — still need store review.
CI/CD: GitHub Actions → EAS Build → TestFlight (iOS) / Internal Testing (Android) → Production.
| Aspect | Technology | Note |
|---|---|---|
| Framework | React Native + Expo | Bare workflow for custom modules |
| Charts | TradingView Lightweight Charts via WebView | Industry standard |
| State | Zustand + React Query | Simplicity + server state |
| Auth storage | expo-secure-store | Keychain/Keystore |
| Push | Expo Notifications + FCM/APNs | Cross-platform |
| Build | EAS Build | Cloud CI/CD |
Development Timeline
- MVP (Markets, Trade, Wallet): 10–14 weeks
- Full feature set with biometrics, push notifications, alerts, KYC: 5–7 months
- App Store + Google Play submission: +2–3 weeks for review and fixes







