7 Commits

Author SHA1 Message Date
3a0cdf5c4e Phase 5/6 polish: end-session flow, notif sound on API 33+, Xendit webview
Customer end-of-session (figma §6):
- PricingBottomSheet: ghost "cukup, akhiri sesi" CTA + dedup divider
- chat_screen._runEndSessionFlow chains ConfirmEndStep1 → ConfirmEndStep2
  → ClosingMessageSheet (or "lewati saja" → close + /home). The four
  popup/sheet widgets already existed; this commit just wires them
- showModalBottomSheet: showDragHandle=false to suppress the Material 3
  auto-injected handle that was stacking with our own pill

Notification sound on API 33+:
- Bump channel halobestie_chat_v1 → halobestie_chat_v2, created from
  native Kotlin in MainActivity.kt with AudioAttributes contentType
  CONTENT_TYPE_SONIFICATION. flutter_local_notifications' default of
  CONTENT_TYPE_UNKNOWN was causing Android 13 to silently drop audio
  focus while the notification still posted (isNoisy=true). Both apps
- Backend FCM payload channelId updated to v2
- AndroidManifest meta-data: default_notification_icon + color → brand
  silhouette tinted pink instead of generic Android bell. Both apps

Customer pairing reliability:
- pairing_notifier: applyPairedFromPush({sessionId, mitraName}) unsticks
  searching screen when WS push failed and FCM/active-session-poll is
  the first signal. Idempotent across PairingSearchingData,
  PairingTargetedWaitingData, PairingErrorData (covers ALREADY_ACTIVE)
- notification_service: dispatches every FCM data payload to an
  onDataMessage callback (foreground + tap + cold-start). main.dart
  wires that to applyPairedFromPush on type=='paired'. Foreground
  'paired' no longer renders a local banner — screen self-advances
- main.dart activeSession listener also calls applyPairedFromPush when
  a session appears server-side while pairing is in a waiting state.
  Covers stale ALREADY_ACTIVE recovery without a full page refresh

Auth refresh token race:
- auth_notifier._refreshFromStorage shares a single in-flight Future
  across all callers (Auth.build + 401-retry path). Backend rotates
  refresh tokens, so concurrent callers using the same stored token
  would race → loser 401s → catch wipes flutter_secure_storage → user
  appears logged out after kill+reopen

Polish:
- method_pick_screen: resizeToAvoidBottomInset=false — prevents the
  one-frame overflow when entering with the previous screen's keyboard
  still animating out
- bestie_history: BestieHistoryItem now carries `status` (backend
  already returns it). Removed _rawHistoryProvider that fetched the
  same endpoint just to read status; the two providers could go out
  of sync mid-rebuild and throw RangeError(length) on indexing

Xendit Stage 8 (carried from WIP):
- xendit_checkout_screen: embedded webview hosting Xendit's invoice
  page (intercepts halobestie:// deeplink + return-page URLs for
  deterministic pop)
- waiting_payment_screen: auto-pushes the webview when the backend
  payload includes xendit_invoice_url; spinner card + "Buka ulang
  halaman pembayaran" CTA for the QR-fallback path
- pubspec: webview_flutter ^4.13.0

Maestro infra:
- subflows/onboarding_returning_user: drop the "Mulai" carousel wait
  (splash auto-advances since 2026-05-26); tap phone-field hint
  instead of point; drop hideKeyboard (sends BACK → /home when the
  IME isn't actually up)
- New flow ts-customer-06-01-end_session_via_timeup_sheet: drives
  the full path to the chat-expired banner. Last step blocked by a
  Maestro+Flutter gesture quirk on the perpanjang ElevatedButton
  (raw `adb input tap` works at the same coords). Documented in
  memory; deeplink fixture or manual verify recommended
- ChatExpiredBanner button wrapped with Semantics(identifier:
  'chat_extend_button', button: true, onTap: …) — good hygiene for
  future tests even though it doesn't fix the dadb tap issue

.dev/: tracked wsl_emulator_bridge.ps1 + wsl_tcp_relay.py for
Maestro-on-WSL setup (Windows-side netsh portproxy + WSL-side
loopback relays). Both referenced from existing CLAUDE.md notes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 21:45:46 +08:00
1653482d54 Client: mirror branded notification sound to customer app
The customer app now uses the same halobestie_notif.ogg as the mitra
app (shipped in the previous commit). Channel ID unified across both
apps so backend FCM stops branching per recipient.

- client_app: same channel bump (chat_messages → halobestie_chat_v1)
  + RawResourceAndroidNotificationSound binding, both at channel-
  create time and per-notification details. .ogg copied to
  client_app/android/app/src/main/res/raw/halobestie_notif.ogg
  (same 32 KB asset, identical file).
- Backend: drop the per-recipientType channel ID branch; everyone
  targets halobestie_chat_v1 now.

Verified on emulator-5554 (customer): dumpsys shows the channel
bound to android.resource://com.mybestie/raw/halobestie_notif.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 20:05:05 +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
7ae8f33b2c Phase 4 Stage 4: notif gate + home permission-denied banner
Notif Gate full screen at /onboarding/notif-gate, reached from waiting
payment on confirmed/consumed status. Auto-advances to /chat/searching
when permission is already granted; otherwise shows izinkan/nanti aja
HaloButton CTAs. NotifPermission helper wraps firebase_messaging +
permission_handler with readStatus/request/openAppSettings; cached in
notifPermissionStatusProvider that re-reads on app foreground via an
internal WidgetsBindingObserver.

home_screen amber banner above-the-fold when notifPermissionStatusProvider
reports denied. Dismissable for the session via homeNotifBannerDismissedProvider
(in-memory StateProvider, no persistence - cold-restart re-shows).
nyalain CTA -> openAppSettings().

Manifest + Info.plist permission entries added.

Note: main.dart still pre-requests FirebaseMessaging permission at boot,
which can pre-resolve status so the gate auto-advances instead of acting
as the first prompt. Left intact for now; can be removed in a later
stage if the gate should be the first-ask UX.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:36:46 +08:00
1a610363bb iOS navigation fixes: deep-link pop fallback + back-button PopScope
- notification_service: use GoRouter.go (not push) for terminal states
  (session_closing, session_expired) so the nav stack doesn't linger
  behind deep-linked screens
- chat_screen: PopScope + canPop fallback in client_app so iOS back
  gestures fall back to /home when there is nothing to pop
2026-04-24 11:58:05 +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
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