fill
This commit is contained in:
156
lib/core/router/QUICK_REFERENCE.md
Normal file
156
lib/core/router/QUICK_REFERENCE.md
Normal file
@@ -0,0 +1,156 @@
|
||||
# GoRouter Quick Reference
|
||||
|
||||
## Import
|
||||
```dart
|
||||
import 'package:minhthu/core/router/app_router.dart';
|
||||
```
|
||||
|
||||
## Navigation Commands
|
||||
|
||||
### Basic Navigation
|
||||
```dart
|
||||
// Login page
|
||||
context.goToLogin();
|
||||
|
||||
// Warehouses list
|
||||
context.goToWarehouses();
|
||||
|
||||
// Operations (requires warehouse)
|
||||
context.goToOperations(warehouse);
|
||||
|
||||
// Products (requires warehouse and operation type)
|
||||
context.goToProducts(
|
||||
warehouse: warehouse,
|
||||
operationType: 'import', // or 'export'
|
||||
);
|
||||
|
||||
// Go back
|
||||
context.goBack();
|
||||
```
|
||||
|
||||
### Named Routes (Alternative)
|
||||
```dart
|
||||
context.goToLoginNamed();
|
||||
context.goToWarehousesNamed();
|
||||
context.goToOperationsNamed(warehouse);
|
||||
context.goToProductsNamed(
|
||||
warehouse: warehouse,
|
||||
operationType: 'export',
|
||||
);
|
||||
```
|
||||
|
||||
## Common Usage Patterns
|
||||
|
||||
### Warehouse Selection → Operations
|
||||
```dart
|
||||
onTap: () {
|
||||
context.goToOperations(warehouse);
|
||||
}
|
||||
```
|
||||
|
||||
### Operation Selection → Products
|
||||
```dart
|
||||
// Import
|
||||
onTap: () {
|
||||
context.goToProducts(
|
||||
warehouse: warehouse,
|
||||
operationType: 'import',
|
||||
);
|
||||
}
|
||||
|
||||
// Export
|
||||
onTap: () {
|
||||
context.goToProducts(
|
||||
warehouse: warehouse,
|
||||
operationType: 'export',
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Logout
|
||||
```dart
|
||||
IconButton(
|
||||
icon: const Icon(Icons.logout),
|
||||
onPressed: () async {
|
||||
await ref.read(authProvider.notifier).logout();
|
||||
// Router auto-redirects to /login
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
## Route Paths
|
||||
|
||||
| Path | Name | Description |
|
||||
|------|------|-------------|
|
||||
| `/login` | `login` | Login page |
|
||||
| `/warehouses` | `warehouses` | Warehouse list (protected) |
|
||||
| `/operations` | `operations` | Operation selection (protected) |
|
||||
| `/products` | `products` | Product list (protected) |
|
||||
|
||||
## Authentication
|
||||
|
||||
### Check Status
|
||||
```dart
|
||||
final isAuth = await SecureStorage().isAuthenticated();
|
||||
```
|
||||
|
||||
### Auto-Redirect Rules
|
||||
- Not authenticated → `/login`
|
||||
- Authenticated on `/login` → `/warehouses`
|
||||
- Missing parameters → Previous valid page
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Missing Parameters
|
||||
```dart
|
||||
// Automatically redirected to safe page
|
||||
// Error screen shown briefly
|
||||
```
|
||||
|
||||
### Page Not Found
|
||||
```dart
|
||||
// Custom 404 page shown
|
||||
// Can navigate back to login
|
||||
```
|
||||
|
||||
## Complete Example
|
||||
|
||||
```dart
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:minhthu/core/router/app_router.dart';
|
||||
|
||||
class WarehouseCard extends ConsumerWidget {
|
||||
final WarehouseEntity warehouse;
|
||||
|
||||
const WarehouseCard({required this.warehouse});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return Card(
|
||||
child: ListTile(
|
||||
title: Text(warehouse.name),
|
||||
subtitle: Text(warehouse.code),
|
||||
trailing: Icon(Icons.arrow_forward),
|
||||
onTap: () {
|
||||
// Navigate to operations
|
||||
context.goToOperations(warehouse);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Tips
|
||||
|
||||
1. **Use extension methods** - They provide type safety and auto-completion
|
||||
2. **Let router handle auth** - Don't manually check authentication in pages
|
||||
3. **Validate early** - Router validates parameters automatically
|
||||
4. **Use named routes** - For better route management in large apps
|
||||
|
||||
## See Also
|
||||
|
||||
- Full documentation: `/lib/core/router/README.md`
|
||||
- Setup guide: `/ROUTER_SETUP.md`
|
||||
- Examples: `/lib/features/warehouse/presentation/pages/warehouse_selection_page_example.dart`
|
||||
382
lib/core/router/README.md
Normal file
382
lib/core/router/README.md
Normal file
@@ -0,0 +1,382 @@
|
||||
# App Router Documentation
|
||||
|
||||
Complete navigation setup for the warehouse management application using GoRouter.
|
||||
|
||||
## Overview
|
||||
|
||||
The app router implements authentication-based navigation with proper redirect logic:
|
||||
- **Unauthenticated users** are redirected to `/login`
|
||||
- **Authenticated users** on `/login` are redirected to `/warehouses`
|
||||
- Type-safe parameter passing between routes
|
||||
- Integration with SecureStorage for authentication checks
|
||||
|
||||
## App Flow
|
||||
|
||||
```
|
||||
Login → Warehouses → Operations → Products
|
||||
```
|
||||
|
||||
1. **Login**: User authenticates and token is stored
|
||||
2. **Warehouses**: User selects a warehouse
|
||||
3. **Operations**: User chooses Import or Export
|
||||
4. **Products**: Display products based on warehouse and operation
|
||||
|
||||
## Routes
|
||||
|
||||
### `/login` - Login Page
|
||||
- **Name**: `login`
|
||||
- **Purpose**: User authentication
|
||||
- **Parameters**: None
|
||||
- **Redirect**: If authenticated → `/warehouses`
|
||||
|
||||
### `/warehouses` - Warehouse Selection Page
|
||||
- **Name**: `warehouses`
|
||||
- **Purpose**: Display list of warehouses
|
||||
- **Parameters**: None
|
||||
- **Protected**: Requires authentication
|
||||
|
||||
### `/operations` - Operation Selection Page
|
||||
- **Name**: `operations`
|
||||
- **Purpose**: Choose Import or Export operation
|
||||
- **Parameters**:
|
||||
- `extra`: `WarehouseEntity` object
|
||||
- **Protected**: Requires authentication
|
||||
- **Validation**: Redirects to `/warehouses` if warehouse data is missing
|
||||
|
||||
### `/products` - Products List Page
|
||||
- **Name**: `products`
|
||||
- **Purpose**: Display products for warehouse and operation
|
||||
- **Parameters**:
|
||||
- `extra`: `Map<String, dynamic>` containing:
|
||||
- `warehouse`: `WarehouseEntity` object
|
||||
- `warehouseName`: `String`
|
||||
- `operationType`: `String` ('import' or 'export')
|
||||
- **Protected**: Requires authentication
|
||||
- **Validation**: Redirects to `/warehouses` if parameters are invalid
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Navigation
|
||||
|
||||
```dart
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
// Navigate to login
|
||||
context.go('/login');
|
||||
|
||||
// Navigate to warehouses
|
||||
context.go('/warehouses');
|
||||
```
|
||||
|
||||
### Navigation with Extension Methods
|
||||
|
||||
```dart
|
||||
import 'package:minhthu/core/router/app_router.dart';
|
||||
|
||||
// Navigate to login
|
||||
context.goToLogin();
|
||||
|
||||
// Navigate to warehouses
|
||||
context.goToWarehouses();
|
||||
|
||||
// Navigate to operations with warehouse
|
||||
context.goToOperations(warehouse);
|
||||
|
||||
// Navigate to products with warehouse and operation type
|
||||
context.goToProducts(
|
||||
warehouse: warehouse,
|
||||
operationType: 'import',
|
||||
);
|
||||
|
||||
// Go back
|
||||
context.goBack();
|
||||
```
|
||||
|
||||
### Named Route Navigation
|
||||
|
||||
```dart
|
||||
// Using named routes
|
||||
context.goToLoginNamed();
|
||||
context.goToWarehousesNamed();
|
||||
context.goToOperationsNamed(warehouse);
|
||||
context.goToProductsNamed(
|
||||
warehouse: warehouse,
|
||||
operationType: 'export',
|
||||
);
|
||||
```
|
||||
|
||||
## Integration with Warehouse Selection
|
||||
|
||||
### Example: Navigate from Warehouse to Operations
|
||||
|
||||
```dart
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:minhthu/core/router/app_router.dart';
|
||||
import 'package:minhthu/features/warehouse/domain/entities/warehouse_entity.dart';
|
||||
|
||||
class WarehouseCard extends StatelessWidget {
|
||||
final WarehouseEntity warehouse;
|
||||
|
||||
const WarehouseCard({required this.warehouse});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
child: ListTile(
|
||||
title: Text(warehouse.name),
|
||||
subtitle: Text('Code: ${warehouse.code}'),
|
||||
trailing: Icon(Icons.arrow_forward),
|
||||
onTap: () {
|
||||
// Navigate to operations page
|
||||
context.goToOperations(warehouse);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example: Navigate from Operations to Products
|
||||
|
||||
```dart
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:minhthu/core/router/app_router.dart';
|
||||
import 'package:minhthu/features/warehouse/domain/entities/warehouse_entity.dart';
|
||||
|
||||
class OperationButton extends StatelessWidget {
|
||||
final WarehouseEntity warehouse;
|
||||
final String operationType;
|
||||
|
||||
const OperationButton({
|
||||
required this.warehouse,
|
||||
required this.operationType,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ElevatedButton(
|
||||
onPressed: () {
|
||||
// Navigate to products page
|
||||
context.goToProducts(
|
||||
warehouse: warehouse,
|
||||
operationType: operationType,
|
||||
);
|
||||
},
|
||||
child: Text(operationType == 'import'
|
||||
? 'Import Products'
|
||||
: 'Export Products'),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Authentication Integration
|
||||
|
||||
The router automatically checks authentication status on every navigation:
|
||||
|
||||
```dart
|
||||
// In app_router.dart
|
||||
Future<String?> _handleRedirect(
|
||||
BuildContext context,
|
||||
GoRouterState state,
|
||||
) async {
|
||||
// Check if user has access token
|
||||
final isAuthenticated = await secureStorage.isAuthenticated();
|
||||
final isOnLoginPage = state.matchedLocation == '/login';
|
||||
|
||||
// Redirect logic
|
||||
if (!isAuthenticated && !isOnLoginPage) {
|
||||
return '/login'; // Redirect to login
|
||||
}
|
||||
|
||||
if (isAuthenticated && isOnLoginPage) {
|
||||
return '/warehouses'; // Redirect to warehouses
|
||||
}
|
||||
|
||||
return null; // Allow navigation
|
||||
}
|
||||
```
|
||||
|
||||
### SecureStorage Integration
|
||||
|
||||
The router uses `SecureStorage` to check authentication:
|
||||
|
||||
```dart
|
||||
// Check if authenticated
|
||||
final isAuthenticated = await secureStorage.isAuthenticated();
|
||||
|
||||
// This checks if access token exists
|
||||
Future<bool> isAuthenticated() async {
|
||||
final token = await getAccessToken();
|
||||
return token != null && token.isNotEmpty;
|
||||
}
|
||||
```
|
||||
|
||||
## Reactive Navigation
|
||||
|
||||
The router automatically reacts to authentication state changes:
|
||||
|
||||
```dart
|
||||
class GoRouterRefreshStream extends ChangeNotifier {
|
||||
final Ref ref;
|
||||
|
||||
GoRouterRefreshStream(this.ref) {
|
||||
// Listen to auth state changes
|
||||
ref.listen(
|
||||
authProvider, // From auth_dependency_injection.dart
|
||||
(_, __) => notifyListeners(),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When authentication state changes (login/logout), the router:
|
||||
1. Receives notification
|
||||
2. Re-evaluates redirect logic
|
||||
3. Automatically redirects to appropriate page
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Missing Parameters
|
||||
|
||||
If route parameters are missing, the user is redirected:
|
||||
|
||||
```dart
|
||||
GoRoute(
|
||||
path: '/operations',
|
||||
builder: (context, state) {
|
||||
final warehouse = state.extra as WarehouseEntity?;
|
||||
|
||||
if (warehouse == null) {
|
||||
// Show error and redirect
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
context.go('/warehouses');
|
||||
});
|
||||
return const _ErrorScreen(
|
||||
message: 'Warehouse data is required',
|
||||
);
|
||||
}
|
||||
|
||||
return OperationSelectionPage(warehouse: warehouse);
|
||||
},
|
||||
),
|
||||
```
|
||||
|
||||
### Page Not Found
|
||||
|
||||
Custom 404 error page:
|
||||
|
||||
```dart
|
||||
errorBuilder: (context, state) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Page Not Found')),
|
||||
body: Center(
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(Icons.error_outline, size: 64),
|
||||
Text('Page "${state.uri.path}" does not exist'),
|
||||
ElevatedButton(
|
||||
onPressed: () => context.go('/login'),
|
||||
child: const Text('Go to Login'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Setup in main.dart
|
||||
|
||||
```dart
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:minhthu/core/router/app_router.dart';
|
||||
import 'package:minhthu/core/theme/app_theme.dart';
|
||||
|
||||
void main() {
|
||||
runApp(
|
||||
const ProviderScope(
|
||||
child: MyApp(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class MyApp extends ConsumerWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
// Get router from provider
|
||||
final router = ref.watch(appRouterProvider);
|
||||
|
||||
return MaterialApp.router(
|
||||
title: 'Warehouse Manager',
|
||||
theme: AppTheme.lightTheme,
|
||||
routerConfig: router,
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Use Extension Methods
|
||||
Prefer extension methods for type-safe navigation:
|
||||
```dart
|
||||
// Good
|
||||
context.goToProducts(warehouse: warehouse, operationType: 'import');
|
||||
|
||||
// Avoid
|
||||
context.go('/products', extra: {'warehouse': warehouse, 'operationType': 'import'});
|
||||
```
|
||||
|
||||
### 2. Validate Parameters
|
||||
Always validate route parameters:
|
||||
```dart
|
||||
final warehouse = state.extra as WarehouseEntity?;
|
||||
if (warehouse == null) {
|
||||
// Handle error
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Handle Async Operations
|
||||
Use post-frame callbacks for navigation in builders:
|
||||
```dart
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
context.go('/warehouses');
|
||||
});
|
||||
```
|
||||
|
||||
### 4. Logout Implementation
|
||||
Clear storage and let router handle redirect:
|
||||
```dart
|
||||
Future<void> logout() async {
|
||||
await ref.read(authProvider.notifier).logout();
|
||||
// Router will automatically redirect to /login
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Issue: Redirect loop
|
||||
**Cause**: Authentication check is not working properly
|
||||
**Solution**: Verify SecureStorage has access token
|
||||
|
||||
### Issue: Parameters are null
|
||||
**Cause**: Wrong parameter passing format
|
||||
**Solution**: Use extension methods with correct types
|
||||
|
||||
### Issue: Navigation doesn't update
|
||||
**Cause**: Auth state changes not triggering refresh
|
||||
**Solution**: Verify GoRouterRefreshStream is listening to authProvider
|
||||
|
||||
## Related Files
|
||||
|
||||
- `/lib/core/router/app_router.dart` - Main router configuration
|
||||
- `/lib/core/storage/secure_storage.dart` - Authentication storage
|
||||
- `/lib/features/auth/di/auth_dependency_injection.dart` - Auth providers
|
||||
- `/lib/features/auth/presentation/pages/login_page.dart` - Login page
|
||||
- `/lib/features/warehouse/presentation/pages/warehouse_selection_page.dart` - Warehouse page
|
||||
- `/lib/features/operation/presentation/pages/operation_selection_page.dart` - Operation page
|
||||
- `/lib/features/products/presentation/pages/products_page.dart` - Products page
|
||||
360
lib/core/router/app_router.dart
Normal file
360
lib/core/router/app_router.dart
Normal file
@@ -0,0 +1,360 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../features/auth/presentation/pages/login_page.dart';
|
||||
import '../../features/auth/di/auth_dependency_injection.dart';
|
||||
import '../../features/warehouse/presentation/pages/warehouse_selection_page.dart';
|
||||
import '../../features/operation/presentation/pages/operation_selection_page.dart';
|
||||
import '../../features/products/presentation/pages/products_page.dart';
|
||||
import '../../features/warehouse/domain/entities/warehouse_entity.dart';
|
||||
import '../storage/secure_storage.dart';
|
||||
|
||||
/// Application router configuration using GoRouter
|
||||
///
|
||||
/// Implements authentication-based redirect logic:
|
||||
/// - Unauthenticated users are redirected to /login
|
||||
/// - Authenticated users on /login are redirected to /warehouses
|
||||
/// - Proper parameter passing between routes
|
||||
///
|
||||
/// App Flow: Login → Warehouses → Operations → Products
|
||||
class AppRouter {
|
||||
final Ref ref;
|
||||
final SecureStorage secureStorage;
|
||||
|
||||
AppRouter({
|
||||
required this.ref,
|
||||
required this.secureStorage,
|
||||
});
|
||||
|
||||
late final GoRouter router = GoRouter(
|
||||
debugLogDiagnostics: true,
|
||||
initialLocation: '/login',
|
||||
refreshListenable: GoRouterRefreshStream(ref),
|
||||
redirect: _handleRedirect,
|
||||
routes: [
|
||||
// ==================== Auth Routes ====================
|
||||
|
||||
/// Login Route
|
||||
/// Path: /login
|
||||
/// Initial route for unauthenticated users
|
||||
GoRoute(
|
||||
path: '/login',
|
||||
name: 'login',
|
||||
builder: (context, state) => const LoginPage(),
|
||||
),
|
||||
|
||||
// ==================== Main App Routes ====================
|
||||
|
||||
/// Warehouse Selection Route
|
||||
/// Path: /warehouses
|
||||
/// Shows list of available warehouses after login
|
||||
GoRoute(
|
||||
path: '/warehouses',
|
||||
name: 'warehouses',
|
||||
builder: (context, state) => const WarehouseSelectionPage(),
|
||||
),
|
||||
|
||||
/// Operation Selection Route
|
||||
/// Path: /operations
|
||||
/// Takes warehouse data as extra parameter
|
||||
/// Shows Import/Export operation options for selected warehouse
|
||||
GoRoute(
|
||||
path: '/operations',
|
||||
name: 'operations',
|
||||
builder: (context, state) {
|
||||
final warehouse = state.extra as WarehouseEntity?;
|
||||
|
||||
if (warehouse == null) {
|
||||
// If no warehouse data, redirect to warehouses
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
context.go('/warehouses');
|
||||
});
|
||||
return const _ErrorScreen(
|
||||
message: 'Warehouse data is required',
|
||||
);
|
||||
}
|
||||
|
||||
return OperationSelectionPage(warehouse: warehouse);
|
||||
},
|
||||
),
|
||||
|
||||
/// Products List Route
|
||||
/// Path: /products
|
||||
/// Takes warehouse, warehouseName, and operationType as extra parameter
|
||||
/// Shows products for selected warehouse and operation
|
||||
GoRoute(
|
||||
path: '/products',
|
||||
name: 'products',
|
||||
builder: (context, state) {
|
||||
final params = state.extra as Map<String, dynamic>?;
|
||||
|
||||
if (params == null) {
|
||||
// If no params, redirect to warehouses
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
context.go('/warehouses');
|
||||
});
|
||||
return const _ErrorScreen(
|
||||
message: 'Product parameters are required',
|
||||
);
|
||||
}
|
||||
|
||||
// Extract required parameters
|
||||
final warehouse = params['warehouse'] as WarehouseEntity?;
|
||||
final warehouseName = params['warehouseName'] as String?;
|
||||
final operationType = params['operationType'] as String?;
|
||||
|
||||
// Validate parameters
|
||||
if (warehouse == null || warehouseName == null || operationType == null) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
context.go('/warehouses');
|
||||
});
|
||||
return const _ErrorScreen(
|
||||
message: 'Invalid product parameters',
|
||||
);
|
||||
}
|
||||
|
||||
return ProductsPage(
|
||||
warehouseId: warehouse.id,
|
||||
warehouseName: warehouseName,
|
||||
operationType: operationType,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
|
||||
// ==================== Error Handling ====================
|
||||
|
||||
errorBuilder: (context, state) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Page Not Found'),
|
||||
),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
size: 64,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Page Not Found',
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'The page "${state.uri.path}" does not exist.',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
onPressed: () => context.go('/login'),
|
||||
child: const Text('Go to Login'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
/// Handle global redirect logic based on authentication status
|
||||
///
|
||||
/// Redirect rules:
|
||||
/// 1. Check authentication status using SecureStorage
|
||||
/// 2. If not authenticated and not on login page → redirect to /login
|
||||
/// 3. If authenticated and on login page → redirect to /warehouses
|
||||
/// 4. Otherwise, allow navigation
|
||||
Future<String?> _handleRedirect(
|
||||
BuildContext context,
|
||||
GoRouterState state,
|
||||
) async {
|
||||
try {
|
||||
// Check if user has access token
|
||||
final isAuthenticated = await secureStorage.isAuthenticated();
|
||||
final isOnLoginPage = state.matchedLocation == '/login';
|
||||
|
||||
// User is not authenticated
|
||||
if (!isAuthenticated) {
|
||||
// Allow access to login page
|
||||
if (isOnLoginPage) {
|
||||
return null;
|
||||
}
|
||||
// Redirect to login for all other pages
|
||||
return '/login';
|
||||
}
|
||||
|
||||
// User is authenticated
|
||||
if (isAuthenticated) {
|
||||
// Redirect away from login page to warehouses
|
||||
if (isOnLoginPage) {
|
||||
return '/warehouses';
|
||||
}
|
||||
// Allow access to all other pages
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (e) {
|
||||
// On error, redirect to login for safety
|
||||
debugPrint('Error in redirect: $e');
|
||||
return '/login';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Provider for AppRouter
|
||||
///
|
||||
/// Creates and provides the GoRouter instance with dependencies
|
||||
final appRouterProvider = Provider<GoRouter>((ref) {
|
||||
final secureStorage = SecureStorage();
|
||||
final appRouter = AppRouter(
|
||||
ref: ref,
|
||||
secureStorage: secureStorage,
|
||||
);
|
||||
return appRouter.router;
|
||||
});
|
||||
|
||||
/// Helper class to refresh router when auth state changes
|
||||
///
|
||||
/// This allows GoRouter to react to authentication state changes
|
||||
/// and re-evaluate redirect logic
|
||||
class GoRouterRefreshStream extends ChangeNotifier {
|
||||
final Ref ref;
|
||||
|
||||
GoRouterRefreshStream(this.ref) {
|
||||
// Listen to auth state changes
|
||||
// When auth state changes, notify GoRouter to re-evaluate redirects
|
||||
ref.listen(
|
||||
authProvider,
|
||||
(_, __) => notifyListeners(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Error screen widget for route parameter validation errors
|
||||
class _ErrorScreen extends StatelessWidget {
|
||||
final String message;
|
||||
|
||||
const _ErrorScreen({required this.message});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Error'),
|
||||
),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
size: 64,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Navigation Error',
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Text(
|
||||
message,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
onPressed: () => context.go('/warehouses'),
|
||||
child: const Text('Go to Warehouses'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Extension methods for easier type-safe navigation
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// context.goToLogin();
|
||||
/// context.goToWarehouses();
|
||||
/// context.goToOperations(warehouse);
|
||||
/// context.goToProducts(warehouse, 'import');
|
||||
/// ```
|
||||
extension AppRouterExtension on BuildContext {
|
||||
/// Navigate to login page
|
||||
void goToLogin() => go('/login');
|
||||
|
||||
/// Navigate to warehouses list
|
||||
void goToWarehouses() => go('/warehouses');
|
||||
|
||||
/// Navigate to operation selection with warehouse data
|
||||
void goToOperations(WarehouseEntity warehouse) {
|
||||
go('/operations', extra: warehouse);
|
||||
}
|
||||
|
||||
/// Navigate to products list with required parameters
|
||||
///
|
||||
/// [warehouse] - Selected warehouse entity
|
||||
/// [operationType] - Either 'import' or 'export'
|
||||
void goToProducts({
|
||||
required WarehouseEntity warehouse,
|
||||
required String operationType,
|
||||
}) {
|
||||
go(
|
||||
'/products',
|
||||
extra: {
|
||||
'warehouse': warehouse,
|
||||
'warehouseName': warehouse.name,
|
||||
'operationType': operationType,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Pop current route
|
||||
void goBack() => pop();
|
||||
}
|
||||
|
||||
/// Extension for named route navigation
|
||||
extension AppRouterNamedExtension on BuildContext {
|
||||
/// Navigate to login page using named route
|
||||
void goToLoginNamed() => goNamed('login');
|
||||
|
||||
/// Navigate to warehouses using named route
|
||||
void goToWarehousesNamed() => goNamed('warehouses');
|
||||
|
||||
/// Navigate to operations using named route with warehouse
|
||||
void goToOperationsNamed(WarehouseEntity warehouse) {
|
||||
goNamed('operations', extra: warehouse);
|
||||
}
|
||||
|
||||
/// Navigate to products using named route with parameters
|
||||
void goToProductsNamed({
|
||||
required WarehouseEntity warehouse,
|
||||
required String operationType,
|
||||
}) {
|
||||
goNamed(
|
||||
'products',
|
||||
extra: {
|
||||
'warehouse': warehouse,
|
||||
'warehouseName': warehouse.name,
|
||||
'operationType': operationType,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user