- 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>
488 lines
13 KiB
Markdown
488 lines
13 KiB
Markdown
# CLAUDE.md - Plan B Logistics Flutter App
|
|
|
|
This file provides guidance to Claude Code when working with this Flutter/Dart project.
|
|
|
|
## Project Overview
|
|
|
|
Plan B Logistics Flutter is a complete refactor of the Ionic Angular delivery management app into Flutter/Dart, maintaining all functionality while applying Svrnty design system (colors, typography, and Material Design 3).
|
|
|
|
**Key Features:**
|
|
- OAuth2/OIDC authentication with Keycloak
|
|
- CQRS pattern for API integration with Result<T> error handling
|
|
- Delivery route and delivery management
|
|
- Photo upload for delivery proof
|
|
- i18n support (French/English)
|
|
- Native features: Camera, Phone calls, Maps
|
|
|
|
## Essential Commands
|
|
|
|
### Setup & Dependencies
|
|
```bash
|
|
flutter pub get
|
|
flutter pub upgrade
|
|
flutter pub run build_runner build --delete-conflicting-outputs
|
|
```
|
|
|
|
### Development
|
|
```bash
|
|
flutter run -d chrome # Web
|
|
flutter run -d macos # macOS
|
|
flutter run -d ios # iOS simulator
|
|
flutter run -d android # Android emulator
|
|
```
|
|
|
|
### Testing & Analysis
|
|
```bash
|
|
flutter test
|
|
flutter analyze
|
|
flutter test --coverage
|
|
```
|
|
|
|
### Build
|
|
```bash
|
|
flutter build web
|
|
flutter build ios
|
|
flutter build android
|
|
```
|
|
|
|
## Code Architecture
|
|
|
|
### Core Structure
|
|
```
|
|
lib/
|
|
├── api/ # CQRS API client and types
|
|
│ ├── types.dart # Result<T>, Serializable, ApiError
|
|
│ ├── client.dart # CqrsApiClient implementation
|
|
│ └── openapi_config.dart
|
|
├── models/ # Data models (strict typing)
|
|
│ ├── delivery.dart
|
|
│ ├── delivery_route.dart
|
|
│ ├── user_profile.dart
|
|
│ └── ...
|
|
├── services/ # Business logic
|
|
│ └── auth_service.dart
|
|
├── providers/ # Riverpod state management
|
|
│ └── providers.dart
|
|
├── pages/ # Screen widgets
|
|
│ ├── login_page.dart
|
|
│ ├── routes_page.dart
|
|
│ ├── deliveries_page.dart
|
|
│ └── settings_page.dart
|
|
├── components/ # Reusable UI components
|
|
├── l10n/ # i18n translations (*.arb files)
|
|
├── utils/ # Utility functions
|
|
├── theme.dart # Svrnty theme configuration
|
|
└── main.dart # App entry point
|
|
```
|
|
|
|
### Design System (Svrnty)
|
|
|
|
**Primary Colors:**
|
|
- Primary (Crimson): #C91F37 (light) / #FF5A6D (dark)
|
|
- Secondary (Slate Blue): #2D3843 (light) / #A5B6C8 (dark)
|
|
- Tertiary (Green): #16803D (light) / #5EE890 (dark)
|
|
- Error: #D32F2F (light) / #FF8A80 (dark)
|
|
|
|
**Typography:**
|
|
- Primary Font: Montserrat (all weights 300-700)
|
|
- Monospace Font: IBMPlexMono
|
|
- Material Design 3 text styles with explicit color assignments
|
|
|
|
**Theme System:**
|
|
- **2 Theme Variants:** Light and Dark (forest green background)
|
|
- **WCAG AAA Compliant:** All text meets 7:1 contrast minimum
|
|
- **Dark Theme Background:** Forest Green (#0C1410) - unique branding
|
|
- **No Hardcoded Colors:** All components use ColorScheme properties
|
|
- **Theme Files:**
|
|
- `lib/theme.dart` - Material 3 theme with 2 ColorScheme variants
|
|
- `lib/theme/color_system.dart` - Svrnty color constants
|
|
- `lib/theme/status_colors.dart` - Status color utilities
|
|
- `lib/theme/component_themes.dart` - Component-specific themes
|
|
- `lib/theme/README.md` - Complete theme documentation
|
|
|
|
## Core Patterns & Standards
|
|
|
|
### 1. Strict Typing (MANDATORY)
|
|
**NO `dynamic`, NO untyped `var`**
|
|
|
|
```dart
|
|
// FORBIDDEN:
|
|
var data = fetchData();
|
|
dynamic result = api.call();
|
|
|
|
// REQUIRED:
|
|
DeliveryRoute data = fetchData();
|
|
Result<DeliveryRoute> result = api.call();
|
|
```
|
|
|
|
### 2. Serializable Interface
|
|
All models must implement `Serializable`:
|
|
|
|
```dart
|
|
class Delivery implements Serializable {
|
|
final int id;
|
|
final String name;
|
|
|
|
const Delivery({required this.id, required this.name});
|
|
|
|
factory Delivery.fromJson(Map<String, dynamic> json) {
|
|
return Delivery(
|
|
id: json['id'] as int,
|
|
name: json['name'] as String,
|
|
);
|
|
}
|
|
|
|
@override
|
|
Map<String, Object?> toJson() => {
|
|
'id': id,
|
|
'name': name,
|
|
};
|
|
}
|
|
```
|
|
|
|
### 3. Error Handling with Result<T>
|
|
**NEVER use try-catch for API calls. Use Result<T> pattern:**
|
|
|
|
```dart
|
|
final result = await apiClient.executeQuery<DeliveryRoute>(
|
|
endpoint: 'deliveryRoutes',
|
|
query: GetRoutesQuery(),
|
|
fromJson: DeliveryRoute.fromJson,
|
|
);
|
|
|
|
result.when(
|
|
success: (route) => showRoute(route),
|
|
error: (error) {
|
|
switch (error.type) {
|
|
case ApiErrorType.network:
|
|
showSnackbar('No connection');
|
|
case ApiErrorType.timeout:
|
|
showSnackbar('Request timeout');
|
|
case ApiErrorType.validation:
|
|
showValidationErrors(error.details);
|
|
case ApiErrorType.http when error.statusCode == 401:
|
|
navigateToLogin();
|
|
default:
|
|
showSnackbar('Error: ${error.message}');
|
|
}
|
|
},
|
|
);
|
|
```
|
|
|
|
### 4. CQRS API Integration
|
|
|
|
**Query (Read Operations):**
|
|
```dart
|
|
final result = await client.executeQuery<Delivery>(
|
|
endpoint: 'simpleDeliveriesQueryItems',
|
|
query: GetDeliveriesQuery(routeFragmentId: 123),
|
|
fromJson: Delivery.fromJson,
|
|
);
|
|
```
|
|
|
|
**Command (Write Operations):**
|
|
```dart
|
|
final result = await client.executeCommand(
|
|
endpoint: 'completeDelivery',
|
|
command: CompleteDeliveryCommand(deliveryId: 123),
|
|
);
|
|
```
|
|
|
|
### 5. Riverpod State Management
|
|
|
|
**Providers Pattern:**
|
|
```dart
|
|
final authServiceProvider = Provider<AuthService>((ref) {
|
|
return AuthService();
|
|
});
|
|
|
|
final userProfileProvider = FutureProvider<UserProfile?>((ref) async {
|
|
final authService = ref.watch(authServiceProvider);
|
|
final token = await authService.getToken();
|
|
return token != null ? authService.decodeToken(token) : null;
|
|
});
|
|
|
|
// Usage in widget:
|
|
final userProfile = ref.watch(userProfileProvider);
|
|
userProfile.when(
|
|
data: (profile) => Text(profile?.fullName ?? ''),
|
|
loading: () => const CircularProgressIndicator(),
|
|
error: (error, stackTrace) => Text('Error: $error'),
|
|
);
|
|
```
|
|
|
|
### 6. Authentication Flow
|
|
|
|
1. **Login**: `AuthService.login()` triggers OAuth2/OIDC flow with Keycloak
|
|
2. **Token Storage**: Secure storage with `flutter_secure_storage`
|
|
3. **Token Validation**: Check expiration with `JwtDecoder.isExpired()`
|
|
4. **Auto Refresh**: Implement token refresh on 401 responses
|
|
5. **Logout**: Clear tokens from secure storage
|
|
|
|
**Keycloak Configuration:**
|
|
- Realm: planb-internal
|
|
- Client ID: delivery-mobile-app
|
|
- Discovery URL: https://auth.goutezplanb.com/realms/planb-internal/.well-known/openid-configuration
|
|
- Scopes: openid, profile, offline_access
|
|
|
|
### 7. No Emojis Rule
|
|
**MANDATORY: NO emojis in code, comments, or commit messages**
|
|
|
|
```dart
|
|
// FORBIDDEN:
|
|
// Bug fix for delivery issues
|
|
void completeDelivery(int id) { ... } // Done
|
|
|
|
// REQUIRED:
|
|
// Bug fix for delivery completion logic
|
|
void completeDelivery(int id) { ... }
|
|
```
|
|
|
|
### 8. Theme System (MANDATORY)
|
|
|
|
**Standard Color Access Pattern (use everywhere):**
|
|
```dart
|
|
final colorScheme = Theme.of(context).colorScheme;
|
|
|
|
// Primary UI
|
|
color: colorScheme.primary // Primary brand color
|
|
color: colorScheme.onPrimary // Text on primary
|
|
|
|
// Text colors
|
|
color: colorScheme.onSurface // Primary text
|
|
color: colorScheme.onSurfaceVariant // Secondary text
|
|
|
|
// Backgrounds
|
|
color: colorScheme.surface // Page background
|
|
color: colorScheme.surfaceContainer // Card background
|
|
|
|
// Shadows
|
|
color: colorScheme.scrim.withValues(alpha: 0.2)
|
|
```
|
|
|
|
**FORBIDDEN Patterns:**
|
|
```dart
|
|
// NEVER use these in component files:
|
|
color: Colors.white // FORBIDDEN
|
|
color: Colors.black // FORBIDDEN
|
|
color: Color(0xFFXXXXXX) // FORBIDDEN (except in theme files)
|
|
color: SvrntyColors.crimsonRed // FORBIDDEN - use colorScheme.primary
|
|
```
|
|
|
|
**Dark Theme:**
|
|
The app uses a **forest green dark theme** (#0C1410) for unique branding.
|
|
All text colors automatically adapt with WCAG AAA compliance (7:1 minimum contrast).
|
|
|
|
**Status Colors:**
|
|
```dart
|
|
import '../theme/status_colors.dart';
|
|
|
|
// Theme-aware status colors (preferred)
|
|
StatusColorScheme.getStatusColorFromTheme('completed', colorScheme)
|
|
|
|
// Hardcoded status colors (fallback)
|
|
StatusColorScheme.getStatusColor('completed')
|
|
```
|
|
|
|
**Modifying Theme:**
|
|
To change brand colors, edit `/lib/theme.dart`:
|
|
- `lightScheme()` - Light theme ColorScheme
|
|
- `darkScheme()` - Dark theme ColorScheme
|
|
|
|
**Documentation:**
|
|
See `/lib/theme/README.md` for complete theme system documentation.
|
|
|
|
## API Integration
|
|
|
|
### Base URLs
|
|
```dart
|
|
const String queryBaseUrl = 'https://api-route.goutezplanb.com/api/query';
|
|
const String commandBaseUrl = 'https://api-route.goutezplanb.com/api/command';
|
|
```
|
|
|
|
### Key Endpoints
|
|
- Query: `/api/query/simpleDeliveriesQueryItems`
|
|
- Query: `/api/query/simpleDeliveryRouteQueryItems`
|
|
- Command: `/api/command/completeDelivery`
|
|
- Command: `/api/command/markDeliveryAsUncompleted`
|
|
- Upload: `/api/delivery/uploadDeliveryPicture`
|
|
|
|
### Authorization
|
|
All requests to API base URL must include Bearer token:
|
|
```dart
|
|
final authClient = CqrsApiClient(
|
|
config: ApiClientConfig(
|
|
baseUrl: 'https://api-route.goutezplanb.com',
|
|
defaultHeaders: {'Authorization': 'Bearer $token'},
|
|
),
|
|
);
|
|
```
|
|
|
|
## Internationalization (i18n)
|
|
|
|
### File Structure
|
|
```
|
|
lib/l10n/
|
|
├── app_en.arb # English translations
|
|
└── app_fr.arb # French translations
|
|
```
|
|
|
|
### ARB Format
|
|
```json
|
|
{
|
|
"appTitle": "Plan B Logistics",
|
|
"loginButton": "Login with Keycloak",
|
|
"deliveryStatus": "Delivery #{id} is {status}",
|
|
"@deliveryStatus": {
|
|
"placeholders": {
|
|
"id": {"type": "int"},
|
|
"status": {"type": "String"}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Usage in Code
|
|
```dart
|
|
AppLocalizations.of(context)!.appTitle
|
|
AppLocalizations.of(context)!.deliveryStatus(id: 123, status: 'completed')
|
|
```
|
|
|
|
## Native Features
|
|
|
|
### Camera Integration
|
|
- Package: `image_picker`
|
|
- Use: Photo capture for delivery proof
|
|
- Platforms: iOS, Android, Web
|
|
|
|
```dart
|
|
final picker = ImagePicker();
|
|
final pickedFile = await picker.pickImage(source: ImageSource.camera);
|
|
if (pickedFile != null) {
|
|
// Upload to server
|
|
}
|
|
```
|
|
|
|
### Phone Calls
|
|
- Package: `url_launcher`
|
|
- Use: Call customer from delivery details
|
|
|
|
```dart
|
|
final Uri phoneUri = Uri(scheme: 'tel', path: phoneNumber);
|
|
if (await canLaunchUrl(phoneUri)) {
|
|
await launchUrl(phoneUri);
|
|
}
|
|
```
|
|
|
|
### Maps Integration
|
|
- Package: `url_launcher`
|
|
- Use: Open maps app to show delivery address
|
|
|
|
```dart
|
|
final Uri mapUri = Uri(
|
|
scheme: 'https',
|
|
host: 'maps.google.com',
|
|
queryParameters: {'q': '${address.latitude},${address.longitude}'},
|
|
);
|
|
if (await canLaunchUrl(mapUri)) {
|
|
await launchUrl(mapUri);
|
|
}
|
|
```
|
|
|
|
## File Naming Conventions
|
|
|
|
- **Files**: snake_case (e.g., `delivery_route.dart`)
|
|
- **Classes**: PascalCase (e.g., `DeliveryRoute`)
|
|
- **Variables/Functions**: camelCase (e.g., `deliveryId`, `completeDelivery()`)
|
|
- **Constants**: camelCase or UPPER_SNAKE_CASE (e.g., `kPrimaryColor` or `MAX_RETRIES`)
|
|
- **Private members**: Prefix with underscore (e.g., `_secureStorage`)
|
|
|
|
## Git Conventions
|
|
|
|
- **Author**: Svrnty
|
|
- **Co-Author**: Jean-Philippe Brule <jp@svrnty.io>
|
|
- **Commits**: Clear, concise messages describing the "why"
|
|
- **NO emojis in commits**
|
|
|
|
Example:
|
|
```
|
|
Implement OAuth2/OIDC authentication with Keycloak
|
|
|
|
Adds AuthService with flutter_appauth integration, JWT token
|
|
management with secure storage, and automatic token refresh.
|
|
|
|
Co-Authored-By: Claude <noreply@anthropic.com>
|
|
```
|
|
|
|
## Common Development Tasks
|
|
|
|
### Adding a New Page
|
|
1. Create widget file in `lib/pages/[name]_page.dart`
|
|
2. Extend `ConsumerWidget` for Riverpod access
|
|
3. Use strict typing for all parameters and variables
|
|
4. Apply Svrnty colors from theme
|
|
5. Handle loading/error states with `.when()`
|
|
|
|
### Adding a New Data Model
|
|
1. Create in `lib/models/[name].dart`
|
|
2. Implement `Serializable` interface
|
|
3. Add `fromJson` factory constructor
|
|
4. Implement `toJson()` method
|
|
5. Use explicit types (no `dynamic`)
|
|
|
|
### Implementing API Call
|
|
1. Create Query/Command class implementing `Serializable`
|
|
2. Use `CqrsApiClient.executeQuery()` or `.executeCommand()`
|
|
3. Handle Result<T> with `.when()` pattern
|
|
4. Never use try-catch for API calls
|
|
5. Provide proper error messages to user
|
|
|
|
### Adding i18n Support
|
|
1. Add key to `app_en.arb` and `app_fr.arb`
|
|
2. Use `AppLocalizations.of(context)!.keyName` in widgets
|
|
3. For parameterized strings, define placeholders in ARB
|
|
4. Test both English and French text
|
|
|
|
## Testing
|
|
|
|
- Unit tests: `test/` directory
|
|
- Widget tests: `test/` directory with widget_test suffix
|
|
- Use Riverpod's testing utilities for provider testing
|
|
- Mock API client for service tests
|
|
- Maintain >80% code coverage for business logic
|
|
|
|
## Deployment Checklist
|
|
|
|
- [ ] All strict typing rules followed
|
|
- [ ] No `dynamic` or untyped `var`
|
|
- [ ] All API calls use Result<T> pattern
|
|
- [ ] i18n translations complete for both languages
|
|
- [ ] Theme colors correctly applied
|
|
- [ ] No emojis in code or commits
|
|
- [ ] Tests passing (flutter test)
|
|
- [ ] Static analysis clean (flutter analyze)
|
|
- [ ] No secrets in code (tokens, keys, credentials)
|
|
- [ ] APK/IPA builds successful
|
|
|
|
## Key Dependencies
|
|
|
|
- `flutter_riverpod`: State management
|
|
- `flutter_appauth`: OAuth2/OIDC
|
|
- `flutter_secure_storage`: Token storage
|
|
- `jwt_decoder`: JWT token parsing
|
|
- `http`: HTTP client
|
|
- `image_picker`: Camera/photo access
|
|
- `url_launcher`: Phone calls and maps
|
|
- `animate_do`: Animations (from Svrnty)
|
|
- `lottie`: Loading animations
|
|
- `iconsax`: Icon set
|
|
- `intl`: Internationalization
|
|
|
|
## Support & Documentation
|
|
|
|
- **Theme**: See `lib/theme.dart` for complete Svrnty design system
|
|
- **API Types**: See `lib/api/types.dart` for Result<T> and error handling
|
|
- **Models**: See `lib/models/` for data structure examples
|
|
- **Providers**: See `lib/providers/providers.dart` for state management setup
|
|
- **Auth**: See `lib/services/auth_service.dart` for OAuth2/OIDC flow
|