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>
83 lines
6.7 KiB
Markdown
83 lines
6.7 KiB
Markdown
# 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.
|