Add Firebase Analytics (GA4) funnel tracking to client_app:
- AnalyticsService typed wrapper (enum-gated, no PII) + analyticsProvider
- FirebaseAnalyticsObserver on GoRouter (screen_name via nameExtractor)
- user_id = customer UUID, user_type property, set on auth resolve/upgrade
- funnel events: curhat_start, curhat_repeat_start, auth_*, onboarding_usp_view,
payment_view, payment_method_select, payment_started, pairing_matched/no_bestie
- bottom-sheet events: verif_choice_view/select, bestie_choice_view/select,
extension_offer_view, chat_extension_requested
- payment_started carries app_instance_id + ga_session_id in the
/payment-requests body for future server-side stitching (backend ignores)
- curhat_mode_pick screen name disambiguates the chat/call mode picker
(/payment/method-pick) from the payment-channel picker (/payment/method)
- unify both home CTAs to "Aku Mau Curhat"
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Backend
- payment_sessions → payment_requests rename across DB schema + 29 files
- payment.service.js becomes product-agnostic owner: EventEmitter +
Xendit wrapper + requestPayment / confirmPayment public API; legacy
aliases retained for existing chat callers
- Webhook handler at POST /api/shared/payment/webhooks/xendit, with
constant-time token verification (8 vitest cases)
- Server-driven pairing: payment.service emits
payment_request.confirmed → pairing subscriber starts the blast.
Legacy POST /chat/request still works during the cutover.
- Reconciliation sweeper extended (re-emits events for confirmed rows
with no chat session)
- SIGTERM drain + startup reconciliation pass in server.js
Customer app
- waiting_payment_screen opens xendit_invoice_url via
LaunchMode.inAppBrowserView
- searching / no-bestie / targeted-waiting / pairing-notifier updated
to consume the new payment_request_id contract
- pending_payments_provider + bestie-unavailable dialog migrated
Dev / testing
- XENDIT_ENABLED=false is the safe default; .env.example documents the
four new vars
- backend/.dev/xendit-fake-webhook.sh exercises the handler without
ngrok
- 90/92 backend tests pass (two pre-existing session-timer flakes,
unrelated); client_app analyzer clean
- requirement/phase5-xendit-plan.md is the canonical reference
Stage 8 (live E2E) blocked on Xendit test-mode keys. The dashboard's
single-webhook-URL constraint will be worked around via a self-poll
script next session.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
§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>
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>
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>
Flutter half of Stage 10 — the new Chat tab landing in the bottom nav.
The CTA target swaps from /chat/history to /chat, which redirects into
/chat/aktif. Three sibling routes under a single ShellRoute share a
header + sub-tab pills + the existing HaloTabBar footer:
/chat/aktif — the current active session (0 or 1 row)
/chat/pembayaran — pending initial + extension payments
/chat/selesai — past sessions, cursor-paginated infinite scroll
URL is the source of truth for the active sub-tab so deep links, back
stack, and Maestro all agree on state.
New feature dir `lib/features/chat_tab/`:
- providers/pending_payments_provider.dart — FutureProvider against the
Stage-10 backend endpoint, plus pendingPaymentsCountProvider for the
red-dot derivative
- providers/selesai_history_provider.dart — AsyncNotifier over
GET /api/client/chat/history; tracks accumulated items + next_cursor +
hasMore; loadMore() and refresh()
- widgets/chat_row.dart — generic row used by all 3 sub-tabs, with
optional PaymentAmountChip / DurationChip / 📞 Call indicator
- widgets/sub_tab_pill.dart — pill with active underline + optional
numeric badge (null hides; matches Selesai's no-badge rule)
- screens/chat_tab_shell.dart — ShellRoute scaffold + ChatSubTab enum
- screens/{aktif,pembayaran,selesai}_view.dart — the three sub-tab bodies
Router (`router.dart`):
- /chat → redirect → /chat/aktif
- ShellRoute hosts /chat/aktif, /chat/pembayaran, /chat/selesai
- /chat/history retired; /chat/history/:sessionId → /chat/transcript/:sessionId
- ChatHistoryScreen import + file deleted
HaloTabBar (`features/home/widgets/halo_tab_bar.dart` — new in the
working tree from Stage 9 sweep): now a ConsumerWidget. Chat tab goes
to /chat. Red dot renders when pendingPaymentsCountProvider > 0.
Inbound call-site updates:
- bestie_choice_sheet.dart: /chat/history → /chat
- home_screen.dart history-row tap: /chat/history/:id → /chat/transcript/:id
This commit also carries the larger Stage 9 sweep + ESP-removal + USP
gate edits that were already staged in the working tree on
`home_screen.dart` and `router.dart` from the prior session.
flutter analyze: clean except for the pre-existing scaffold
test/widget_test.dart MyApp reference (unrelated, present on master).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stage 9 sweep on Client_Phone AVD + physical mitra phone:
- 01_smoke ✅
- 02_onboarding_verified ✅
- 03_onboarding_anon ✅
- 04_payment_expired ✅
- 05_searching_timeout: in progress when wrap-up began
- 06–08: not yet attempted
## Real shipping bugs fixed (would have hit prod)
1. **Router carve-out too narrow** (router.dart). The AuthAnonymousData
carve-out only protected /auth/display-name. On refreshListenable
notify after loginAnonymous resolves, GoRouter re-evaluates the
*bottom* of the navigation stack (/welcome — also an auth route),
and the AuthAnonymousData fallback redirected to /home, tearing down
the verif sheet before it could open. Loosened to allow any auth
route under AuthAnonymousData.
2. **Phase 4 multi-screen payment never called startSearch**
(searching_screen.dart). The legacy single-screen /payment did
`pairing.startSearch()` on confirm. The Phase 4 flow is
waiting → notif-gate → /chat/searching with no intermediate that
owned the call — customers would land on the searching screen with
no pairing in flight and never get matched. Added the kickoff to
searching_screen::initState when state is PairingInitialData and
paymentDraft.paymentId is set.
## Test infrastructure
- Self-contained Maestro flows 04 + 05 with inline verified-onboarding
prelude, distinct test phones per flow, robust waits.
- 02 + 03 fixed: malformed `extendedWaitUntil` (visible: + notVisible:
true → Maestro parsed as compound predicate); now use proper
notVisible: block.
- New dev-only POST /internal/_test/force-confirm-payment so flows can
advance past the waiting-payment screen without going through Xendit.
- /internal/_test/reset-phone now cascades through chat_messages →
chat_sessions → payment_sessions → auth_sessions before deleting the
customer row (FK 23503 was blocking re-runs).
- /internal/_test/force-pairing-timeout now accepts both
`searching` and `pending_acceptance` states (mitra-online dev means
the chat_session transitions through searching very quickly).
- mark_latest_payment_paid.js helper script for Stage 5+ flows.
## Maestro YAML quirks documented in flows
- text: matches anchored regex against the FULL content-desc — need .*
wildcards for substring, e.g. "mulai.*Rp.*" not "mulai".
- The middot `·` and other special unicode break naive matching;
always use .* anchors when the source string contains them.
- runFlow `when:` evaluates immediately; pair with waitForAnimationToEnd
or a preceding extendedWaitUntil before branching.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bestie Choice Sheet on home Mulai Curhat CTA. When the user has at
least one prior session (bestieHistoryHasItemsProvider hits the chat-
sessions history endpoint), the CTA opens a HaloBottomSheet with two
cards: 'bestie yang udah kenal' -> /chat/history, 'bestie baru' ->
/payment/entry. Empty history -> direct to /payment/entry.
Bestie history list visual upgrade: HaloOrb (mitraId seed) + name +
last-session date + topic pills + sessions count + ONLINE pill.
Backend getCustomerHistory now returns topics, mitra_is_online,
sessions_count in a single payload (no per-row presence round-trip).
BestieOfflinePopup with two variants (returning | new_) replacing the
legacy BestieUnavailableDialog. tanya admin ghost CTA on both variants
opens the new TanyaAdminSheet. Stage 5's targeted-wait declined stub
+ Stage 7's chat-screen 409 stub + searching-screen call site all
migrated to the real component.
TanyaAdminSheet: HaloBottomSheet with WA + Telegram buttons, deeplinks
fetched via supportHandlesProvider (CC-config-driven). url_launcher
added to client_app; ios LSApplicationQueriesSchemes covers
https/http/whatsapp/tg.
Stage 2's OTP-blocked popup hubungi admin SnackBar stub also migrated
to TanyaAdminSheet.
Dev-only POST /internal/_test/seed-history-session lets Maestro 08
flow seed a history row before exercising the choice sheet.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Customer-driven session end flow:
- AppBar 'akhiri' action on chat_screen (visible when connected and
not already closing).
- Tap fires confirm_end_step1 HaloPopup. lanjut akhiri -> step2;
gak jadi balik -> dismiss, stay in chat.
- confirm_end_step2 HaloPopup. tulis pesan penutup -> closing_message_sheet
HaloBottomSheet (textarea + kirim & akhiri / lewat — langsung akhiri).
lewati saja closes immediately.
- Both close paths POST /api/client/session/:sessionId/end via
session_closure_notifier.closeSession() and route to /chat/thank-you.
- 409 from the close endpoint surfaces a ClosureRejectedByMitraData
state and a stub HaloPopup with TODO(stage8) for the BestieOfflinePopup
returning variant.
Removed the legacy _showSessionExpiredDialog modal — Stage 6's
ChatExpiredBanner is the replacement notification.
Inline _buildGoodbyeView retained with a TODO for the mitra-side early
end flow (still reaches it).
endSessionTwoStepConfirmProvider hardcoded to true with a TODO — the
Stage 1.5 app_config row exists but no client-readable config endpoint
exists yet. Flip the provider to a FutureProvider once the read endpoint
ships.
Maestro 07_end_session_2step.yaml chains after the chat-happy flow
and asserts the Indonesian copy at each step.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Searching screen: soft-prompt card reskin, pulsing-dots panel replaces
the spinner, inline 5-min timeout panel with `coba cari lagi` (resets
pairing notifier + routes to /payment/entry for a fresh funnel — the
server-side payment is failed_pairing at that point so a stale retry
isn't valid) and `kembali ke home` ghost CTA.
Bestie-found screen: S9 Match-V4 reskin — HaloOrb + status dot +
'halo, aku bestie {name}' + `mulai sesi {N} menit →` with N pulled from
the active session's duration_minutes.
Targeted-wait overlay (new) at /chat/waiting-targeted/:mitraId. Three
sub-states from pairingProvider's PairingTargetedWaitingData:
waiting (20s countdown) / accepted (routes to chat) / declined (stubbed
BestieOfflinePopup with a TODO pointing to Stage 8). Reached via
payment_screen._routeToSearchOnConfirmed when the confirm carried a
targetedMitraId — keeps the mandatory payment-before-pairing invariant.
Dev-only POST /internal/_test/force-pairing-timeout drives the 5-min
timeout shortcut for the Maestro flow without waiting live.
Maestro 05_searching_timeout.yaml + force_pairing_timeout.js helper.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Promotes the customer-side chat WebSocket to active-session-scoped (driven
by a new `activeSessionProvider`) so home reflects session state in real
time without a per-screen connection. Backend now auto-completes sessions
left in `closing` after a 5-minute grace window so abandoned goodbye flows
don't leave the customer's home permanently locked.
Customer:
- New `activeSessionProvider` (replaces `unread_notifier`) — single source
of truth for the active session + unread count; polled every 15s.
- Chat WS lifecycle moved to `main.dart` listener on activeSessionProvider.
Chat screen joins via `connectIfNotConnected`; the new
`refreshSessionStatus` reconciles flags from the server when re-entering
an already-connected session (covers missed `sessionClosing`/`sessionExpired`
WS events).
- Home filters `closing` from the "Sesi Aktif" CTA so a session pending
goodbye doesn't block "Mulai Curhat".
- Timer-expired UX is a non-dismissible modal (Tutup / Perpanjang) instead
of an inline bar.
- Early-end goodbye composer gets an amber "Sesi telah ditutup oleh Bestie"
banner. Goodbye TextEditingController lifted to state so focus changes
no longer wipe the message.
- Closure provider reset on chat_screen mount to avoid stale
`ClosureCompleteData` from a previous session leaking into a new view.
- Chat history now lists `closing` sessions with a "Belum ditutup" badge
that routes to the live chat (goodbye composer) instead of the transcript.
Mitra:
- Same goodbye-controller fix as customer.
- Same chat-history badge + routing for `closing` items.
Backend:
- New `EndedBy.SYSTEM_AUTO_CLOSE` constant.
- `startClosureGraceTimer` extracted in `session-timer.service.js`; wired
in from `closure.initiateEarlyEnd`, `extension.rejectExtension`, and
`extension.handleExtensionTimeout`. Cancelled when customer submits
goodbye.
- Restart recovery (`restoreActiveTimers`) re-arms grace timers and stamps
any orphaned `closing` rows with `system_auto_close`.
- `getCustomerHistory` / `getMitraHistory` include `closing` alongside
`completed`; ordering uses `COALESCE(ended_at, created_at)`.
Removed: dead `session_active_screen.dart` (no router entry).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- 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
- Redesign chat screens (both apps) to match Figma: pink theme with
doodle pattern background, white app bar with centered name and
chevron back, rose sender bubbles, white receiver bubbles, entry
banners, and session-ended bottom bar
- Add splash_chat_hebat.png as native Android splash screen with
Android 12+ support (values-v31)
- Add Flutter splash screen using splash_chat_hebat.png
- Add onboarding carousel (client_app only): 3 pages with 1s
auto-advance, last page manual "Mulai" button, first-launch only
- Register image assets in both pubspec.yaml files
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
automaticallyImplyLeading was set to false, hiding the back arrow.
iOS has no physical back button so this is needed for navigation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
connect() and disconnect() were modifying provider state inside
initState/dispose, which Riverpod disallows during widget tree building.
Wrapped both in Future.microtask() to defer past the build phase.
Applied to both mitra_app and client_app.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- 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>
- 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>
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>