Phase 3.1 WIP: Riverpod migration (client_app Auth + ChatOpening)

- 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>
This commit is contained in:
2026-04-09 13:51:17 +08:00
parent b0502ac92b
commit d15b2f05fc
25 changed files with 2513 additions and 461 deletions

View File

@@ -1,7 +1,7 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'core/auth/auth_bloc.dart';
import 'core/auth/auth_notifier.dart';
import 'features/auth/screens/welcome_screen.dart';
import 'features/auth/screens/display_name_screen.dart';
import 'features/auth/screens/register_screen.dart';
@@ -16,38 +16,43 @@ import 'features/chat/screens/chat_screen.dart';
import 'features/chat/screens/chat_history_screen.dart';
import 'features/chat/screens/chat_transcript_screen.dart';
/// Converts a BLoC stream into a ChangeNotifier for GoRouter's refreshListenable.
class _BlocRefreshNotifier extends ChangeNotifier {
late final StreamSubscription _subscription;
class RouterNotifier extends ChangeNotifier {
final Ref _ref;
_BlocRefreshNotifier(AuthBloc bloc) {
_subscription = bloc.stream.listen((_) => notifyListeners());
}
@override
void dispose() {
_subscription.cancel();
super.dispose();
RouterNotifier(this._ref) {
_ref.listen(authProvider, (_, __) => notifyListeners());
}
}
GoRouter buildRouter(AuthBloc authBloc) {
final routerProvider = Provider<GoRouter>((ref) => buildRouter(ref));
GoRouter buildRouter(Ref ref) {
final notifier = RouterNotifier(ref);
return GoRouter(
initialLocation: '/splash',
refreshListenable: _BlocRefreshNotifier(authBloc),
refreshListenable: notifier,
redirect: (context, state) {
final authState = authBloc.state;
final authState = ref.read(authProvider);
final isSplash = state.matchedLocation == '/splash';
final isAuthRoute = state.matchedLocation.startsWith('/auth') ||
state.matchedLocation == '/welcome';
// Show splash while loading
if (authState is AuthLoading) return isSplash ? null : '/splash';
if (authState is AsyncLoading) return isSplash ? null : '/splash';
if (authState is AuthAuthenticated || authState is AuthAnonymous) {
final data = authState.valueOrNull;
if (data == null) {
// Error state — show login
if (!isAuthRoute && !isSplash) return '/welcome';
if (isSplash) return '/welcome';
return null;
}
if (data is AuthAuthenticatedData || data is AuthAnonymousData) {
return (isSplash || isAuthRoute) ? '/home' : null;
}
if (authState is AuthForceRegister) return '/auth/force-register';
if (data is AuthForceRegisterData) return '/auth/force-register';
if (!isAuthRoute && !isSplash) return '/welcome';
if (isSplash) return '/welcome';
return null;