diff --git a/assets/fonts/Roboto-Black.ttf b/assets/fonts/Roboto-Black.ttf new file mode 100644 index 0000000..71f01ac Binary files /dev/null and b/assets/fonts/Roboto-Black.ttf differ diff --git a/assets/fonts/Roboto-Bold.ttf b/assets/fonts/Roboto-Bold.ttf new file mode 100644 index 0000000..aaf374d Binary files /dev/null and b/assets/fonts/Roboto-Bold.ttf differ diff --git a/assets/fonts/Roboto-Light.ttf b/assets/fonts/Roboto-Light.ttf new file mode 100644 index 0000000..664e1b2 Binary files /dev/null and b/assets/fonts/Roboto-Light.ttf differ diff --git a/assets/fonts/Roboto-Medium.ttf b/assets/fonts/Roboto-Medium.ttf new file mode 100644 index 0000000..aa00de0 Binary files /dev/null and b/assets/fonts/Roboto-Medium.ttf differ diff --git a/assets/fonts/Roboto-Regular.ttf b/assets/fonts/Roboto-Regular.ttf new file mode 100644 index 0000000..3e6e2e7 Binary files /dev/null and b/assets/fonts/Roboto-Regular.ttf differ diff --git a/assets/fonts/Roboto-Thin.ttf b/assets/fonts/Roboto-Thin.ttf new file mode 100644 index 0000000..d262d14 Binary files /dev/null and b/assets/fonts/Roboto-Thin.ttf differ diff --git a/lib/core/config/dependencies.dart b/lib/core/config/dependencies.dart index d53bd99..3461b4b 100644 --- a/lib/core/config/dependencies.dart +++ b/lib/core/config/dependencies.dart @@ -41,7 +41,14 @@ List get essentialProviders { List get cores { return [ - Provider(create: (context) => NetworkManager(context.read()), lazy: true), + Provider( + create: (context) { + final nm = NetworkManager(context.read()); + + return nm; + }, + lazy: true, + ), ChangeNotifierProvider(create: (context) => ThemeProvider(), lazy: true), ChangeNotifierProvider( create: (context) => NotificationStateService(), diff --git a/lib/core/network/network_manager.dart b/lib/core/network/network_manager.dart index 60f56c2..4ebce31 100644 --- a/lib/core/network/network_manager.dart +++ b/lib/core/network/network_manager.dart @@ -17,6 +17,14 @@ class NetworkManager { bool _interceptorsAdded = false; final SharedPreferences _prefs; + /// Callback invoked when refresh token fails (expired, missing, or errored). + /// Should clear local auth state and navigate to the login screen. + /// // Callback to be called when refresh token fails (logout callback) + Future Function()? _onAuthFailed; + void setAuthFailed(Future Function() callback) { + _onAuthFailed = callback; + } + NetworkManager(this._prefs) : mainDio = Dio( BaseOptions( @@ -75,6 +83,10 @@ class NetworkManager { mainDio.options.headers['Authorization'] = 'Bearer $newAccessToken'; return handler.resolve(await mainDio.fetch(error.requestOptions)); } + + // Refresh token failed – force logout + appLogger.error('❌ Refresh token failed, triggering logout'); + await _onAuthFailed!(); } return handler.next(error); }, diff --git a/lib/core/providers/home_provider.dart b/lib/core/providers/home_provider.dart index 01a37d1..9d142c9 100644 --- a/lib/core/providers/home_provider.dart +++ b/lib/core/providers/home_provider.dart @@ -9,6 +9,7 @@ import '../../view_models/home_view_model.dart'; /// Wraps screens that need HomeViewModel Widget homeProvider({required Widget child}) { return ChangeNotifierProvider( + lazy: true, create: (context) => HomeViewModel( homeRepository: context.read(), diff --git a/lib/data/services/tenders_service.dart b/lib/data/services/tenders_service.dart index 1c1c668..703813b 100644 --- a/lib/data/services/tenders_service.dart +++ b/lib/data/services/tenders_service.dart @@ -37,6 +37,7 @@ class TendersService { method: 'GET', (json) => TendersResponse.fromJson(json), ); + print(result); return result; } diff --git a/lib/main.dart b/lib/main.dart index 8d928cd..551e1e1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -10,6 +10,7 @@ import 'package:provider/single_child_widget.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:tm_app/core/config/dependencies.dart'; import 'package:tm_app/core/config/web_url_strategy.dart'; +import 'package:tm_app/core/network/network_manager.dart'; import 'package:tm_app/core/services/cache_init.dart'; import 'package:tm_app/core/services/firebase_service.dart'; import 'package:tm_app/core/theme/theme_provider.dart'; @@ -45,6 +46,8 @@ void main() async { await FirebaseService().initializeApp(); + + runApp(MultiProvider(providers: providers, child: const MyApp())); } @@ -57,6 +60,24 @@ class MyApp extends StatefulWidget { class _MyAppState extends State { Future setupInteractedMessage() async { + final overlayCtx = rootNavigatorKey.currentContext; + + final networkManager = overlayCtx?.read(); + networkManager?.setAuthFailed(() async { + debugPrint('🔄 Refresh token failed - navigating to auth screen'); + // Use appRouter to navigate to auth screen + final SharedPreferences prefs = await SharedPreferences.getInstance(); + await prefs.clear(); + appRouter.go('/login'); + }); + // networkManager.onAuthFailure(() async { + // debugPrint('🔄 Refresh token failed - navigating to auth screen'); + // // Use appRouter to navigate to auth screen + // final SharedPreferences _prefs = await SharedPreferences.getInstance(); + // await _prefs.clear(); + // appRouter.go(AppRoutes.auth); + // }); + final initialMessage = await FirebaseMessaging.instance.getInitialMessage(); if (initialMessage != null) { @@ -65,7 +86,7 @@ class _MyAppState extends State { // if (kIsWeb) { FirebaseMessaging.onMessage.listen((event) { - final overlayCtx = rootNavigatorKey.currentContext; + if (overlayCtx != null) { if (overlayCtx.mounted) { @@ -88,6 +109,8 @@ class _MyAppState extends State { ); } + + @override void initState() { super.initState(); diff --git a/lib/view_models/auth_view_model.dart b/lib/view_models/auth_view_model.dart index e014e30..9b63ce7 100644 --- a/lib/view_models/auth_view_model.dart +++ b/lib/view_models/auth_view_model.dart @@ -24,8 +24,8 @@ class AuthViewModel with ChangeNotifier { bool _obscurePassword = true; bool get obscurePassword => _obscurePassword; - bool get isLoginEnabled => - usernameController.text.isNotEmpty && passwordController.text.isNotEmpty; + // bool get isLoginEnabled => + // usernameController.text.isNotEmpty && passwordController.text.isNotEmpty; // Auth state bool _isLoading = false; @@ -59,8 +59,22 @@ class AuthViewModel with ChangeNotifier { } } + // void _onTextChanged() { + // notifyListeners(); + // } + bool _isLoginEnabled = false; + + bool get isLoginEnabled => _isLoginEnabled; + void _onTextChanged() { - notifyListeners(); + final newValue = + usernameController.text.isNotEmpty && + passwordController.text.isNotEmpty; + + if (newValue != _isLoginEnabled) { + _isLoginEnabled = newValue; + notifyListeners(); + } } Future login() async { diff --git a/lib/view_models/home_view_model.dart b/lib/view_models/home_view_model.dart index d6940bf..a157e46 100644 --- a/lib/view_models/home_view_model.dart +++ b/lib/view_models/home_view_model.dart @@ -159,7 +159,7 @@ class HomeViewModel with ChangeNotifier { ); _currentPage++; - if (metadata != null && (_currentPage >= metadata.pages!)) { + if (metadata != null && (_currentPage >= (metadata.pages ?? 0))) { _hasMore = false; } } else { diff --git a/lib/views/home/pages/d_home_page.dart b/lib/views/home/pages/d_home_page.dart index 4a91180..12c5048 100644 --- a/lib/views/home/pages/d_home_page.dart +++ b/lib/views/home/pages/d_home_page.dart @@ -128,10 +128,11 @@ class DesktopHomePage extends StatelessWidget { iconPath: AssetsManager.arrows, title: HomeStrings.tenderSubmitted, amount: - homeViewModel - .tenderApprovalsStateResponse! - .data! - .submittedTenders + (homeViewModel + .tenderApprovalsStateResponse + ?.data + ?.submittedTenders ?? + 0) .toString(), textColor: AppColors.mainBlue, @@ -195,7 +196,7 @@ class DesktopHomePage extends StatelessWidget { Widget _yourTenderText(HomeViewModel homeViewModel) { final isYourTenders = - homeViewModel.data?.data?.tenders!.isNotEmpty ?? false; + (homeViewModel.data?.data?.tenders?.isNotEmpty ?? false); return ConstrainedBox( constraints: const BoxConstraints(maxWidth: 740), child: Align( @@ -244,7 +245,7 @@ class DesktopHomePage extends StatelessWidget { tender: homeViewModel.tenders[index], onTap: () { TenderDetailRouteData( - tenderId: homeViewModel.tenders[index].id!, + tenderId: homeViewModel.tenders[index].id ?? '', ).push(context); }, ); diff --git a/lib/views/home/pages/m_home_page.dart b/lib/views/home/pages/m_home_page.dart index f394768..c3ae3ad 100644 --- a/lib/views/home/pages/m_home_page.dart +++ b/lib/views/home/pages/m_home_page.dart @@ -105,7 +105,8 @@ class MobileHomePage extends StatelessWidget { iconPath: AssetsManager.arrows, title: HomeStrings.tenderSubmitted, amount: - tenderApprovalsStateResponse.data!.submittedTenders.toString(), + (tenderApprovalsStateResponse.data?.submittedTenders ?? 0) + .toString(), textColor: AppColors.mainBlue, enableTap: true, onTap: () { @@ -171,7 +172,7 @@ class MobileHomePage extends StatelessWidget { Widget _yourTenderText(HomeViewModel homeViewModel) { final isYourTenders = - homeViewModel.data?.data?.tenders!.isNotEmpty ?? false; + (homeViewModel.data?.data?.tenders?.isNotEmpty ?? false); return Padding( padding: EdgeInsets.symmetric(horizontal: 24.0.w()), child: Text( @@ -220,7 +221,7 @@ class MobileHomePage extends StatelessWidget { tender: homeViewModel.tenders[index], onTap: () { TenderDetailRouteData( - tenderId: homeViewModel.tenders[index].id!, + tenderId: homeViewModel.tenders[index].id ?? '', ).push(context); }, ); diff --git a/lib/views/home/pages/t_home_page.dart b/lib/views/home/pages/t_home_page.dart index 4f624e4..666ad91 100644 --- a/lib/views/home/pages/t_home_page.dart +++ b/lib/views/home/pages/t_home_page.dart @@ -124,7 +124,8 @@ class TabletHomePage extends StatelessWidget { iconPath: AssetsManager.arrows, title: HomeStrings.tenderSubmitted, amount: - tenderApprovalsStateResponse.data!.submittedTenders.toString(), + (tenderApprovalsStateResponse.data?.submittedTenders ?? 0) + .toString(), textColor: AppColors.mainBlue, enableTap: true, @@ -201,7 +202,7 @@ class TabletHomePage extends StatelessWidget { Widget _yourTenderText(HomeViewModel homeViewModel) { final isYourTenders = - homeViewModel.data?.data?.tenders!.isNotEmpty ?? false; + (homeViewModel.data?.data?.tenders?.isNotEmpty ?? false); return Text( isYourTenders ? HomeStrings.yourTenders : HomeStrings.recommendedTenders, style: TextStyle( @@ -241,7 +242,7 @@ class TabletHomePage extends StatelessWidget { tender: homeViewModel.tenders[index], onTap: () { TenderDetailRouteData( - tenderId: homeViewModel.tenders[index].id!, + tenderId: homeViewModel.tenders[index].id ?? '', ).push(context); }, ); diff --git a/lib/views/home/tenders_list_item.dart b/lib/views/home/tenders_list_item.dart index 9e152cc..fd708dd 100644 --- a/lib/views/home/tenders_list_item.dart +++ b/lib/views/home/tenders_list_item.dart @@ -34,7 +34,7 @@ class TendersListItem extends StatelessWidget { SizedBox( width: double.infinity, child: Text( - timeConvertor(tender.publicationDate!), + timeConvertor(tender.publicationDate ?? 0), style: TextStyle( fontSize: 12.0.sp(), fontWeight: FontWeight.w400, @@ -64,7 +64,7 @@ class TendersListItem extends StatelessWidget { ), ), Text( - timeConvertor(tender.tenderDeadline!), + timeConvertor(tender.tenderDeadline ?? 0), style: TextStyle( fontSize: 14.0.sp(), fontWeight: FontWeight.w500, @@ -76,7 +76,7 @@ class TendersListItem extends StatelessWidget { ), SizedBox(height: 12.0.h()), Text( - tender.title!, + tender.title ?? '', style: TextStyle( overflow: TextOverflow.ellipsis, fontSize: 16.0.sp(), @@ -88,7 +88,7 @@ class TendersListItem extends StatelessWidget { SizedBox( width: double.infinity, child: Text( - tender.description!, + tender.description ?? '', textAlign: TextAlign.start, maxLines: 4, overflow: TextOverflow.ellipsis, @@ -110,7 +110,7 @@ class TendersListItem extends StatelessWidget { SvgPicture.asset(AssetsManager.location), SizedBox(width: 1.0.w()), Text( - tender.countryCode!, + tender.countryCode ?? '', style: TextStyle( fontSize: 12.0.sp(), fontWeight: FontWeight.w400, diff --git a/lib/views/liked_tenders/pages/liked_tenders_desktop_page.dart b/lib/views/liked_tenders/pages/liked_tenders_desktop_page.dart index 5000df6..1308a18 100644 --- a/lib/views/liked_tenders/pages/liked_tenders_desktop_page.dart +++ b/lib/views/liked_tenders/pages/liked_tenders_desktop_page.dart @@ -9,6 +9,7 @@ import 'package:tm_app/view_models/liked_tenders_view_model.dart'; import 'package:tm_app/views/liked_tenders/widgets/liked_tenders_list_item.dart'; import 'package:tm_app/views/shared/page_selection_dialog.dart'; +import '../../../core/routes/app_routes.dart'; import '../../../core/utils/app_toast.dart'; import '../../shared/desktop_navigation_widget.dart'; import '../../shared/tablet_desktop_appbar.dart'; @@ -175,7 +176,13 @@ class _LikedTendersDesktopPageState extends State { ), ), SizedBox(width: 5.0.w()), - SvgPicture.asset(AssetsManager.arrowDownSmall,colorFilter: ColorFilter.mode(AppColors.grey60, BlendMode.srcIn),), + SvgPicture.asset( + AssetsManager.arrowDownSmall, + colorFilter: ColorFilter.mode( + AppColors.grey60, + BlendMode.srcIn, + ), + ), SizedBox(width: 5.0.w()), ], ), @@ -230,7 +237,13 @@ class _LikedTendersDesktopPageState extends State { viewModel.removeTenderById(item.tenderId!); } }, - child: LikedListItem(tender: item.tender!, isDesktop: true), + child: LikedListItem( + tender: item.tender!, + isDesktop: true, + onTap: () { + TenderDetailRouteData(tenderId: item.tenderId!).push(context); + }, + ), ), ); }, diff --git a/lib/views/liked_tenders/pages/liked_tenders_mobile_page.dart b/lib/views/liked_tenders/pages/liked_tenders_mobile_page.dart index 83cc82d..c34cf7d 100644 --- a/lib/views/liked_tenders/pages/liked_tenders_mobile_page.dart +++ b/lib/views/liked_tenders/pages/liked_tenders_mobile_page.dart @@ -10,6 +10,8 @@ import 'package:tm_app/views/liked_tenders/strings/liked_tenders_strings.dart'; import 'package:tm_app/views/liked_tenders/widgets/liked_tenders_list_item.dart'; import 'package:tm_app/views/login/widgets/widgets.dart'; +import '../../../core/routes/app_routes.dart'; + class LikedTendersMobilePage extends StatefulWidget { const LikedTendersMobilePage({super.key}); @@ -209,7 +211,14 @@ class _LikedTendersMobilePageState extends State { viewModel.removeTenderById(item.tenderId!); } }, - child: LikedListItem(tender: item.tender!), + child: LikedListItem( + tender: item.tender!, + onTap: () { + TenderDetailRouteData( + tenderId: item.tenderId!, + ).push(context); + }, + ), ), ); } else { diff --git a/lib/views/liked_tenders/pages/liked_tenders_tablet_page.dart b/lib/views/liked_tenders/pages/liked_tenders_tablet_page.dart index c66e011..5c1c793 100644 --- a/lib/views/liked_tenders/pages/liked_tenders_tablet_page.dart +++ b/lib/views/liked_tenders/pages/liked_tenders_tablet_page.dart @@ -11,6 +11,7 @@ import 'package:tm_app/views/liked_tenders/widgets/liked_tenders_list_item.dart' import 'package:tm_app/views/shared/page_selection_dialog.dart'; import 'package:tm_app/views/shared/tender_app_bar.dart'; +import '../../../core/routes/app_routes.dart'; import '../../../core/utils/app_toast.dart'; import '../../shared/tablet_desktop_appbar.dart'; import '../../shared/tablet_navigation_widget.dart'; @@ -256,7 +257,12 @@ class _LikedTendersTabletPageState extends State { viewModel.removeTenderById(item.tenderId!); } }, - child: LikedListItem(tender: item.tender!), + child: LikedListItem( + tender: item.tender!, + onTap: () { + TenderDetailRouteData(tenderId: item.tenderId!).push(context); + }, + ), ), ); }, diff --git a/lib/views/liked_tenders/widgets/liked_tenders_list_item.dart b/lib/views/liked_tenders/widgets/liked_tenders_list_item.dart index f2da7d4..26c0d07 100644 --- a/lib/views/liked_tenders/widgets/liked_tenders_list_item.dart +++ b/lib/views/liked_tenders/widgets/liked_tenders_list_item.dart @@ -14,161 +14,169 @@ import '../../shared/flag.dart'; class LikedListItem extends StatelessWidget { const LikedListItem({ required this.tender, + required this.onTap, this.isDesktop = false, super.key, }); final TenderData tender; final bool isDesktop; + final VoidCallback onTap; @override Widget build(BuildContext context) { final viewModel = context.read(); - return Container( - decoration: BoxDecoration( - color: AppColors.cardBackground, - borderRadius: BorderRadius.circular(4), - border: Border.all(color: AppColors.grey30), - ), - width: double.infinity, - height: 250.0.h(), - child: Padding( - padding: EdgeInsets.symmetric(horizontal: 16.0.w(), vertical: 16.0.h()), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 10.0.h()), - isDesktop - ? Row( - children: [ - Expanded( - child: Container( - padding: EdgeInsets.symmetric( - horizontal: 10.0.w(), - vertical: 5.0.h(), - ), - decoration: BoxDecoration( - color: AppColors.primary20, - borderRadius: BorderRadius.circular(4), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - '${HomeStrings.tenderDeadline} :', - style: TextStyle( - fontSize: 15.0.sp(), - fontWeight: FontWeight.w500, - color: AppColors.textBlue, - ), - ), - Text( - timeConvertor(tender.tenderDeadline!), - style: TextStyle( - fontSize: 14.0.sp(), - fontWeight: FontWeight.w500, - color: AppColors.grey80, - ), - ), - ], - ), - ), - ), - SizedBox(width: 8.0.w()), - InkWell( - child: SvgPicture.asset( - AssetsManager.trash, - width: 32.0.w(), - height: 32.0.h(), - ), - onTap: () { - if (tender.id != null) { - viewModel.removeTenderById(tender.id!); - } - }, - ), - ], - ) - : Container( - padding: EdgeInsets.symmetric( - horizontal: 10.0.w(), - vertical: 5.0.h(), - ), - decoration: BoxDecoration( - color: AppColors.primary20, - borderRadius: BorderRadius.circular(4), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + return InkWell( + onTap: onTap, + child: Container( + decoration: BoxDecoration( + color: AppColors.cardBackground, + borderRadius: BorderRadius.circular(4), + border: Border.all(color: AppColors.grey30), + ), + width: double.infinity, + height: 250.0.h(), + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: 16.0.w(), + vertical: 16.0.h(), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 10.0.h()), + isDesktop + ? Row( children: [ - Text( - '${HomeStrings.tenderDeadline} :', - style: TextStyle( - fontSize: 15.0.sp(), - fontWeight: FontWeight.w500, - color: AppColors.textBlue, + Expanded( + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 10.0.w(), + vertical: 5.0.h(), + ), + decoration: BoxDecoration( + color: AppColors.primary20, + borderRadius: BorderRadius.circular(4), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '${HomeStrings.tenderDeadline} :', + style: TextStyle( + fontSize: 15.0.sp(), + fontWeight: FontWeight.w500, + color: AppColors.textBlue, + ), + ), + Text( + timeConvertor(tender.tenderDeadline!), + style: TextStyle( + fontSize: 14.0.sp(), + fontWeight: FontWeight.w500, + color: AppColors.grey80, + ), + ), + ], + ), ), ), - Text( - timeConvertor(tender.tenderDeadline!), - style: TextStyle( - fontSize: 14.0.sp(), - fontWeight: FontWeight.w500, - color: AppColors.grey80, + SizedBox(width: 8.0.w()), + InkWell( + child: SvgPicture.asset( + AssetsManager.trash, + width: 32.0.w(), + height: 32.0.h(), ), + onTap: () { + if (tender.id != null) { + viewModel.removeTenderById(tender.id!); + } + }, ), ], + ) + : Container( + padding: EdgeInsets.symmetric( + horizontal: 10.0.w(), + vertical: 5.0.h(), + ), + decoration: BoxDecoration( + color: AppColors.primary20, + borderRadius: BorderRadius.circular(4), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '${HomeStrings.tenderDeadline} :', + style: TextStyle( + fontSize: 15.0.sp(), + fontWeight: FontWeight.w500, + color: AppColors.textBlue, + ), + ), + Text( + timeConvertor(tender.tenderDeadline!), + style: TextStyle( + fontSize: 14.0.sp(), + fontWeight: FontWeight.w500, + color: AppColors.grey80, + ), + ), + ], + ), ), - ), - SizedBox(height: 12.0.h()), - Text( - tender.title!, - style: TextStyle( - overflow: TextOverflow.ellipsis, - fontSize: 16.0.sp(), - fontWeight: FontWeight.w600, - color: AppColors.grey80, - ), - ), - SizedBox(height: 8.0.h()), - SizedBox( - width: double.infinity, - child: Text( - tender.description!, - textAlign: TextAlign.left, - maxLines: 4, - overflow: TextOverflow.ellipsis, + SizedBox(height: 12.0.h()), + Text( + tender.title!, style: TextStyle( - fontSize: 14.0.sp(), - fontWeight: FontWeight.w400, - color: AppColors.grey70, + overflow: TextOverflow.ellipsis, + fontSize: 16.0.sp(), + fontWeight: FontWeight.w600, + color: AppColors.grey80, ), ), - ), - const Spacer(), - - Row( - children: [ - SvgPicture.asset(AssetsManager.location), - SizedBox(width: 1.0.w()), - Text( - tender.countryCode!, + SizedBox(height: 8.0.h()), + SizedBox( + width: double.infinity, + child: Text( + tender.description!, + textAlign: TextAlign.left, + maxLines: 4, + overflow: TextOverflow.ellipsis, style: TextStyle( - fontSize: 12.0.sp(), + fontSize: 14.0.sp(), fontWeight: FontWeight.w400, - color: AppColors.grey80, + color: AppColors.grey70, ), ), - SizedBox(width: 8.0.w()), - tender.countryCode != null - ? Flag(countryCode: tender.countryCode!) - : const Flag(countryCode: ''), - const Spacer(), + ), + const Spacer(), - SizedBox(width: 30.0.w()), - ], - ), - ], + Row( + children: [ + SvgPicture.asset(AssetsManager.location), + SizedBox(width: 1.0.w()), + Text( + tender.countryCode!, + style: TextStyle( + fontSize: 12.0.sp(), + fontWeight: FontWeight.w400, + color: AppColors.grey80, + ), + ), + SizedBox(width: 8.0.w()), + tender.countryCode != null + ? Flag(countryCode: tender.countryCode!) + : const Flag(countryCode: ''), + const Spacer(), + + SizedBox(width: 30.0.w()), + ], + ), + ], + ), ), ), ); diff --git a/lib/views/login/pages/login_desktop_page.dart b/lib/views/login/pages/login_desktop_page.dart index df008d6..1f43fbb 100644 --- a/lib/views/login/pages/login_desktop_page.dart +++ b/lib/views/login/pages/login_desktop_page.dart @@ -56,76 +56,89 @@ class _LoginDesktopPageState extends State { child: Center( child: SizedBox( width: 364, - child: Consumer( - builder: (context, viewModel, _) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - SvgPicture.asset( - AssetsManager.logoBig, - width: 320, - height: 50, - fit: BoxFit.cover, - ), - SizedBox(height: 72.0.h()), - const LoginTitle(), - SizedBox(height: 32.0.h()), - LoginTextField( - controller: viewModel.usernameController, - focusNode: viewModel.usernameFocus, - label: LoginStrings.usernameLabel, - iconPath: AssetsManager.userIcon, - fieldHeight: 60.0.h(), - borderRedus: 12, - prefixIconPadding: 7.0.w(), - ), - SizedBox(height: 16.0.h()), - LoginTextField( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SvgPicture.asset( + AssetsManager.logoBig, + width: 320, + height: 50, + fit: BoxFit.cover, + ), + SizedBox(height: 72.0.h()), + const LoginTitle(), + SizedBox(height: 32.0.h()), + LoginTextField( + controller: viewModel.usernameController, + focusNode: viewModel.usernameFocus, + label: LoginStrings.usernameLabel, + iconPath: AssetsManager.userIcon, + fieldHeight: 60.0.h(), + borderRedus: 12, + prefixIconPadding: 7.0.w(), + ), + SizedBox(height: 16.0.h()), + Selector( + selector: (_, vm) => vm.obscurePassword, + builder: (_, obscurePassword, __) { + return LoginTextField( controller: viewModel.passwordController, focusNode: viewModel.passwordFocus, label: LoginStrings.passwordLabel, iconPath: AssetsManager.passwordIcon, - prefixIconPadding: 7.0.w(), isPassword: true, - obscureText: viewModel.obscurePassword, - fieldHeight: 60.0.h(), + obscureText: obscurePassword, onToggleVisibility: viewModel.togglePasswordVisibility, - borderRedus: 12, - ), - SizedBox(height: 32.0.h()), - if (viewModel.isLoading) - const BaseButton( - isEnabled: false, - onPressed: null, - isLoading: true, - ) - else - BaseButton( - isEnabled: viewModel.isLoginEnabled, - onPressed: () async { - if (viewModel.isLoginEnabled) { - await viewModel.login(); - } - }, - ), - const SizedBox(height: 32.0), - TextButton( - onPressed: () { - const ForgotPasswordRouteData().push(context); + ); + }, + ), + SizedBox(height: 32.0.h()), + + /// ✅ Only this part rebuilds now + Selector( + selector: (_, vm) => vm.isLoading, + builder: (_, isLoading, __) { + return Selector( + selector: (_, vm) => vm.isLoginEnabled, + builder: (_, isEnabled, __) { + if (isLoading) { + return BaseButton( + isEnabled: false, + onPressed: null, + isLoading: true, + textColor: AppColors.white, + ); + } + + return BaseButton( + isEnabled: isEnabled, + onPressed: + isEnabled + ? () async { + await viewModel.login(); + } + : null, + ); }, - child: Text( - LoginStrings.forgotPassword, - style: TextStyle( - fontSize: 14.0.sp(), - fontWeight: FontWeight.w500, - color: AppColors.mainBlue, - ), - ), + ); + }, + ), + const SizedBox(height: 32.0), + TextButton( + onPressed: () { + const ForgotPasswordRouteData().push(context); + }, + child: Text( + LoginStrings.forgotPassword, + style: TextStyle( + fontSize: 14.0.sp(), + fontWeight: FontWeight.w500, + color: AppColors.mainBlue, ), - ], - ); - }, + ), + ), + ], ), ), ), diff --git a/lib/views/login/pages/login_mobile_page.dart b/lib/views/login/pages/login_mobile_page.dart index 134e2ce..971cf87 100644 --- a/lib/views/login/pages/login_mobile_page.dart +++ b/lib/views/login/pages/login_mobile_page.dart @@ -50,76 +50,90 @@ class _LoginMobilePageState extends State { child: Center( child: SingleChildScrollView( padding: EdgeInsets.symmetric(horizontal: 24.0.w()), - child: Consumer( - builder: (context, viewModel, _) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - // LoginLogo(width: 190.0.w(), height: 88.0.h()), - Padding( - padding: EdgeInsets.symmetric(horizontal: 22.0.w()), - child: SvgPicture.asset( - AssetsManager.logoBig, - width: double.infinity, - height: 50.0.h(), - ), - ), - SizedBox(height: 32.0.h()), - const LoginTitle(), - SizedBox(height: 32.0.h()), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // LoginLogo(width: 190.0.w(), height: 88.0.h()), + Padding( + padding: EdgeInsets.symmetric(horizontal: 22.0.w()), + child: SvgPicture.asset( + AssetsManager.logoBig, + width: double.infinity, + height: 50.0.h(), + ), + ), + SizedBox(height: 32.0.h()), + const LoginTitle(), + SizedBox(height: 32.0.h()), - LoginTextField( - controller: viewModel.usernameController, - focusNode: viewModel.usernameFocus, - label: LoginStrings.usernameLabel, - iconPath: AssetsManager.userIcon, - ), - SizedBox(height: 16.0.h()), + LoginTextField( + controller: viewModel.usernameController, + focusNode: viewModel.usernameFocus, + label: LoginStrings.usernameLabel, + iconPath: AssetsManager.userIcon, + ), + SizedBox(height: 16.0.h()), - LoginTextField( + Selector( + selector: (_, vm) => vm.obscurePassword, + builder: (_, obscurePassword, __) { + return LoginTextField( controller: viewModel.passwordController, focusNode: viewModel.passwordFocus, label: LoginStrings.passwordLabel, iconPath: AssetsManager.passwordIcon, isPassword: true, - obscureText: viewModel.obscurePassword, + obscureText: obscurePassword, onToggleVisibility: viewModel.togglePasswordVisibility, - ), - SizedBox(height: 32.0.h()), + ); + }, + ), + SizedBox(height: 32.0.h()), - if (viewModel.isLoading) - BaseButton( - isEnabled: false, - onPressed: null, - isLoading: true, - textColor: AppColors.white, - ) - else - BaseButton( - isEnabled: viewModel.isLoginEnabled, - onPressed: () async { - if (viewModel.isLoginEnabled) { - await viewModel.login(); - } - }, - ), - SizedBox(height: 32.0.h()), - TextButton( - onPressed: () { - const ForgotPasswordRouteData().push(context); + /// ✅ Only this part rebuilds now + Selector( + selector: (_, vm) => vm.isLoading, + builder: (_, isLoading, __) { + return Selector( + selector: (_, vm) => vm.isLoginEnabled, + builder: (_, isEnabled, __) { + if (isLoading) { + return BaseButton( + isEnabled: false, + onPressed: null, + isLoading: true, + textColor: AppColors.white, + ); + } + + return BaseButton( + isEnabled: isEnabled, + onPressed: + isEnabled + ? () async { + await viewModel.login(); + } + : null, + ); }, - child: Text( - LoginStrings.forgotPassword, - style: TextStyle( - fontSize: 14.0.sp(), - fontWeight: FontWeight.w500, - color: AppColors.mainBlue, - ), - ), + ); + }, + ), + SizedBox(height: 32.0.h()), + TextButton( + onPressed: () { + const ForgotPasswordRouteData().push(context); + }, + child: Text( + LoginStrings.forgotPassword, + style: TextStyle( + fontSize: 14.0.sp(), + fontWeight: FontWeight.w500, + color: AppColors.mainBlue, ), - ], - ); - }, + ), + ), + ], ), ), ), diff --git a/lib/views/login/pages/login_tablet_page.dart b/lib/views/login/pages/login_tablet_page.dart index 82f824f..445bcbe 100644 --- a/lib/views/login/pages/login_tablet_page.dart +++ b/lib/views/login/pages/login_tablet_page.dart @@ -52,78 +52,90 @@ class _LoginTabletPageState extends State { padding: EdgeInsets.symmetric(horizontal: 24.0.w()), child: SizedBox( width: 364.0.w(), - child: Consumer( - builder: (context, viewModel, _) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: EdgeInsets.symmetric(horizontal: 22.0.w()), - child: SvgPicture.asset( - AssetsManager.logoBig, - width: double.infinity, - height: 50.0.h(), - ), - ), - SizedBox(height: 72.0.h()), - const LoginTitle(), - SizedBox(height: 40.0.h()), - LoginTextField( - controller: viewModel.usernameController, - focusNode: viewModel.usernameFocus, - label: LoginStrings.usernameLabel, - iconPath: AssetsManager.userIcon, - fieldHeight: 60.0.h(), - borderRedus: 12, - prefixIconPadding: 4.0.w(), - ), - SizedBox(height: 24.0.h()), - LoginTextField( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 22.0.w()), + child: SvgPicture.asset( + AssetsManager.logoBig, + width: double.infinity, + height: 50.0.h(), + ), + ), + SizedBox(height: 72.0.h()), + const LoginTitle(), + SizedBox(height: 40.0.h()), + LoginTextField( + controller: viewModel.usernameController, + focusNode: viewModel.usernameFocus, + label: LoginStrings.usernameLabel, + iconPath: AssetsManager.userIcon, + fieldHeight: 60.0.h(), + borderRedus: 12, + prefixIconPadding: 4.0.w(), + ), + SizedBox(height: 24.0.h()), + Selector( + selector: (_, vm) => vm.obscurePassword, + builder: (_, obscurePassword, __) { + return LoginTextField( controller: viewModel.passwordController, focusNode: viewModel.passwordFocus, label: LoginStrings.passwordLabel, iconPath: AssetsManager.passwordIcon, - prefixIconPadding: 4.0.w(), isPassword: true, - obscureText: viewModel.obscurePassword, - fieldHeight: 60.0.h(), + obscureText: obscurePassword, onToggleVisibility: viewModel.togglePasswordVisibility, - borderRedus: 12, - ), - SizedBox(height: 70.0.h()), - if (viewModel.isLoading) - const BaseButton( - isEnabled: false, - onPressed: null, - isLoading: true, - ) - else - BaseButton( - isEnabled: viewModel.isLoginEnabled, - onPressed: - viewModel.isLoginEnabled - ? () async { - await viewModel.login(); - } - : null, - ), - SizedBox(height: 30.0.h()), - TextButton( - onPressed: () { - const ForgotPasswordRouteData().push(context); + ); + }, + ), + SizedBox(height: 70.0.h()), + + /// ✅ Only this part rebuilds now + Selector( + selector: (_, vm) => vm.isLoading, + builder: (_, isLoading, __) { + return Selector( + selector: (_, vm) => vm.isLoginEnabled, + builder: (_, isEnabled, __) { + if (isLoading) { + return BaseButton( + isEnabled: false, + onPressed: null, + isLoading: true, + textColor: AppColors.white, + ); + } + + return BaseButton( + isEnabled: isEnabled, + onPressed: + isEnabled + ? () async { + await viewModel.login(); + } + : null, + ); }, - child: Text( - LoginStrings.forgotPassword, - style: TextStyle( - fontSize: 14.0.sp(), - fontWeight: FontWeight.w500, - color: AppColors.mainBlue, - ), - ), + ); + }, + ), + SizedBox(height: 30.0.h()), + TextButton( + onPressed: () { + const ForgotPasswordRouteData().push(context); + }, + child: Text( + LoginStrings.forgotPassword, + style: TextStyle( + fontSize: 14.0.sp(), + fontWeight: FontWeight.w500, + color: AppColors.mainBlue, ), - ], - ); - }, + ), + ), + ], ), ), ), diff --git a/pubspec.yaml b/pubspec.yaml index bc878f5..0728f45 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -58,18 +58,19 @@ flutter: - assets/icons/ - assets/svgs/ - assets/pngs/ + - assets/fonts/ - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.dev/to/font-from-package + fonts: + - family: Roboto + fonts: + - asset: assets/fonts/Roboto-Black.ttf + weight: 900 + - asset: assets/fonts/Roboto-Bold.ttf + weight: 700 + - asset: assets/fonts/Roboto-Medium.ttf + weight: 500 + - asset: assets/fonts/Roboto-Regular.ttf + weight: 400 + - asset: assets/fonts/Roboto-Light.ttf + weight: 300 + - asset: assets/fonts/Roboto-Thin.ttf diff --git a/web/flutter_bootstrap.js b/web/flutter_bootstrap.js new file mode 100644 index 0000000..dd050c5 --- /dev/null +++ b/web/flutter_bootstrap.js @@ -0,0 +1,24 @@ +(() => { + var P = () => navigator.vendor === "Google Inc." || navigator.agent === "Edg/", E = () => typeof ImageDecoder > "u" ? !1 : P(), L = () => typeof Intl.v8BreakIterator < "u" && typeof Intl.Segmenter < "u", W = () => { let n = [0, 97, 115, 109, 1, 0, 0, 0, 1, 5, 1, 95, 1, 120, 0]; return WebAssembly.validate(new Uint8Array(n)) }, w = { hasImageCodecs: E(), hasChromiumBreakIterators: L(), supportsWasmGC: W(), crossOriginIsolated: window.crossOriginIsolated }; function l(...n) { return new URL(C(...n), document.baseURI).toString() } function C(...n) { return n.filter(t => !!t).map((t, i) => i === 0 ? _(t) : j(_(t))).filter(t => t.length).join("/") } function j(n) { let t = 0; for (; t < n.length && n.charAt(t) === "/";)t++; return n.substring(t) } function _(n) { let t = n.length; for (; t > 0 && n.charAt(t - 1) === "/";)t--; return n.substring(0, t) } function T(n, t) { return n.canvasKitBaseUrl ? n.canvasKitBaseUrl : t.engineRevision && !t.useLocalCanvasKit ? C("https://www.gstatic.com/flutter-canvaskit", t.engineRevision) : "canvaskit" } var v = class { constructor() { this._scriptLoaded = !1 } setTrustedTypesPolicy(t) { this._ttPolicy = t } async loadEntrypoint(t) { let { entrypointUrl: i = l("main.dart.js"), onEntrypointLoaded: r, nonce: e } = t || {}; return this._loadJSEntrypoint(i, r, e) } async load(t, i, r, e, a) { a ??= o => { o.initializeEngine(r).then(c => c.runApp()) }; let { entryPointBaseUrl: s } = r; if (t.compileTarget === "dart2wasm") return this._loadWasmEntrypoint(t, i, s, a); { let o = t.mainJsPath ?? "main.dart.js", c = l(s, o); return this._loadJSEntrypoint(c, a, e) } } didCreateEngineInitializer(t) { typeof this._didCreateEngineInitializerResolve == "function" && (this._didCreateEngineInitializerResolve(t), this._didCreateEngineInitializerResolve = null, delete _flutter.loader.didCreateEngineInitializer), typeof this._onEntrypointLoaded == "function" && this._onEntrypointLoaded(t) } _loadJSEntrypoint(t, i, r) { let e = typeof i == "function"; if (!this._scriptLoaded) { this._scriptLoaded = !0; let a = this._createScriptTag(t, r); if (e) console.debug("Injecting