# Technical documentation — formulys-mobile-app

Evidence date: 2026-10-14. Source revision: `main` at `8fef7bc` (2024-01-30). Remote: `https://bitbucket.org/overdrive/formulys-mobile-app.git`. Only branch: `main`.

The scan-host checkout was not on this machine. Paths below are from a clone of `8fef7bc`. The app was not started.

## Stack

| Layer | Choice | Evidence |
|---|---|---|
| Language | JavaScript (no TypeScript config) | `*.js` / `*.jsx` |
| UI | React 18.2.0, React Native 0.72.6 | `package.json` |
| App framework | Expo SDK `~49.0.15`, entry `expo-router/entry` | `package.json` |
| Navigation | Expo Router `^2.0.0`, drawer via `@react-navigation/drawer` `^6.6.6` | `package.json`, `app/dashboard/_layout.js` |
| HTTP | axios `^1.6.5` on the routed screens | login, sign-up, three lists |
| Token store | `@react-native-async-storage/async-storage` `1.18.2`, key `token` | `app/_layout.js` |
| Embedded forms | `react-native-webview` `^13.6.4` | detail, edit, `app/forms/[id].js` |
| State library | zustand `^4.5.0` declared and used only by an unimported store | `stores/userStore.js` |
| JS engine (Android) | Hermes on (`hermesEnabled=true`), new architecture off | `android/gradle.properties` |
| Android Gradle | AGP 7.4.2, compile/target SDK 33, min SDK 21, build-tools 33.0.0, NDK `23.1.7779620`, Kotlin 1.8.10 | `android/build.gradle` |

Also declared and not referenced by routed screens: `@formio/react` `^5.3.0`, `formiojs` `^4.18.1`, `expo-font`, `expo-linking`, `expo-splash-screen`, `expo-status-bar`, `expo-constants`, `@expo/vector-icons`. Form rendering goes through a WebView, not the Form.io React SDK.

`@babel/core` is the only devDependency. `babel.config.js` still names `babel-preset-expo`, which Expo projects usually resolve from the `expo` package rather than from a direct devDependency.

## Repository layout

```text
app/                      Expo Router tree (the running UI)
  _layout.js              token gate and stack header
  index.js                home
  login/index.js
  sign-up/index.js
  forms/[id].js           create WebView
  dashboard/_layout.js    drawer
  dashboard/sites|clients|pesticides
android/                  prebuilt native project
components/               unused by app/ (see features)
hooks/                    unused
stores/                   unused (siteInspectionStore.js is empty)
lib/constants.js          FORMIO_API_BASE_URL and card colours
lib/helpers.js            logger and an unused formioAPI
app.json                  Expo config
eas.json                  EAS build profiles
babel.config.js
metro.config.js           default Expo Metro config
package.json
package-lock.json
.env                      tracked; not imported by routed screens
README.md                 Expo Router template
.vscode/settings.json     cSpell words Digimonk, FORMIO
```

There is no `ios/` directory, no `__tests__`, no `bitbucket-pipelines.yml`, and no `sonar-project.properties`.

A tracked file sits at `%ProgramData%/Microsoft/Windows/UUS/State/_active.uusver`. The app does not import it. Provenance: **Implemented** as a git path. Purpose: **Unknown**. It looks like a Windows Update session file that was committed by accident.

## Application structure

Expo Router maps files in `app/` to URLs. `app/_layout.js` renders a single `Stack` with title "Pest Elite", header background `#ebebeb`, and a header-right logout control.

`app/dashboard/_layout.js` renders an Expo Router `Drawer` with three screens. Each of those screens has its own `Stack` layout (`All Sites`, `All Clients`, `All Pesticides`).

Login success calls `router.replace("/dashboard")`. There is no `app/dashboard/index.js`. Which screen Expo Router 2 mounts for that URL was not executed. **Unknown / Requires Confirmation.**

## Form.io and FormView

Constant in `lib/constants.js`:

```text
FORMIO_API_BASE_URL = https://rzkiqpzlxxtfqjl.form.io
```

The list screens do not use that constant. They repeat the host in the URL string. The value matches.

| Call | Method | URL | Auth header |
|---|---|---|---|
| Login | POST | `https://rzkiqpzlxxtfqjl.form.io/user/login/submission` | `x-jwt-token: ""` |
| Register | POST | `.../user/register/submission` | empty |
| Sites | GET | `.../digimonkv1/submission` | stored token |
| Clients | GET | `.../clientregister/submission` | stored token |
| Pesticides | GET | `.../pesticidesregister/submission` | stored token |

POST bodies are `{ data: { email, password } }`. GET calls send no query.

WebView base: `https://pro.formview.io/#/rzkiqpzlxxtfqjl/`. Query `header=0&reset=1` on every FormView URL in `app/`. The WebView `source` is a `uri` only. No `headers` object is passed, so the JWT is not attached.

`lib/helpers.js` `formioAPI` would call `` `${FORMIO_API_BASE_URL}/${url}` `` and would throw `axios is not defined` if invoked. It treats the token as missing whenever `AsyncStorage.getItem` returns an object, which a Promise is. Routed screens do not call it.

`hooks/useAxios.js` appends `/submission` itself and also forgets to await the token, so the header would be a Promise if the hook were used. It is not used.

## Auth

1. Login or register reads `x-jwt-token` from the axios response headers and writes AsyncStorage.
2. The root layout reads that key to decide whether to force `/login`.
3. List GETs send it back as `x-jwt-token`.
4. Logout removes the key.

No refresh token, no expiry clock, no secure-store wrapper, no certificate pinning. AsyncStorage is unencrypted application storage. Provenance: **Implemented** (the library in use). Whether that meets a security requirement is **Unknown / Requires Confirmation** — no security requirement is written in the repo.

## Android

| Item | Value |
|---|---|
| Namespace / applicationId | `com.siliconoverdrive.pestelite` |
| versionCode / versionName | `1` / `1.0.0` |
| min / target / compile SDK | 21 / 33 / 33 |
| App label | `pestelite-mobile` (`android/app/src/main/res/values/strings.xml`) |
| JS engine | Hermes |
| Architectures | `armeabi-v7a`, `arm64-v8a`, `x86`, `x86_64` |
| Flipper | `0.182.0` (`FLIPPER_VERSION`) |
| New architecture | `newArchEnabled=false` |
| GIF / webp | enabled; animated webp disabled |
| Network inspector flag | `EX_DEV_CLIENT_NETWORK_INSPECTOR=true` |

Permissions in the main manifest: `INTERNET`, `READ_EXTERNAL_STORAGE`, `WRITE_EXTERNAL_STORAGE`, `SYSTEM_ALERT_WINDOW`, `VIBRATE`. The JavaScript that was read does not call vibration or the file APIs. `SYSTEM_ALERT_WINDOW` is also in the debug manifest. These match a stock Expo prebuild more than a feature in `app/`.

Intent-filter schemes on `MainActivity`: `acme`, `com.jasonoverdrive.formulysmobile`, `com.siliconoverdrive.pestelite`. `app.json` `scheme` is only `acme`.

`allowBackup` is `true`.

Expo updates metadata: `ENABLED=false`, `EXPO_SDK_VERSION=49.0.0`, `EXPO_UPDATES_CHECK_ON_LAUNCH=ALWAYS`, `EXPO_UPDATES_LAUNCH_WAIT_MS=0`.

Debug manifest sets `usesCleartextTraffic=true`. Main manifest does not.

Signing: debug keystore `android/app/debug.keystore` with the stock Android debug alias. Release `buildTypes.release.signingConfig` points at that debug config. A comment in `android/app/build.gradle` says production needs its own keystore. The debug passwords are the published Android debug defaults. They are not repeated here.

`MainActivity.java` and `MainApplication.java` are the Expo/React Native templates under `com.siliconoverdrive.pestelite`. No custom native module was added.

## iOS and web

`app.json` sets `ios.bundleIdentifier` to `com.yourcompany.yourappname`. There is no Xcode project. `package.json` scripts:

- `start` — `expo start`
- `android` — `expo run:android`
- `ios` — `expo run:ios`
- `web` — `expo start --web`

`react-native-web` and `react-dom` are dependencies. No web-specific entry beyond Expo's web bundler setting (`app.json` `web.bundler` = `metro`). Whether anyone ran the web target is **Unknown**.

## EAS and environment

`app.json` `extra.eas.projectId` is `d2ffa4b5-2390-43bc-93f6-289744487883`. `extra.router.origin` is `false`.

`eas.json` profiles:

| Profile | Settings |
|---|---|
| `preview` | Android `buildType` `apk` |
| `preview2` | Android `gradleCommand` `:app:assembleRelease` |
| `preview3` | `developmentClient` true |
| `preview4` | `distribution` `internal` |
| `production` | empty object |

No `submit` block, no env block, no channel. Build history is not in git. **Unknown** if Expo still has this project.

Tracked `.env` (not secret material; same hosts as the source):

```text
FORMIO_BASE_URL=https://pro.formview.io/#/rzkiqpzlxxtfqjl
FORMIO_API_BASE_URL=https://rzkiqpzlxxtfqjl.form.io/
```

`babel.config.js` plugins: `expo-router/babel`, `react-native-reanimated/plugin`, `module:react-native-dotenv`. Routed code does not import `@env`.

`.gitignore` ignores `node_modules/`, `.expo/`, `dist/`, `web-build/`, macOS `.DS_Store`, and keystore-like extensions `*.jks`, `*.p8`, `*.p12`, `*.key`, `*.mobileprovision`. It does not ignore `.env` or `debug.keystore`. Both are tracked. `debug.keystore` does not match `*.key`.

## Background work, storage, monitoring

- No queue, cron, or worker.
- No submission cache. Device storage used by the UI is the one AsyncStorage key.
- No Sentry, Crashlytics, or analytics dependency in `package.json`.
- Sonar project file and `sonarqube_scan.bat` are not in `main`. The onboarding scan listed both. **Unknown** if they existed only on the scan host.
- Logging is `console.log` / `console.error`. Site and pesticide list success paths log the full JSON response. `lib/helpers.js` `logger` prints the message and `JSON.stringify` of the data. Sign-up uses it for the swallowed register error.

## Security notes from the tree

These are observations, not a pentest.

- JWT in AsyncStorage.
- Login and register request the token with an empty JWT header and then trust the response header.
- WebViews load FormView without that token and without an `originWhitelist` prop (WebView default applies).
- Release builds sign with the debug keystore.
- `.env` is committed. Its current contents are the public Form.io URLs, not an API key.
- List and login errors are not shown beyond one sentence or a console line.
- `injectedJavaScript` on two detail screens mutates the FormView DOM.

No rate limit, captcha, or certificate pinning is in the client.

## Development and testing

Install and run are the Expo defaults (`npm start`, `expo run:android`). No engine constraint is in `package.json`. No test script. No test files. No CI config. No Docker.

`metro.config.js` is the Expo default. `README.md` tells the reader to `npx create-expo-app -e with-router` and links the Expo Router docs. It is not a setup guide for Pest Elite.

## Defects visible in source

Recorded so a later reader does not treat them as intended product rules.

| Item | Where | What the source does |
|---|---|---|
| Register swallows HTTP errors | `app/sign-up/index.js` | `.catch` logs and returns undefined, then `response.headers` throws into the generic message |
| Token not awaited in the helper and `useAxios` | `lib/helpers.js`, `hooks/useAxios.js` | Unused by routes. Would send a bad or empty JWT if wired up |
| `axios` not imported | `lib/helpers.js` | `formioAPI` would throw if called |
| `contractExpiry.split` | `app/dashboard/clients/index.js` | Throws when expiry is missing |
| `selectClient.data` | `app/dashboard/sites/index.js` | Throws when the select is missing |
| `setTimeout(observe(...), 2000)` | site and pesticide detail | `observe` runs immediately |
| Stray `j` attribute | those same two files | JSX boolean prop `j={true}` with no consumer |
| Unused `use` import | `app/forms/[id].js` | Imported from `expo-router`, never called |
| `loggedIn` after logout | `app/_layout.js` | Login segment skips the effect, so the flag is not cleared on the way to `/login` |
| Two Digimonk URLs | routed create uses `digimonkv1`; unused component uses `digimonk` | Only the routed URL is on the navigation path |

## Related Silicon Ark documents

- `pestelite_html` — Cordova/HTML client of `digimonkv1`, `clientregister`, and `pesticidesregister` on the same Form.io host, with a different login path.
- `formulys_simpleflow` — React web app on the same Form.io host, SimFlow forms, not these registers.

Neither repository is imported by this one. Shared-host conclusions are inferences and are listed as confirmation items in the project plan.
