# S10 Chat Screen โ€” Figma Rewrite + Bug Fixes (Test Plan) > Sub-plan of [phase4-customer-flow-plan.md](phase4-customer-flow-plan.md). > Source-of-truth visuals: [requirement/Figma/screens/session.jsx](Figma/screens/session.jsx#L150) (S10Chat, lines 150โ€“284) + > [requirement/Figma/screens/v3.jsx](Figma/screens/v3.jsx#L423) (HBChatExpiredBanner). > Decided 2026-05-12: discard the pre-Figma S10 implementation and follow Figma strictly. ## Scope 1. Replace [client_app/lib/features/chat/screens/chat_screen.dart](../client_app/lib/features/chat/screens/chat_screen.dart) with a Figma-faithful S10 implementation. 2. Rewrite [client_app/lib/features/chat/widgets/chat_expired_banner.dart](../client_app/lib/features/chat/widgets/chat_expired_banner.dart) to match `HBChatExpiredBanner` (brand-pink, copy "habis nih... mau lanjutin curhat sama {name}?", white `perpanjang` chip). 3. Drop: the "[Bestie/User] Sudah Memasuki Ruangan" entry banners, the AppBar `akhiri` button, the doodle-pattern background, the voice-call mode pill. 4. Add: HBOrb avatar (placeholder gradient), "online ยท ngetik..." inline status, SISA WAKTU pill, 3px progress bar under header, animated 3-dot typing pill in messages list, 2-minute soft-warning inline banner, `+` attachment button on input bar (no-op for now), `terenkripsi ยท gak disimpan ๐Ÿ”’` footer. ## Bug fixes shipped alongside the rewrite ### Bug 1 โ€” 3-min snackbar doesn't fire reliably **Symptom:** when the chat timer drops below 3 minutes, no snackbar reminder ("sisa 3 menit lagi ya ๐Ÿค") appears. **Cause:** [chat_screen.dart](../client_app/lib/features/chat/screens/chat_screen.dart) only listened to the backend `session_warning` WebSocket event. In dev/test scenarios where the backend doesn't emit that event (e.g. force-confirmed payments via `/internal/_test/force-confirm-payment`), the snackbar never fires. **Fix:** fire the snackbar locally as soon as `chatRemainingSecondsProvider` crosses below 180s, using `_threeMinShown` to dedupe with the backend event. Re-arm when an extension pushes remaining back above 180s so the *next* crossing also fires. ### Bug 2 โ€” Floating expired banner sticks after extension **Symptom:** after extending a session via the time-up sheet, the floating "habis nih..." banner stays on-screen and the SISA WAKTU pill in the header is invisible (timer not ticking). **Cause:** [extension.service.js#finalizeExtension](../backend/src/services/extension.service.js#L185) sends `EXTENSION_RESPONSE` to the customer **without** the freshly-extended `expires_at`. The client's local `chatRemainingSecondsProvider` is computed off `chatState.expiresAt`, which still points at the just-elapsed moment from the `SESSION_EXPIRED` snap. The provider yields 0 and returns. The new `expires_at` only reaches the client on the next periodic `SESSION_TIMER` ping โ€” up to 60s later. **Fix (backend):** include `expires_at: extended.expires_at` in the accept-side `EXTENSION_RESPONSE` payload (`extension.service.js`). **Fix (client):** in [chat_notifier.dart](../client_app/lib/core/chat/chat_notifier.dart) `extensionResponse` case, parse `expires_at` from the payload when `accepted=true` and update `expiresAt` on the state. The provider re-runs, computes a positive remaining, and the banner/pill recover immediately. --- ## Test plan ### Backend Vitest โ€” [test/services/extension.service.test.js](../backend/test/services/extension.service.test.js) (new, 2/2 passing) | # | Test | Setup | Assert | |---|------|-------|--------| | 1 | Accepted extension broadcasts `expires_at` | Seed active session w/ `expires_at = now+30s`, confirmed extension payment, pending extension row. Call `respondToExtension(..., accepted=true)`. | `EXTENSION_RESPONSE` payload sent to customer has `accepted=true`, `duration_minutes=10`, `expires_at` set, ~10min after the seeded baseline. DB row matches. | | 2 | Rejected extension omits `expires_at` | Same setup, `accepted=false` | `EXTENSION_RESPONSE` payload has `accepted=false` and **no** `expires_at` (timer wasn't extended). | ### Manual smoke (operator) Both scenarios run on the emulator with the dev backend + a real-or-stubbed mitra. Use `.maestro/scripts/mark_latest_payment_paid.js` to force-confirm payments and skip Xendit. **S10-A. 3-min snackbar fires from local tick.** 1. Pair into a session with a short duration (e.g. 5 min). 2. Wait until the SISA WAKTU pill shows โ‰ค 3:00. 3. **Expect:** snackbar "sisa 3 menit lagi ya ๐Ÿค" appears once. 4. Send a message โ€” snackbar should NOT re-fire on rebuild. **S10-B. 3-min snackbar re-arms after extension.** 1. Continue from S10-A (snackbar already fired, timer < 3 min). 2. Tap the soft-warning's `+30 menit` to open the time-up sheet. 3. Pick a tier, force-confirm payment. 4. **Expect:** SISA WAKTU pill resumes counting (e.g. ~14:00 if you picked +12min), progress bar refills, soft-warning + expired banner gone. 5. Let the timer drift down again to < 3 min. 6. **Expect:** snackbar fires again. **S10-C. Floating expired banner clears after extension.** 1. Pair into a session and let the timer expire (or use `.maestro/scripts/force_session_expires_at.js` to short-circuit). 2. **Expect:** floating brand-pink "habis nih... mau lanjutin curhat sama {name}?" banner appears, SISA WAKTU pill disappears (remaining โ‰ค 0). 3. Tap `perpanjang`, pick a tier, force-confirm payment. 4. **Expect (within ~1s of backend ack):** banner disappears, SISA WAKTU pill returns with the new remaining, progress bar redraws. Input bar is reactivated. **S10-D. Voice-call session** *(known gap, not a regression)* - Voice-call mode badge was dropped per strict-Figma. If voice-call sessions need an indicator, raise as a follow-up. **S10-E. Mid-session manual end** *(known gap)* - Figma S10 has no `akhiri` button. PricingBottomSheet doesn't currently have a "cukup, akhiri sesi" option either, so manual end mid-session is unreachable until either (a) the time-up sheet grows that button or (b) we add an end-session affordance per business call. ### Maestro automation Deferred โ€” the Phase 4 Stage 9 Semantics regression on `SHome1st` still blocks the upstream onboarding flows from reaching S10 in Maestro (see [phase4-esp-removal-usp-gate.md "Known blocker"](phase4-esp-removal-usp-gate.md#known-blocker)). Once those flows unblock, add: - `09_chat_three_min_snackbar.yaml` โ€” covers S10-A + S10-B - `10_chat_extension_recovers_timer.yaml` โ€” covers S10-C --- ## Done criteria - [x] Backend Vitest 2/2 green for `EXTENSION_RESPONSE.expires_at`. - [x] `flutter analyze` clean on `chat_screen.dart` + `chat_expired_banner.dart` + `chat_notifier.dart`. - [ ] Manual smoke S10-A, S10-B, S10-C all green on emulator-5554. - [ ] Side-by-side visual diff vs `requirement/Figma/screens/session.jsx::S10Chat` โ€” no obvious drift.