Files
halobestie-clone/requirement/phase4-chat-screen-figma.md
ramadhan sjamsani a09f37135c Phase 4 checkpoint: chat-screen perf refactor + retryable blast-failure + repo-wide dispose-ref guardrail
Chat-screen performance (customer + mitra):
- Parent screens have zero `ref.watch` — only `ref.listen` for side effects
- Body extracted into its own `ConsumerStatefulWidget`; AppBar parts split
  into narrow `.select` consumers (mode, sensitivity, timer)
- Per-second timer ticks routed to dedicated providers
  (`chatRemainingSecondsProvider` + new `mitraChatRemainingSecondsProvider`)
  so WS `session_tick` frames don't invalidate the rest of the chat state

Dispose-in-ref bug fix:
- `home_screen.dart`, `payment_screen.dart`, `mitra_chat_screen.dart` —
  ref-using cleanup moved from `dispose()` to `deactivate()`. Modern
  Riverpod invalidates `ref` the moment `dispose()` runs; the resulting
  silent error corrupts the widget-tree finalize and the next screen
  appears frozen
- `halo_lints` package added at repo root with `no_ref_in_dispose` rule
  to catch this pattern in CI / IDE analysis
- `custom_lint` activated in both apps' `analysis_options.yaml`
  (was installed but never wired in — also brings `riverpod_lint`'s
  `avoid_ref_inside_state_dispose` online)
- CLAUDE.md Pitfalls section added to client_app + mitra_app

Phase 4 §3 retryable blast-failure (Option A):
- Backend `expirePairingRequest` + all-rejected use
  `recordIntermediateFailure` instead of `failPaymentSession` so the
  payment session stays `confirmed` for re-blast
- WS `pairing_failed` payload carries `is_terminal: false` on the
  retryable paths; client parses the flag and exposes `retryBlast()`
- "Coba cari lagi" CTA on S7 Timeout now re-blasts on the same payment
- Pairing service test updated to reflect the new semantics

Customer waiting-payment screen navigation patch:
- `_navigateTerminal` uses `Future.microtask` + `addPostFrameCallback`
  redundancy after a release-mode bug where polling stopped but
  `context.go` never fired, leaving the screen visually stuck on
  "menunggu pembayaran"

See requirement/resume-2026-05-15.md for next-day pickup checklist
(mitra release rebuild + S21 Ultra install + retest is the gating item).

Bundles unrelated in-flight Phase 4 §2.x work that was already on disk
(ESP screen removal, USP one-time gate scaffolding, bestie-availability
public route, OTP service edits, Maestro flow tweaks) — kept together
to avoid a partial-rebase mess.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 19:12:34 +08:00

83 lines
6.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 150284) +
> [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.