# 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.
