§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>
4.7 KiB
§5 — Chat Room test plan
Spec: requirement/flow_customer.mermaid.md §5 plus the user-stated message-delivery contract:
- WebSocket when app is foreground / WS connected.
- FCM when app is background / WS disconnected.
The lifecycle observers that enforce (2) live at:
- Customer side:
client_app/lib/main.dart::_AppState.didChangeAppLifecycleState - Mitra side:
mitra_app/lib/main.dart::_AppState.didChangeAppLifecycleState+mitra_app/lib/core/chat/mitra_chat_notifier.dart(_connectedSessionId- getter for the observer to read on
paused).
- getter for the observer to read on
A 15s Timer.periodic in active_session_notifier.dart would otherwise
re-open the customer WS right after the observer closed it; the
_appPaused gate in customer main.dart suppresses that race.
Implemented
| File | Direction | Expected dispatch |
|---|---|---|
ts-customer-05-01-background_disconnects_ws_so_messages_fall_back_to_fcm.yaml |
mitra → customer, customer is backgrounded | delivered_via=fcm |
ts-customer-05-02-customer_message_to_backgrounded_mitra_falls_back_to_fcm.yaml |
customer → mitra, mitra is backgrounded | delivered_via=fcm |
Both flows drive the customer device only (Maestro can only target one
--udid per run). The mitra side is verified server-side via
/internal/_test/ws-connection-state
and
/internal/_test/send-chat-message-as-customer
(introduced 2026-05-18 with the lifecycle fix).
Helper scripts (under ../scripts/)
| Script | Purpose |
|---|---|
assert_ws_state.js |
Reads /internal/_test/ws-connection-state?session_id=…; throws on mismatch with EXPECTED_CUSTOMER_CONNECTED / EXPECTED_MITRA_CONNECTED. |
assert_delivered_via.js |
Sends a chat message via the test endpoint matching SENDER (mitra default, or customer); asserts the response's delivered_via equals EXPECTED_DELIVERED_VIA. |
inspect_ws_state.js |
Same data as assert_ws_state.js but exports the booleans into output.CUSTOMER_CONNECTED / output.MITRA_CONNECTED for downstream conditions without throwing. |
send_chat_message_as_mitra.js |
Mitra→customer one-shot send. Captures output.MESSAGE_ID + output.DELIVERED_VIA. |
Pre-reqs for both flows
- Backend reachable;
NODE_ENV != 'production'. - ≥1 mitra online (the
seed_history_session.jshelper picks the most-recently-online mitra to act as the eventual acceptor). - For 05-01: customer device is the one Maestro drives;
pressKey: HOMEinside the flow backgrounds it. No mitra-app interaction required. - For 05-02: the mitra app on
emulator-5556must be backgrounded or force-stopped before the flow runs. Theassert_ws_state.jsguard fails loudly if mitra is foreground (proving WS-over-FCM precedence — also a correct test outcome, just the wrong side of the matrix). To ensure FCM actually arrives on the device, the mitra account onemulator-5556must have afcm_tokeninmitras.fcm_token(i.e., the app has signed in and called/api/shared/device-tokenat some point) — verified byseed_history_session.js's output mitra ID and theMITRA_NAME_REit captures.
Manual mitra-side verification
After 05-02 passes, optionally inspect the mitra device's notification shade to confirm the FCM tray notification rendered:
export ADB_SERVER_SOCKET=tcp:172.22.240.1:5037
adb -s emulator-5556 shell cmd statusbar expand-notifications
adb -s emulator-5556 exec-out screencap -p > /tmp/mitra_shade.png
adb -s emulator-5556 shell cmd statusbar collapse
Expect to see a Pesan baru dari Customer notification within ~1s on an
API 28+ AVD (older AVDs queue FCM for 5-30 min — see the per-AVD
Play-Services notes in the project memory).
Not yet automated
Mitra-side lifecycle observer end-to-end. To verify that the mitra
app's didChangeAppLifecycleState(paused) actually closes its chat WS,
the test would need to drive the mitra emulator (Maestro on
--udid emulator-5556) through login → chat-screen → pressKey: HOME →
assert backend WS state. This requires:
- Pre-seeding an active chat session for the currently-signed-in mitra (no existing test endpoint for "seed customer + pair + active session"; the existing scripts assume the customer app drives it).
- Driving the mitra OTP login + chat-request acceptance UI from Maestro.
Both are feasible but out of scope for the current iteration. The mitra-side fix is covered indirectly: ts-customer-05-02 asserts the backend correctly dispatches via FCM whenever the mitra WS is closed, and the mitra app's lifecycle observer was verified manually (2026-05-18) to be the mechanism that closes that WS.