# Android (Capacitor) Ship the CRA web build as an Android app via [Capacitor](https://capacitorjs.com/). One codebase: `llm-fe/` → web + Android + iOS WebView. **Build & Play deploy steps:** [`android/README.md`](android/README.md). **iOS sibling:** [`IOS.md`](IOS.md) / [`ios/README.md`](ios/README.md). Prerequisites for blockers already merged: JWT-only auth (#22), WebSocket lifecycle (#23), HashRouter (#24). ## Prerequisites - **Node.js 20** (matches CI; Capacitor **7.x**) - **JDK 17+** (OpenJDK fine) - **Android Studio** (Ladybug+) with Android SDK Platform **35**, build-tools, and an emulator or device - Set `ANDROID_HOME` (or open the project once in Android Studio so `local.properties` is generated) ## One-time / day-to-day ```bash cd llm-fe npm ci npm run build:mobile # loads .env.mobile → CRA build → cap sync android npm run android:open # opens android/ in Android Studio ``` Or sync only after an existing `build/`: ```bash npm run android:sync ``` ## Environment | File | Purpose | |------|---------| | `.env.mobile` | Endpoints baked into the **mobile** bundle (`build:mobile`) | | `.env.production` | Web prod deploys (`build` / `build:prod`) | | `.env.beta` | Web beta deploys | Default `.env.mobile` matches production (`chatbackend.aimloperations.com`). Point it at beta to flip the shell without touching web deploys. Optional gitignored override: `.env.mobile.local`. ### Play Billing (#78) Native billing uses the local Capacitor plugin `native-store-billing` (Play Billing on Android, StoreKit on iOS). Web keeps Stripe Checkout / Customer Portal. The Play product id is the plan's `store_product_id`. The backend verifies the purchase token with the Play Developer API (`GOOGLE_PLAY_SERVICE_ACCOUNT_JSON`). More detail: [`MONETIZATION.md`](MONETIZATION.md). After plugin changes: `npm run build:mobile` (runs `cap sync`). ## Versioning `android/app/build.gradle` keeps a floor: - `hesychiaVersionNameFloor` — `"0.1.0"` (user-visible string) - `hesychiaVersionCodeFloor` — `1` (integer; Play requires a higher code on every upload) Local `./gradlew bundleRelease` uses that floor when you do not pass properties. **Deploy Prod** does not edit the file. After the web deploy it looks up existing `android-v*` Gitea releases, reuses the pair when the tag already points at this commit, otherwise ships the next patch and the next `versionCode` via `-PhesychiaVersionName` / `-PhesychiaVersionCode`. Raise the floor in git only when you want the next automatic release to jump ahead of older tags. ## Deploy Prod publishes the Play bundle **Deploy Prod** (`.gitea/workflows/deploy-prod.yml`) runs `android-release` after the prod web deploy of the same commit. The job: 1. Resolves the Play version (`llm-fe/scripts/android-version.mjs`) 2. `npm run build:android:store` — `.env.mobile` (prod API) plus `cap sync android` 3. Signs with the upload keystore on the runner (`~/.android/`, not in git) 4. `jarsigner -verify` on `app-release.aab` 5. Creates or reuses a Gitea release tagged `android-v-` and attaches `hesychia--.aab` Example of the same shape on RealPath: [android-v1.0.2-3](https://git.aimloperations.com/Ditch_The_Agent/dta_webapp/releases/tag/android-v1.0.2-3). Download that AAB from the Gitea release and upload it to the Play **internal** track. Deploy Prod does not call the Play Developer API. A separate **Deploy Android** workflow is not used ([#58](https://git.aimloperations.com/ai_ml_operations/chat_web_app/issues/58) is superseded by this job, [#121](https://git.aimloperations.com/ai_ml_operations/chat_web_app/issues/121)). Manual store binary (same web bundle the job builds): ```bash cd llm-fe npm run build:android:store cd android ./gradlew bundleRelease ``` ## Icons / splash Source art lives in `llm-fe/assets/`: - `icon.png` / `splash.png` / `splash-dark.png` — Capacitor inputs (Hesychia meander mark) - `logo-source.png` — cropped square master - `logo-mark-transparent.png` — circular mark with transparent corners (web / in-app) In-app UI also imports `src/llm-fe/assets/brand/hesychia-mark.png`. Regenerate Android densities after changing source art: ```bash npm run assets:generate ``` Same command also regenerates iOS `Assets.xcassets` (see [`IOS.md`](IOS.md)). ## Signing (upload keystore) **Never commit keystores or `keystore.properties`.** Passwords stay on the runner, not in Gitea Actions secrets. Deploy Prod expects these host files (separate from RealPath’s `keystore.properties`, so the two apps do not share one symlink target): ```text /home/westfarn/.android/hesychia-upload.jks /home/westfarn/.android/hesychia-keystore.properties # mode 600 ``` Create the upload key **once**. If Play already has an upload key for `ai.hesychia.chat`, reuse that keystore. A second keystore will not upload. ```bash keytool -genkey -v \ -keystore /home/westfarn/.android/hesychia-upload.jks \ -keyalg RSA -keysize 2048 -validity 10000 \ -alias aiml-chat-upload ``` `hesychia-keystore.properties` (`storeFile` may be absolute): ```properties storeFile=/home/westfarn/.android/hesychia-upload.jks storePassword=... keyAlias=aiml-chat-upload keyPassword=... ``` The workflow symlinks that file to `llm-fe/android/keystore.properties` and writes `android/local.properties` with `sdk.dir=/home/westfarn/Android/Sdk`. For a local release build, put the same `keystore.properties` in `llm-fe/android/` (gitignored). Also on the runner: JDK 17+ and Android SDK Platform 35 at `/home/westfarn/Android/Sdk`. Gitea Actions secret on `chat_web_app`: `GITHUB_TOKEN` (repo release write). The workflow maps it to `GITEA_TOKEN` for version lookup and release upload. Purchase verification uses backend `GOOGLE_PLAY_SERVICE_ACCOUNT_JSON`, not this keystore. Release App Bundle: ```bash cd llm-fe/android ./gradlew bundleRelease # → app/build/outputs/bundle/release/app-release.aab ``` Debug APK (no signing props needed): ```bash ./gradlew assembleDebug ``` ## Manual QA checklist On emulator **and** physical device: - [ ] Login / logout / password reset flows - [ ] Chat streaming + markdown / code blocks - [ ] Charts (`recharts`) - [ ] Dark / light theme - [ ] Hardware back: navigates history, exits at root - [ ] Keyboard does not cover compose input; notch/safe-area OK - [ ] Background → resume keeps or reconnects WebSocket - [ ] Airplane mode → restore reconnects ## Play Store (internal testing first) Out of band ops (not automated here): 1. Google Play developer account 2. Privacy policy URL (product site / legal) 3. **Account deletion URL** (Data safety / Account deletion) → public page: - Canonical: `https://hesychia.ai/account-deletion/` - Legacy host until DNS cutover: `https://chat.aimloperations.com/account-deletion/` - Capacitor HashRouter: `/#/account-deletion/` 4. Data safety form (JWT auth, chat content, analytics if Tianji runs in WebView) 5. Screenshots for phone (+ tablet if targeting) 6. Upload `.aab` to **internal testing** track before production In-app delete remains Account → **Delete my account** (`DELETE /api/user/`). The public URL above is for Play reviewers and users who never open the app (#103). ## Related issues - [#20](https://git.aimloperations.com/ai_ml_operations/chat_web_app/issues/20) — this Android wrap - [#21](https://git.aimloperations.com/ai_ml_operations/chat_web_app/issues/21) — iOS (reuses this scaffolding) - [#22](https://git.aimloperations.com/ai_ml_operations/chat_web_app/issues/22) / [#23](https://git.aimloperations.com/ai_ml_operations/chat_web_app/issues/23) / [#24](https://git.aimloperations.com/ai_ml_operations/chat_web_app/issues/24) — auth / WS / routing - [#103](https://git.aimloperations.com/ai_ml_operations/chat_web_app/issues/103) — Play public account-deletion URL