Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
4aa8159
Ajout du package et des fichiers concernant la page login
Shafaatul-Islam Jan 25, 2026
47be59e
Ajout des variables dans router pour login
Shafaatul-Islam Jan 25, 2026
63275e8
premier visuel complete pour login
Shafaatul-Islam Feb 1, 2026
50ee34f
meilleur respect de l'issue, vue refaite
Shafaatul-Islam Feb 7, 2026
e6b875d
Page login complete
Shafaatul-Islam Feb 16, 2026
868ea8b
Page login complete
Shafaatul-Islam Feb 16, 2026
98ace77
Merge branch 'master' into feature/1237-login-page-that-redirects-to-…
Shafaatul-Islam Feb 16, 2026
9f4f796
version anglais/francais
Shafaatul-Islam Feb 27, 2026
a36967d
ajout dans localization en et fr
Shafaatul-Islam Feb 27, 2026
7c01397
Merge branch 'master' into feature/1237-login-page-that-redirects-to-…
Shafaatul-Islam Feb 27, 2026
b5ad9ae
changement du texte de la version anglaise
Shafaatul-Islam Feb 27, 2026
88268c7
tests + changement nom variable + enlevement code inutile
Shafaatul-Islam Feb 27, 2026
f15d4fb
[BOT] Applying version.
Shafaatul-Islam Feb 27, 2026
1839528
pubspec new version
Shafaatul-Islam Mar 7, 2026
6aac832
pull 5.9.2
Shafaatul-Islam Mar 7, 2026
1dc3194
changement du logo dans la page de login
Shafaatul-Islam Mar 7, 2026
848b331
[BOT] Applying format.
Shafaatul-Islam Mar 7, 2026
3cbcc59
ajout des icones aux boutons
Shafaatul-Islam Mar 7, 2026
cd98379
Merge branch 'feature/1237-login-page-that-redirects-to-the-browser' …
Shafaatul-Islam Mar 7, 2026
7db1803
ajout des icones aux boutons
Shafaatul-Islam Mar 7, 2026
84e9925
Merge branch 'master' into feature/1237-login-page-that-redirects-to-…
Shafaatul-Islam Mar 13, 2026
11f55f9
fix workflow + debut fix test
Shafaatul-Islam Mar 31, 2026
fad1eed
mouvement des tests de startup a login
Shafaatul-Islam Apr 4, 2026
525f249
[BOT] Applying format.
Shafaatul-Islam Apr 4, 2026
a851352
ajout du gradient et espacement entre les boutons
Shafaatul-Islam Apr 8, 2026
c734f72
Merge branch 'feature/1237-login-page-that-redirects-to-the-browser' …
Shafaatul-Islam Apr 8, 2026
d38495d
[BOT] Applying format.
Shafaatul-Islam Apr 8, 2026
24a32ff
merge avec V5
Shafaatul-Islam Apr 27, 2026
70e5b87
deletion of the linux and macos folders+changes after mergin with V5+…
Shafaatul-Islam Apr 27, 2026
2a84bf3
[BOT] Applying format.
Shafaatul-Islam Apr 27, 2026
9e81d15
removing useless imports in router.dart
Shafaatul-Islam Apr 27, 2026
245a309
indentation in login_viewmodel and login_viewmodel_test
Shafaatul-Islam Apr 27, 2026
94b29c3
Merge branch 'feature/1237-login-page-that-redirects-to-the-browser' …
Shafaatul-Islam Apr 27, 2026
3f0f79d
deletion of redundant constants in router_paths + cleaner loading man…
Shafaatul-Islam Apr 28, 2026
8fda8c2
correct exception pattern that does not catch locally in login_viewmodel
Shafaatul-Islam May 5, 2026
c8361fd
change in login to better suit dark mode + name change of variable lo…
Shafaatul-Islam May 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/domain/constants/router_paths.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
class RouterPaths {
static const String startup = "/startup";
static const String faq = "/faq";
static const String login = "/login";
static const String root = "/root";
static const String defaultSchedule = "/schedule/default";
static const String gradeDetails = "/student/grade/details";
Expand Down
2 changes: 2 additions & 0 deletions lib/l10n/intl_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"close_button_text": "Close",

"startup_viewmodel_acquire_token_fail": "Connection failed after multiple attempts. Please try again.",
"login_viewmodel_acquire_token_fail": "Connection failed due to cancellation. Please try again.",

"title_schedule": "Schedule",
"title_student": "Student",
Expand All @@ -57,6 +58,7 @@
"title_ets_mobile": "ÉTSMobile",

"login_title": "Login",
"login_startup_title": "Please sign in to continue",
"login_prompt_universal_code": "Universal Code",
"login_prompt_password": "Password",
"login_action_sign_in": "Sign in",
Expand Down
2 changes: 2 additions & 0 deletions lib/l10n/intl_fr.arb
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"close_button_text": "Fermer",

"startup_viewmodel_acquire_token_fail": "Échec de la connexion après plusieurs tentatives. Veuillez réessayer.",
"login_viewmodel_acquire_token_fail": "Échec de la connexion à cause de l'annulation. Veuillez réessayer.",

"title_schedule": "Horaire",
"title_student": "Étudiant",
Expand All @@ -56,6 +57,7 @@
"title_ets_mobile": "ÉTSMobile",

"login_title": "Connexion",
"login_startup_title": "Veuillez vous authentifier pour continuer",
"login_prompt_universal_code": "Code universel",
"login_prompt_password": "Mot de passe",
"login_action_sign_in": "Se connecter",
Expand Down
6 changes: 6 additions & 0 deletions lib/router.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import 'package:notredame/ui/ets/events/author/widgets/author_view.dart';
import 'package:notredame/ui/ets/events/news/widgets/news_view.dart';
import 'package:notredame/ui/ets/events/news_details/widgets/news_details_view.dart';
import 'package:notredame/ui/ets/quick_links/security_info/widgets/security_view.dart';
import 'package:notredame/ui/login/widgets/login_view.dart';
import 'package:notredame/ui/more/about/widgets/about_view.dart';
import 'package:notredame/ui/more/contributors/widgets/contributors_view.dart';
import 'package:notredame/ui/more/faq/widgets/faq_view.dart';
Expand Down Expand Up @@ -100,6 +101,11 @@ Route<dynamic> generateRoute(RouteSettings routeSettings) {
settings: RouteSettings(name: routeSettings.name),
builder: (_) => const ChooseLanguageView(),
);
case RouterPaths.login:
return MaterialPageRoute(
settings: RouteSettings(name: routeSettings.name),
builder: (_) => const LoginView(),
);
default:
return PageRouteBuilder(
settings: RouteSettings(name: routeSettings.name),
Expand Down
2 changes: 1 addition & 1 deletion lib/ui/choose_language/widgets/choose_language_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ class _ChooseLanguageViewState extends State<ChooseLanguageView> {
child: ListView(
shrinkWrap: true,
children: <Widget>[
Icon(Icons.language, size: 80, color: context.theme.appColors.loginAccent),
Icon(Icons.language, size: 80, color: context.theme.appColors.splashScreenLogo),
Padding(
padding: const EdgeInsets.only(left: 20, top: 60),
child: Align(
Expand Down
10 changes: 5 additions & 5 deletions lib/ui/core/themes/app_colors_extension.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class AppColorsExtension extends ThemeExtension<AppColorsExtension> {
required this.dayIndicatorWeekView,
required this.dayIndicatorDayView,
required this.loginMain,
required this.loginAccent,
required this.splashScreenLogo,
required this.inputError,
required this.mapLegend,
});
Expand Down Expand Up @@ -62,7 +62,7 @@ class AppColorsExtension extends ThemeExtension<AppColorsExtension> {
final Color dayIndicatorWeekView;
final Color dayIndicatorDayView;
final Color loginMain;
final Color loginAccent;
final Color splashScreenLogo;
final Color inputError;
final Color mapLegend;

Expand Down Expand Up @@ -95,7 +95,7 @@ class AppColorsExtension extends ThemeExtension<AppColorsExtension> {
Color? dayIndicatorWeekView,
Color? dayIndicatorDayView,
Color? loginMain,
Color? loginAccent,
Color? splashScreenLogo,
Color? inputError,
Color? mapLegend,
}) {
Expand Down Expand Up @@ -127,7 +127,7 @@ class AppColorsExtension extends ThemeExtension<AppColorsExtension> {
dayIndicatorWeekView: dayIndicatorWeekView ?? this.dayIndicatorWeekView,
dayIndicatorDayView: dayIndicatorDayView ?? this.dayIndicatorDayView,
loginMain: loginMain ?? this.loginMain,
loginAccent: loginAccent ?? this.loginAccent,
splashScreenLogo: splashScreenLogo ?? this.splashScreenLogo,
inputError: inputError ?? this.inputError,
mapLegend: mapLegend ?? this.mapLegend,
);
Expand Down Expand Up @@ -169,7 +169,7 @@ class AppColorsExtension extends ThemeExtension<AppColorsExtension> {
dayIndicatorWeekView: _lerp(dayIndicatorWeekView, other.dayIndicatorWeekView, t),
dayIndicatorDayView: _lerp(dayIndicatorDayView, other.dayIndicatorDayView, t),
loginMain: _lerp(loginMain, other.loginMain, t),
loginAccent: _lerp(loginAccent, other.loginAccent, t),
splashScreenLogo: _lerp(splashScreenLogo, other.splashScreenLogo, t),
inputError: _lerp(inputError, other.inputError, t),
mapLegend: _lerp(mapLegend, other.mapLegend, t),
);
Expand Down
4 changes: 2 additions & 2 deletions lib/ui/core/themes/app_theme.dart
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ class AppTheme with ChangeNotifier {
dayIndicatorWeekView: const Color(0x83ef3e45),
dayIndicatorDayView: AppPalette.grey.lightGrey,
loginMain: AppPalette.etsLightRed,
loginAccent: AppPalette.grey.white,
splashScreenLogo: AppPalette.grey.white,
inputError: Colors.amberAccent,
mapLegend: const Color(0xfffffdfd),
);
Expand Down Expand Up @@ -208,7 +208,7 @@ class AppTheme with ChangeNotifier {
dayIndicatorWeekView: const Color(0x96ef3e45),
dayIndicatorDayView: AppPalette.grey.darkGrey,
loginMain: AppPalette.grey.white,
loginAccent: AppPalette.etsLightRed,
splashScreenLogo: AppPalette.etsLightRed,
inputError: Colors.redAccent,
mapLegend: const Color(0xff121212),
);
Expand Down
41 changes: 41 additions & 0 deletions lib/ui/login/view_model/login_viewmodel.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//

// Package imports:
import 'package:fluttertoast/fluttertoast.dart';
import 'package:msal_auth/msal_auth.dart';
import 'package:stacked/stacked.dart';

// Project imports:
import 'package:notredame/data/repositories/settings_repository.dart';
import 'package:notredame/data/services/analytics_service.dart';
import 'package:notredame/data/services/auth_service.dart';
import 'package:notredame/data/services/navigation_service.dart';
import 'package:notredame/domain/constants/router_paths.dart';
import 'package:notredame/l10n/app_localizations.dart';
import 'package:notredame/locator.dart';

class LoginViewModel extends BaseViewModel {
/// Localization class of the application.
final AppIntl _appIntl;

final SettingsRepository _settingsManager = locator<SettingsRepository>();
final AuthService _authService = locator<AuthService>();
final NavigationService navigationService = locator<NavigationService>();
final AnalyticsService _analyticsService = locator<AnalyticsService>();

LoginViewModel({required AppIntl intl}) : _appIntl = intl;

Future authenticate() async {
AuthenticationResult? token;
token = (await _authService.acquireToken()).$1;

if (token == null) {
Fluttertoast.showToast(msg: _appIntl.login_viewmodel_acquire_token_fail);
await _analyticsService.logError('LoginViewmodel', 'Failed to acquire token due to cancellation');
return;
}

_settingsManager.isLoggedIn = true;
navigationService.pushNamedAndRemoveUntil(RouterPaths.startup);
}
}
120 changes: 120 additions & 0 deletions lib/ui/login/widgets/login_view.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// Flutter imports:
import 'package:flutter/material.dart';

// Package imports:
import 'package:flutter_svg/flutter_svg.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:stacked/stacked.dart';

// Project imports:
import 'package:notredame/domain/constants/router_paths.dart';
import 'package:notredame/l10n/app_localizations.dart';
import 'package:notredame/ui/core/themes/app_palette.dart';
import 'package:notredame/ui/login/view_model/login_viewmodel.dart';

class LoginView extends StatefulWidget {
const LoginView({super.key});

@override
State<LoginView> createState() => _LoginViewState();
}

class _LoginViewState extends State<LoginView> {
bool _isLoading = false;

@override
Widget build(BuildContext context) {
return ViewModelBuilder<LoginViewModel>.reactive(
viewModelBuilder: () => LoginViewModel(intl: AppIntl.of(context)!),
builder: (context, model, child) => Scaffold(
body: Container(
width: double.infinity,
height: double.infinity,
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [AppPalette.etsLightRed, AppPalette.etsDarkRed],
),
),
child: Center(
child: Padding(
padding: const EdgeInsets.all(30.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.only(bottom: 20),
child: Hero(
tag: 'ets_logo',
child: SvgPicture.asset(
"assets/images/ets_white_logo.svg",
excludeFromSemantics: true,
width: 90,
height: 90,
colorFilter: ColorFilter.mode(AppPalette.grey.white, BlendMode.srcIn),
),
),
),
Padding(
padding: const EdgeInsets.only(bottom: 30),
child: Text(
AppIntl.of(context)!.login_startup_title,
style: TextStyle(fontWeight: FontWeight.normal, fontSize: 18, color: AppPalette.grey.white),
),
),
Padding(
padding: const EdgeInsets.only(bottom: 40),
child: Stack(
alignment: Alignment.centerRight,
children: [
ElevatedButton.icon(
onPressed: () async {
setState(() => _isLoading = true);
await model.authenticate();
setState(() => _isLoading = false);
},
icon: const FaIcon(FontAwesomeIcons.lockOpen, color: Colors.white),
label: Text(
AppIntl.of(context)!.login_action_sign_in,
style: TextStyle(color: AppPalette.grey.white, fontSize: 16, fontWeight: FontWeight.bold),
),
style: ElevatedButton.styleFrom(
backgroundColor: AppPalette.grey.black,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
minimumSize: const Size(300, 50),
),
),
if (_isLoading) ...[
CircularProgressIndicator(valueColor: AlwaysStoppedAnimation<Color>(AppPalette.grey.white)),
],
],
),
),
Padding(
padding: const EdgeInsets.only(bottom: 20),
child: ElevatedButton.icon(
onPressed: () {
model.navigationService.pushNamed(RouterPaths.faq);
},
icon: const FaIcon(FontAwesomeIcons.question, color: Colors.white),
label: Text(
"FAQ",
style: TextStyle(color: AppPalette.grey.white, fontSize: 13, fontWeight: FontWeight.bold),
),
style: ElevatedButton.styleFrom(
backgroundColor: AppPalette.grey.darkGrey,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
),
),
],
),
),
),
),
),
);
}
}
18 changes: 1 addition & 17 deletions lib/ui/startup/view_model/startup_viewmodel.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
// Package imports:
import 'package:fluttertoast/fluttertoast.dart';
import 'package:msal_auth/msal_auth.dart';
import 'package:stacked/stacked.dart';

Expand Down Expand Up @@ -50,22 +49,7 @@ class StartUpViewModel extends BaseViewModel {
_settingsManager.isLoggedIn = true;
_navigationService.pushNamedAndRemoveUntil(RouterPaths.root);
} else {
AuthenticationResult? token;
int attempts = 0;
const maxAttempts = 3;

while (token == null && attempts < maxAttempts) {
attempts++;
token = (await _authService.acquireToken()).$1;
if (token == null && attempts >= maxAttempts) {
Fluttertoast.showToast(msg: intl.startup_viewmodel_acquire_token_fail, toastLength: Toast.LENGTH_LONG);
await _analyticsService.logError('StartupViewmodel', 'Failed to acquire token after $maxAttempts attempts');
return;
}
}

_settingsManager.isLoggedIn = true;
_navigationService.pushNamedAndRemoveUntil(RouterPaths.root);
_navigationService.pushNamedAndRemoveUntil(RouterPaths.login);
}
}

Expand Down
2 changes: 1 addition & 1 deletion lib/ui/startup/widgets/startup_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class StartUpView extends StatelessWidget {
excludeFromSemantics: true,
width: 90,
height: 90,
colorFilter: ColorFilter.mode(context.theme.appColors.loginAccent, BlendMode.srcIn),
colorFilter: ColorFilter.mode(context.theme.appColors.splashScreenLogo, BlendMode.srcIn),
),
),
const SizedBox(height: 15),
Expand Down
65 changes: 65 additions & 0 deletions test/ui/login/view_model/login_viewmodel_test.dart
Comment thread
Shafaatul-Islam marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Package imports:
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';

// Project imports:
import 'package:notredame/data/repositories/settings_repository.dart';
import 'package:notredame/data/repositories/user_repository.dart';
import 'package:notredame/data/services/analytics_service.dart';
import 'package:notredame/data/services/auth_service.dart';
import 'package:notredame/data/services/navigation_service.dart';
import 'package:notredame/data/services/networking_service.dart';
import 'package:notredame/domain/constants/router_paths.dart';
import 'package:notredame/l10n/app_localizations.dart';
import 'package:notredame/ui/login/view_model/login_viewmodel.dart';
import '../../../data/mocks/repositories/settings_repository_mock.dart';
import '../../../data/mocks/services/auth_service_mock.dart';
import '../../../data/mocks/services/navigation_service_mock.dart';
import '../../../data/mocks/services/networking_service_mock.dart';
import '../../../helpers.dart';

void main() {
late NavigationServiceMock navigationServiceMock;
late SettingsRepositoryMock settingsRepositoryMock;
late NetworkingServiceMock networkingServiceMock;
late AuthServiceMock authServiceMock;

late LoginViewModel viewModel;
late AppIntl appIntl;

group('LoginViewModel - ', () {
setUp(() async {
setupAnalyticsServiceMock();
navigationServiceMock = setupNavigationServiceMock();
settingsRepositoryMock = setupSettingsRepositoryMock();
networkingServiceMock = setupNetworkingServiceMock();
authServiceMock = setupAuthServiceMock();

appIntl = await setupAppIntl();
viewModel = LoginViewModel(intl: appIntl);
});

tearDown(() {
unregister<AuthService>();
unregister<NetworkingService>();
unregister<UserRepository>();
unregister<SettingsRepository>();
unregister<NavigationService>();
unregister<AnalyticsService>();
});

test('silent sign in failed redirect to startup', () async {
NetworkingServiceMock.stubHasConnectivity(networkingServiceMock);
SettingsRepositoryMock.stubIsLocaleDefined(settingsRepositoryMock, toReturn: true);
AuthServiceMock.stubCreatePublicClientApplication(authServiceMock);
AuthServiceMock.stubAcquireTokenSilent(authServiceMock, success: false);
AuthServiceMock.stubAcquireToken(authServiceMock, success: true);

await viewModel.authenticate();

verify(authServiceMock.acquireToken()).called(1);
verify(navigationServiceMock.pushNamedAndRemoveUntil(RouterPaths.startup));
verify(settingsRepositoryMock.isLoggedIn = true).called(1);
});
});
}
Loading
Loading