React Native mobile application for Stellar Insights payment analytics.
- 📱 Cross-platform (iOS & Android)
- 🔐 Secure authentication with SEP-10
- 🌐 Network switching (testnet/mainnet)
- 📴 Offline-first architecture
- 🔔 Push notifications via Firebase Cloud Messaging
- 🔒 Biometric authentication
- 🎨 Native UI patterns
- Node.js 18+
- React Native CLI
- Xcode 14+ (for iOS)
- Android Studio Hedgehog+ (for Android)
- CocoaPods (for iOS)
- A Firebase project with iOS and Android apps registered
- Install dependencies:
npm install- Install iOS pods:
cd ios && pod install && cd ..- Configure environment:
cp .env.example .env
# Edit .env with your configuration- Run the app:
# iOS
npm run ios
# Android
npm run android- Go to Firebase Console and create a new project (or use an existing one).
- Register an Android app and an iOS app under the same project.
- Download
google-services.jsonfrom the Firebase console (Project Settings → Android app). - Place it at
android/app/google-services.json. - Confirm
android/build.gradlecontains the Google Services classpath:
dependencies {
classpath 'com.google.gms:google-services:4.4.0'
}- Confirm
android/app/build.gradleapplies the plugin at the bottom:
apply plugin: 'com.google.gms.google-services'- Rebuild the Android project:
cd android && ./gradlew clean && cd ..
npm run android- Download
GoogleService-Info.plistfrom the Firebase console (Project Settings → iOS app). - Open Xcode:
open ios/StellarInsights.xcworkspace - Drag
GoogleService-Info.plistinto theStellarInsightstarget in Xcode (check "Copy items if needed"). - Ensure Push Notifications and Background Modes → Remote notifications capabilities are enabled in Xcode (Signing & Capabilities tab).
- Re-run pod install:
cd ios && pod install && cd ..
npm run iosAdd the following to your .env (values from Firebase console):
FIREBASE_API_KEY=your_api_key
FIREBASE_PROJECT_ID=your_project_id
FIREBASE_APP_ID=your_app_id
- iOS simulator: FCM token is issued but remote push delivery is blocked on simulator. Use a physical device or APNs sandbox for end-to-end validation.
- Android emulator: Requires Google Play Services. Use an AVD image with Play Store.
- Request permission and log the FCM token in development to confirm registration:
import messaging from '@react-native-firebase/messaging';
const token = await messaging().getToken();
console.log('FCM token:', token); // send a test message from Firebase consoleThe app follows an offline-first pattern: all reads are served from a local cache and network data refreshes the cache in the background.
| Data type | Storage layer | TTL |
|---|---|---|
| Corridors list | MMKV (via useOfflineCaching) |
24 hours |
| Anchors list | MMKV (via useOfflineCaching) |
24 hours |
| Dashboard stats | React Query in-memory + MMKV | 24 hours |
| Auth token | Platform keychain | Until expiry |
| Token expiry | AsyncStorage | Until expiry |
useOfflineCaching(src/hooks/useOfflineCaching.ts) wraps React Query with a persistent MMKV layer. Cache entries are JSON-serialised with anexpiresAttimestamp. Expired entries are pruned on read.useOfflineQueue(src/hooks/useOfflineQueue.ts) holds write operations (e.g. pending transactions) while the device is offline.- Network state is tracked by
@react-native-community/netinfoviasrc/services/network.ts.
When connectivity is restored:
MobileContractServicefires aprocessQueuesweep for any queued transactions.- React Query's
refetchOnReconnectflag triggers background refetches for stale queries. usePullToRefreshlets users manually trigger a full invalidation when online.
- Read failures are logged as warnings via the structured logger and the stale in-memory cache is returned.
- Write failures are logged; the app continues with the last valid cached state.
- If offline cache is fully unavailable, the UI renders a "no data available offline" empty state.
- Stale data is marked with staleness indicators in the UI.
NetworkStatusIndicatorshows a banner when the device is offline.
Use react-native-config to load environment-specific .env files:
| File | Usage |
|---|---|
.env |
Local development |
.env.staging |
Staging / QA |
.env.production |
Production release |
Set ENVFILE=.env.production when running release builds (see platform steps below).
Generate a keystore (one-time setup):
keytool -genkeypair -v \
-storetype PKCS12 \
-keystore android/app/release.keystore \
-alias stellar-insights \
-keyalg RSA -keysize 2048 -validity 10000Configure signing in android/app/build.gradle:
android {
signingConfigs {
release {
storeFile file('release.keystore')
storePassword System.getenv("KEYSTORE_PASSWORD")
keyAlias 'stellar-insights'
keyPassword System.getenv("KEY_PASSWORD")
}
}
buildTypes {
release {
signingConfig signingConfigs.release
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}Build release APK / AAB:
# APK (for direct install / testing)
ENVFILE=.env.production cd android && ./gradlew assembleRelease
# AAB (for Google Play submission)
ENVFILE=.env.production cd android && ./gradlew bundleReleaseOutput: android/app/build/outputs/bundle/release/app-release.aab
Configure signing in Xcode:
- Open
ios/StellarInsights.xcworkspacein Xcode. - Set the team and provisioning profile under Signing & Capabilities.
- Set the scheme to Release: Product → Scheme → Edit Scheme → Run → Build Configuration = Release.
Archive and export:
# Build archive
ENVFILE=.env.production xcodebuild \
-workspace ios/StellarInsights.xcworkspace \
-scheme StellarInsights \
-configuration Release \
-archivePath ios/build/StellarInsights.xcarchive \
archive
# Export IPA (requires ExportOptions.plist)
xcodebuild -exportArchive \
-archivePath ios/build/StellarInsights.xcarchive \
-exportPath ios/build/export \
-exportOptionsPlist ios/ExportOptions.plistUpload to TestFlight:
xcrun altool --upload-app \
--type ios \
--file ios/build/export/StellarInsights.ipa \
--username "$APPLE_ID" \
--password "$APPLE_APP_SPECIFIC_PASSWORD"| Symptom | Fix |
|---|---|
| Metro bundler cache stale | npm start -- --reset-cache |
iOS pod conflicts after npm install |
cd ios && pod deintegrate && pod install |
| Android build cache issues | cd android && ./gradlew clean |
google-services.json not found |
Confirm file is at android/app/google-services.json |
GoogleService-Info.plist not found |
Confirm file is in Xcode project tree (not just filesystem) |
| FCM token not issued on iOS | Enable Push Notifications capability in Xcode |
mobile/
├── src/
│ ├── components/ # Reusable UI components
│ ├── screens/ # Screen components
│ │ ├── auth/ # Authentication screens
│ │ └── main/ # Main app screens
│ ├── navigation/ # Navigation configuration
│ ├── services/
│ │ ├── api.ts # REST API client
│ │ ├── auth.ts # SEP-10 authentication
│ │ ├── database.ts # Local DB initialization
│ │ ├── initialization.ts # App bootstrap
│ │ ├── logger.ts # Structured logging service
│ │ ├── network.ts # Network state monitoring
│ │ ├── notifications.ts # FCM / Notifee setup
│ │ └── tokenStorage.ts # Keychain token persistence
│ ├── hooks/
│ │ ├── useOfflineCaching.ts # MMKV-backed offline cache
│ │ ├── useOfflineQueue.ts # Write-op queue for offline mode
│ │ └── usePullToRefresh.ts # Pull-to-refresh with query invalidation
│ ├── store/ # Zustand state (auth, app)
│ ├── types/ # TypeScript types
│ ├── config/ # App constants
│ └── App.tsx # Root component
├── android/ # Android native code
├── ios/ # iOS native code
└── package.json
| Package | Purpose |
|---|---|
@react-native-firebase/messaging |
FCM push notifications |
@notifee/react-native |
Local notification display |
@tanstack/react-query |
Server-state fetching and caching |
react-native-mmkv |
Fast synchronous local storage |
react-native-keychain |
Secure credential storage (keychain/keystore) |
react-native-config |
Environment variable management |
zustand |
Lightweight global state |
axios |
HTTP client |
npm testnpm run type-checknpm run lintAll modules use a centralised structured logger (src/services/logger.ts) instead of raw console.* calls:
import { createScopedLogger } from '@services/logger';
const log = createScopedLogger('MyModule');
log.info('Something happened');
log.warn('Degraded state', { reason: 'cache miss' });
log.error('Request failed', error, { endpoint: '/api/data' });- Development: all levels printed to console with timestamps and platform tag.
- Production:
debug/infosuppressed;warn/erroralways emitted; errors forwarded to Crashlytics. - Sensitive data (Stellar keys, JWTs, email addresses) is automatically redacted before output.
- Auth tokens stored in platform keychain (iOS Keychain / Android Keystore).
- Biometric authentication support.
- Certificate pinning enabled in production builds.
- Structured logger redacts PII and Stellar keys automatically.
- Go to Settings.
- Tap "Current Network".
- Select testnet or mainnet.
- The app clears its cache and reconnects.
See the root CONTRIBUTING.md.
See the root LICENSE.