From bfb072ddfb726c97ff29165d7d7167329caaf4de Mon Sep 17 00:00:00 2001 From: Ramadhan Sjamsani Date: Thu, 21 May 2026 22:38:50 +0800 Subject: [PATCH] Docs: textfield-centering pitfall + config-source / FCM channel conventions - mitra_app/CLAUDE.md: pitfall entry for the InputDecorationTheme min-height collision that broke chat-input centering. Walks through the working recipe (constraints: BoxConstraints(), Material + StadiumBorder + Center wrapper). Points at chat_screen.dart::_InputBar in both apps as the source of truth. - backend/CLAUDE.md: two new convention sections. - Config-source: when to use DB-stored (operator-tunable via CC) vs env-driven (deploy-fixed). Codifies the pattern shipped today for MITRA_HEARTBEAT_CADENCE_SECONDS so Xendit credentials / callback tokens follow the same shape tomorrow. - FCM channel: single shared `halobestie_chat_v1` channel for both apps, target via android.notification.channelId. Bump the channel ID when introducing a new sound (Android API 26+ binds sound at channel-create time). Co-Authored-By: Claude Opus 4.7 (1M context) --- backend/CLAUDE.md | 19 +++++++++++++++++++ mitra_app/CLAUDE.md | 26 ++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/backend/CLAUDE.md b/backend/CLAUDE.md index eec6bb0..45ff767 100644 --- a/backend/CLAUDE.md +++ b/backend/CLAUDE.md @@ -48,3 +48,22 @@ Internal listener must never be exposed to the public internet. - Use Fastify plugins for shared middleware (auth, error handling, logging) - Business logic lives in `services/` — never directly in route handlers - Never reintroduce Firebase Auth. `firebase-admin` is FCM-only; do not import `.auth()` from it. + +## Config-Source Convention + +Two distinct knob-types exist; do not conflate them: + +- **DB-stored** (`app_config` table, mutable via CC SettingsPage at runtime): used for operator-tunable values that may change between deploys without a code roll — `mitra_stale_after_seconds`, `extension_timeout_seconds`, `pricing_tiers`, `support_handles_json`, `max_customers_per_mitra`, etc. Read via getters in `services/config.service.js`. Cache invalidation goes through `valkey` pub/sub when needed. +- **Env-driven** (`process.env`, set per deployment via `.env` or Cloud Run env vars): used for deploy-fixed values that should never differ between operator actions — `MITRA_HEARTBEAT_CADENCE_SECONDS`, `FIREBASE_SERVICE_ACCOUNT_PATH`, `AUTH_JWT_SECRET`, `DATABASE_URL`. Always expose via a getter helper with a sane default + numeric parsing (see `getMitraHeartbeatCadenceSeconds` in config.service.js for the pattern). + +When a new value needs to flow from CC → app, prefer DB. When it's a deploy-fixed contract (e.g. heartbeat cadence the apps must honor, Xendit credentials, callback tokens), prefer env. CC inputs that depend on env values (e.g. min/max validation) read the env-derived value via the same config endpoint that surfaces the DB value, and the PATCH route validates against it. + +## FCM Channel Convention + +Single channel `halobestie_chat_v1` is shared by both apps (registered in each app's `core/notifications/notification_service.dart`) and ships the branded `halobestie_notif.ogg` sound. Backend FCM payloads should always target this channel ID via `android.notification.channelId`: + +```js +android: { priority: 'high', notification: { channelId: 'halobestie_chat_v1' } } +``` + +Do not introduce per-recipient or per-feature channels lightly. If a new sound is required (e.g. payment alert), bump the channel ID (`halobestie_chat_v2`) and update both apps simultaneously — Android binds channel sound at create-time on API 26+, so mutating the existing channel doesn't pick up the new sound for installed users. diff --git a/mitra_app/CLAUDE.md b/mitra_app/CLAUDE.md index abdc3a9..2642882 100644 --- a/mitra_app/CLAUDE.md +++ b/mitra_app/CLAUDE.md @@ -63,3 +63,29 @@ void disconnect() { ``` The synchronous side-effects (closing the WS, cancelling timers) still happen immediately. Only the `state =` assignment is deferred, which is a no-op for users — they're navigating away anyway. Regression coverage: `.maestro/flows/ts-mitra-3-08-back_press_after_session_expired_no_red_screen.yaml`. + +### Custom-styled TextField must override the theme's min-height constraint + +The app-wide `InputDecorationTheme` in `lib/core/theme/halo_theme.dart` sets a 48dp min-height for form fields (auth, profile, etc.). Any pill-style chat-input or compact TextField that has a fixed-height parent (≤ 48dp) will **silently lose vertical centering** — the field refuses to collapse below 48dp, the line-box can't sit on the parent's midline, and `textAlignVertical` becomes a no-op. Text anchors top. + +**Rule:** when building a custom-shaped TextField (pill, dense, fixed-height), explicitly null the theme constraint: + +```dart +TextField( + textAlignVertical: TextAlignVertical.center, + decoration: const InputDecoration( + isCollapsed: true, + contentPadding: EdgeInsets.symmetric(horizontal: 16), + constraints: BoxConstraints(), // ← REQUIRED — overrides theme min-height + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + disabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + focusedErrorBorder: InputBorder.none, + filled: false, + ), +) +``` + +Wrap in `Material(shape: StadiumBorder(...), clipBehavior: antiAlias) + Center(child: TextField)` for proper pill clipping. The chat input bar in [mitra_chat_screen.dart](lib/features/chat/screens/mitra_chat_screen.dart) and [client_app/chat_screen.dart::_InputBar](../client_app/lib/features/chat/screens/chat_screen.dart) both use this pattern; copy from there rather than reinventing.