Commit Graph

28 Commits

Author SHA1 Message Date
82c9b1eee8 Mitra chat: real customer name + ticking timer + goodbye pill
Four small fixes on the mitra chat screen, all surfacing through the
chat connected-state.

1. AppBar customer name. The hardcoded "Customer" only ever came from
   the FCM-tap navigation fallback (notification_service:
   `extra: {'customerName': 'Customer'}`); the popup-overlay path passes
   the real name but FCM had no way to know it. /chat/:sessionId/info
   already returns `customer_display_name` — propagate it into
   MitraChatConnectedData and read in the AppBar via .select. Falls
   back to the route arg for the brief connecting window.

2. SISA WAKTU stuck at --:--. The pill watches a remaining-seconds
   provider that's only updated by backend WS frames. Backend only
   fires session_timer at 3-min + 1-min warnings + expiry, so the pill
   sat at --:-- for the first ~7 minutes of a 10-min chat. Added a
   local 1s ticker in the notifier that drives the provider against
   expires_at (also pulled from /info). WS warning frames still
   overwrite normally on top.

3. Pesan Penutup textbox. Replaced the rounded-rect OutlineInputBorder
   field with a fixed-height Container pill whose border matches the
   previous "shadow" tone (HaloTokens.border). Pill borderRadius is
   the full 100 (was 12).

4. Goodbye textbox text was top-aligned because maxLines: 3 +
   OutlineInputBorder left vertical alignment to InputDecoration's
   built-in padding. Switched to maxLines: 1 + textAlignVertical:
   center + isCollapsed: true inside the fixed-height container —
   text now sits on the vertical center.

Bonus: the goodbye subhead "Tuliskan pesan terakhirmu untuk Customer"
also picked up the real name ("…untuk Andi Pratama").

Verified end-to-end on emulator-5556 (TestMitra-1501 + customer
"Andi Pratama"): AppBar shows Andi Pratama, SISA WAKTU ticks (04:57 →
04:35 across screenshots), goodbye pill renders with centered hint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 21:09:00 +08:00
e4bffe1a71 Extension request: WS→FCM fallback + chat-recovery on connect
Today the customer's "Perpanjang" only reaches the mitra via session-
scoped WS. If the mitra is on Home/Undangan, in a different session, or
backgrounded, the WS send no-ops and the 10s safeguard timeout fires
auto-reject (or auto-approve if the mitra happens to also have an
active general WS, depending on config) — either way the mitra never
saw the request.

Backend:
- extension.service.js::requestExtension now falls back to FCM via
  notification.service when the mitra isn't on the session WS. Mirrors
  the pairing notifyMitra pattern (Curhat Baru). Customer display name
  is pulled into the session lookup for the FCM body.
- shared.chat.routes.js: /chat/:sessionId/info now returns
  pending_extension (extension_id, duration_minutes, price,
  requested_at, expires_at, timeout_seconds) so the chat screen can
  rehydrate the accept/reject UI after a cold-start FCM tap. expires_at
  is derived from requested_at + extension_timeout_seconds config.

Mitra app:
- mitra_chat_notifier.dart::connect parses pending_extension from /info
  and seeds MitraChatConnectedData.extensionRequest — the existing
  _buildExtensionView renders unchanged.
- notification_service.dart::_navigateFromMessage handles
  type=extension_request → pushes /chat/session/<id>. Composes with
  the new /info pending_extension to bring the mitra straight into the
  accept/reject view.

Verified end-to-end on dev backend (FCM call returned sent=true; /info
returns pending_extension when within timeout window). Visual delivery
on emulator-5556 deferred — API 24 AVD queues FCM 5-30 min per
feedback-emulator-avd-versions.

Out of scope (follow-ups):
- Customer-side FCM for EXTENSION_RESPONSE (accepted/rejected/timeout)
- Perpanjang tab list endpoint + Flutter provider + UI

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 13:24:40 +08:00
fbc94daac7 Mitra Bestie §1–§3: shell + Undangan + popup + chat polish
Brings the mitra app to figma-bestie parity for Home (§1), Undangan
inbox with Curhat Baru + Perpanjang tabs (§2), and the incoming-popup
+ active-chat flow (§3). Home now lives inside a StatefulShellRoute
with BestieTabBar so Profil + Undangan + Home share one shell.

- Shell: features/shell/ (StatefulShellRoute, BestieTabBar, 3 branches)
- Undangan: features/undangan/ — Curhat Baru reads
  chatRequestProvider.pendingInvites; row Terima delegates accept to
  the notifier and ChatRequestOverlay owns nav (no double-push).
  Perpanjang tab stubbed (empty state) until backend exposes
  pendingExtensionsProvider.
- Profil: features/profile/ — Bestie-styled stub
- Home: refactored to body-only (shell owns chrome)
- Popup: chat_request_overlay + chat_request_notifier updated to
  serve the list rows, not just the modal
- Chat: mitra_chat_screen polish
- Theme: accentAmber tokens for the Perpanjang tab + halo_orb widget
  (loading spinner used by undangan list states)
- Login: replace broken GoRouterState location guard with
  _expectOtpPush flag — was stacking duplicate /otp pages on OTP
  resend (see project-otp-nav-bug-fixed-2026-05-21)

Maestro:
- 17 new flows under .maestro/flows/ts-mitra-{1,2,3}-* covering home
  online/offline variants, undangan empty/populated/tolak states,
  popup curhat-baru → accept → chat → ended banner, plus popup
  dismiss/expire/cancelled edge cases
- 4 new §A OTP flows (07/08/09/10) for invalid/mismatch/expired/cooldown
- Helper scripts: force_mitra_online/offline, force_pairing_timeout,
  force_session_expires_at, delete_mitra_status_row,
  customer_blast_now (js), customer_cancel_latest_blast
- Backend: POST /internal/_test/delete-mitra-status-row supports the
  "fresh mitra with no status row" test setup

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 11:14:30 +08:00
ad02ee252d Phase 4 §1/§5: notif banner detection on API <33 + chat-delivery WS→FCM lifecycle
§1 notif banner: permission_handler v11 returns granted unconditionally
for Permission.notification on Android <13 because POST_NOTIFICATIONS
didn't exist as a runtime permission. Result: SHome1st amber "notifikasi
off" banner never showed on API 24-32 even when the user toggled
notifications off in Settings → Apps. Add a
NotificationManagerCompat.areNotificationsEnabled() pre-check via
flutter_local_notifications (works from API 19+) so the banner reflects
the real OS state on older Android.

§5 chat delivery: the contract is "WS when foreground, FCM when
background", but the previous build only honoured (1) — Android keeps
the TCP socket alive after the Dart isolate is paused, so backend's
`socket.readyState === 1` check returned true and FCM never fired.
Fix has five parts (all required together):

 1. Customer-side lifecycle observer in client_app/main.dart closes
    chatProvider's WS on paused/detached, reconnects on resumed.
 2. `_appPaused` gate in main.dart suppresses the activeSessionProvider
    listener's auto-reconnect (15s poll in active_session_notifier
    would otherwise re-open the WS the next tick after the observer
    closed it — defeating the fallback).
 3. Mitra-side lifecycle observer in mitra_app/main.dart stashes
    `_pausedChatSessionId`, calls mitraChatProvider.disconnect(), and
    re-issues connect(saved) on resumed.
 4. MitraChat gains a `_connectedSessionId` field + getter so the
    observer in step 3 can read it back across disconnect (disconnect
    clears it; the next connect overwrites it).
 5. SearchingScreen resets pairingProvider when entering with a new
    draft.paymentId — previously it retained PairingActiveData with
    the *old* sessionId after a session ended, and the next pairing
    flow navigated straight to that completed session showing
    "Sesi sudah berakhir".

Backend additions under /internal/_test/* for assertion harness:
inspectSessionWsState + GET /ws-connection-state,
POST /send-chat-message-as-mitra (with delivered_via),
POST /send-chat-message-as-customer (with delivered_via),
POST /send-fcm-chat-message (raw FCM dispatch).

Maestro coverage:
 - ts-customer-05-01: mitra → customer message when customer is
   backgrounded → delivered_via=fcm.
 - ts-customer-05-02: customer → mitra message when mitra is
   backgrounded → delivered_via=fcm.
 - ts-customer-01-01: §1 notif-denied banner on home. Documented
   precondition: mitra must be force-stopped or backgrounded on the
   chat screen before 05-02 runs (Maestro can only drive one --udid
   per run; mitra-side lifecycle observer end-to-end is deferred).

Helper scripts under client_app/.maestro/scripts/:
inspect_ws_state.js, assert_ws_state.js,
send_chat_message_as_mitra.js, assert_delivered_via.js (takes
SENDER=mitra|customer to route to the matching backend endpoint).

README_section_05.md documents the test plan, helper scripts, and the
deferred mitra-side maestro driving. Both apps tested manually on
API 28 AVDs where FCM delivery is sub-second; API 24 has 5-30 min
heartbeats that make it impractical for FCM-related testing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 21:50:46 +08:00
e09f76ceb6 Phase 4 §4: payment-before-pair for returning users + Maestro suite
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>
2026-05-17 20:25:15 +08:00
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
14b5cc966b Phase 4 Stage 6: chat-room countdown UX + voice-call mode pill
Customer chat screen:
- Voice-call header pill (mode == 'call' renders accent-colored pill;
  chat mode renders no pill).
- HaloSnackbar fires once per session at 180s remaining ('sisa 3 menit
  lagi ya 🤍'), driven by the backend session_warning WS event.
- Last-2-min danger styling: timer pill flips to HaloTokens.danger +
  bold JetBrainsMono when remaining <= 120s.
- Floating ChatExpiredBanner widget injected above the input bar when
  remaining hits 0 in closing-grace state. perpanjang -> existing
  pricing bottom sheet.
- pricing_bottom_sheet.dart rewritten to the 5-option layout with
  chat|call mode toggle (mirrors duration-pick from Stage 3).

Mitra chat screen: voice-call header pill only (no countdown UX per PRD).

Backend:
- session.service.js getSessionById JOINs payment_sessions so mode +
  expires_at ship in /api/shared/chat/:id/info.
- session-timer.service.js onThreeMinuteWarning now emits expires_at +
  remaining_seconds for client resync.
- Dev-only POST /internal/_test/force-session-expires-at clears the
  3-min flag, reschedules the timer, and broadcasts WS resync. Lets
  the Maestro flow drive 175s -> 90s -> 0s without waiting live.

New chatRemainingSeconds StreamProvider derived from expiresAt, fed by
session_warning / session_timer / session_expired resync messages
(plan referenced a secondsLeftProvider that didn't actually exist).

Maestro 06_chat_countdown.yaml + force_session_expires_at.js helper.

Out of scope: meet.google.com URL launching - url_launcher isn't a
client_app dependency and message bubbles render plain Text. Defer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 17:25:11 +08:00
2645bcd0e5 Phase 4 Stage 2: onboarding redesign (client_app + mitra_app)
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>
2026-05-10 16:23:57 +08:00
d09e50af55 Phase 3.7: paid pairing flow + returning chat + extension flip
- Backend: payment_sessions + pairing_failures tables; payment.service.js
  and pairing-failure.service.js (new); rewritten pairing.service.js
  (payment-gated blast + targeted "Curhat lagi" + cancel + fallback);
  rewritten extension.service.js (data-driven auto-approve with offline
  safeguard, charge-at-approval); pricing.service.js (extension tiers
  without free trial); mitra-status.service.js (countAvailableMitras
  cached path); 60s sweeper for stale payment sessions
- Backend routes: client.payment.routes, client.mitra-availability.routes,
  internal/failed-pairings.routes; client.chat.routes rewritten for
  payment-gated start + /returning + /cancel + /fallback-to-blast;
  internal/config.routes adds 4 new keys with Valkey invalidate publish
- client_app: mitra-availability poll, payment screen + notifier, pairing
  notifier rewrite (PairingTargetedWaiting + PairingFailed states),
  targeted-waiting overlay + bestie-unavailable dialog, "Curhat lagi"
  CTA, failed-pairing terminal, extension via payment-session
- mitra_app: PairingRequestType enum, returning-chat 20s countdown
  auto-dismiss, extension card "otomatis disetujui" copy
- control_center: 4 new config rows in Settings, Failed Pairings page
  (filter + paginate + action menu), sidebar + route registered
- Test infrastructure: Vitest backend (7/7 pass), Playwright CC (4/4
  pass), Maestro mobile scaffold (CLI install pending)
- Bugs found via Playwright + fixed: LoginPage labels not associated
  with inputs (a11y); backend internal CORS missing PATCH/PUT/DELETE
  in allow-methods (silent settings breakage in browsers since Stage 4)
- Docs: phase3.7.md PRD, phase3.7-plan.md, phase3.7-questions.md (Q&A),
  phase3.7-testing.md (E2E checklist), phase3.7-test-run-2026-05-03.md
  (today's run results)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 23:02:49 +08:00
89afd01899 Phase 3.5: Mitra Chat Request History (backend route + mitra app screens)
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>
2026-04-27 18:59:17 +08:00
6801001b64 Phase 3: closing-overlay fix + goodbye-composer dedupe
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>
2026-04-27 13:43:19 +08:00
2b61c79a86 Phase 3.4: mitra_app self-managed auth cutover
Rips firebase_auth; phone OTP flow now talks directly to the new
backend endpoints, JWT access token lives in memory, refresh token
persists via flutter_secure_storage. WebSocket handshakes read the
access token from AuthBridge instead of Firebase.

Smoke-tested end-to-end against the backend via curl:
- otp/request → read stub code from backend log → otp/verify
- /api/mitra/auth/me + /api/shared/auth/refresh rotation
- logout → post-logout refresh correctly fails REFRESH_INVALID
- ACCOUNT_INACTIVE (403) + WRONG_FLOW (400) error paths verified
- Debug APK links cleanly

- pubspec: drop firebase_auth, add flutter_secure_storage
- core/auth/auth_bridge.dart: shared mutable state (access token +
  refresh callback + in-flight de-dup) as keepAlive provider
- core/auth/token_storage.dart: flutter_secure_storage wrapper
- core/auth/auth_notifier.dart: bootstrap → refresh; requestOtp +
  verifyOtp via /api/mitra/auth/*; logout; granular OTP error codes
- core/api/api_client.dart: Bearer from bridge + postRaw(skipAuth) for
  auth endpoints + single-retry 401 refresh
- core/chat/*_notifier.dart: WS auth frame reads bridge.accessToken
- features/auth/screens/otp_screen.dart: verificationId → otpRequestId
- mitra_app/CLAUDE.md: Auth section rewritten (was stale on Firebase)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 15:58:25 +08:00
780cade3db Phase 3.3: topic sensitivity + Phase 3.4: auth foundation
Phase 3.3 — Session Topic Sensitivity (complete):
- Backend: topic_sensitivity column + session_sensitivity_log, sensitivity service
  (flip with one-way-latch + audit), PATCH /api/shared/chat/sessions/:id/topic,
  topic carried in pairing + extension WS payloads, CC filter + sensitive stats
  + per-mitra sensitive columns on activity page
- client_app: TopicSelectionBottomSheet before pricing, topic flows through
  pairing request, silent WS handler for session_topic_updated
- mitra_app: SensitivityBadge + SensitivityTheme + sensitivityConfigProvider,
  overlay badge + yellow accent, chat screen app-bar toggle with configurable
  confirmation + latch, extension card shows current flag, history + transcript
  yellow theme
- control_center: Sensitivitas Topik settings section, topic filter + column
  with inline audit log, sensitive stats dashboard card, mitra activity
  sensitive columns with QC flag

Phase 3.4 — Self-Managed Auth (foundation only):
- Migration: auth_sessions + otp_requests tables, social identity columns on
  customers, password_hash + lockout on control_center_users, OTP + CC lockout
  app_config keys
- New services: password (bcrypt + complexity), token (JWT HS256 + refresh
  rotation, session_id claim pre-wires future Valkey revocation),
  social-identity (Google + Apple JWKS), OTP (Fazpass stub — real API TBD)
- Constants: AuthProvider + OtpChannel
- Middleware, auth route rewrites, WS auth update, Firebase → FCM isolation
  still pending (next chunk); Fazpass docs + Apple Developer setup still
  required before E2E testing

Docs:
- requirement/phase3.3.md, phase3.3-plan.md, phase3.3-testing.md
- requirement/phase3.4.md, phase3.4-plan.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 10:15:12 +08:00
50d31260dc Fix auth errors, CORS, control center login, and stale session handling
- 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>
2026-04-15 20:49:57 +08:00
1920507ec5 Add pending chat requests CTA on mitra home screen
- 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>
2026-04-10 21:21:47 +08:00
bb0346a843 Fix overlay: Directionality widget + startListening state guard
- 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>
2026-04-09 23:09:36 +08:00
b9c4841eb1 Phase 3.2 WS1: Chat request overlay, queue, stale reasons
- Backend: add reason field to chat_request_closed WS messages
  (cancelled_by_customer, accepted_by_other, expired)
- Backend: include duration_minutes, is_free_trial in chat_request WS
- ChatRequestNotifier: add ChatRequestStaleData, StaleReason enum,
  request queue (List<Map>), ignore(), acknowledgeStale(), _advanceQueue()
- New ChatRequestOverlay widget: slides up from bottom, dimmed background,
  swipe to dismiss, shows active/stale request content
- Integrate overlay in main.dart wrapping MaterialApp.router
- Cleanup: convert HomeScreen to ConsumerWidget, remove showModalBottomSheet,
  remove IncomingRequestSheet, remove lifecycle observer

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 22:16:30 +08:00
4158fb9432 Phase 3.2 docs + Phase 3.1 testing fixes
- Add phase3.2.md requirement: overlay UX, mitra activity log
- Add phase3.2-plan.md implementation plan
- Fix stale request validation: add GET /:sessionId/status endpoint
- Fix notification tap flow: setIncomingFromNotification + onChatRequestTapped
- IncomingRequestSheet shows stale message instead of auto-dismiss
- Home screen validates on resume, shows immediately on fresh WS

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 22:09:25 +08:00
e3da863f3c Validate stale chat requests, show info instead of auto-dismiss
- 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>
2026-04-09 16:40:52 +08:00
212e1e8ac6 Fix auth: auto-create customer, display name flow, OTP auto-verify
- 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>
2026-04-09 16:22:28 +08:00
2e80434e9b Phase 3.1: Local notification for WS chat requests, router fix, cleanup
- 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>
2026-04-09 14:57:36 +08:00
229f889551 Phase 3.1 WS2: FCM fallback Flutter + CC, unread badges, dynamic ping
- Control center: add mitra ping config UI (require ping toggle + interval)
- Mitra app StatusNotifier: honor require_ping and ping_interval_seconds
  from API; skip heartbeat when ping not required
- Both apps: update notification services for FCM deep-linking
  - mitra_app: handle chat_request (open_accept), session_closing
  - client_app: handle session_closing, paired
- Unread badge providers:
  - mitra_app: UnreadSessions provider (polls active-with-unread, badge
    on active sessions button)
  - client_app: UnreadCount provider (polls active-with-unread, badge
    on _ActiveSessionCard)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 14:29:06 +08:00
fa8c963d92 Phase 3.1: Remove flutter_bloc + equatable, delete old bloc files
- 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>
2026-04-09 14:12:28 +08:00
35d470b851 Phase 3.1: Complete mitra_app Riverpod migration (all blocs, fix auth bug)
- Migrate AuthBloc → MitraAuthNotifier (fixes stuck-loading bug: now returns
  MitraAuthInitialData when currentUser is null)
- Migrate StatusBloc → OnlineStatusNotifier (heartbeat timer + lifecycle)
- Migrate ExtensionBloc → MitraExtensionNotifier (accept/reject + goodbye)
- Migrate ChatRequestBloc → ChatRequestNotifier (WebSocket incoming requests)
- Migrate MitraChatBloc → MitraChatNotifier (WebSocket chat + messages)
- Update router to use Riverpod auth state for redirects
- Remove all flutter_bloc usage from mitra_app screens and main.dart
- MultiBlocProvider fully removed from mitra_app

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 14:08:45 +08:00
b0502ac92b Phase 3 testing fixes: Fastify 5, SSE→WebSocket+FCM, enums, security, session lifecycle
- 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>
2026-04-09 00:17:25 +08:00
b4efcf14c2 Phase 3 scaffold: chat engine (WebSocket, FCM, pricing, timer, extension, history)
- Backend: WebSocket plugin, chat/pricing/timer/extension/closure/notification services
- Client app: ChatBloc, pricing dialog, chat screen with message status, extension/goodbye flow, history
- Mitra app: MitraChatBloc, ExtensionBloc, chat screen, extension accept/reject, history
- Control center: free trial, extension timeout, early end config toggles
- DB migration: chat_messages, session_closures, session_extensions, customer_transactions tables

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 23:58:11 +08:00
844d7234e6 Phase 2 refinements: Firebase config, dev environment fixes, phase 3 requirement draft
- 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>
2026-04-07 19:16:34 +08:00
d668112edd Phase 2 scaffold: mitra online status & pairing logic
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>
2026-04-05 23:17:49 +08:00