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

6.7 KiB
Raw Permalink Blame History

S10 Chat Screen — Figma Rewrite + Bug Fixes (Test Plan)

Sub-plan of phase4-customer-flow-plan.md. Source-of-truth visuals: requirement/Figma/screens/session.jsx (S10Chat, lines 150284) + requirement/Figma/screens/v3.jsx (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 with a Figma-faithful S10 implementation.
  2. Rewrite 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 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 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 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 (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"). 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

  • Backend Vitest 2/2 green for EXTENSION_RESPONSE.expires_at.
  • 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.