Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions mobile/.claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"enabledPlugins": {
"expo@claude-plugins-official": true
}
}
41 changes: 41 additions & 0 deletions mobile/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files

# dependencies
node_modules/

# Expo
.expo/
dist/
web-build/
expo-env.d.ts

# Native
.kotlin/
*.orig.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision

# Metro
.metro-health-check*

# debug
npm-debug.*
yarn-debug.*
yarn-error.*

# macOS
.DS_Store
*.pem

# local env files
.env*.local

# typescript
*.tsbuildinfo

# generated native folders
/ios
/android
1 change: 1 addition & 0 deletions mobile/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node-linker=hoisted
7 changes: 7 additions & 0 deletions mobile/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
node_modules
.expo
dist
ios
android
pnpm-lock.yaml
expo-env.d.ts
6 changes: 6 additions & 0 deletions mobile/.prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"semi": true,
"singleQuote": true,
"trailingComma": "all",
"printWidth": 100
}
41 changes: 41 additions & 0 deletions mobile/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
This is an Expo/React Native mobile application. Prioritize mobile-first patterns, performance, and cross-platform compatibility.

## Expo has changed — do not trust your training data

Expo ships breaking changes every SDK release. APIs you remember are likely renamed, moved, or removed. Before writing any code that touches an Expo, EAS, or React Native API:

1. Read the major version of the `expo` package in `package.json`.
2. Fetch the matching versioned docs: `https://docs.expo.dev/versions/v<major>.0.0/`
3. For anything else, fetch https://docs.expo.dev/llms.txt — an index of all Expo docs with corrections to common LLM misconceptions. Follow its links to the specific page you need; never answer from memory.

## Commands

Use `bunx` instead of `npx` if the project uses bun (`bun.lock` present).

```bash
npx expo install <package> # ALWAYS use instead of npm/yarn/pnpm/bun add — resolves SDK-compatible versions
npx expo start # start the dev server
npx expo lint # lint
npx tsc --noEmit # typecheck
npx expo-doctor # diagnose dependency and config issues
npx expo install --fix # fix incompatible package versions
```

Run lint and typecheck before declaring any task done.

## Navigation & Routing

- Use **Expo Router** for all navigation. Routes live in `src/app/` — every file there is a screen, `_layout.tsx` files define navigators. Keep non-route code (components, hooks, utils) outside `src/app/`.
- Import `Link`, `router`, and `useLocalSearchParams` from `expo-router`.
- Docs: https://docs.expo.dev/router/introduction.md

## Building with EAS

Use EAS to build, sign, and submit the app in the cloud (`eas build`, `eas submit`) and to ship over-the-air updates (`eas update`) — no local Xcode or Android Studio required. Run EAS CLI as `bunx eas-cli <command>` in Bun projects, or `npx eas-cli@latest <command>` otherwise; substitute that for bare `eas` in docs examples.
Docs: https://docs.expo.dev/eas/index.md

## Rules

- If `ios/` and `android/` directories do not exist, they are generated (Continuous Native Generation). Never create or edit them by hand — configure native behavior in `app.json` and config plugins.
- Expo Go only includes its bundled native modules. After adding a library with native code, the app needs a development build: `npx expo run:ios|android` locally, or `eas build --profile development`.
- Prefer recommended Expo modules over third-party libraries, and check your available skills before adding dependencies. Docs: https://docs.expo.dev/versions/latest/index.md
342 changes: 342 additions & 0 deletions mobile/README.md

Large diffs are not rendered by default.

75 changes: 75 additions & 0 deletions mobile/app.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { type ConfigContext, type ExpoConfig } from 'expo/config';

/** The brand's dark navy, behind the adaptive icon. */
const ICON_BACKGROUND = '#030711';

// @koushikdotdev/splitpro on expo.dev. Push tokens and EAS builds are tied to it.
const EAS_PROJECT_ID = '82694b46-ca71-4652-a8cf-2674a5ced9e4';

export default ({ config }: ConfigContext): ExpoConfig => ({
...config,
name: 'SplitPro',
slug: 'splitpro',
owner: 'koushikdotdev',
scheme: 'splitpro',
version: '1.0.0',
orientation: 'portrait',
icon: './assets/icon.png',
// Follows the system appearance; colors come from semantic tokens (src/theme/tokens.ts).
userInterfaceStyle: 'automatic',
ios: {
bundleIdentifier: 'app.splitpro',
supportsTablet: true,
usesAppleSignIn: true,
// TODO: enable universal links once https://splitpro.app/.well-known/apple-app-site-association
// is served: associatedDomains: ['applinks:splitpro.app'],
},
android: {
package: 'app.splitpro',
adaptiveIcon: {
foregroundImage: './assets/icon.png',
backgroundColor: ICON_BACKGROUND,
},
// TODO: add App Links once https://splitpro.app/.well-known/assetlinks.json is served:
// intentFilters: [{ action: 'VIEW', autoVerify: true, category: ['BROWSABLE', 'DEFAULT'],
// data: [{ scheme: 'https', host: 'splitpro.app' }] }],
},
plugins: [
'expo-router',
'expo-status-bar',
// No biometrics, so no Face ID usage text.
['expo-secure-store', { faceIDPermission: false }],
'expo-localization',
'expo-font',
'expo-image',
'expo-apple-authentication',
'expo-web-browser',
[
'expo-image-picker',
{
photosPermission: 'SplitPro uses your photos to attach receipts to expenses.',
cameraPermission: 'SplitPro uses the camera to take photos of receipts.',
// Photos only, never video.
microphonePermission: false,
},
],
// TODO: add `icon`, a 96x96 white-on-transparent PNG, or Android shows a white square.
['expo-notifications', { color: '#06b6d4', defaultChannel: 'default' }],
[
'expo-splash-screen',
{
image: './assets/splash-icon.png',
imageWidth: 160,
backgroundColor: '#ffffff',
dark: { backgroundColor: '#000000' },
},
],
],
extra: {
...config.extra,
eas: { ...config.extra?.eas, projectId: EAS_PROJECT_ID },
},
experiments: {
typedRoutes: true,
},
});
Binary file added mobile/assets/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added mobile/assets/splash-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
36 changes: 36 additions & 0 deletions mobile/eas.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"cli": {
"version": ">= 25.0.0",
"appVersionSource": "remote"
},
"build": {
"base": {
"node": "22.20.0",
"pnpm": "10.32.1",
"env": {
"EXPO_PUBLIC_API_URL": "https://splitpro.app"
}
},
"simulator": {
"extends": "base",
"distribution": "internal",
"ios": {
"simulator": true
}
},
"preview": {
"extends": "base",
"distribution": "internal",
"android": {
"buildType": "apk"
}
},
"production": {
"extends": "base",
"autoIncrement": true
}
},
"submit": {
"production": {}
}
}
15 changes: 15 additions & 0 deletions mobile/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// https://docs.expo.dev/guides/using-eslint/
const { defineConfig, globalIgnores } = require('eslint/config');
const expoConfig = require('eslint-config-expo/flat');

module.exports = defineConfig([
globalIgnores(['dist/*', '.expo/*', 'ios/*', 'android/*']),
expoConfig,
{
rules: {
// Match the web app's style (AGENTS.md at the repo root).
yoda: ['error', 'always', { onlyEquality: true }],
'no-console': ['warn', { allow: ['info', 'warn', 'error', 'debug', 'trace'] }],
},
},
]);
48 changes: 48 additions & 0 deletions mobile/metro.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Learn more https://docs.expo.dev/guides/customizing-metro
const path = require('path');
const { getDefaultConfig } = require('expo/metro-config');

const projectRoot = __dirname;
const repoRoot = path.resolve(projectRoot, '..');
const sharedRoot = path.join(repoRoot, 'src');
const prismaShim = path.join(projectRoot, 'src/shims/prisma-client.ts');

/** @type {import('expo/metro-config').MetroConfig} */
const config = getDefaultConfig(projectRoot);

// The app reuses pure logic from the web app (../src/utils, ../src/lib). Only watch that folder:
// the rest of the repo (the web's node_modules, .next, prisma) must never end up in the bundle.
config.watchFolders = [sharedRoot];

// Fail loudly instead of silently bundling a package from the web's node_modules.
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const defaultBlockList = config.resolver.blockList ?? [];
config.resolver.blockList = [
...(Array.isArray(defaultBlockList) ? defaultBlockList : [defaultBlockList]),
new RegExp(`^${escapeRegExp(path.join(repoRoot, 'node_modules'))}/.*`),
];

const isBareSpecifier = (moduleName) =>
!moduleName.startsWith('.') && !moduleName.startsWith('/') && !moduleName.startsWith('~/');

config.resolver.resolveRequest = (context, moduleName, platform) => {
// The shared files import enums from @prisma/client. The real package is Node-only, so swap in a
// tiny shim with the same enum values.
if ('@prisma/client' === moduleName) {
return { type: 'sourceFile', filePath: prismaShim };
}

// Packages imported by shared files (zod, date-fns, ...) resolve from mobile/node_modules as if
// they were imported by the app, so the bundle never picks up the web's copies in ../node_modules.
if (context.originModulePath.startsWith(sharedRoot + path.sep) && isBareSpecifier(moduleName)) {
return context.resolveRequest(
{ ...context, originModulePath: path.join(projectRoot, 'package.json') },
moduleName,
platform,
);
}

return context.resolveRequest(context, moduleName, platform);
};

module.exports = config;
69 changes: 69 additions & 0 deletions mobile/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
{
"name": "splitpro-mobile",
"version": "1.0.0",
"private": true,
"main": "expo-router/entry",
"scripts": {
"start": "expo start",
"ios": "expo run:ios",
"android": "expo run:android",
"typecheck": "tsc --noEmit",
"test": "../node_modules/.bin/tsx --tsconfig tsconfig.json --test 'src/**/*.test.ts' && node --test src/lib/pkce.test.mjs",
"lint": "eslint .",
"format": "prettier --write .",
"format:check": "prettier --check ."
},
"dependencies": {
"@expo/ui": "~58.0.21",
"@formatjs/intl-pluralrules": "^6.3.16",
"@tanstack/react-query": "^4.44.0",
"@trpc/client": "10.45.4",
"@trpc/react-query": "10.45.4",
"@trpc/server": "10.45.4",
"date-fns": "^3.6.0",
"expo": "~58.0.16",
"expo-apple-authentication": "~58.0.2",
"expo-constants": "~58.0.12",
"expo-crypto": "~58.0.7",
"expo-device": "~58.0.5",
"expo-file-system": "~58.0.10",
"expo-font": "~58.0.9",
"expo-glass-effect": "~58.0.4",
"expo-haptics": "~58.0.7",
"expo-image": "~58.0.16",
"expo-image-picker": "~58.0.10",
"expo-linking": "~58.0.13",
"expo-localization": "~58.0.5",
"expo-notifications": "~58.1.5",
"expo-router": "~58.0.24",
"expo-secure-store": "~58.0.2",
"expo-sharing": "~58.0.19",
"expo-splash-screen": "~58.0.10",
"expo-status-bar": "~58.0.3",
"expo-symbols": "~58.0.4",
"expo-system-ui": "~58.0.6",
"expo-web-browser": "~58.0.6",
"i18next": "^26.5.3",
"react": "19.3.0",
"react-dom": "19.3.0",
"react-i18next": "^17.1.0",
"react-native": "0.88.0",
"react-native-gesture-handler": "~3.3.0",
"react-native-reanimated": "4.7.1",
"react-native-safe-area-context": "~5.10.1",
"react-native-screens": "~4.30.0",
"react-native-svg": "15.16.0",
"react-native-web": "^0.21.4",
"react-native-worklets": "0.13.0",
"superjson": "^2.2.6",
"zod": "^3.25.76",
"zustand": "^5.0.15"
},
"devDependencies": {
"@types/react": "~19.3.0",
"eslint": "^9.39.5",
"eslint-config-expo": "~58.0.4",
"prettier": "^3.9.10",
"typescript": "~6.0.3"
}
}
Loading
Loading