16 KiB
AGENTS.md
This file provides guidance for AI coding agents working with code in this repository.
Project Overview
iNaturalistReactNative is the official iNaturalist mobile client written in React Native, replacing the legacy iOS and Android native apps. It's a community science platform where users photograph organisms, upload observations, and get AI-powered species identifications.
Development Commands
Running the App
# Start Metro bundler (with cache reset recommended to avoid build issues)
npm start -- --reset-cache
# Run on iOS
npm run ios
# Run on Android
npm run android
# Run release builds
npm run ios:release
npm run android:release
Testing
# Run all tests (integration + unit)
npm test
# Run only unit tests
npm run test:unit
# Run only integration tests
npm run test:integration
# Run individual test by name
npx jest
# E2E tests (requires Detox setup)
npm run e2e # Build and test both iOS + Android (use e2e:ios for iOS-only)
npm run e2e:android # Build and test Android
npm run e2e:test # Run tests without rebuilding
Linting
# Run all linters (eslint, flow, rubocop)
npm run lint
# Auto-fix linting issues
npm run lint:fix
# Run individual linters
npm run lint:eslint
npm run lint:flow # Flow type checking
npm run lint:rubocop # Ruby linting (fastlane)
npm run lint:tsc # TypeScript checking
Note on lint:tsc: The repo has a large pre-existing TypeScript error baseline (over 1,000 errors), so a clean run is not expected. The standard is to add no new errors in files you touch — verify by filtering the tsc output for your filenames and comparing against the pre-change state. TS7016 implicit-any errors from importing untyped .js modules are part of the accepted baseline; do not add @ts-ignore comments for them.
Translations
# Build translation JSON from Fluent files
npm run translate
# Prepare fastlane metadata for app stores
npm run prepare-fastlane-metadata
Icons & Assets
# Rebuild icon font from SVG files in src/images/icons/
npm run icons
# Add example AI model files
npm run add-example-model
Cleanup
# Clean project (interactive menu for caches, builds, pods, node_modules)
npx react-native clean-project
# Full clean and restart
npm run clean-start
Code Architecture
Navigation Structure
The app uses React Navigation 7 with @react-navigation/native-stack and @react-navigation/bottom-tabs in a nested hierarchy:
- RootStackNavigator (NativeStack, top level) -
src/navigation/RootStackNavigator.tsx- OnboardingStackNavigator - Rendered in place of the tab navigator when
!onboardingShown(not a modal over it) - BottomTabNavigator - Shown when
onboardingShown. Contains four tabs:MenuTab→ TabStackNavigator (initialRouteName: "Menu")ExploreTab→ TabStackNavigator (initialRouteName: "RootExplore")ObservationsTab→ TabStackNavigator (initialRouteName: "ObsList")NotificationsTab→ TabStackNavigator (initialRouteName: "Notifications")
- NoBottomTabStackNavigator - Camera, PhotoLibrary, GroupPhotos, SoundRecorder, plus SharedStackScreens
- LoginStackNavigator - Login, SignUp, ForgotPassword, etc.
- OnboardingStackNavigator - Rendered in place of the tab navigator when
Key design patterns:
- All four bottom tabs share the same
TabStackNavigatorcomponent (src/navigation/StackNavigators/TabStackNavigator.tsx) with differentinitialRouteNamevalues, giving every tab access to the full screen catalog. SharedStackScreens(src/navigation/StackNavigators/SharedStackScreens.tsx) is aStack.Grouprendered in bothTabStackNavigatorandNoBottomTabStackNavigator, so screens like ObsEdit, TaxonDetails, Match, and Suggestions work from either context.- The
NavigationContainerlives insrc/navigation/OfflineNavigationGuard.tsxwith a globalnavigationReffromsrc/navigation/navigationUtils.ts. - Deep linking is handled manually via React Native's
LinkingAPI insrc/components/hooks/useLinking.ts(no React Navigationlinkingconfig).
To add a new screen, place it in the appropriate navigator: TabStackNavigator if it needs bottom tabs visible, NoBottomTabStackNavigator if not, or SharedStackScreens if it needs to be reachable from both contexts.
Details: the full navigator hierarchy, screen-param typing, and deep-linking are in agent-docs/architecture/navigation-patterns.md.
State Management
The app uses a hybrid state management approach:
-
Zustand stores (
src/stores/) - Global app state slices. Key examples include:createObservationFlowSlice- Observation creation/editing flowcreateUploadObservationsSlice- Upload queue and statuscreateSyncObservationsSlice- Syncing observations from servercreateLayoutSlice- UI preferences and flags (default vs advanced mode, obs-detail tab, screen-after-photo, shown-once flags)createExploreSlice/createRootExploreSlice- Explore screen filters and statecreateMyObsSlice- My Observations filterscreateFeatureFlagSlice- Feature flag state- See
src/stores/for the complete list of slices.
-
React Query - Server state management and API caching (uses
@tanstack/react-query) -
React Context - Feature-specific state:
ExploreContext- Complex explore screen stateRealmContext- Realm database access
Details: store architecture, MMKV persistence, and the Realm/Zustand division of responsibilities are in agent-docs/architecture/realm-and-zustand.md.
Data Persistence
-
Realm Database (
src/realmModels/) - Primary local data store- Key models include
Observation,Photo,ObservationPhoto,Sound,ObservationSound,Taxon,TaxonPhoto,User,Identification,Comment,Vote,Flag,Application, andQueueItem(seesrc/realmModels/for the full set) - Bump
schemaVersioninrealmModels/index.tswhenever the schema changes, and add a migration — see that file for the current version - Migration logic for schema updates is in
realmModels/index.ts - All observations are stored locally first, then uploaded asynchronously
- Key models include
-
MMKV - Fast key-value storage via
zustandStoragefor Zustand persistence
Details: working with Realm objects in the React layer (live-object gotchas, the missing Realm→API converter, mapTo field-name debt) is documented in agent-docs/architecture/realm-and-zustand.md.
API Layer
API calls are organized in src/api/:
- Uses
inaturalistjslibrary as the primary API client - Wrapper functions in
src/api/*.{js,ts}handle error logging and data transformation (the layer is mid-migration to TypeScript) - API responses are transformed to match local Realm schema
- Base API URL configured via
.envfile (API_URL)
Details: the wrapper-only rule and the useAuthenticatedQuery/useAuthenticatedMutation base hooks are covered in agent-docs/architecture/api-layer.md.
Upload System
The upload system (src/uploaders/) handles offline-first observation uploads:
observationUploader.ts- Uploads observation data to APImediaUploader.ts- Uploads photos/sounds to the iNaturalist API (inatjs.photos.create/sounds.create), then associates them with observations- Upload queue is held in Zustand (
createUploadObservationsSlice.uploadQueue), not in Realm. (The RealmQueueItemmodel is a separate settings-sync work queue used byuseWorkQueueforlocale-change/taxon-names-change.) - Zustand slice
createUploadObservationsSlicemanages upload state and progress - Uploads use keep-awake and can continue briefly while in-flight if the app is backgrounded, but there is no dedicated background/headless upload task
Details: the four-step pipeline, error-recovery table, and timeout/keep-awake rules are in agent-docs/architecture/upload-system.md.
AI Camera & Computer Vision
The AI Camera uses on-device computer vision models:
- Vision Camera plugin:
vision-camera-plugin-inatvision - Model files:
.tflite(Android) and.mlmodel(iOS) for species prediction - Taxonomy files:
taxonomy.csv(Android) /taxonomy.json(iOS) - Geomodel files for location-based filtering
- Model file paths configured in
.envviaANDROID_MODEL_FILE_NAME,IOS_MODEL_FILE_NAME, etc. - Models not in repo - downloaded via
npm run add-example-modelor from releases ininaturalist/model-filesrepo
Internationalization (i18n)
Translation system using Fluent and i18next:
- Source strings:
src/i18n/strings.ftl(US English only) - Translations:
src/i18n/l10n/(pulled from Crowdin, do not edit directly) - Use
useTranslation()hook in components:const { t } = useTranslation(); - For complex interpolation with components, use
<Trans>component - Run
npm run translateafter modifyingstrings.ftlto validate and build JSON - Fluent syntax: https://projectfluent.org/fluent/guide/
Key translation guidelines:
- Labels should match content (max 100 chars)
- Change label when content changes (don't reuse keys for different meanings)
- Add comments for context unless self-explanatory
- Use double-dashes for disambiguation (e.g.,
Unknown--rank,Unknown--taxon) - Avoid variables when possible - create separate strings for each case
- Pluralize with selectors:
{ $count } { $count -> [one] observation *[other] observations }
Details: agent-docs/conventions/i18n-conventions.md.
Styling
- Nativewind (Tailwind CSS for React Native) - Primary styling system
- Uses Tailwind utility classes via
classNameprop - Custom Tailwind config:
tailwind.config.js - Some legacy components use StyleSheet.create()
- React Native Paper - Material Design components for some UI elements
Details: component structure, styling, and accessibility conventions are in agent-docs/conventions/component-conventions.md.
Module Aliases
Babel module resolver provides import aliases (defined in babel.config.js):
import Component from "components/MyComponent"; // instead of ../../../components/MyComponent
import { useObservation } from "sharedHooks";
import { formatDate } from "sharedHelpers";
import { Observation } from "realmModels";
Available aliases: api, appConstants, components, dictionaries, i18n, images, navigation, providers, realmModels, sharedHelpers, sharedHooks, stores, styles, tests, uploaders
Details: full alias table with example imports in agent-docs/conventions/import-aliases.md.
Key Directories
src/components/- React components organized by feature (Camera, Explore, MyObservations, etc.)src/sharedHooks/- Reusable React hooks across featuressrc/sharedHelpers/- Pure utility functionssrc/navigation/- React Navigation configuration and navigatorssrc/realmModels/- Realm database schemas and model logicsrc/api/- API wrapper functionssrc/uploaders/- Upload queue and processing logictests/unit/- Unit tests for individual modulestests/integration/- Integration tests with mocked APIs and Realme2e/- End-to-end Detox tests
Testing Guidelines
- Jest + React Native Testing Library for unit/integration tests;
factoria+@faker-js/fakerfor mock data (Local*= locally persisted,Remote*= API/external) - Initialize i18next in test files:
beforeAll( async () => { await initI18next(); } ); - Test user behavior, not implementation details
- E2E tests require real iNaturalist credentials in
.env(E2E_TEST_USERNAME,E2E_TEST_PASSWORD) - Details: unit/component/integration conventions — factory overrides,
userEventvsfireEvent, mocking, running a single test — inagent-docs/testing/(start withtest-core.md); end-to-end (Detox + Maestro) inagent-docs/testing/e2e.md
Code Style & Conventions
- ESLint config extends Airbnb with custom rules (
.eslintrc.js) - Double quotes for strings
- Spaces inside parentheses:
if ( condition ) { } - Max line length: 100 characters
- No console statements in production (removed via Babel plugin)
- Arrow functions for React components:
const MyComponent = () => { }; - i18next string literal checking enforced - use
t()for all user-facing text - Prefer TypeScript for new files (partial adoption, not required)
- Husky pre-commit hook (
.husky/pre-commit) runslint-staged(eslint--fixon staged files), regenerates i18n translations, and runs a GitGuardian (ggshield) secrets scan
Details: TypeScript conventions (interface-vs-type, Flow coexistence) in agent-docs/conventions/typescript.md.
Authentication & OAuth
- OAuth flow uses
inaturalistjslibrary - JWT tokens stored in react-native-sensitive-info (secure storage)
- Support for Apple Sign-In, Google Sign-In, and email/password
- User state managed in Zustand and persisted to MMKV
- JWT included in API requests via custom headers
Logging & Error Handling
- Custom logger:
react-native-logs.config.ts - Sentry-style error tracking with Grafana integration
- Sentinel files (
sharedHelpers/sentinelFiles.ts) - Debug difficult hardware issues:- Created at flow start, deleted on success
- Log stages during flow (e.g., camera permissions, save photo, location fetch)
- Orphaned files logged as errors to Grafana on next app launch
- Example: Camera flow tracks freezing, location stalls, photo save failures
Environment Configuration
.envfiles contain secrets and config (not in repo).env- Production.env.staging- Staging environmentenv.example- Template with all required variables
- Key variables:
API_URL, OAuth client IDs, Firebase configs, model file names - Platform-specific configs:
- iOS:
GoogleService-Info.plist - Android:
google-services.json,keystore.properties
- iOS:
Git Workflow
- Issues are tracked in Linear with
MOB-identifiers - Branch naming:
mob-{number}-{short-description}(e.g.,mob-123-fix-upload-crash) - Commit messages: Imperative mood, describe user impact
- Include
Closes MOB-123in the commit body to auto-close issues - Husky pre-commit hook runs linters automatically
- Main branch:
main
Accessibility
- eslint-plugin-react-native-a11y enforces accessibility rules
- Use
accessibilityLabel,accessibilityHint,accessibilityRoleprops - Hints written in third person singular ending with period: "Opens the camera."
- Test with VoiceOver (iOS) and TalkBack (Android)
Agent Docs (agent-docs/)
In-depth architecture and convention docs for both humans and AI agents. Read the relevant doc before exploring or modifying a subsystem — it captures patterns that aren't obvious from the code alone.
architecture/—upload-system.md,navigation-patterns.md,api-layer.md,realm-and-zustand.md(includes working with Realm objects in the React layer)conventions/—component-conventions.md,i18n-conventions.md,import-aliases.md,typescript.mdtesting/—test-core.md,test-components.md,integration-test-analysis.md,e2e.md(Detox + Maestro + iOS verification)
Common Pitfalls
- Build issues: Run
npm start -- --reset-cacheornpx react-native clean-project - Realm schema changes: Always increment
schemaVersionand provide migration - Translation missing: Run
npm run translateafter updatingstrings.ftl - Import paths: Use module aliases, not relative paths (enforced by
module-resolver/use-aliasrule) - Icon changes: Run
npm run iconsand rebuild (can't hot reload assets) - M-series Mac Android builds: May need specific NDK version (see
android/build.gradle) - i18next in tests: Must initialize with
await initI18next()inbeforeAll - Detox simulator: Create simulator matching
.detoxrc.jsconfig in Xcode