- 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>
138 lines
6.6 KiB
Markdown
138 lines
6.6 KiB
Markdown
# 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 `chatRequestProvider` state — it does NOT depend on any specific page or lifecycle event
|
|
- Three sources can trigger the overlay:
|
|
1. **WebSocket** — real-time delivery when app is in foreground
|
|
2. **FCM notification tap** — user taps push notification, app opens, state is set from notification payload
|
|
3. **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-requests` page 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 → `chatRequestProvider` state 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 request
|
|
- `session_id` — which chat request
|
|
- `response` — `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 request
|
|
- `notified_at` — when the request was delivered to the mitra
|
|
- `responded_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 `missed` from `ignored` (currently both stored as `ignored`)
|
|
- Add `active_session_count` column — recorded when the notification is created
|
|
- `response_time_seconds` can be calculated from `responded_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 `OverlayEntry` managed from `main.dart`
|
|
- Wrapped around `MaterialApp.router` in a `Stack`
|
|
- Watches `chatRequestProvider` — shows/hides based on state
|
|
- Uses `AnimatedPositioned` or `SlideTransition` for bottom-up animation
|
|
- `GestureDetector` for 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 `showModalBottomSheet` for incoming requests from home screen
|
|
- Remove `IncomingRequestSheet` widget
|
|
- Remove `didChangeAppLifecycleState` incoming request check from home screen
|
|
|
|
|
|
# Tech Stack
|
|
- Flutter `Overlay` / `OverlayEntry` or `Stack` with `AnimatedPositioned`
|
|
- Existing Riverpod `chatRequestProvider`
|
|
- No backend changes
|