# iNaturalistReactNative   
This is an official iNaturalist client written in React Native that will eventually replace our existing iOS and Android apps. Achieving parity with those established apps is taking some time, but we're getting there!
## Contributing
See [CONTRIBUTING](CONTRIBUTING.md) for guidelines on contributing to this project.
## Setup
### Requirements
* Xcode 15 or above
* [Android and iOS environment setup](https://reactnative.dev/docs/environment-setup) described in the RN docs
### Install packages and pods
1. Run `npm install`
1. Run `npx pod-install` or `cd ios && pod install` from the root directory
1. `cp env.example .env.staging` for staging and `cp env.example .env` for production and fill in appropriate values. This is not part of the code repo (contains secrets, such as OAuth client ID).
1. To run on Android, do this `cp android/example-keystore.properties android/keystore.properties`. Fill in the relevant values. If you are a member of iNat staff, get them from another member of iNat Staff.
1. Firebase is optional, you can remove it if you don't need it. If you do want to use it, you have to add platform-specific config files that are not part of the code repo.
1. On iOS, `cp ios/GoogleService-Info.example.plist ios/GoogleService-Info.staging.plist` and `cp ios/GoogleService-Info.example.plist ios/GoogleService-Info.production.plist`, and fill in the relevant values. If you are a member of iNat staff, get them from another member of iNat Staff.
1. On Android, `cp android/app/google-services.example.json android/app/src/debug/google-services.json` and `cp android/app/google-services.example.json android/app/src/release/google-services.json`, and fill in the relevant values. If you are a member of iNat staff, get them from another member of iNat Staff.
1. Add AI Camera model and taxonomy files. The computer vision model and Geomodel files are not part of the code repo, and have to be installed. The app itself will load the model file with the filename specified in a .env file. On Android, the current file names are specified in these env variables `ANDROID_MODEL_FILE_NAME`, `ANDROID_TAXONOMY_FILE_NAME`, and `ANDROID_GEOMODEL_FILE_NAME`. On iOS, the current file names are specified in these env variables `IOS_MODEL_FILE_NAME`, `IOS_TAXONOMY_FILE_NAME`, and `IOS_GEOMODEL_FILE_NAME`. After a fresh clone of the repo and copying the env.example file (see above), you have to add the files by following these steps:
1. Add the example model files by executing `npm run add-example-model`. If that does not work continue with the next step.
1. If the download script fails: The sample model files are available in the latest release in this [`repository`](https://github.com/inaturalist/model-files).
1. On Android, these files are named `INatVision_Small_2_fact256_8bit.tflite`, `INatGeomodel_Small_2_8bit.tflite` and `taxonomy.csv`. Create a camera folder within Android assets (i.e. `android/app/src/debug/assets/camera`) and place the files there.
1. On iOS, these files are named `smallINatVision_Small_2_fact256_8bit.mlmodel`, `INatGeomodel_Small_2_8bit.mlmodel` and `taxonomy.json` and should be added to the `ios` folder.
### Set up pre-commit hooks
1. We're using [Husky](https://typicode.github.io/husky/#/) to automatically run `eslint` before each commit. Run `npm run postinstall` to install Husky locally.
1. (Staff only) Set up GitGuardian to prevent yourself from committing secrets
1. [Install `ggshield`](https://docs.gitguardian.com/ggshield-docs/getting-started)
1. Get a GitGuardian API token from another staff developer and put it in the `GITGUARDIAN_API_KEY` env variable.
### Run build
1. Run `npm start -- --reset-cache` (`npm start` works too, but resetting the cache each time makes for a lot less build issues)
2. Run `npm run ios` or `npm run android`
### Running with staging environment
If you're on staff you can configure the app to read from and write to our staging server. Override `API_URL` to a staging API domain, either using local `.env.staging` file, or overriding the environment variable when calling `npm start`, e.g. `API_URL=http://example.com npm start -- --reset-cache`.
## Tests
We currently have three kinds of tests:
1. `tests/integration`: Tests the integration of multiple modules, e.g. a list of observation that makes requests to a mocked API, persists the response data in local storage, retrieves the data from local storage and renders components.
2. `tests/unit`: Tests only specific modules, like a single component, or a hook.
3. `e2e`: Tests user interactions on the finished app build running on the iOS simulator (see below).
### Unit tests & integration tests
We're using [Jest](https://jestjs.io/) and [React Native Testing Library](https://callstack.github.io/react-native-testing-library/) for most of our tests, [factoria](https://github.com/phanan/factoria) and [faker.js](https://github.com/Marak/faker.js/) to generate test data for use with mocks. `Local*` model factories represent locally persisted data, while `Remote*` factories represent that kinds of records we get from an API or external package.
```bash
# Run all tests
npm test
# Run test paths matching a pattern
npm test MyObs
# Run individual tests matching a pattern. Note the `--` to pass arguments to jest
npm test -- -t accessibility
# Update snapshots for a specific path
npm test Button.test.js -- --updateSnapshot
```
Note that you can run `npx jest` as well, but that will omit some environment variables we need to set for the test environment, so for consistent test runs please use `npm test`.
Also note that `i18next` needs to be initialized in individual test files (haven't figured out a way to await initialization before *all* tests, plus allowing tests to control initialization helps when testing different locales). Add `beforeAll( async ( ) => { await initI18next( ); } );` to a test file if it depends on localized text.
### E2E tests
> [!NOTE]
> Status of e2e tests in CI: internal / staff PRs run iOS Detox tests as a PR check. Android Detox tests are currently disabled. We are currently working to restore Android e2e tests. For now, we have a skeleton Android flow running a single smoketest flow for simplicity. This is not a PR check yet and can only be run manually (`workflow_dispatch` GH Actions trigger).
We're using [Detox](https://wix.github.io/Detox/docs/introduction/getting-started/) for E2E tests. If you want to run the e2e tests on your local machine, make sure you follow the Detox environment setup instructions.
Then you have to populate `E2E_TEST_USERNAME` and `E2E_TEST_PASSWORD` in `.env` with real iNaturalist login credentials so the e2e test can actually authenticate.
Then you can run the tests:
```bash
# Build the app and run the tests
npm run e2e
```
If you don't have the iOS simulator the e2e tests are [configured](https://github.com/inaturalist/iNaturalistReactNative/blob/main/.detoxrc.js#L51) to use, you may need to create it in XCode 15: Go to Window > Devices and Simulators, click the Simulators tab, click the "+" button in the lower left, and create a simulator that matches the `device.simulator.device.type` setting in `.detoxrc.js`.
If you have built the app already for a previous test, and just want to run an updated test without changing the app code, you can run `npm run e2e:test`.
If you are running into some issues after the tests have been working for some time, try updating `applesimutils` with `brew update && brew upgrade applesimutils`.
If you want to run the Android tests you need to prepare your environment. Before you dive into the [setup](https://wix.github.io/Detox/docs/19.x/introduction/android-dev-env), know that alternatively you might find it easier setting up the required local emulator, preferrably an AOSP (Android Open Source Project) version, using Android Studio. Make sure the emulator has the same name as in the `.detoxrc.js` file.
Run `npm run e2e:build:android && npm run e2e:test:android` to build the APK for testing purposes and install and run it on the emulator with the name as stated in the `.detoxrc.js` file.
#### Maestro
> [!NOTE]
> By the Maestro's team own disclosure, the `maestro record` command for generating a test run recording is unusably slow. As a workaround, we use the inline `startRecording` Maestro directive within tests, which is more performant. For convenience, this is defined in an `onFlowStart.yaml` intented to be referred to in each flow's `onFlowStart` lifecycle hook.
Some desired test flows cannot be acheived with Detox and for those, we use Maestro. These are tests that rely on behavior external to the application such as deeplinks and sharing photos into the app. These are not currently run in CI as a PR check. The in-progress Android CI uses a Maestro smoketest flow as an exemplary command while we work on restoring Android Detox tests.
## Translations
### Adding and changing new source strings
Source strings are in `src/i18n/strings.ftl` and should be in US English. Don't edit the files in `src/i18n/l10n/` because they will get overwritten when we pull in translations. All translation is done by volunteers on [Crowdin](https://crowdin.com/editor/inaturalistios/724), so please follow these guidelines to make things easier for those generous people.
1. **Labels should match content as closesly as possible** (without exceeding 100 characters)
* Bad
```fluent
collection-project-screen-title = ABOUT COLLECTION PROJECTS
```
* Good
```fluent
ABOUT-COLLECTION-PROJECTS = ABOUT COLLECTION PROJECTS
```
1. **Labels should change when the content changes**
* Bad
* Old
```fluent
ABOUT-COLLECTION-PROJECTS = ABOUT COLLECTION PROJECTS
```
* New
```fluent
ABOUT-COLLECTION-PROJECTS = ABOUT COLLECTION AND UMBRELLA PROJECTS
```
* Good
* Old
```fluent
ABOUT-COLLECTION-PROJECTS = ABOUT COLLECTION PROJECTS
```
* New
```fluent
ABOUT-COLLECTION-AND-UMBRELLA-PROJECTS = ABOUT COLLECTION AND UMBRELLA PROJECTS
```
1. **Annotate strings with comments** unless the string is very self-descriptive
* Bad
```fluent
Change-date = Change date
```
Is this a verb phrase or a noun phrase? Are we talking about spare change in your pocket?
* Good
```fluent
# Label for a button that changes a selected date
Change-date = Change date
```
1. **Use double-dashes to append extra context to keys and to keep them unique and descriptive.** For example, translators might need to translate the word "Unknown" differently if it refers to a place or a taxon, so you might include both `Unknown--place = Unknown` and `Unknown--taxon = Unknown`
1. **Accessibility hints** are used by screen readers to describe what happens
when the user interacts with an element. The [iOS Guidelines](https://developer.apple.com/documentation/uikit/uiaccessibilityelement/1619585-accessibilityhint) define it as "A string that briefly describes the result of performing an action on the accessibility element." We write them in third person singular ending with a period
1. **Pluralize text with a count** using [selectors](https://projectfluent.org/fluent/guide/selectors.html)
* Bad
```fluent
x-observations = { $count } observations
```
* Good
```fluent
x-observations = { $count } { $count ->
[one] observation
*[other] observations
}
```
1. **Avoid variables when possible.** Variables make translation and static code checks harder
* Bad
```fluent
quality-grade-with-label = Quality Grade: { $qualityGrade }
```
* Good
```fluent
quality-grade-with-label--research = Quality Grade: Research
quality-grade-with-label--needs-id = Quality Grade: Needs ID
quality-grade-with-label--casual = Quality Grade: Casual
```
There are only 3 possible quality grades, so this can just be three separate strings. Translators don't have to worry about the possible values of `$qualityGrade` and it's much easier to check for unglobalized or unused keys.
### Adding new text to code
1. Add new strings in English to `src/i18n/strings.ftl` using [Fluent syntax](https://projectfluent.org/fluent/guide/), e.g.
```fluent
# Header for a paragraph describing projects
ABOUT-PROJECTS = ABOUT
# Text describing what projects are
projects-description =
Projects are really great, probably iNat's best feature.
```
1. Run `npm run translate` to validate strings and build the JSON files i18next needs to access text in the app
1. In a commponent, use the `useTranslation` hook to reference your new string, e.g.
```jsx
import { useTranslation } from "sharedHooks";
const MyComponent = ( ) => {
const { t } = useTranslation( );
return (
{ t( "ABOUT-PROJECTS" ) }{ t( "projects-description" ) }
);
};
```
When components need to be included around interpolated variables, use the `` component:
Fluent:
```fluent
Welcome-user = <0>Welcome back,0><1>{ $userHandle }1>
```
Usage:
```jsx
,
]}
/>
```
### Pushing / Pulling Translations
We manage translations through Crowdin. Actually updating the translation files should be largely automated, but this is what it looks like to do it manually (you must have the [Crowdin CLI](https://github.com/crowdin/crowdin-cli) installed and have an [access token](https://crowdin.com/settings#api-key) associated with a Crowdin user that can post files to the specified project):
```bash
# Upload new strings. Source and destination paths are specified in crowdin.yml
crowdin upload --token YOUR_ACCESS_TOKEN --project-id YOUR_PROJECT_ID
# Download new translations and build for use in the app
crowdin download --token YOUR_ACCESS_TOKEN --project-id YOUR_PROJECT_ID
npm run translate
git add src/i18n/l10n/*
git commit -a -m "Updated translations"
```
## Styling
We're using Nativewind, a styling system for React Native based on Tailwind CSS. Check the [Nativewind documentation](https://www.nativewind.dev/) to see what styles can be used in RN.
## Icons
We have a custom set of icons stored as SVG files and compiled into a font. New icons should be included with issues in a ready-to-use form, but some editing may be required.
1. Add / edit SVGs to / in `src/images/icons/` (`git add` any new icons). Icon SVGs must meet the following requirements
* `