- The mobile application is built from the Mobile-Expensify submodule, not directly from the App repository.
- NewDot refers to the New Expensify App, OldDot or Expensify Classic refers to our Old expensify app and website
- App (NewDot) and Mobile-Expensify (OldDot) are combined into a single mobile application
- The HybridApp module (
@expensify/react-native-hybrid-app) manages transitions between OldDot and NewDot - Build process merges
.envfiles from both repositories - Mobile-Expensify environment variables supersede App variables
- Mobile builds must be initiated from the Mobile-Expensify directory
- Standalone: Pure NewDot application (web)
- HybridApp: Combined OldDot + NewDot (mobile apps)
- Controlled via
STANDALONE_NEW_DOTenvironment variable
- AuthScreens: Authenticated app screens
- PublicScreens: Login and onboarding screens
- RHP (Right Hand Pane): Contextual panels — settings and details
- Central Pane: Main content area
- LHN (Left Hand Navigation): Report list and navigation
Onyx (react-native-onyx) is our custom offline-first key-value store; keys are defined in src/ONYXKEYS.ts.
Prefer pure functions taking data as params over direct Onyx reads: callers read and pass data in. When a read is needed, use useOnyx (@hooks/useOnyx) by default; reach for Onyx.connectWithoutView only for non-render logic (actions/libraries) that genuinely can't use useOnyx. Don't copy-paste Onyx.connectWithoutView usages. Justify each with a comment. Using it in components for perf needs @frontend-performance Slack approval linked in PR description. Onyx.get and Onyx.multiGet are one-shot reads for event handlers in components, pages and hooks. See Onyx Data Management.
- Path:
App/Mobile-Expensify/ - Purpose: Legacy OldDot application and mobile build source
- Critical: All mobile builds originate from this directory
- Contains platform-specific code for iOS and Android
- Manages the HybridApp integration layer
- Submodule pointer: bumped automatically by OSBotify on every merge to Mobile-Expensify
main
Use the /react-native-best-practices skill when working on performance-sensitive code, native modules, or release preparation. This ensures code respects established best practices from the start, resulting in more consistent code, fewer review iterations, and better resilience against regressions.
React Compiler auto-memoizes code in components and hooks (excluding tests/). oxc-transform-react runs it on web (see config/rsbuild/), on native (see config/repack/) and under Jest (see config/babel/oxcJestTransformer.js), all with the options in config/babel/oxcReactCompilerConfig.js. The compliance check and the ESLint processor use the same compiler via config/reactCompiler/checkWithOxc.mjs, and the ESLint processor only relaxes manual-memoization rules when it memoizes the file.
Do not use useMemo, useCallback, or React.memo in components or hooks that compile with React Compiler.
- ESLint: Linter. Pre-existing violations are grandfathered via the seatbelt ratchet in
scripts/lint/. - Design tokens: font sizes come from
<Text variant="...">, and paddings or margins that differ between narrow and wide layouts come fromuseLayoutSpacing()(cardPaddingHorizontal,pageGutter, ...). Never writeshouldUseNarrowLayout ? styles.ph5 : styles.ph8or a rawfontSize. SeecontributingGuides/STYLING.md.
ALWAYS run these steps after making code changes, before committing:
- ESLint: Run
npm run lint-changedto catch lint errors early. - TypeScript: Run
npm run typecheckafter changes that may affect typing (types, interfaces, or function signatures). It runs the TypeScript 7 native compiler and is the required merge gate in CI. - React Compiler: If you added new React components/hooks or modified existing ones, run
npm run react-compiler-compliance-check check-changedto verify they compile with React Compiler. This applies the same rules as CI: new components/hooks must compile, and existing compiled files must not regress. SeecontributingGuides/REACT_COMPILER.mdfor details and common fixes. - Spelling: Run
npm run spell-changedto catch spelling errors (it discovers changed files itself; pass an explicit file list only if you want to check specific files instead). CI validates with cspell, which remains the required merge gate.
App tests use Jest. Tests for .github/ and scripts/ live in tests/tooling/ and run under bun:test (npm run test:bun) — see tests/tooling/README.md.
Every test case must be documented with // Given, // When and // Then comments explaining why the test does what it does — see tests/README.md.
- All features work offline
- Optimistic updates with rollback
- Queue-based request handling
- Conflict resolution strategies
- NewDot Help: https://help.expensify.com/new-expensify/hubs/
- OldDot/Expensify Classic Help: https://help.expensify.com/expensify-classic/hubs/
Use the /playwright-app-testing skill to test and debug the App in a browser. Use this skill after making frontend changes to verify your work, or when the user requests testing.
Use the /agent-device skill to drive the App on iOS and Android (simulators or real devices) for interactive testing, performance profiling, bug reproduction, and device-specific debugging.
Use the Sentry skill whenever the user wants to analyze any data from Sentry: spans, metrics, crashes, crash free rate, etc.