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.
7.7 KiB
Android (Capacitor)
Ship the CRA web build as an Android app via Capacitor.
One codebase: llm-fe/ → web + Android + iOS WebView.
Build & Play deploy steps: android/README.md.
iOS sibling: IOS.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 solocal.propertiesis generated)
One-time / day-to-day
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/:
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.
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:
- Resolves the Play version (
llm-fe/scripts/android-version.mjs) npm run build:android:store—.env.mobile(prod API) pluscap sync android- Signs with the upload keystore on the runner (
~/.android/, not in git) jarsigner -verifyonapp-release.aab- Creates or reuses a Gitea release tagged
android-v<versionName>-<versionCode>and attacheshesychia-<versionName>-<versionCode>.aab
Example of the same shape on RealPath: 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 is superseded by this job, #121).
Manual store binary (same web bundle the job builds):
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 masterlogo-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:
npm run assets:generate
Same command also regenerates iOS Assets.xcassets (see 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):
/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.
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):
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:
cd llm-fe/android
./gradlew bundleRelease
# → app/build/outputs/bundle/release/app-release.aab
Debug APK (no signing props needed):
./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):
- Google Play developer account
- Privacy policy URL (product site / legal)
- 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/
- Canonical:
- Data safety form (JWT auth, chat content, analytics if Tianji runs in WebView)
- Screenshots for phone (+ tablet if targeting)
- Upload
.aabto 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).