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>
This commit is contained in:
82
requirement/phase4-chat-screen-figma.md
Normal file
82
requirement/phase4-chat-screen-figma.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# 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.
|
||||
224
requirement/phase4-esp-removal-usp-gate.md
Normal file
224
requirement/phase4-esp-removal-usp-gate.md
Normal file
@@ -0,0 +1,224 @@
|
||||
# ESP Removal + USP One-Time Gate — Implementation & Test Plan
|
||||
|
||||
> Sub-plan of [phase4-customer-flow-plan.md](phase4-customer-flow-plan.md).
|
||||
> Source-of-truth diagram: [flow_customer.mermaid.md §2](flow_customer.mermaid.md).
|
||||
> Business decision: 2026-05-12 — retire S5 ESP entirely; show S5b USP at most once per user.
|
||||
|
||||
## Scope
|
||||
|
||||
1. Remove the S5 ESP screen + state from `client_app`.
|
||||
2. Rewire `VerifChoiceSheet` to go straight to a `usp_seen?` gate, then USP, then the original next step (S3a for verified, PickMethod for anon).
|
||||
3. Add a `customers.usp_seen` boolean to the backend; expose on `/api/client/me`; add `POST /api/client/usp-seen` to set it.
|
||||
4. Add a Riverpod `uspSeenProvider` backed by `SharedPreferences` with DB sync on login and on dismissal.
|
||||
5. Update Maestro flows + add new flows for the gate behaviour.
|
||||
|
||||
Out of scope: changing USP copy/visuals; reordering USP relative to OTP (business accepted the cross-device first-view edge case).
|
||||
|
||||
---
|
||||
|
||||
## Build order
|
||||
|
||||
The work splits into 5 ordered stages. Backend lands first so the client has a real endpoint to call.
|
||||
|
||||
### Stage 1 — Backend: schema + read + write
|
||||
|
||||
**Files:**
|
||||
- `backend/src/db/migrate.js` — append a Phase-4 ALTER block:
|
||||
```sql
|
||||
ALTER TABLE customers ADD COLUMN IF NOT EXISTS usp_seen BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
```
|
||||
- `backend/src/services/customer.service.js`
|
||||
- Add `usp_seen` to `CUSTOMER_SELECT` so every read includes it.
|
||||
- Add `markCustomerUspSeen(customerId)` — idempotent UPDATE that sets `usp_seen = TRUE` and returns the row via `CUSTOMER_SELECT`.
|
||||
- `backend/src/routes/public/client.auth.routes.js`
|
||||
- Already returns the customer from `getCustomerById` on `/api/client/me`. The new column rides along automatically once it's in `CUSTOMER_SELECT`.
|
||||
- Add `POST /api/client/usp-seen` handler: requires JWT, calls `markCustomerUspSeen`, returns the updated customer. No request body needed.
|
||||
|
||||
**Acceptance:**
|
||||
- New customer row has `usp_seen = false`.
|
||||
- `POST /api/client/usp-seen` flips it; second POST is a no-op (still returns true).
|
||||
- `/api/client/me` response includes `usp_seen` for both first-time and returning users.
|
||||
|
||||
### Stage 2 — client_app: `uspSeenProvider` + DB hydrate
|
||||
|
||||
**New file:** `client_app/lib/features/onboarding/usp_seen_provider.dart`
|
||||
- Async-init `Notifier` (or `AsyncNotifier`) that:
|
||||
- On build, reads SharedPreferences key `usp_seen` (default false).
|
||||
- Exposes `bool get hasSeen` synchronously after init.
|
||||
- `Future<void> markSeen()`:
|
||||
1. Write `true` to SharedPreferences.
|
||||
2. If JWT is present (authProvider state is `AuthAuthenticatedData`), call `POST /api/client/usp-seen` via `ApiClient` — fire-and-forget with logging; don't block UX on the network call.
|
||||
- Add `hydrateFromCustomer(Customer c)` — call from auth bootstrap (e.g. wherever `/api/client/me` is fetched and stored in `AuthAuthenticatedData`). OR-merge: if `c.uspSeen == true`, write `true` to SharedPreferences.
|
||||
|
||||
**Edit:** the auth notifier that already calls `/api/client/me` on app boot — add a call to `uspSeenProvider.hydrateFromCustomer(...)` after the response lands. (Per Explore: `auth_notifier.dart` has the `AuthAuthenticatedData` carrying the profile.)
|
||||
|
||||
**Acceptance:**
|
||||
- Fresh install: provider returns `false`.
|
||||
- After `markSeen()`: provider returns `true`; SharedPreferences key set; backend hit (if auth'd).
|
||||
- Login on a fresh device where DB has `usp_seen=true`: provider returns `true` after auth hydrate completes.
|
||||
|
||||
### Stage 3 — client_app: rewire VerifChoiceSheet → USP gate
|
||||
|
||||
**Edit:** `client_app/lib/features/auth/widgets/verif_choice_sheet.dart`
|
||||
- Replace `routeForVerifChoice()` body. New logic (pseudo):
|
||||
```dart
|
||||
final seen = ref.read(uspSeenProvider).hasSeen;
|
||||
if (choice == VerifChoice.verifWA) {
|
||||
if (seen) context.push('/auth/register'); // straight to S3a
|
||||
else context.push('/onboarding/verif/usp');
|
||||
} else {
|
||||
if (seen) context.push('/payment/method-pick'); // straight to PickMethod
|
||||
else context.push('/onboarding/anon/usp');
|
||||
}
|
||||
```
|
||||
(Exact target routes per existing `router.dart` registrations.)
|
||||
|
||||
**Edit:** `client_app/lib/features/onboarding/screens/usp_screen.dart`
|
||||
- On the primary "Continue" / next CTA tap, `await ref.read(uspSeenProvider.notifier).markSeen()` BEFORE navigating to the next route.
|
||||
- The existing post-USP routing (verified → S3a, anon → PickMethod) stays — the `markSeen()` call just precedes it.
|
||||
|
||||
**Edit:** `client_app/lib/features/auth/screens/otp_screen.dart` (or wherever the OTP-Blocked popup lives) — the fallback to anon path. Currently it pushes ESP; change to the same USP-gate logic above (`uspSeenProvider.hasSeen ? PickMethod : USP`).
|
||||
|
||||
**Acceptance:**
|
||||
- First-time verified flow: VerifChoice "verif WA" → USP (with markSeen on continue) → S3a.
|
||||
- Second-time verified flow: VerifChoice "verif WA" → S3a directly.
|
||||
- First-time anon flow: VerifChoice "tanpa verif" → USP → PickMethod.
|
||||
- Second-time anon flow: VerifChoice "tanpa verif" → PickMethod directly.
|
||||
- OTP-Blocked fallback respects the gate.
|
||||
|
||||
### Stage 4 — client_app: delete ESP
|
||||
|
||||
This is the cleanup step; it intentionally runs *after* the new gate is wired so we never have a moment where the build is broken.
|
||||
|
||||
**Delete:**
|
||||
- `client_app/lib/features/onboarding/screens/esp_screen.dart`
|
||||
- `client_app/lib/features/onboarding/esp_state.dart` (the two `espSelectionProvider` / `espSkippedProvider`)
|
||||
|
||||
**Edit:** `client_app/lib/router.dart`
|
||||
- Remove the two `/onboarding/verif/esp` and `/onboarding/anon/esp` `GoRoute` entries.
|
||||
- Leave the `/onboarding/*` redirect carve-out intact (USP still uses it).
|
||||
|
||||
**Verify:** `flutter analyze` clean — no dangling imports, no orphan references to `EspTopic`, `espSelectionProvider`, `espSkippedProvider`.
|
||||
|
||||
### Stage 5 — Tests
|
||||
|
||||
Detailed below in the **Test plan** section. Updates:
|
||||
- Existing Maestro flows `02_onboarding_verified.yaml` + `03_onboarding_anon.yaml` need to drop ESP steps and add gate-aware assertions.
|
||||
- New Vitest cases for the migration + service + route.
|
||||
- New Maestro flow for the USP-skip-on-second-run case.
|
||||
|
||||
---
|
||||
|
||||
## Data contract
|
||||
|
||||
### `customers.usp_seen` (new column)
|
||||
- Type: `BOOLEAN NOT NULL DEFAULT FALSE`
|
||||
- Set true only on `POST /api/client/usp-seen` or via direct backfill.
|
||||
- Never read by anything except `/api/client/me` and the dedicated POST handler.
|
||||
|
||||
### `POST /api/client/usp-seen`
|
||||
- Auth: JWT required (same middleware as `/me`).
|
||||
- Request: empty body.
|
||||
- Response: 200 with the updated customer object (same shape as `/me`).
|
||||
- Errors: 401 on missing/invalid JWT; 404 if customer row doesn't exist (shouldn't happen in normal flow).
|
||||
- Idempotent: calling twice is fine.
|
||||
|
||||
### `/api/client/me` (extended)
|
||||
- Existing payload + `usp_seen: boolean`.
|
||||
|
||||
### SharedPreferences key (client)
|
||||
- Key: `usp_seen`
|
||||
- Value: `bool` (default `false`).
|
||||
- Owned by `uspSeenProvider`; no other code reads/writes it directly.
|
||||
|
||||
---
|
||||
|
||||
## Test plan
|
||||
|
||||
### Unit tests (Vitest, in `backend/test/`)
|
||||
|
||||
**New file:** `backend/test/customer.usp-seen.test.js`
|
||||
|
||||
| # | Test | Setup | Assert |
|
||||
|---|------|-------|--------|
|
||||
| 1 | Migration default | Insert customer via `createCustomerWithIdentity` with no usp_seen | `getCustomerById(...).usp_seen === false` |
|
||||
| 2 | `markCustomerUspSeen` flips flag | Customer with `usp_seen=false` | After call, row has `usp_seen=true`; return value's `usp_seen` is `true` |
|
||||
| 3 | Idempotent | Customer with `usp_seen=true` | Calling again still returns `usp_seen=true`; no error |
|
||||
| 4 | `POST /api/client/usp-seen` requires auth | No `Authorization` header | 401 |
|
||||
| 5 | `POST /api/client/usp-seen` happy path | Authed customer, `usp_seen=false` | 200, response `usp_seen=true`, DB row `usp_seen=true` |
|
||||
| 6 | `/api/client/me` includes flag | Authed customer | Response has `usp_seen` key (true or false) |
|
||||
|
||||
Target: 6/6 green via `npm test` in `backend/`.
|
||||
|
||||
### Flutter widget/integration tests
|
||||
|
||||
These are not currently a major surface in this repo (Maestro is the main client-side gate). Skip Flutter-level widget tests unless something breaks.
|
||||
|
||||
### Maestro flows (`client_app/.maestro/flows/`)
|
||||
|
||||
**Existing flow updates:**
|
||||
|
||||
- **`02_onboarding_verified.yaml`** — first-time-verified path
|
||||
- Remove: any `assertVisible` / tap targets on ESP chips ("Hubungan", "Lewati").
|
||||
- Update sequence: VerifChoiceSheet → tap "Verif WA Rp2k" → **assert USP visible** → tap continue → S3a WhatsApp input → ...
|
||||
- Add at the start: `runScript: scripts/reset_phone_and_local.js` so the run always starts from a clean state (no `usp_seen` in DB, no SharedPreferences value).
|
||||
|
||||
- **`03_onboarding_anon.yaml`** — first-time-anonymous path
|
||||
- Remove: ESP chip taps / "Lewati" tap.
|
||||
- Update sequence: VerifChoiceSheet → tap "tanpa verif Rp5k" → **assert USP visible** → tap continue → `/payment/method-pick`.
|
||||
- Add reset script at start.
|
||||
|
||||
**New flows / deferrals:**
|
||||
|
||||
The "second-run skip" and "DB hydrate" cases turned out hard to script in Maestro: once a customer logs in (anonymously or with phone), the app is in an authenticated session and the only path back to `VerifChoiceSheet` (where the gate is consulted) is via logout, which clears local SharedPreferences too. Returning-user CTAs go through `BestieChoiceSheet`, not `VerifChoiceSheet`, so the gate is never re-evaluated on the returning path.
|
||||
|
||||
What we *do* have:
|
||||
- Flows `02` and `03` (first run, USP visible, ESP not visible) — covered above.
|
||||
- Vitest 8/8 covers the backend: column default, `markCustomerUspSeen`, route 401/200/403, `/me` payload before + after the flag flips.
|
||||
|
||||
What stays in **manual smoke** (operator-driven, documented in the next section):
|
||||
1. Local flag persists across `stopApp/launchApp` — `adb shell pm clear` should NOT happen between runs; verify USP is skipped on second walk through VerifChoice.
|
||||
2. DB hydrate — pre-seed a customer row with `usp_seen=true` via control center or psql, sign in via phone OTP, verify USP is skipped on first ever appearance.
|
||||
3. OTP-blocked popup — exit via "lanjut tanpa verif" still lands at `/payment/method-pick`. (Pre-USP-gate this was a direct redirect; the gate doesn't fire on this path because USP has already been shown/skipped upstream.)
|
||||
|
||||
**Known blocker (2026-05-12):** flows `02` and `03` had their ESP steps removed and USP gate assertions added, but the runtime can't currently execute them end-to-end. The new `SHome1st` view (Phase 4 Stage 9) wraps the home column in a single Semantics node, so the `"aku mau curhat"` CTA's `text` attribute reads as empty (its label only lives in the parent's merged `accessibilityText`). Maestro's `text:` matcher can't locate the button, blocking the entire flow before USP is even reached. This is a Stage-9 accessibility regression, not an ESP/USP issue — the flow YAML edits are correct and will pass once SHome1st's CTA is wrapped in its own `Semantics(label: '…', button: true)` or given a `Key`.
|
||||
|
||||
### Manual smoke (real device)
|
||||
|
||||
After Maestro is green on emulator, hand-run on the physical Samsung:
|
||||
1. Fresh install → "aku mau curhat" → name → VerifChoice → verif WA → USP (visible) → S3a → OTP → S6 paywall.
|
||||
2. Force-stop → relaunch → "aku mau curhat" → VerifChoice → verif WA → S3a (USP skipped) → ...
|
||||
3. Same but anon path.
|
||||
4. Uninstall + reinstall → login with same phone → "aku mau curhat" → verify USP skipped (DB hydrate proved end-to-end).
|
||||
|
||||
### Visual regression
|
||||
|
||||
`flutter analyze` clean. Spot-check VerifChoiceSheet, USP screen, OTP-Blocked popup in the emulator — no broken navigation, no leftover ESP icon/copy.
|
||||
|
||||
---
|
||||
|
||||
## Rollout & migration
|
||||
|
||||
- Migration is additive (NOT NULL DEFAULT FALSE), safe to run on existing DB. All existing customers come out with `usp_seen=false` — meaning every returning user will see USP one more time on next "aku mau curhat". Business accepted this.
|
||||
- No backfill needed.
|
||||
- Cloud Run rollout: backend first (migration runs on boot), then client_app build.
|
||||
|
||||
## Risks
|
||||
|
||||
1. **Provider init race** — if `uspSeenProvider` is read before SharedPreferences finishes loading, the gate could return false (default) and show USP unnecessarily. Mitigation: use `AsyncNotifier` and gate the VerifChoice navigation on the loaded state, or read SharedPreferences synchronously at app boot before the first VerifChoice render.
|
||||
2. **Network failure on `markSeen()`** — if `POST /usp-seen` fails after local flag is set, DB stays false. Next session uses local (still true) so user UX is fine. On a new device, USP shows once more. Acceptable per the cross-device edge case decision.
|
||||
3. **Two-tab race** — not applicable; mobile app.
|
||||
|
||||
---
|
||||
|
||||
## Done criteria
|
||||
|
||||
- [ ] `customers.usp_seen` column exists in dev DB.
|
||||
- [ ] `POST /api/client/usp-seen` returns 200 for authed call.
|
||||
- [ ] `/api/client/me` payload includes `usp_seen`.
|
||||
- [ ] Vitest 6/6 green on the new test file.
|
||||
- [ ] No `esp_screen.dart` / `esp_state.dart` / `/onboarding/*/esp` routes in client_app.
|
||||
- [ ] `flutter analyze` clean on client_app.
|
||||
- [ ] Maestro: `02`, `03`, `04`, `05`, `06` green on the Client_Phone emulator.
|
||||
- [ ] Manual smoke 1–4 above pass on physical device.
|
||||
- [ ] `TECH_DEBT.md` "S5 ESP screen retired" entry closed / removed once cleanup lands.
|
||||
78
requirement/resume-2026-05-15.md
Normal file
78
requirement/resume-2026-05-15.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# Resume — 2026-05-15
|
||||
|
||||
> Cross-device pickup note. Mirror of the local Claude memory `project_resume_next.md` so this is reachable on any machine that clones the repo. Delete this file when fully resumed.
|
||||
|
||||
Paused **2026-05-14 evening**. Chat-screen perf refactor done in code on both apps; release rebuild + install + retest on mitra is the gating step that didn't complete (S21 Ultra unplugged before the final build could finish).
|
||||
|
||||
## What needs doing tomorrow — in order
|
||||
|
||||
### 1. Rebuild + install mitra release on S21 Ultra
|
||||
|
||||
Code is on disk in `mitra_app/lib/features/chat/screens/mitra_chat_screen.dart` (full refactor) and `mitra_app/lib/core/chat/mitra_chat_notifier.dart` (timer-extraction provider). The APK currently on the S21 Ultra only has the timer-extraction fix — NOT the full body/AppBar split.
|
||||
|
||||
```bash
|
||||
# Plug the S21 Ultra, authorize USB debugging if needed:
|
||||
adb devices # confirm device shows as `device`, not `unauthorized`
|
||||
|
||||
# Build + install + run:
|
||||
cd mitra_app
|
||||
flutter run -d <S21_DEVICE_ID> --release --dart-define=API_BASE_URL=http://<DEV_MACHINE_IP>:3000
|
||||
```
|
||||
|
||||
Yesterday's IDs (will differ on a new host):
|
||||
- S21 Ultra: `RRCR100NN7Z`
|
||||
- Customer SM-A530F: `52002a5db8e0c46b`
|
||||
- Dev machine static IP: `192.168.88.247`
|
||||
|
||||
Backend dev server (`cd backend && npm run dev`) needs to be running first. The dev `API_BASE_URL` defaults to production if you forget the dart-define.
|
||||
|
||||
### 2. Test mitra chat under release
|
||||
|
||||
After install: open a chat session, send a few messages, watch the partner type. Expected:
|
||||
- Timer ticks every 1s rebuild ONLY the timer pill in the AppBar.
|
||||
- Sending/receiving messages rebuilds ONLY the body widget.
|
||||
- Typing pulses don't cause whole-screen flicker.
|
||||
|
||||
Bar: it should feel as snappy as the customer app does now (which is the reference point).
|
||||
|
||||
### 3. Verify customer waiting_payment_screen navigation patch
|
||||
|
||||
Yesterday the customer app got stuck on "menunggu pembayaran" after a payment was confirmed (polling stopped but `addPostFrameCallback(context.go(...))` never fired). Patched with belt-and-suspenders in `waiting_payment_screen.dart::_navigateTerminal` — `Future.microtask` + `addPostFrameCallback` redundancy.
|
||||
|
||||
End-to-end test path:
|
||||
1. Customer app: tap "aku mau curhat" → pick tier → create payment.
|
||||
2. SQL-confirm the payment (or use the dev confirm endpoint).
|
||||
3. Watch the waiting screen — should advance off "menunggu pembayaran" into notif-gate → searching within ~3s (one poll cycle).
|
||||
|
||||
If still stuck: I added `print` instrumentation would surface debug-mode only; consider running customer in debug to capture log output.
|
||||
|
||||
### 4. If mitra chat is still laggy after #1
|
||||
|
||||
Next suspect: message-list rebuilds on every state change re-iterate visible ListView.builder items. Try:
|
||||
- Convert `_MessageBubble` to `const` constructor (immutable inputs).
|
||||
- Wrap bubbles in `RepaintBoundary` to isolate paint.
|
||||
|
||||
Don't touch until #1 confirms whether the body-extraction refactor was sufficient.
|
||||
|
||||
## What landed today (already on disk / committed)
|
||||
|
||||
- **Dispose-in-ref fix** in `home_screen.dart`, `payment_screen.dart` (customer), `mitra_chat_screen.dart` (mitra). Pattern: ref-using cleanup goes in `deactivate()`, not `dispose()`. Symptom of regression: next screen looks frozen after navigation, even though app is alive.
|
||||
- **`halo_lints`** package at repo root with `no_ref_in_dispose` rule. Wired into both apps' `analysis_options.yaml`. Also activates the already-installed `riverpod_lint` package (which ships `avoid_ref_inside_state_dispose` for the same case).
|
||||
- **CLAUDE.md Pitfalls section** added to `client_app/CLAUDE.md` and `mitra_app/CLAUDE.md` documenting the dispose-ref landmine.
|
||||
- **Customer chat refactor** — `chat_screen.dart` split into `_ChatHeader` + `_ChatBodySection` + `_TimerBanner`. Parent has zero `ref.watch`.
|
||||
- **Mitra chat refactor** — `mitra_chat_screen.dart` mirrors customer pattern: `_MitraChatBodyContent`, `_MitraChatTopicToggle`, `_MitraChatVoicePill`, `_MitraChatTimerAction`. Plus the `mitraChatRemainingSecondsProvider` for per-second ticks.
|
||||
- **Customer waiting screen nav** — `Future.microtask` + `addPostFrameCallback` redundancy at terminal status.
|
||||
- **Phase 4 Option A retryable blast-failure** — backend `expirePairingRequest` + all-rejected use `recordIntermediateFailure` instead of `failPaymentSession`; WS payload has `is_terminal: false`; client carries `topicSensitivity` through `PairingFailedData`; "coba cari lagi" CTA re-blasts on the same payment via `retryBlast()`. Test updated to match new semantics.
|
||||
|
||||
## Hazards / gotchas to remember
|
||||
|
||||
- **Release mode is the bar.** Debug-mode JIT on both phones (SM-A530F + S21 Ultra) was unusably laggy. Always rebuild release to test real perf.
|
||||
- **`node --watch` doesn't pick up newly-added module files.** When you add a brand-new route file or service, kill + restart the backend dev server. Don't trust the auto-reload for new files.
|
||||
- **AVD on the dev host is unusable for interactive rendering** — use the physical devices.
|
||||
- **`.claude/settings.local.json` + `.claude/agent-memory/` + `client_app/devtools_options.yaml`** stay modified — local-only, never commit.
|
||||
|
||||
## Decisions explicitly deferred
|
||||
|
||||
- **CI integration** — user raised the topic but we punted. Scope to gather when resuming: GitHub Actions vs other; per-PR triggers; which projects (backend vitest + control_center playwright + client/mitra flutter analyze + dart run custom_lint); APK build artifacts; Maestro Cloud or self-hosted device runner.
|
||||
- **Phase 4 §2.1 real-device verification** — still pending from before today. See `requirement/phase3.4-testing.md` §1.5.1 for the runbook.
|
||||
- **`backend/test/services/session-timer.service.test.js`** — 2 pre-existing failures (uuid-string fixture bug). Unrelated to anything we touched.
|
||||
Reference in New Issue
Block a user