Stages 5.1, 5.3, 5.4 of the returning-user flow rework. All three §4
entry paths now require payment BEFORE pairing, matching the updated
mermaid spec.
* Spec (requirement/flow_customer.mermaid.md §4): payment block converges
three call-sites (bestie-yang-udah-kenal-online, bestie-baru,
offline-popup → cari bestie lain). PairRoute dispatches lama → targeted
pair, baru/cari-lain → §3 blast. §3 retains its post-payment-shared
contract.
* Stage 5.1 (client_app): PaymentDraft carries targetedMitraId +
topicSensitivity. bestie_history_list seeds the draft + pushes
/payment/entry (was legacy /payment). searching_screen branches on
draft.targetedMitraId for blast-vs-targeted dispatch.
payment_entry uses resetExceptTarget(); bestie_choice_sheet + home
_onCurhatBestieBaruPressed call explicit reset() before push so
the keepAlive draft can't leak stale targeting into a blast.
* Stage 5.3 (client_app): new BestieOfflineVariant.prePayReturning.
Bestie-history-list _BestieRow splits tappable from dim so offline
rows render dimmed but route taps into the popup. CTA "cari bestie
lain" resets the draft + pushes /payment/entry.
* Stage 5.4 (client_app): deleted legacy /payment route,
payment_screen.dart, payment_notifier.dart(+.g.dart). router cleaned.
* Tests (requirement/phase4-customer-flow.md + client_app/.maestro/):
six Maestro flows TS-01..TS-06 covering every §4 branching point,
all passing end-to-end. Shared onboarding prelude under
.maestro/subflows/. New helper scripts: accept_latest_pending,
force_mitra_offline, force_other_mitra_online,
reset_all_mitras_online, mitra_accept_latest_internal. New backend
_test endpoints to match. /reset-phone now cascade-deletes
customer_transactions (FK was blocking). /force-pairing-timeout
branches targeted (RETURNING_CHAT_TIMEOUT via
expireTargetedPairingRequest, now exported) vs blast (PAIRING_FAILED).
seed_history_session also outputs MITRA_NAME_RE (regex-escaped) for
reliable selectors against display names containing regex specials.
* mitra_app: dispose-during-deactivate guardrail for back-press on the
mitra chat screen after the customer's goodbye message. Pending real
emulator repro verification (carried over from 2026-05-15).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Verif Choice Sheet on display_name_screen drives the user into either
the verified or anonymous onboarding sub-flow. ESP screen (12 chips,
multi-select, info-only) + USP screen are shared between both branches;
selections persist through to chat_sessions.topics on session start.
OTP-blocked popup (HaloPopup) listens for the four real OTP-rate-limit
error codes (OTP_RATE_LIMIT_PHONE, OTP_RATE_LIMIT_IP, OTP_COOLDOWN,
OTP_ATTEMPTS_EXCEEDED) and drops the user onto the anonymous path with
ESP/USP state preserved.
Auth-providers gating replaces the --dart-define=ENABLE_SOCIAL_AUTH
build flag with server-driven discovery. authProvidersProvider preloads
GET /api/shared/auth-providers at cold start; welcome/register/
force-register screens render Google/Apple buttons only when the
backend reports enabled:true. Falls back to phone-OTP-only when both
providers are off. social_auth_enabled.dart deleted; client_app/CLAUDE.md
updated to reflect the new gating contract.
Mitra app: chat screen renders an ESP chip strip above the first message
bubble when chat_sessions.topics is non-empty.
Backend session.service.js getSessionById SELECTs cs.topics so the mitra
side can read the customer's selected topics.
Maestro flows 02_onboarding_verified.yaml + 03_onboarding_anon.yaml.
Deviation from plan: plan referenced OTP error code 'otp_retry_exhausted';
real codes are OTP_RATE_LIMIT_*/OTP_COOLDOWN/OTP_ATTEMPTS_EXCEEDED -
popup listens for all four. Plan said 'has_paid_first_session'; live
endpoint returns 'has_consulted_before' - used the live field.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the home-screen pending-requests banner with a "Riwayat
Permintaan" CTA that opens a list of the mitra's last 20 chat requests
(any status). Pending rows pin to the top; non-pending rows open a
read-only detail screen with a "Lihat percakapan" CTA on accepted rows.
Backend:
- New service `getRecentRequestsForMitra(mitraId, { limit })` capped at
20, pending pinned via `(response IS NULL AND status='pending_acceptance')
DESC`. Customer call_name returned verbatim, with `'Anonim'` only as
null-safety fallback (no anonymity-flag masking — see project memory).
- New route `GET /api/mitra/chat-requests/recent`. Strictly per-mitra
scoped via the existing `resolveMitra` preHandler.
Mitra app:
- New `RequestResponse` enum in core/constants.dart.
- New Riverpod notifier `requestHistoryProvider` (AsyncValue<List<...>>,
keepAlive) — pull-to-refresh + screen-mount fetch only, no WS.
- Two new screens (history list + detail) and two new GoRoutes.
- Home screen: `_PendingRequestsBanner` removed → `_RequestHistoryButton`
Card with red count badge. Live count comes from the existing
chatRequestProvider so nothing changes about the WS-driven badge math.
Plan + acceptance criteria in requirement/phase3.5-plan.md. flutter
analyze clean (zero new issues). Backend smoke-tested against real DB.
Real-device E2E pending.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Customer chat refreshSessionStatus now clears sessionExpired carryover so the
goodbye composer renders correctly when re-opening a closing session from
history. Backend /api/shared/chat/:id/info returns goodbye_submitted_by_me;
both apps suppress the composer for the side that has already submitted and
render an awaiting-banner view instead.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Mitra auth: parse DioException response for proper error messages
(ACCOUNT_NOT_FOUND, ACCOUNT_INACTIVE) instead of generic "OTP invalid"
- Backend: add CORS to internal app (port 3001) for control center
- Control center: fix login race condition (wait for AuthContext verify
before navigating), fix MitraActivityPage fetching paginated data
- Stale session goodbye: both apps detect SESSION_NOT_ACTIVE/409 and
move to complete state instead of retrying endlessly
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Backend: new GET /api/mitra/chat-requests/pending endpoint
- Backend: getPendingRequestsForMitra() queries unresponded notifications
for sessions still in pending_acceptance status
- Mitra app: loadPendingRequests() fetches on screen load + status toggle
- Mitra app: activeRequestCount getter exposes queue size
- Mitra app: _PendingRequestsBanner widget shows count with tap-to-view
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Wrap overlay Stack with Directionality (required above MaterialApp)
- Guard startListening() for IncomingData/StaleData states to prevent
overlay dismissal when status reloads on app resume
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add validateIncomingRequest() — checks session status with backend
- Home screen validates before showing sheet (on resume + listener)
- IncomingRequestSheet shows "cancelled/accepted by other" message
instead of silently dismissing when request becomes stale
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Backend: getOrCreateCustomer with phone fallback for re-login
- Backend: PATCH /api/client/auth/profile for display name update
- Client app: AuthNeedsDisplayNameData state + SetDisplayNameScreen
- Client app: ApiClient.patch method
- Both apps: handle verificationCompleted for auto-verify (test numbers)
- Both apps: skip credential sign-in if already auto-verified
- Remove debug prints from mitra auth + OTP screens
- Fix ChatRequestNotifier.startListening skips when accepting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Show local notification (sound + vibrate) when chat_request arrives
via WebSocket while mitra app is backgrounded
- Add NotificationService.showLocalNotification() for programmatic use
- Fix router redirect: don't redirect auth routes to splash during loading
- Handle binary/string WebSocket frames in ChatRequestNotifier
- Remove debug logging from backend and Flutter
- Control center: mitra ping config UI
- Both apps: dynamic ping, FCM deep-linking, unread badges
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove flutter_bloc and equatable dependencies from both apps
- Delete all 10 old bloc files (5 per app)
- Fix 6 remaining screens that used context.read<ApiClient>() from
flutter_bloc → converted to ConsumerStatefulWidget/ConsumerWidget
with ref.read(apiClientProvider)
- Both apps now use Riverpod exclusively for state management
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Upgrade Fastify 4→5 with all plugins (@fastify/websocket 11, cors 11, sensible 6)
- Migrate all SSE endpoints to WebSocket + FCM push (mitra chat requests, customer pairing status)
- Add flutter_local_notifications for foreground push notifications with sound
- Add splash screen to both apps (hide auth loading flash)
- Introduce constants/enums across entire codebase (no raw string literals)
- Move price tiers from hardcoded array to app_config DB (data-driven, includes 1-min test tier)
- Add session ownership validation on all shared chat routes
- Add ownership checks on endSession, respondToExtension, requestExtension
- Fix session timer: auto-complete expired/stale sessions on server restart
- Add 5-min grace period for abandoned closing sessions
- Fix extension flow: proper session_resumed handling, clearExtensionRequest, closure grace timer cleanup
- Fix chat screens: ConnectChat in initState, session status check on connect
- Fix customer expired view: 5-min countdown, closure state priority over expired state
- Fix mitra extension UI: loading spinner, disable buttons, handle EXTENSION_RESOLVED error
- Fix GoRouter navigation consistency (no more Navigator.pushNamed)
- Fix goodbye view keyboard overflow (SingleChildScrollView)
- Add active session card on customer home screen with refresh on navigate back
- Fix PricingBottomSheet extension mode (RequestExtension instead of new pairing)
- Send session_resumed to both parties on extension accept
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Integrated Firebase SDK in both Flutter apps (google-services, firebase_options)
- Fixed auth flow, API client, and pairing/status blocs for dev environment
- Added full Flutter project scaffolds (android, ios, web, etc.)
- Added phase 3 chat engine requirement document
- Added bugreport zip pattern to gitignore
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add mitra online/offline status with heartbeat-based auto-offline,
customer-mitra pairing via Valkey pub/sub blast, session management,
and control center dashboard with real-time stats.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>