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>
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>
Six new screens under /payment/* + a paymentDraftProvider holding
mode/durationId/durationMinutes/priceIDR/paymentId/isFirstSessionDiscount
across the flow. PaymentEntryScreen handles the routing decision
(eligible+enabled -> /payment/discount-paywall, else /payment/method-pick)
and clears the draft on fresh entry.
Screens:
- discount_paywall_screen: S6 first-session discount with struck-through
gimmick price + actual price + 'mulai · Rp{actual}' CTA -> /payment/method
- method_pick_screen: chat vs call cards
- duration_pick_screen: tier list with chat|call mode toggle that resets
the selection on swap
- payment_method_screen: QRIS-first list, posts to existing
/api/client/payment-sessions with mode/duration/price/discount/method
- waiting_payment_screen: qr_flutter QR (encodes paymentId in mock mode),
20-min countdown header, 3s polling for status, pauses on background
via WidgetsBindingObserver
- payment_expired_screen: retry CTA -> /payment/method with draft retained
Status mapping: real payment_sessions.status uses 'confirmed'/'consumed'
for paid (not 'paid' as in plan) and 'expired'/'abandoned' as terminal.
home_screen 'Mulai Curhat' CTA now pushes /payment/entry.
Dev-only /internal/_test/force-expire-payment endpoint to drive Maestro
flow 04_payment_expired.yaml without waiting 20 minutes. Gated behind
NODE_ENV !== 'production'.
chat_opening_provider PricingData extended to carry Phase 4 chat/call
groups + firstSessionDiscount, back-compat with the Phase 3 shape.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Verif Choice Sheet on display_name_screen drives the user into either
the verified or anonymous onboarding sub-flow. ESP screen (12 chips,
multi-select, info-only) + USP screen are shared between both branches;
selections persist through to chat_sessions.topics on session start.
OTP-blocked popup (HaloPopup) listens for the four real OTP-rate-limit
error codes (OTP_RATE_LIMIT_PHONE, OTP_RATE_LIMIT_IP, OTP_COOLDOWN,
OTP_ATTEMPTS_EXCEEDED) and drops the user onto the anonymous path with
ESP/USP state preserved.
Auth-providers gating replaces the --dart-define=ENABLE_SOCIAL_AUTH
build flag with server-driven discovery. authProvidersProvider preloads
GET /api/shared/auth-providers at cold start; welcome/register/
force-register screens render Google/Apple buttons only when the
backend reports enabled:true. Falls back to phone-OTP-only when both
providers are off. social_auth_enabled.dart deleted; client_app/CLAUDE.md
updated to reflect the new gating contract.
Mitra app: chat screen renders an ESP chip strip above the first message
bubble when chat_sessions.topics is non-empty.
Backend session.service.js getSessionById SELECTs cs.topics so the mitra
side can read the customer's selected topics.
Maestro flows 02_onboarding_verified.yaml + 03_onboarding_anon.yaml.
Deviation from plan: plan referenced OTP error code 'otp_retry_exhausted';
real codes are OTP_RATE_LIMIT_*/OTP_COOLDOWN/OTP_ATTEMPTS_EXCEEDED -
popup listens for all four. Plan said 'has_paid_first_session'; live
endpoint returns 'has_consulted_before' - used the live field.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dev-only /internal/_test/peek-otp + /internal/_test/reset-phone endpoints
gated by NODE_ENV !== 'production'. peek-otp reads the latest stub OTP
out of an in-memory map populated by otp.service.js fazpassSendStub;
reset-phone wipes otp_requests rows (and optionally the customers row)
so flows can re-run without tripping cooldowns.
JS + shell helpers under .maestro/scripts/ wrap the endpoints for use
inside Maestro runScript steps. 01_smoke.yaml expanded from a launch-only
sanity check to a full cold-start onboarding -> force-register -> OTP ->
home walk.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the Phase 4 requirement docs that align the customer app with the new
HaloBestie Figma design dump.
- requirement/flow_customer.md: source-of-truth numbered flow (input)
- requirement/flow_customer.mermaid.md: 6 mermaid diagrams + Figma cross-ref
- requirement/phase4-customer-flow.md: PRD (15 functional sections)
- requirement/phase4-customer-flow-plan.md: 10-stage implementation plan
- .gitignore: exclude requirement/Figma.zip + extracted Figma/ folder
Resolved product decisions: no free trial (replaced by configurable
first-session discount), pricing has independent chat/call groups,
voice-call mode is chat-with-badge (mitra shares Meet link manually),
social login is server-driven via /api/shared/auth-providers, ESP tags
are info-only (not used for matching).
No code changes; implementation starts at plan stage 0 (design system).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Commit project-scoped subagents (backend, control center, client_app,
mitra_app) so they travel with the repo across machines.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the home-screen pending-requests banner with a "Riwayat
Permintaan" CTA that opens a list of the mitra's last 20 chat requests
(any status). Pending rows pin to the top; non-pending rows open a
read-only detail screen with a "Lihat percakapan" CTA on accepted rows.
Backend:
- New service `getRecentRequestsForMitra(mitraId, { limit })` capped at
20, pending pinned via `(response IS NULL AND status='pending_acceptance')
DESC`. Customer call_name returned verbatim, with `'Anonim'` only as
null-safety fallback (no anonymity-flag masking — see project memory).
- New route `GET /api/mitra/chat-requests/recent`. Strictly per-mitra
scoped via the existing `resolveMitra` preHandler.
Mitra app:
- New `RequestResponse` enum in core/constants.dart.
- New Riverpod notifier `requestHistoryProvider` (AsyncValue<List<...>>,
keepAlive) — pull-to-refresh + screen-mount fetch only, no WS.
- Two new screens (history list + detail) and two new GoRoutes.
- Home screen: `_PendingRequestsBanner` removed → `_RequestHistoryButton`
Card with red count badge. Live count comes from the existing
chatRequestProvider so nothing changes about the WS-driven badge math.
Plan + acceptance criteria in requirement/phase3.5-plan.md. flutter
analyze clean (zero new issues). Backend smoke-tested against real DB.
Real-device E2E pending.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Fastify public app now passes `trustProxy: true` so request.ip resolves to
the real client IP from X-Forwarded-For when behind Cloud Run / a load
balancer. Without this the per-IP rate limit was either useless or
collapsed all users into one shared LB IP.
- The `anonymity_enabled` config row + JS default + migration seed now
default to `false`. The flag is dead code (no business logic ever
consumed it) and the actual rule is simpler than the toggle implied:
mitras always see the customer's chosen call_name; only phone+email
are private. The whole feature is queued for rip-out as a separate
cleanup pass.
The per-IP OTP rate limit (10/hr) was also effectively disabled by
upserting `app_config.otp_max_per_ip_per_hour = 1000000` — a runtime
config change, not a code change. Per-phone (3/hr) + Fazpass cost
remains the real abuse gate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 3.5: replace _PendingRequestsBanner with a Riwayat Permintaan card
on the mitra home, plus a screen listing the last 20 entries from
chat_request_notifications. Backend endpoint TBD.
Phase 3.6: plan to re-enable mitra force-close (Akhiri) once the moderation
/ accountability story is in place. Backend route and config flag are
already preserved from Phase 3.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
OTP screen rewrite: 6 rounded boxes, auto-advance focus, auto-submit on the
6th digit, hardware-backspace on empty boxes (intercepted via Focus.onKeyEvent
since TextField.onChanged doesn't fire on already-empty input), resend
cooldown sourced from /api/shared/config/otp, and an inline error message
under the boxes instead of a SnackBar.
Several bugs fixed inline that surfaced during testing:
- ref.listen inside build() accumulates listeners on every rebuild — the
resend countdown's per-second setState was piling up duplicate listeners
so one error triggered N callback fires. Moved to ref.listenManual in
initState; subscription disposed in dispose().
- RouterNotifier was calling notifyListeners() on every auth state change
including AsyncError, which rebuilt the Navigator/Scaffold mid-snackbar
and visually duplicated the error toast. Now skips AsyncError and
same-data-variant transitions.
- ScaffoldMessenger.showSnackBar from a Riverpod listener callback could
still render twice even with hideCurrentSnackBar — replaced with an
inline error widget to sidestep the snackbar machinery entirely.
- register_screen now uses context.go instead of context.push for the
OTP route, so re-submitting the phone form doesn't stack multiple
OtpScreen instances with active subscriptions.
Lockout UX: AuthErrorInfo wraps the error message + code + retry_after_seconds
parsed from the backend's structured error response. On rate-limit codes
(OTP_COOLDOWN, OTP_RATE_LIMIT_PHONE, OTP_RATE_LIMIT_IP), the OTP screen
extends "Kirim ulang kode" cooldown to match the server's wait, and the
register screen disables "Kirim OTP" with a "Coba lagi dalam …" countdown.
formatCountdown() in core/constants.dart renders Xd under 90 seconds and
Xm Yd above (clearer than raw seconds for long lockouts).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Removes the Akhiri button + confirmation dialog + _endSession from the
mitra active sessions screen. Backend POST .../end-early route and the
early_end_mitra_enabled config flag are preserved — re-enable plan lives
in requirement/phase3.6.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
OtpError now carries an optional details object; rate-limit branches in
checkRateLimits compute retry_after_seconds (cooldown delta for OTP_COOLDOWN,
window-roll-out delta for OTP_RATE_LIMIT_PHONE / OTP_RATE_LIMIT_IP) so the
client can disable Kirim OTP / Kirim ulang CTAs with a real countdown.
All four sendAuthError helpers (client, mitra, shared, internal) now surface
err.details and log unhandled (no statusCode) errors at level 50.
New GET /api/shared/config/otp returns the resend cooldown so the OTP screen
can gate the resend countdown without hardcoding.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop NOT NULL on customers.display_name so phone-OTP and social signups can
land before the user picks a name; frontend then routes them to /auth/set-name.
Google sign-in no longer requests the name claim and Apple SDK scope is
trimmed to email only.
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>
Both apps were inheriting `flutter.minSdkVersion`, which currently resolves
to 24 (Android 7.0) in this Flutter SDK but could drift if Flutter bumps
its default. Per product requirement we support Android 7+; making the
floor explicit so it doesn't move silently.
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
Added progress snapshot at the top of phase3.4-plan.md noting:
- Backend cutover complete (commit f860ab6)
- Fazpass stubbed until real API docs arrive
- Frontend rewrites (client_app, mitra_app, control_center) pending
- Apple Developer prereqs still required
- Consolidated phase3.4-testing.md still to be written
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- 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>
- Mitra auth: parse DioException response for proper error messages
(ACCOUNT_NOT_FOUND, ACCOUNT_INACTIVE) instead of generic "OTP invalid"
- Backend: add CORS to internal app (port 3001) for control center
- Control center: fix login race condition (wait for AuthContext verify
before navigating), fix MitraActivityPage fetching paginated data
- Stale session goodbye: both apps detect SESSION_NOT_ACTIVE/409 and
move to complete state instead of retrying endlessly
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Backend: new GET /api/mitra/chat-requests/pending endpoint
- Backend: getPendingRequestsForMitra() queries unresponded notifications
for sessions still in pending_acceptance status
- Mitra app: loadPendingRequests() fetches on screen load + status toggle
- Mitra app: activeRequestCount getter exposes queue size
- Mitra app: _PendingRequestsBanner widget shows count with tap-to-view
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add Runner.entitlements with aps-environment capability
- Add UIBackgroundModes (remote-notification, fetch) to Info.plist
- Add CODE_SIGN_ENTITLEMENTS to Debug/Release/Profile build configs
- Add GoogleService-Info.plist for both apps
- Upgrade Firebase packages and web_socket_channel to fix CocoaPods conflict
- Set client_app Podfile iOS platform to 15.0
- Fix mitra_app Xcode bundle ID to match Firebase (com.halobestie.mitra)
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>
- Wrap overlay Stack with Directionality (required above MaterialApp)
- Guard startListening() for IncomingData/StaleData states to prevent
overlay dismissal when status reloads on app resume
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- DB migration: add active_session_count column + mitra_notified index
- Constants: add MISSED to NotificationResponse
- Pairing service: record active_session_count on notification creation,
use MISSED (not IGNORED) when another mitra accepts first
- New mitra-activity.service.js: getMitraActivityLog (paginated),
getMitraActivitySummary (per-mitra aggregates with acceptance rate)
- New mitra-activity.routes.js: GET /internal/mitra-activity/log,
GET /internal/mitra-activity/summary
- Control center: new MitraActivityPage with summary table + detail log,
filters (mitra, date range), color-coded response types, pagination
- Register route in App.jsx, add "Aktivitas Mitra" nav link in Layout
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add validateIncomingRequest() — checks session status with backend
- Home screen validates before showing sheet (on resume + listener)
- IncomingRequestSheet shows "cancelled/accepted by other" message
instead of silently dismissing when request becomes stale
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Backend: getOrCreateCustomer with phone fallback for re-login
- Backend: PATCH /api/client/auth/profile for display name update
- Client app: AuthNeedsDisplayNameData state + SetDisplayNameScreen
- Client app: ApiClient.patch method
- Both apps: handle verificationCompleted for auto-verify (test numbers)
- Both apps: skip credential sign-in if already auto-verified
- Remove debug prints from mitra auth + OTP screens
- Fix ChatRequestNotifier.startListening skips when accepting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Show local notification (sound + vibrate) when chat_request arrives
via WebSocket while mitra app is backgrounded
- Add NotificationService.showLocalNotification() for programmatic use
- Fix router redirect: don't redirect auth routes to splash during loading
- Handle binary/string WebSocket frames in ChatRequestNotifier
- Remove debug logging from backend and Flutter
- Control center: mitra ping config UI
- Both apps: dynamic ping, FCM deep-linking, unread badges
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
AsyncLoading during OTP request was redirecting from /login to /splash,
bouncing users back to login. Now auth routes stay put during loading —
only redirect to splash from non-auth routes (initial app startup).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add require_mitra_ping + mitra_ping_interval_seconds config keys (migration)
- Add getMitraPingConfig/setMitraPingConfig to config service
- Add GET/PATCH /internal/config/mitra-ping routes for control center
- Update mitra status service: honor ping config in auto-offline sweep,
include ping config in GET /api/mitra/status response
- Enhance pairing FCM payload with action: 'open_accept' for deep-link
- Add FCM fallback to closure.service (initiateEarlyEnd, completeSession)
- Add FCM fallback to session-timer.service (onSessionExpired)
- Add unread count queries (getActiveSessionByCustomerWithUnread,
getActiveSessionsByMitraWithUnread)
- Add GET /api/client/chat/session/active-with-unread route
- Add GET /api/mitra/chat-requests/sessions/active-with-unread route
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>
- Add phase3.1 requirement and implementation plan docs
- Add Riverpod dependencies to both client_app and mitra_app
- Wrap both app roots with ProviderScope
- Migrate client_app AuthBloc → AuthNotifier (@riverpod annotation)
- Migrate client_app ChatOpeningBloc → chatPricingProvider (FutureProvider)
- Update router to use Riverpod-based auth state for redirects
- Update all auth screens (display name, register, OTP, force register)
- Update home screen and pricing bottom sheet
- Add android:usesCleartextTraffic for dev HTTP access on both apps
- mitra_app prepared with ProviderScope + ApiClient provider (blocs next)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>