615 lines
21 KiB
Dart
615 lines
21 KiB
Dart
/// App Router Configuration
|
|
///
|
|
/// Centralized routing configuration using go_router.
|
|
/// Defines all routes, navigation guards, and deep linking.
|
|
library;
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
|
|
import 'package:worker/features/account/domain/entities/address.dart';
|
|
import 'package:worker/features/account/presentation/pages/address_form_page.dart';
|
|
import 'package:worker/features/account/presentation/pages/addresses_page.dart';
|
|
import 'package:worker/features/account/presentation/pages/change_password_page.dart';
|
|
import 'package:worker/features/account/presentation/pages/profile_edit_page.dart';
|
|
import 'package:worker/features/auth/presentation/providers/auth_provider.dart';
|
|
import 'package:worker/features/auth/domain/entities/business_unit.dart';
|
|
import 'package:worker/features/auth/presentation/pages/business_unit_selection_page.dart';
|
|
import 'package:worker/features/auth/presentation/pages/forgot_password_page.dart';
|
|
import 'package:worker/features/auth/presentation/pages/login_page.dart';
|
|
import 'package:worker/features/auth/presentation/pages/otp_verification_page.dart';
|
|
import 'package:worker/features/auth/presentation/pages/register_page.dart';
|
|
import 'package:worker/features/auth/presentation/pages/splash_page.dart';
|
|
import 'package:worker/features/cart/presentation/pages/cart_page.dart';
|
|
import 'package:worker/features/cart/presentation/pages/checkout_page.dart';
|
|
import 'package:worker/features/chat/presentation/pages/chat_list_page.dart';
|
|
import 'package:worker/features/favorites/presentation/pages/favorites_page.dart';
|
|
import 'package:worker/features/loyalty/presentation/pages/loyalty_page.dart';
|
|
import 'package:worker/features/loyalty/presentation/pages/points_history_page.dart';
|
|
import 'package:worker/features/loyalty/presentation/pages/rewards_page.dart';
|
|
import 'package:worker/features/main/presentation/pages/main_scaffold.dart';
|
|
import 'package:worker/features/news/presentation/pages/news_detail_page.dart';
|
|
import 'package:worker/features/news/presentation/pages/news_list_page.dart';
|
|
import 'package:worker/features/orders/presentation/pages/order_detail_page.dart';
|
|
import 'package:worker/features/orders/presentation/pages/orders_page.dart';
|
|
import 'package:worker/features/orders/presentation/pages/payment_detail_page.dart';
|
|
import 'package:worker/features/orders/presentation/pages/payment_qr_page.dart';
|
|
import 'package:worker/features/orders/presentation/pages/payments_page.dart';
|
|
import 'package:worker/features/price_policy/price_policy.dart';
|
|
import 'package:worker/features/products/presentation/pages/product_detail_page.dart';
|
|
import 'package:worker/features/products/presentation/pages/products_page.dart';
|
|
import 'package:worker/features/products/presentation/pages/write_review_page.dart';
|
|
import 'package:worker/features/promotions/presentation/pages/promotion_detail_page.dart';
|
|
import 'package:worker/features/quotes/presentation/pages/quotes_page.dart';
|
|
import 'package:worker/features/showrooms/presentation/pages/design_request_create_page.dart';
|
|
import 'package:worker/features/showrooms/presentation/pages/design_request_detail_page.dart';
|
|
import 'package:worker/features/showrooms/presentation/pages/model_houses_page.dart';
|
|
|
|
/// Router Provider
|
|
///
|
|
/// Provides GoRouter instance with auth state management
|
|
final routerProvider = Provider<GoRouter>((ref) {
|
|
final authState = ref.watch(authProvider);
|
|
|
|
return GoRouter(
|
|
// Initial route - start with splash screen
|
|
initialLocation: RouteNames.splash,
|
|
|
|
// Redirect based on auth state
|
|
redirect: (context, state) {
|
|
final isLoading = authState.isLoading;
|
|
final isLoggedIn = authState.value != null;
|
|
final isOnSplashPage = state.matchedLocation == RouteNames.splash;
|
|
final isOnLoginPage = state.matchedLocation == RouteNames.login;
|
|
final isOnForgotPasswordPage =
|
|
state.matchedLocation == RouteNames.forgotPassword;
|
|
final isOnRegisterPage = state.matchedLocation == RouteNames.register;
|
|
final isOnBusinessUnitPage =
|
|
state.matchedLocation == RouteNames.businessUnitSelection;
|
|
final isOnOtpPage = state.matchedLocation == RouteNames.otpVerification;
|
|
final isOnAuthPage =
|
|
isOnLoginPage ||
|
|
isOnForgotPasswordPage ||
|
|
isOnRegisterPage ||
|
|
isOnBusinessUnitPage ||
|
|
isOnOtpPage;
|
|
|
|
// While loading auth state, show splash screen
|
|
if (isLoading) {
|
|
return RouteNames.splash;
|
|
}
|
|
|
|
// After loading, redirect from splash to appropriate page
|
|
if (isOnSplashPage && !isLoading) {
|
|
return isLoggedIn ? RouteNames.home : RouteNames.login;
|
|
}
|
|
|
|
// If not logged in and not on auth/splash pages, redirect to login
|
|
if (!isLoggedIn && !isOnAuthPage && !isOnSplashPage) {
|
|
return RouteNames.login;
|
|
}
|
|
|
|
// If logged in and on login page, redirect to home
|
|
if (isLoggedIn && isOnLoginPage) {
|
|
return RouteNames.home;
|
|
}
|
|
|
|
// No redirect needed
|
|
return null;
|
|
},
|
|
|
|
// Route definitions
|
|
routes: [
|
|
// Splash Screen Route
|
|
GoRoute(
|
|
path: RouteNames.splash,
|
|
name: RouteNames.splash,
|
|
pageBuilder: (context, state) =>
|
|
MaterialPage(key: state.pageKey, child: const SplashPage()),
|
|
),
|
|
|
|
// Authentication Routes
|
|
GoRoute(
|
|
path: RouteNames.login,
|
|
name: RouteNames.login,
|
|
pageBuilder: (context, state) =>
|
|
MaterialPage(key: state.pageKey, child: const LoginPage()),
|
|
),
|
|
GoRoute(
|
|
path: RouteNames.forgotPassword,
|
|
name: RouteNames.forgotPassword,
|
|
pageBuilder: (context, state) =>
|
|
MaterialPage(key: state.pageKey, child: const ForgotPasswordPage()),
|
|
),
|
|
GoRoute(
|
|
path: RouteNames.otpVerification,
|
|
name: RouteNames.otpVerification,
|
|
pageBuilder: (context, state) {
|
|
final phoneNumber = state.extra as String? ?? '';
|
|
return MaterialPage(
|
|
key: state.pageKey,
|
|
child: OtpVerificationPage(phoneNumber: phoneNumber),
|
|
);
|
|
},
|
|
),
|
|
GoRoute(
|
|
path: RouteNames.register,
|
|
name: RouteNames.register,
|
|
pageBuilder: (context, state) {
|
|
final extra = state.extra as Map<String, dynamic>?;
|
|
return MaterialPage(
|
|
key: state.pageKey,
|
|
child: RegisterPage(
|
|
selectedBusinessUnit: extra?['businessUnit'] as BusinessUnit?,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
GoRoute(
|
|
path: RouteNames.businessUnitSelection,
|
|
name: RouteNames.businessUnitSelection,
|
|
pageBuilder: (context, state) {
|
|
final extra = state.extra as Map<String, dynamic>?;
|
|
return MaterialPage(
|
|
key: state.pageKey,
|
|
child: BusinessUnitSelectionPage(
|
|
businessUnits: extra?['businessUnits'] as List<BusinessUnit>?,
|
|
isRegistrationFlow:
|
|
(extra?['isRegistrationFlow'] as bool?) ?? false,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
|
|
// Main Route (with bottom navigation)
|
|
GoRoute(
|
|
path: RouteNames.home,
|
|
name: RouteNames.home,
|
|
pageBuilder: (context, state) =>
|
|
MaterialPage(key: state.pageKey, child: const MainScaffold()),
|
|
),
|
|
|
|
// Products Route (full screen, no bottom nav)
|
|
GoRoute(
|
|
path: RouteNames.products,
|
|
name: RouteNames.products,
|
|
pageBuilder: (context, state) =>
|
|
MaterialPage(key: state.pageKey, child: const ProductsPage()),
|
|
),
|
|
|
|
// Product Detail Route
|
|
GoRoute(
|
|
path: RouteNames.productDetail,
|
|
name: RouteNames.productDetail,
|
|
pageBuilder: (context, state) {
|
|
final productId = state.pathParameters['id'];
|
|
return MaterialPage(
|
|
key: state.pageKey,
|
|
child: ProductDetailPage(productId: productId ?? ''),
|
|
);
|
|
},
|
|
routes: [
|
|
// Write Review Route (nested under product detail)
|
|
GoRoute(
|
|
path: 'write-review',
|
|
name: RouteNames.writeReview,
|
|
pageBuilder: (context, state) {
|
|
final productId = state.pathParameters['id'];
|
|
return MaterialPage(
|
|
key: state.pageKey,
|
|
child: WriteReviewPage(productId: productId ?? ''),
|
|
);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
|
|
// Promotion Detail Route
|
|
GoRoute(
|
|
path: RouteNames.promotionDetail,
|
|
name: RouteNames.promotionDetail,
|
|
pageBuilder: (context, state) {
|
|
final promotionId = state.pathParameters['id'];
|
|
return MaterialPage(
|
|
key: state.pageKey,
|
|
child: PromotionDetailPage(promotionId: promotionId),
|
|
);
|
|
},
|
|
),
|
|
|
|
// Cart Route
|
|
GoRoute(
|
|
path: RouteNames.cart,
|
|
name: RouteNames.cart,
|
|
pageBuilder: (context, state) =>
|
|
MaterialPage(key: state.pageKey, child: const CartPage()),
|
|
),
|
|
|
|
// Checkout Route
|
|
GoRoute(
|
|
path: RouteNames.checkout,
|
|
name: RouteNames.checkout,
|
|
pageBuilder: (context, state) => MaterialPage(
|
|
key: state.pageKey,
|
|
child: CheckoutPage(
|
|
checkoutData: state.extra as Map<String, dynamic>?,
|
|
),
|
|
),
|
|
),
|
|
|
|
// Favorites Route
|
|
GoRoute(
|
|
path: RouteNames.favorites,
|
|
name: RouteNames.favorites,
|
|
pageBuilder: (context, state) =>
|
|
MaterialPage(key: state.pageKey, child: const FavoritesPage()),
|
|
),
|
|
|
|
// Loyalty Route
|
|
GoRoute(
|
|
path: RouteNames.loyalty,
|
|
name: RouteNames.loyalty,
|
|
pageBuilder: (context, state) =>
|
|
MaterialPage(key: state.pageKey, child: const LoyaltyPage()),
|
|
),
|
|
|
|
// Loyalty Rewards Route
|
|
GoRoute(
|
|
path: '/loyalty/rewards',
|
|
name: 'loyalty_rewards',
|
|
pageBuilder: (context, state) =>
|
|
MaterialPage(key: state.pageKey, child: const RewardsPage()),
|
|
),
|
|
|
|
// Points History Route
|
|
GoRoute(
|
|
path: RouteNames.pointsHistory,
|
|
name: 'loyalty_points_history',
|
|
pageBuilder: (context, state) =>
|
|
MaterialPage(key: state.pageKey, child: const PointsHistoryPage()),
|
|
),
|
|
|
|
// Orders Route
|
|
GoRoute(
|
|
path: RouteNames.orders,
|
|
name: RouteNames.orders,
|
|
pageBuilder: (context, state) =>
|
|
MaterialPage(key: state.pageKey, child: const OrdersPage()),
|
|
),
|
|
|
|
// Order Detail Route
|
|
GoRoute(
|
|
path: RouteNames.orderDetail,
|
|
name: RouteNames.orderDetail,
|
|
pageBuilder: (context, state) {
|
|
final orderId = state.pathParameters['id'];
|
|
return MaterialPage(
|
|
key: state.pageKey,
|
|
child: OrderDetailPage(orderId: orderId ?? ''),
|
|
);
|
|
},
|
|
),
|
|
|
|
// Payments Route
|
|
GoRoute(
|
|
path: RouteNames.payments,
|
|
name: RouteNames.payments,
|
|
pageBuilder: (context, state) =>
|
|
MaterialPage(key: state.pageKey, child: const PaymentsPage()),
|
|
),
|
|
|
|
// Payment Detail Route
|
|
GoRoute(
|
|
path: RouteNames.paymentDetail,
|
|
name: RouteNames.paymentDetail,
|
|
pageBuilder: (context, state) {
|
|
final invoiceId = state.pathParameters['id'];
|
|
return MaterialPage(
|
|
key: state.pageKey,
|
|
child: PaymentDetailPage(invoiceId: invoiceId ?? ''),
|
|
);
|
|
},
|
|
),
|
|
|
|
// Payment QR Route
|
|
GoRoute(
|
|
path: RouteNames.paymentQr,
|
|
name: RouteNames.paymentQr,
|
|
pageBuilder: (context, state) {
|
|
final orderId = state.uri.queryParameters['orderId'] ?? '';
|
|
final amountStr = state.uri.queryParameters['amount'] ?? '0';
|
|
final amount = double.tryParse(amountStr) ?? 0.0;
|
|
return MaterialPage(
|
|
key: state.pageKey,
|
|
child: PaymentQrPage(orderId: orderId, amount: amount),
|
|
);
|
|
},
|
|
),
|
|
|
|
// Quotes Route
|
|
GoRoute(
|
|
path: RouteNames.quotes,
|
|
name: RouteNames.quotes,
|
|
pageBuilder: (context, state) =>
|
|
MaterialPage(key: state.pageKey, child: const QuotesPage()),
|
|
),
|
|
|
|
// Price Policy Route
|
|
GoRoute(
|
|
path: RouteNames.pricePolicy,
|
|
name: RouteNames.pricePolicy,
|
|
pageBuilder: (context, state) =>
|
|
MaterialPage(key: state.pageKey, child: const PricePolicyPage()),
|
|
),
|
|
|
|
// News Route
|
|
GoRoute(
|
|
path: RouteNames.news,
|
|
name: RouteNames.news,
|
|
pageBuilder: (context, state) =>
|
|
MaterialPage(key: state.pageKey, child: const NewsListPage()),
|
|
),
|
|
|
|
// News Detail Route
|
|
GoRoute(
|
|
path: RouteNames.newsDetail,
|
|
name: RouteNames.newsDetail,
|
|
pageBuilder: (context, state) {
|
|
final articleId = state.pathParameters['id'];
|
|
return MaterialPage(
|
|
key: state.pageKey,
|
|
child: NewsDetailPage(articleId: articleId ?? ''),
|
|
);
|
|
},
|
|
),
|
|
|
|
// Profile Edit Route
|
|
GoRoute(
|
|
path: RouteNames.profile,
|
|
name: RouteNames.profile,
|
|
pageBuilder: (context, state) =>
|
|
MaterialPage(key: state.pageKey, child: const ProfileEditPage()),
|
|
),
|
|
|
|
// Addresses Route
|
|
GoRoute(
|
|
path: RouteNames.addresses,
|
|
name: RouteNames.addresses,
|
|
pageBuilder: (context, state) {
|
|
final extra = state.extra as Map<String, dynamic>?;
|
|
return MaterialPage(
|
|
key: state.pageKey,
|
|
child: AddressesPage(extra: extra),
|
|
);
|
|
},
|
|
),
|
|
|
|
// Address Form Route (Create/Edit)
|
|
GoRoute(
|
|
path: RouteNames.addressForm,
|
|
name: RouteNames.addressForm,
|
|
pageBuilder: (context, state) {
|
|
final address = state.extra as Address?;
|
|
return MaterialPage(
|
|
key: state.pageKey,
|
|
child: AddressFormPage(address: address),
|
|
);
|
|
},
|
|
),
|
|
|
|
// Change Password Route
|
|
GoRoute(
|
|
path: RouteNames.changePassword,
|
|
name: RouteNames.changePassword,
|
|
pageBuilder: (context, state) =>
|
|
MaterialPage(key: state.pageKey, child: const ChangePasswordPage()),
|
|
),
|
|
|
|
// Chat List Route
|
|
GoRoute(
|
|
path: RouteNames.chat,
|
|
name: RouteNames.chat,
|
|
pageBuilder: (context, state) =>
|
|
MaterialPage(key: state.pageKey, child: const ChatListPage()),
|
|
),
|
|
|
|
// Model Houses Route
|
|
GoRoute(
|
|
path: RouteNames.modelHouses,
|
|
name: RouteNames.modelHouses,
|
|
pageBuilder: (context, state) =>
|
|
MaterialPage(key: state.pageKey, child: const ModelHousesPage()),
|
|
),
|
|
|
|
// Design Request Create Route
|
|
GoRoute(
|
|
path: RouteNames.designRequestCreate,
|
|
name: RouteNames.designRequestCreate,
|
|
pageBuilder: (context, state) => MaterialPage(
|
|
key: state.pageKey,
|
|
child: const DesignRequestCreatePage(),
|
|
),
|
|
),
|
|
|
|
// Design Request Detail Route
|
|
GoRoute(
|
|
path: RouteNames.designRequestDetail,
|
|
name: RouteNames.designRequestDetail,
|
|
pageBuilder: (context, state) {
|
|
final requestId = state.pathParameters['id'];
|
|
return MaterialPage(
|
|
key: state.pageKey,
|
|
child: DesignRequestDetailPage(requestId: requestId ?? 'YC001'),
|
|
);
|
|
},
|
|
),
|
|
|
|
// TODO: Add more routes as features are implemented
|
|
],
|
|
|
|
// Error page for unknown routes
|
|
errorPageBuilder: (context, state) => MaterialPage(
|
|
key: state.pageKey,
|
|
child: Scaffold(
|
|
appBar: AppBar(title: const Text('Không tìm thấy trang')),
|
|
body: Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
const Icon(Icons.error_outline, size: 64, color: Colors.red),
|
|
const SizedBox(height: 16),
|
|
const Text(
|
|
'Trang không tồn tại',
|
|
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
state.uri.toString(),
|
|
style: const TextStyle(color: Colors.grey),
|
|
),
|
|
const SizedBox(height: 24),
|
|
ElevatedButton.icon(
|
|
onPressed: () => context.go(RouteNames.home),
|
|
icon: const Icon(Icons.home),
|
|
label: const Text('Về trang chủ'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
|
|
// Debug logging (disable in production)
|
|
debugLogDiagnostics: true,
|
|
);
|
|
});
|
|
|
|
/// Route Names
|
|
///
|
|
/// Centralized route name constants for type-safe navigation.
|
|
/// Use these constants instead of hardcoded strings.
|
|
///
|
|
/// Example:
|
|
/// ```dart
|
|
/// context.go(RouteNames.home);
|
|
/// context.push(RouteNames.products);
|
|
/// ```
|
|
class RouteNames {
|
|
// Private constructor to prevent instantiation
|
|
RouteNames._();
|
|
|
|
// Main Routes
|
|
static const String home = '/';
|
|
static const String products = '/products';
|
|
static const String productDetail = '/products/:id';
|
|
static const String writeReview = 'write-review';
|
|
static const String cart = '/cart';
|
|
static const String favorites = '/favorites';
|
|
static const String checkout = '/checkout';
|
|
static const String orderSuccess = '/order-success';
|
|
|
|
// Loyalty Routes
|
|
static const String loyalty = '/loyalty';
|
|
static const String rewards = '/loyalty/rewards';
|
|
static const String pointsHistory = '/loyalty/points-history';
|
|
static const String myGifts = '/loyalty/gifts';
|
|
static const String referral = '/loyalty/referral';
|
|
|
|
// Orders & Payments Routes
|
|
static const String orders = '/orders';
|
|
static const String orderDetail = '/orders/:id';
|
|
static const String payments = '/payments';
|
|
static const String paymentDetail = '/payments/:id';
|
|
static const String paymentQr = '/payment-qr';
|
|
|
|
// Projects & Quotes Routes
|
|
static const String projects = '/projects';
|
|
static const String projectDetail = '/projects/:id';
|
|
static const String projectCreate = '/projects/create';
|
|
static const String quotes = '/quotes';
|
|
static const String quoteDetail = '/quotes/:id';
|
|
static const String quoteCreate = '/quotes/create';
|
|
|
|
// Account Routes
|
|
static const String account = '/account';
|
|
static const String profile = '/account/profile';
|
|
static const String addresses = '/account/addresses';
|
|
static const String addressForm = '/account/addresses/form';
|
|
static const String changePassword = '/account/change-password';
|
|
static const String settings = '/account/settings';
|
|
|
|
// Promotions & Notifications Routes
|
|
static const String promotions = '/promotions';
|
|
static const String promotionDetail = '/promotions/:id';
|
|
static const String notifications = '/notifications';
|
|
|
|
// Price Policy Route
|
|
static const String pricePolicy = '/price-policy';
|
|
|
|
// News Route
|
|
static const String news = '/news';
|
|
static const String newsDetail = '/news/:id';
|
|
|
|
// Chat Route
|
|
static const String chat = '/chat';
|
|
|
|
// Model Houses & Design Requests Routes
|
|
static const String modelHouses = '/model-houses';
|
|
static const String designRequestCreate =
|
|
'/model-houses/design-request/create';
|
|
static const String designRequestDetail = '/model-houses/design-request/:id';
|
|
|
|
// Authentication Routes
|
|
static const String splash = '/splash';
|
|
static const String login = '/login';
|
|
static const String forgotPassword = '/forgot-password';
|
|
static const String otpVerification = '/otp-verification';
|
|
static const String register = '/register';
|
|
static const String businessUnitSelection = '/business-unit-selection';
|
|
}
|
|
|
|
/// Route Extensions
|
|
///
|
|
/// Helper extensions for common navigation patterns.
|
|
extension GoRouterExtension on BuildContext {
|
|
/// Navigate to login page
|
|
void goLogin() => go(RouteNames.login);
|
|
|
|
/// Navigate to register page
|
|
void goRegister() => go(RouteNames.register);
|
|
|
|
/// Navigate to home page
|
|
void goHome() => go(RouteNames.home);
|
|
|
|
/// Navigate to products page
|
|
void goProducts() => go(RouteNames.products);
|
|
|
|
/// Navigate to cart page
|
|
void goCart() => go(RouteNames.cart);
|
|
|
|
/// Navigate to favorites page
|
|
void goFavorites() => go(RouteNames.favorites);
|
|
|
|
/// Navigate to loyalty page
|
|
void goLoyalty() => go(RouteNames.loyalty);
|
|
|
|
/// Navigate to orders page
|
|
void goOrders() => go(RouteNames.orders);
|
|
|
|
/// Navigate to projects page
|
|
void goProjects() => go(RouteNames.projects);
|
|
|
|
/// Navigate to account page
|
|
void goAccount() => go(RouteNames.account);
|
|
|
|
/// Navigate to promotions page
|
|
void goPromotions() => go(RouteNames.promotions);
|
|
|
|
/// Navigate to notifications page
|
|
void goNotifications() => go(RouteNames.notifications);
|
|
|
|
/// Navigate to chat page
|
|
void goChat() => go(RouteNames.chat);
|
|
}
|