ionic-planb-logistic-app-fl.../lib/providers/providers.dart
Mathias Beaulieu-Duncan edb106a7fd Refactor theme system and remove unused platforms
- Overhaul theme system with Svrnty design and WCAG AAA compliance
- Remove android, macos, and web platform files (iOS-only focus)
- Update components with improved dark mode map and UI refinements
- Enhance settings page with additional configuration options
- Add theme system documentation in lib/theme/README.md
- Update CLAUDE.md with comprehensive theme guidelines

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 14:47:51 -05:00

483 lines
15 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../api/types.dart';
import '../api/client.dart';
import '../api/grpc_client.dart';
import '../api/grpc_config.dart';
import '../api/openapi_config.dart';
import '../services/auth_service.dart';
import '../models/user_profile.dart';
import '../models/delivery_route.dart';
import '../models/delivery.dart';
// ============================================================================
// API Mode Configuration - Feature Flag for HTTP/gRPC Transport
// ============================================================================
/// Enum representing the available API transport modes.
enum ApiMode {
/// Use HTTP/REST-based CQRS API client
http,
/// Use gRPC-based CQRS API client
grpc,
}
/// Configuration for API transport mode selection.
///
/// This class allows switching between HTTP and gRPC transports
/// for API calls. Following the pattern from [ApiClientConfig].
///
/// Example usage:
/// ```dart
/// // To switch to gRPC, override the apiModeConfigProvider:
/// ProviderScope(
/// overrides: [
/// apiModeConfigProvider.overrideWithValue(ApiModeConfig.developmentGrpc),
/// ],
/// child: MyApp(),
/// )
/// ```
class ApiModeConfig {
/// The transport mode to use for API calls.
final ApiMode mode;
/// Whether to fall back to HTTP on gRPC failures.
/// Only applicable when [mode] is [ApiMode.grpc].
final bool fallbackToHttpOnError;
const ApiModeConfig({
required this.mode,
this.fallbackToHttpOnError = true,
});
/// Development configuration - defaults to HTTP for stability.
/// Use this for safe development when gRPC backend may be unavailable.
static const ApiModeConfig development = ApiModeConfig(
mode: ApiMode.http,
fallbackToHttpOnError: true,
);
/// gRPC-first development configuration for testing gRPC integration.
/// Use this when actively developing/testing gRPC functionality.
static const ApiModeConfig developmentGrpc = ApiModeConfig(
mode: ApiMode.grpc,
fallbackToHttpOnError: true,
);
/// Production configuration - uses HTTP until gRPC is verified stable.
static const ApiModeConfig production = ApiModeConfig(
mode: ApiMode.http,
fallbackToHttpOnError: false,
);
/// Production gRPC configuration for when gRPC is production-ready.
static const ApiModeConfig productionGrpc = ApiModeConfig(
mode: ApiMode.grpc,
fallbackToHttpOnError: false,
);
/// Whether the current mode is gRPC.
bool get isGrpc => mode == ApiMode.grpc;
/// Whether the current mode is HTTP.
bool get isHttp => mode == ApiMode.http;
}
/// Provider for API mode configuration.
///
/// Override this provider to switch between HTTP and gRPC:
/// ```dart
/// ProviderScope(
/// overrides: [
/// apiModeConfigProvider.overrideWithValue(ApiModeConfig.developmentGrpc),
/// ],
/// child: MyApp(),
/// )
/// ```
final apiModeConfigProvider = Provider<ApiModeConfig>((ref) {
// Use gRPC for development - HTTP backend has been deprecated
return ApiModeConfig.developmentGrpc;
});
// ============================================================================
// Core Service Providers
// ============================================================================
final authServiceProvider = Provider<AuthService>((ref) {
return AuthService(config: AuthConfig.development);
});
final apiClientProvider = Provider<CqrsApiClient>((ref) {
final authService = ref.watch(authServiceProvider);
return CqrsApiClient(
config: ApiClientConfig.production,
authService: authService,
);
});
/// Provider for the gRPC-based CQRS API client.
///
/// Uses auto-dispose to properly shut down the gRPC channel when the provider
/// is no longer being watched. This ensures network resources are cleaned up.
///
/// Example usage:
/// ```dart
/// final grpcClient = ref.watch(grpcClientProvider);
/// final result = await grpcClient.getDeliveryRoutes();
/// ```
final grpcClientProvider = Provider.autoDispose<GrpcCqrsApiClient>((ref) {
final authService = ref.watch(authServiceProvider);
final client = GrpcCqrsApiClient(
config: GrpcConfig.development,
authService: authService,
);
// Register disposal callback to clean up gRPC channel resources
ref.onDispose(() {
client.shutdown();
});
return client;
});
final isAuthenticatedProvider = FutureProvider<bool>((ref) async {
final authService = ref.watch(authServiceProvider);
return await authService.isAuthenticated();
});
final userProfileProvider = FutureProvider<UserProfile?>((ref) async {
final authService = ref.watch(authServiceProvider);
final token = await authService.getToken();
if (token == null) return null;
return authService.decodeToken(token);
});
final authTokenProvider = FutureProvider<String?>((ref) async {
final authService = ref.watch(authServiceProvider);
return await authService.getToken();
});
/// Internal HTTP-based delivery routes provider.
/// Use [deliveryRoutesProvider] instead, which respects the API mode configuration.
final _httpDeliveryRoutesProvider = FutureProvider<List<DeliveryRoute>>((ref) async {
final authService = ref.watch(authServiceProvider);
final isAuthenticated = await authService.isAuthenticated();
if (!isAuthenticated) {
throw Exception('User not authenticated');
}
// Create a new client with auth service for automatic token refresh
final authClient = CqrsApiClient(
config: ApiClientConfig.production,
authService: authService,
);
final result = await authClient.executeQuery<List<DeliveryRoute>>(
endpoint: 'simpleDeliveryRouteQueryItems',
query: _EmptyQuery(),
fromJson: (json) {
// API returns data wrapped in object with "data" field
final data = json['data'];
if (data is List<dynamic>) {
return data.map((r) => DeliveryRoute.fromJson(r as Map<String, dynamic>)).toList();
}
return [];
},
);
return result.whenSuccess((routes) => routes) ?? [];
});
/// Unified delivery routes provider that respects the API mode configuration.
///
/// Automatically switches between HTTP and gRPC based on [apiModeConfigProvider].
/// When gRPC mode is enabled and [ApiModeConfig.fallbackToHttpOnError] is true,
/// falls back to HTTP on gRPC failures.
///
/// Example usage:
/// ```dart
/// final routes = ref.watch(deliveryRoutesProvider);
/// routes.when(
/// data: (data) => displayRoutes(data),
/// loading: () => showLoading(),
/// error: (error, stack) => showError(error),
/// );
/// ```
final deliveryRoutesProvider = FutureProvider<List<DeliveryRoute>>((ref) async {
final apiModeConfig = ref.watch(apiModeConfigProvider);
if (apiModeConfig.isGrpc) {
try {
return await ref.watch(grpcDeliveryRoutesProvider.future);
} catch (e) {
if (apiModeConfig.fallbackToHttpOnError) {
debugPrint('gRPC failed, falling back to HTTP: $e');
return await ref.watch(_httpDeliveryRoutesProvider.future);
}
rethrow;
}
}
return await ref.watch(_httpDeliveryRoutesProvider.future);
});
/// Provider for delivery routes using gRPC.
///
/// This is the gRPC-based alternative to [deliveryRoutesProvider].
/// Uses [GrpcCqrsApiClient] for improved performance and type safety.
///
/// Example usage:
/// ```dart
/// final routes = ref.watch(grpcDeliveryRoutesProvider);
/// routes.when(
/// data: (data) => displayRoutes(data),
/// loading: () => showLoading(),
/// error: (error, stack) => showError(error),
/// );
/// ```
final grpcDeliveryRoutesProvider = FutureProvider<List<DeliveryRoute>>((ref) async {
final authService = ref.watch(authServiceProvider);
final isAuthenticated = await authService.isAuthenticated();
if (!isAuthenticated) {
throw Exception('User not authenticated');
}
final grpcClient = ref.watch(grpcClientProvider);
final result = await grpcClient.getDeliveryRoutes();
return result.when(
success: (routes) => routes,
onError: (error) {
debugPrint('ERROR fetching delivery routes via gRPC: ${error.message}');
if (error.originalException != null) {
debugPrint('Original exception: ${error.originalException}');
}
throw Exception(error.message);
},
);
});
/// Provider for deliveries using gRPC.
///
/// This is the gRPC-based alternative to [deliveriesProvider].
/// Uses [GrpcCqrsApiClient] for improved performance and type safety.
/// Takes a [routeFragmentId] parameter to fetch deliveries for a specific route.
///
/// Example usage:
/// ```dart
/// final deliveries = ref.watch(grpcDeliveriesProvider(routeFragmentId));
/// deliveries.when(
/// data: (data) => displayDeliveries(data),
/// loading: () => showLoading(),
/// error: (error, stack) => showError(error),
/// );
/// ```
final grpcDeliveriesProvider = FutureProvider.family<List<Delivery>, int>((ref, routeFragmentId) async {
final authService = ref.watch(authServiceProvider);
final isAuthenticated = await authService.isAuthenticated();
if (!isAuthenticated) {
throw Exception('User not authenticated');
}
final grpcClient = ref.watch(grpcClientProvider);
final result = await grpcClient.getDeliveries(routeFragmentId: routeFragmentId);
final deliveries = result.when(
success: (deliveries) => deliveries,
onError: (error) {
debugPrint('ERROR fetching deliveries for route $routeFragmentId via gRPC: ${error.message}');
if (error.originalException != null) {
debugPrint('Original exception: ${error.originalException}');
}
throw Exception(error.message);
},
);
// Always append the warehouse delivery at the end
return [...deliveries, Delivery.createWarehouseDelivery()];
});
/// Internal HTTP-based deliveries provider.
/// Use [deliveriesProvider] instead, which respects the API mode configuration.
final _httpDeliveriesProvider = FutureProvider.family<List<Delivery>, int>((ref, routeFragmentId) async {
final authService = ref.watch(authServiceProvider);
final isAuthenticated = await authService.isAuthenticated();
if (!isAuthenticated) {
throw Exception('User not authenticated');
}
final authClient = CqrsApiClient(
config: ApiClientConfig.production,
authService: authService,
);
final result = await authClient.executeQuery<List<Delivery>>(
endpoint: 'simpleDeliveriesQueryItems',
query: _DeliveriesQuery(routeFragmentId: routeFragmentId),
fromJson: (json) {
// API returns data wrapped in object with "data" field
final data = json['data'];
if (data is List<dynamic>) {
return data.map((d) => Delivery.fromJson(d as Map<String, dynamic>)).toList();
}
return [];
},
);
// Log error if API call failed
result.whenError((error) {
debugPrint('ERROR fetching deliveries for route $routeFragmentId: ${error.message}');
if (error.originalException != null) {
debugPrint('Original exception: ${error.originalException}');
}
});
final deliveries = result.whenSuccess((deliveries) => deliveries) ?? [];
// Always append the warehouse delivery at the end
return [...deliveries, Delivery.createWarehouseDelivery()];
});
/// Unified deliveries provider that respects the API mode configuration.
///
/// Automatically switches between HTTP and gRPC based on [apiModeConfigProvider].
/// When gRPC mode is enabled and [ApiModeConfig.fallbackToHttpOnError] is true,
/// falls back to HTTP on gRPC failures.
///
/// Takes a [routeFragmentId] parameter to fetch deliveries for a specific route.
///
/// Example usage:
/// ```dart
/// final deliveries = ref.watch(deliveriesProvider(routeFragmentId));
/// deliveries.when(
/// data: (data) => displayDeliveries(data),
/// loading: () => showLoading(),
/// error: (error, stack) => showError(error),
/// );
/// ```
final deliveriesProvider = FutureProvider.family<List<Delivery>, int>((ref, routeFragmentId) async {
final apiModeConfig = ref.watch(apiModeConfigProvider);
if (apiModeConfig.isGrpc) {
try {
return await ref.watch(grpcDeliveriesProvider(routeFragmentId).future);
} catch (e) {
if (apiModeConfig.fallbackToHttpOnError) {
debugPrint('gRPC failed for route $routeFragmentId, falling back to HTTP: $e');
return await ref.watch(_httpDeliveriesProvider(routeFragmentId).future);
}
rethrow;
}
}
return await ref.watch(_httpDeliveriesProvider(routeFragmentId).future);
});
/// Provider to get all deliveries from all routes
final allDeliveriesProvider = FutureProvider<List<Delivery>>((ref) async {
final routes = await ref.watch(deliveryRoutesProvider.future);
if (routes.isEmpty) {
return [];
}
// Fetch deliveries for all routes in parallel using Future.wait
final deliveriesFutures = routes.map((route) {
return ref.watch(deliveriesProvider(route.id).future);
}).toList();
// Wait for all futures to complete
final deliveriesLists = await Future.wait(deliveriesFutures);
// Combine all deliveries into a single list
final allDeliveries = <Delivery>[];
for (final deliveries in deliveriesLists) {
allDeliveries.addAll(deliveries);
}
return allDeliveries;
});
// Language notifier for state management with SharedPreferences persistence
class LanguageNotifier extends AsyncNotifier<String> {
static const String _languageKey = 'app_language';
@override
Future<String> build() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getString(_languageKey) ?? 'system';
}
Future<void> setLanguage(String lang) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_languageKey, lang);
state = AsyncValue.data(lang);
}
}
final languageProvider = AsyncNotifierProvider<LanguageNotifier, String>(() {
return LanguageNotifier();
});
// Theme mode notifier for manual theme switching
class ThemeModeNotifier extends Notifier<ThemeMode> {
@override
ThemeMode build() => ThemeMode.dark;
void setThemeMode(ThemeMode mode) => state = mode;
}
final themeModeProvider = NotifierProvider<ThemeModeNotifier, ThemeMode>(() {
return ThemeModeNotifier();
});
// Collapse state notifier for sidebar persistence
class CollapseStateNotifier extends Notifier<bool> {
@override
bool build() => true; // Default: expanded
void toggle() => state = !state;
void setExpanded(bool expanded) => state = expanded;
}
final collapseStateProvider = NotifierProvider<CollapseStateNotifier, bool>(() {
return CollapseStateNotifier();
});
// Mobile deliveries list toggle state notifier for mobile overlay
class MobileDeliveriesListOpenNotifier extends Notifier<bool> {
@override
bool build() => false; // Default: closed
void toggle() => state = !state;
void setOpen(bool open) => state = open;
}
final mobileDeliveriesListOpenProvider = NotifierProvider<MobileDeliveriesListOpenNotifier, bool>(() {
return MobileDeliveriesListOpenNotifier();
});
class _EmptyQuery implements Serializable {
@override
Map<String, Object?> toJson() => {};
}
class _DeliveriesQuery implements Serializable {
final int routeFragmentId;
_DeliveriesQuery({required this.routeFragmentId});
@override
Map<String, Object?> toJson() => {
'params': {
'routeFragmentId': routeFragmentId,
},
};
}