Skip to content

fubaritico/react-and-react-native-financial-app

Repository files navigation

React & React Native Financial App

A cross-platform Personal Finance application (Frontend Mentor challenge) built on a shared design system targeting React Native (Expo) and React web (React Router), managed with pnpm workspaces.

Technologies

React Native · Expo SDK 54 · React · TypeScript · pnpm · Turborepo · twrnc · Tailwind CSS · Style Dictionary · CVA · Supabase · Jotai · TanStack Query · Expo Router · react-hook-form · zod


Table of Contents


About this project

This project started from the perspective of a React developer asking: "How do I build a consistent design across web and mobile?"

The answer is a shared design system (@financial-app/ui) where styles are shared between web and native component implementations via a file extension split (.web.tsx / .native.tsx). Styling uses Tailwind CSS on web and twrnc on native, with shared variant logic through CVA (class-variance-authority). This keeps both platforms visually aligned while respecting each renderer's idioms. A migration to NativeWind (Tailwind v4 + unified className) is planned once it reaches stable release.

Three mobile apps coexist for comparison: mobile (bare React Native CLI), mobile-expo (Expo managed — the canonical app), and mobile-expo-ejected (ejected Expo). A web app uses React Router v7 in framework mode. An API server built on Express delivers the backend.

Business logic shared across all apps (auth, state, types, utils) lives in the shared package. Screen-specific compositions of design system primitives — configured DataTables, modal content, overview sections — live in the features package, keeping both the design system and the apps focused on their own concerns.

Back to top


Project Structure

react-and-react-native-financial-app/
├── package.json            # Monorepo root config
├── pnpm-workspace.yaml     # pnpm workspaces + catalog
├── turbo.json              # Turborepo task pipeline
├── apps/
│   ├── mobile-expo/        # Expo SDK 54 (canonical mobile app)
│   ├── web/                # React Router v7 + Vite (SSR)
│   ├── storybook/          # Storybook — component browser (web + native)
│   ├── mobile/             # Bare React Native CLI (learning reference)
│   └── mobile-expo-ejected/ # Ejected Expo (learning reference)
├── packages/
│   ├── tokens/             # @financial-app/tokens — Style Dictionary (DTCG)
│   ├── tailwind-config/    # @financial-app/tailwind-config — shared Tailwind config
│   ├── icons/              # @financial-app/icons — cross-platform SVG icon library
│   ├── ui/                 # @financial-app/ui — cross-platform design system
│   ├── features/           # @financial-app/features — screen-specific composed blocks
│   └── shared/             # @financial-app/shared — auth, types, utils, atoms
└── scripts/                # Utility scripts (reset, rebuild, changelogs)

Back to top


Packages

Apps

App Path Status Description
mobile-expo apps/mobile-expo/ Active Expo managed (SDK 54) — canonical mobile app, primary focus
web apps/web/ Active React Router v7 + Vite with SSR
mobile apps/mobile/ Active Bare React Native CLI — learning reference
mobile-expo-ejected apps/mobile-expo-ejected/ Active Expo bare/ejected — learning reference
storybook apps/storybook/ Active Component browser (web + native stories via react-native-web)
api apps/api/ Planned Express REST API (OpenAPI + zod-to-openapi)

Shared Packages

Package Path Status Description
@financial-app/tokens packages/tokens/ Active Style Dictionary (DTCG) — colors, spacing, typography, radii
@financial-app/tailwind-config packages/tailwind-config/ Active Shared Tailwind config consuming token outputs
@financial-app/icons packages/icons/ Active Cross-platform SVG icon library — type-safe <Icon name="..." /> component
@financial-app/ui packages/ui/ Active Cross-platform design system (file extension split: .native.tsx / .web.tsx)
@financial-app/features packages/features/ Active Screen-specific composed blocks (overview, transactions, budgets, pots)
@financial-app/shared packages/shared/ Active Auth (Supabase), Jotai atoms, domain types, utils
@financial-app/http-client packages/http-client/ Planned HeyAPI client consuming the Express REST API

Dependency Graph

@financial-app/tokens           -> depends on nothing
@financial-app/icons            -> depends on nothing (react-native-svg is a peer dep)
@financial-app/tailwind-config  -> @financial-app/tokens
@financial-app/ui               -> @financial-app/tokens, @financial-app/tailwind-config, @financial-app/icons
@financial-app/features         -> @financial-app/ui, @financial-app/tailwind-config
@financial-app/shared           -> depends on nothing (pure TS, no renderer)
apps/*  (mobile, web)           -> @financial-app/features, @financial-app/ui, @financial-app/shared, @financial-app/icons

Back to top


Prerequisites

  • Node.js (v18+ recommended)
  • pnpm: npm install -g pnpm
  • Ruby (v3.1.x recommended): for CocoaPods (iOS)
  • Xcode (macOS): for iOS
  • Android Studio: for Android

Back to top


Environment Setup (macOS)

This section details the steps to configure the React Native development environment on macOS.

1. Install pnpm

npm install -g pnpm

2. Install Ruby 3.1 via rbenv

The system Ruby version (2.6) is too old for CocoaPods. Ruby 3.4+ can also cause compatibility issues. Ruby 3.1.x is recommended.

# Install rbenv
brew install rbenv

# Install Ruby 3.1.4
rbenv install 3.1.4

# Set as global version
rbenv global 3.1.4

# Add rbenv to shell (once only)
echo 'eval "$(rbenv init - zsh)"' >> ~/.zshrc && source ~/.zshrc

# Verify version
ruby -v  # Should display ruby 3.1.4

3. Configure Xcode (iOS)

# Select Xcode as the active build tool
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer

Open Xcode at least once to accept the license and install additional components (iOS simulators).

4. Configure Android Studio (Android)

  1. Install Android Studio
  2. Open Android Studio -> More Actions -> Virtual Device Manager
  3. Create an emulator (e.g. Pixel 7, API 34)

Configure environment variables:

# Java 17 (required by Gradle 9)
brew install openjdk@17
sudo ln -sfn /opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk /Library/Java/JavaVirtualMachines/openjdk-17.jdk
echo 'export JAVA_HOME="/opt/homebrew/opt/openjdk@17"' >> ~/.zshrc

# Android SDK
echo 'export ANDROID_HOME=$HOME/Library/Android/sdk' >> ~/.zshrc
echo 'export PATH=$PATH:$ANDROID_HOME/emulator:$ANDROID_HOME/platform-tools' >> ~/.zshrc
source ~/.zshrc

Verify:

java -version   # Should display openjdk 17.x.x
adb --version   # Should display Android Debug Bridge

5. Install project dependencies

# From the monorepo root
pnpm install

6. Install iOS Pods

# In apps/mobile
cd apps/mobile
bundle install
bundle exec pod install --project-directory=ios

7. Run the app

# Start Metro (in a dedicated terminal)
pnpm --filter mobile-financial-app start

# In another terminal:
pnpm --filter mobile-financial-app ios      # iOS (simulator)
pnpm --filter mobile-financial-app android  # Android (emulator must be running)

Back to top


Main Commands

All orchestrated commands use Turborepo for caching and correct dependency ordering.

Install & build

pnpm install       # Install all dependencies
pnpm build         # Build all packages (tokens → tailwind-config → ui → apps)
pnpm tokens        # Rebuild token outputs only
pnpm icons         # Regenerate icon data from SVGs

Quality checks

pnpm type-check    # TypeScript --noEmit across all packages
pnpm lint          # ESLint across all packages
pnpm test          # Jest tests across all packages

Daily development

For day-to-day JS/TS work (editing components, screens, styles, logic), Metro hot-reloads changes instantly — no rebuild needed. Just keep Metro running in one terminal.

# Web
pnpm web:dev                   # React Router dev server

# Expo (canonical mobile)
pnpm expo:start                # Metro bundler (press i/a for iOS/Android)
pnpm expo:ios                  # Build + launch on default iPhone simulator
pnpm expo:ios:iphone           # iPhone 16 Pro simulator
pnpm expo:ios:ipad             # iPad Pro 11-inch (M4) simulator
pnpm expo:android              # Build + launch on default Android emulator
pnpm expo:android:phone        # Small_Phone AVD
pnpm expo:android:tablet       # Medium_Tablet AVD
pnpm expo:ios:device           # Pick from connected iOS devices
pnpm expo:android:device       # Pick from connected Android devices

# Bare RN CLI
pnpm mobile:start              # Metro bundler
pnpm mobile:ios                # Build + launch on iOS simulator
pnpm mobile:android            # Build + launch on Android emulator

# Storybook
pnpm storybook                 # Component browser (web + native stories)

These commands assume a native binary is already installed on the simulator/emulator. If it's your first run or you changed native dependencies, use a rebuild command instead.

Rebuild (clean native build from scratch)

Use rebuild when the native layer changed — adding/removing a native library, changing build.gradle, Podfile, app.json native config, or upgrading RN/Expo SDK. Also use it when a build fails for no obvious reason (cache corruption).

For pure JS/TS changes, you do not need this — Metro hot-reloads automatically.

Android

rebuild:android (scripts/rebuild-android.sh) is a single command that handles everything:

  1. Stops Gradle daemon (cached JVM that survives rm -rf on disk)
  2. Kills stale Metro on port 8081 (if any)
  3. Cleans Gradle build dirs (.gradle/, build/, .cxx/)
  4. Cleans Metro/Haste/RN temp caches + Watchman
  5. Runs expo prebuild --clean (Expo only, skipped for bare RN)
  6. Starts Metro in background, waits for it to be ready
  7. Builds and launches on emulator
# Bare RN CLI
pnpm mobile:rebuild:android           # default emulator
pnpm mobile:rebuild:android:phone     # Small_Phone AVD
pnpm mobile:rebuild:android:tablet    # Medium_Tablet AVD

# Expo managed
pnpm expo:rebuild:android             # default emulator
pnpm expo:rebuild:android:phone       # Small_Phone AVD
pnpm expo:rebuild:android:tablet      # Medium_Tablet AVD

First build after a clean takes ~5 min (Gradle re-downloads dependencies). Subsequent runs reuse the global Gradle cache and finish faster.

After the script finishes, Metro keeps running in the background. The output shows the PID and the command to stop it.

iOS

pnpm mobile:rebuild:ios        # Bare RN CLI — clean + pod install + build + launch
pnpm expo:rebuild:ios          # Expo managed — clean + prebuild + build + launch

Reset (nuclear — full project clean)

Use pnpm reset when everything is broken and you want to simulate a fresh clone. It removes all node_modules, caches, build outputs, reinstalls everything, and rebuilds tokens. See docs/modus-operandi/reset.md for the full checklist.

pnpm reset         # Full clean + reinstall + pod install + token rebuild
pnpm clean         # Remove all node_modules only
pnpm clean:build   # Remove all build/dist outputs only

After a reset, you still need to rebuild native binaries — use the rebuild commands above.

Back to top


How the Monorepo Works

pnpm workspaces + Turborepo

The monorepo uses pnpm workspaces for dependency management and Turborepo for task orchestration.

# pnpm-workspace.yaml
packages:
  - "packages/*"
  - "apps/*"

Turborepo ensures tasks run in the correct dependency order with caching:

pnpm build → tokens → tailwind-config → ui → shared → web (parallel where possible)

Cached tasks replay instantly — a full pnpm type-check with warm cache runs in ~30ms.

Add a dependency to a package

# Add a dependency to a specific package
pnpm --filter mobile-expo-financial-app add <package-name>

# Add a dev dependency
pnpm --filter mobile-expo-financial-app add -D <package-name>

Add a dependency to the root (shared tools)

pnpm add -w -D <package-name>

Cross-platform component architecture

UI components use a file extension split pattern — shared types + CVA variants with platform-specific implementations:

packages/ui/src/components/Button/
  Button.tsx           # Types + props interface only (no JSX)
  Button.native.tsx    # React Native implementation (twrnc)
  Button.web.tsx       # DOM/HTML implementation (Tailwind CSS + cn())
  index.ts             # Native barrel (Metro picks this)
  index.web.ts         # Web barrel (Vite picks this)

Styling: twrnc on native, Tailwind CSS on web. Shared variant logic via CVA (class-variance-authority).

Back to top


React Native Version Alignment (0.81.5)

Why React Native 0.81.5?

This monorepo contains multiple apps:

  • mobile: React Native CLI (without Expo)
  • mobile-expo: Expo managed (SDK 54)
  • mobile-expo-ejected: Expo bare/ejected
  • ui: Shared components with twrnc (Tailwind for RN)

Expo SDK 54 requires React Native 0.81.x. To avoid multiple version conflicts in the monorepo (which cause "Invalid hook call" errors and crashes), all apps must use the same React Native version.

The Problem Encountered

Initially, mobile used RN 0.82.1 (latest version) while Expo SDK 54 used RN 0.81.5. This caused:

  • 3 versions of react-native in node_modules/.pnpm
  • "Invalid hook call" errors due to multiple React instances
  • Android crashes with library "libreact_featureflagsjni.so" not found

The Solution

1. Force a single version with pnpm overrides

In the root package.json:

{
  "pnpm": {
    "overrides": {
      "react-native": "0.81.5",
      "@react-native/babel-plugin-codegen": "0.81.5",
      "@react-native/babel-preset": "0.81.5",
      "@react-native/codegen": "0.81.5",
      "@react-native/js-polyfills": "0.81.5",
      "@react-native/metro-babel-transformer": "0.81.5",
      "@react-native/metro-config": "0.81.5"
    }
  }
}

2. Align versions in each package.json

In apps/mobile/package.json, apps/mobile-expo/package.json, etc.:

{
  "dependencies": {
    "react-native": "0.81.5"
  },
  "devDependencies": {
    "@react-native-community/cli": "20.0.0",
    "@react-native/babel-preset": "0.81.5",
    "@react-native/metro-config": "0.81.5",
    "@react-native/typescript-config": "0.81.5"
  }
}

3. Regenerate the Android folder (if created with a different version)

If the android/ folder was created with RN 0.82, it contains incompatible files. Solution:

cd apps/mobile
mv android android_backup
npx @react-native-community/cli init mobile --version 0.81.5 --skip-install --directory temp_mobile
mv temp_mobile/android .
rm -rf temp_mobile

Checking installed versions

# See how many react-native versions are installed
ls node_modules/.pnpm | grep "^react-native@0"

# Check version in a package
cat apps/mobile/node_modules/react-native/package.json | grep version

Android Debugging

To view Android crash logs:

# Clear old logs
adb logcat -c

# Record logs to file
adb logcat > /tmp/crash.log

# (Launch the crashing app, then Ctrl+C)

# View errors
grep -A 10 "FATAL EXCEPTION" /tmp/crash.log

Back to top


Troubleshooting

General (iOS & Android)

"Unable to resolve module" error in simulator/emulator

In a pnpm monorepo, dependencies are hoisted to the root. Metro must be configured to find them.

The apps/mobile/metro.config.js file must include:

const path = require('path');
const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');

const monorepoRoot = path.resolve(__dirname, '../..');

const config = {
  watchFolders: [monorepoRoot],
  resolver: {
    nodeModulesPaths: [
      path.resolve(__dirname, 'node_modules'),
      path.resolve(monorepoRoot, 'node_modules'),
    ],
  },
};

module.exports = mergeConfig(getDefaultConfig(__dirname), config);

After modification, restart Metro (Ctrl + C then pnpm --filter mobile-financial-app start).

"Unable to load script" or white screen / splash screen stuck

Metro is not running or the emulator can't reach it.

If you used a rebuild command, Metro is started automatically — check the output for Metro is ready (PID ...). If you used a regular run command, start Metro manually:

# Terminal 1: start Metro
pnpm mobile:start   # or pnpm expo:start

# Terminal 2: build + launch
pnpm mobile:android  # or pnpm expo:android

If Metro is running but the emulator still can't connect, forward the port:

adb reverse tcp:8081 tcp:8081

Then reload in the emulator: press R twice.


iOS

xcodebuild requires Xcode

sudo xcode-select -s /Applications/Xcode.app/Contents/Developer

pod: command not found or CocoaPods errors

Use Bundler (installed locally in the project):

cd apps/mobile
bundle install
bundle exec pod install --project-directory=ios

Ruby / kconv / securerandom errors

Install Ruby 3.1.x via rbenv (see section 2 above).

"No script URL provided" error when launching the app

Metro is not started. Run in a separate terminal:

pnpm mobile:start

Then reload the app in the simulator (Cmd + R).


Android

"Gradle requires JVM 17 or later" error

Gradle 9 requires Java 17 minimum. Install and configure Java 17:

brew install openjdk@17
sudo ln -sfn /opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk /Library/Java/JavaVirtualMachines/openjdk-17.jdk
echo 'export JAVA_HOME="/opt/homebrew/opt/openjdk@17"' >> ~/.zshrc
source ~/.zshrc

Verify: java -version should display openjdk 17.x.x.

"adb: command not found" error

The Android SDK is not in the PATH:

echo 'export ANDROID_HOME=$HOME/Library/Android/sdk' >> ~/.zshrc
echo 'export PATH=$PATH:$ANDROID_HOME/emulator:$ANDROID_HOME/platform-tools' >> ~/.zshrc
source ~/.zshrc

"Could not find org.asyncstorage.shared_storage:storage-android:1.0.0"

AsyncStorage v3 ships its Android artifact via a local Maven repo, not Maven Central. The project's android/build.gradle must include:

allprojects {
    repositories {
        maven {
            url = uri(project(":react-native-async-storage_async-storage").file("local_repo"))
        }
    }
}

This is already configured in apps/mobile/android/build.gradle. If you see this error after a fresh clone or reset, verify the block is present.

"Included build node_modules/@react-native/gradle-plugin does not exist" error

In a pnpm monorepo, some React Native dependencies are not installed by default. Add the missing dependencies:

pnpm --filter mobile-financial-app add -D @react-native/gradle-plugin@0.82.1 @react-native/codegen@0.82.1

Any other Android build failure

When in doubt, use the rebuild command — it handles all cache layers:

pnpm mobile:rebuild:android        # bare RN CLI
pnpm expo:rebuild:android          # Expo managed

Back to top