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/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/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.lock b/pubspec.lock index 70c03ee..386acf0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -125,10 +125,10 @@ packages: dependency: transitive description: name: characters - sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.0" checked_yaml: dependency: transitive description: @@ -269,10 +269,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" url: "https://pub.dev" source: hosted - version: "1.3.3" + version: "1.3.2" ffi: dependency: transitive description: @@ -572,26 +572,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec url: "https://pub.dev" source: hosted - version: "11.0.2" + version: "10.0.8" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 url: "https://pub.dev" source: hosted - version: "3.0.10" + version: "3.0.9" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.1" lints: dependency: transitive description: @@ -620,26 +620,26 @@ packages: dependency: transitive description: name: matcher - sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.18" + version: "0.12.17" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.13.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.16.0" mime: dependency: transitive description: @@ -961,10 +961,10 @@ packages: dependency: transitive description: name: test_api - sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd url: "https://pub.dev" source: hosted - version: "0.7.9" + version: "0.7.4" time: dependency: transitive description: @@ -1041,10 +1041,10 @@ packages: dependency: transitive description: name: vector_math - sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.1.4" vm_service: dependency: transitive description: @@ -1118,5 +1118,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.9.0-0 <4.0.0" + dart: ">=3.7.2 <4.0.0" flutter: ">=3.29.0"