Unit Tests / test (pull_request) Successful in 14s
Closes #121. Supersedes the separate Deploy Android workflow in #58 by attaching hesychia-<version>-<code>.aab to an android-v* Gitea release after the prod web deploy.
187 lines
7.7 KiB
Markdown
187 lines
7.7 KiB
Markdown
# 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<versionName>-<versionCode>` and attaches `hesychia-<versionName>-<versionCode>.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
|