- 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>
6.6 KiB
PRD: Mitra Chat Request Overlay & Pairing UX
Overview
Goal: Reliable incoming chat request experience for Mitra — always visible, non-blocking, works in all app states (foreground, background, killed)
Success looks like: When a customer requests a chat, the mitra is always notified and can accept/reject without leaving their current screen. The notification works regardless of whether the app is in foreground, background, or opened from a push notification.
Background
- Current bottom sheet approach (
showModalBottomSheet) fails silently when the app is backgrounded - Flutter UI cannot render when the app is not in foreground
- Push notifications work in background but the in-app response (bottom sheet) doesn't show reliably when the user taps the notification
- The state change from WebSocket gets "consumed" by listeners while backgrounded, so returning to foreground shows stale data
- Need a solution that is non-blocking (doesn't interrupt active chat sessions) and works on any page
Functional Requirement
Incoming Chat Request Overlay
Trigger
- Overlay watches
chatRequestProviderstate — it does NOT depend on any specific page or lifecycle event - Three sources can trigger the overlay:
- WebSocket — real-time delivery when app is in foreground
- FCM notification tap — user taps push notification, app opens, state is set from notification payload
- App resume — app returns to foreground, validates pending request with backend
Appearance
- Slides up from the bottom of the screen, like a bottom sheet
- Rounded top corners
- Page behind is partially dimmed to get mitra's attention
- Shows on top of ANY page (home, chat, history, settings, etc.)
Content
- Session information (duration, etc.)
- Accept button
- Reject button
- Swipe down to close (= ignore, NOT reject)
Behavior
- Accept → overlay dismisses, navigate to chat session with customer
- Reject → overlay dismisses, mitra continues what they were doing
- Swipe down / close → overlay dismisses, request is ignored (no explicit reject sent to backend)
- Request cancelled by customer → overlay updates to show "Permintaan dibatalkan oleh customer"
- Request accepted by other mitra → overlay updates to show "Permintaan diterima oleh Bestie lain"
- Request expired (60s timeout) → overlay updates to show "Permintaan kedaluwarsa"
- Stale messages do NOT auto-dismiss — mitra must acknowledge by tapping OK or swiping down
Multiple Requests
- Show one request at a time
- Requests are queued — when current request is resolved (accepted/rejected/ignored/expired), next queued request appears
- Future enhancement: dedicated
/chat-requestspage with full list
Push Notification (Background/Killed)
When App is Backgrounded
- Local notification with sound + vibration (already implemented via WebSocket listener)
- FCM push notification as fallback when WebSocket is disconnected
When Notification is Tapped
- App opens →
chatRequestProviderstate is set from notification payload (session_id) - Overlay appears with the request detail
- Backend validation confirms request is still pending before showing accept/reject
Stale Notification Handling
- If user taps a notification for an already-resolved request, overlay shows appropriate message ("dibatalkan" / "diterima Bestie lain" / "kedaluwarsa") then auto-dismisses
Mitra Request Activity Log
Goal
Track every mitra's response to incoming chat requests for QC measurement — how many accepted, rejected, ignored (expired without action), and how many active sessions they had at the time.
Logged Data
For each incoming request delivered to a mitra, log:
mitra_id— which mitra received the requestsession_id— which chat requestresponse—accepted,rejected,ignored(expired without action),missed(taken by other mitra before response)response_time_seconds— how long it took the mitra to respond (null if ignored)active_session_count— how many active sessions the mitra had at the time of the requestnotified_at— when the request was delivered to the mitraresponded_at— when the mitra responded (null if ignored)
When to Log
All logging is backend-side and event-driven — no polling or constant monitoring needed. The frontend only triggers accepted and rejected through existing API calls.
| Response | Triggered by | Frontend action? |
|---|---|---|
accepted |
Backend — when mitra calls POST /:sessionId/accept |
Yes — mitra taps Accept |
rejected |
Backend — when mitra calls POST /:sessionId/decline |
Yes — mitra taps Reject |
missed |
Backend — when acceptPairingRequest marks other mitras' notifications (another mitra accepted first) |
No — fully backend |
ignored |
Backend — when expirePairingRequest fires after 60s timeout with no response |
No — fully backend |
Existing Infrastructure
The chat_request_notifications table already tracks notified_at, response, responded_at. Current changes needed:
- Distinguish
missedfromignored(currently both stored asignored) - Add
active_session_countcolumn — recorded when the notification is created response_time_secondscan be calculated fromresponded_at - notified_at(no new column needed)
Control Center
- New dedicated page: Mitra Request Activity
- Dashboard: mitra acceptance rate, average response time, rejection count, ignore count per mitra
- Filter by date range and mitra
- Auto-flagging mitras with high rejection/ignore rate is out of scope for this phase (planned for future)
Technical Implementation
Overlay Component
- Single
OverlayEntrymanaged frommain.dart - Wrapped around
MaterialApp.routerin aStack - Watches
chatRequestProvider— shows/hides based on state - Uses
AnimatedPositionedorSlideTransitionfor bottom-up animation GestureDetectorfor swipe-to-dismiss
State Flow
WebSocket ──→
FCM tap ──→ chatRequestProvider ──→ Overlay shows/hides
App resume ──→
No Changes Required
- Backend pairing service (already sends WS + FCM)
- Push notification payload (already contains session_id + type)
- Chat request notifier (already manages state from WS + FCM)
Cleanup from Phase 3.1
- Remove
showModalBottomSheetfor incoming requests from home screen - Remove
IncomingRequestSheetwidget - Remove
didChangeAppLifecycleStateincoming request check from home screen
Tech Stack
- Flutter
Overlay/OverlayEntryorStackwithAnimatedPositioned - Existing Riverpod
chatRequestProvider - No backend changes