diff --git a/l10n/intl_en.arb b/l10n/intl_en.arb index b1216dac3..fafd6d270 100644 --- a/l10n/intl_en.arb +++ b/l10n/intl_en.arb @@ -263,6 +263,7 @@ "discovery_navbar_more_title": "More", "discovery_navbar_more_details": "The More page gives you access to additional options of the application.", "discovery_page_more_contributors": "Here you will find the list of all the students who contributed and developed this application.", + "discovery_page_faq": "Here you will find the answers to the most frequently asked questions.", "discovery_page_more_report_bug": "The app is not perfect and we are always open to suggestions! If you see a problem or want us to be part of an idea, you can use this feature.", "discovery_page_more_settings": "Here you can change the general settings of the application.", @@ -309,7 +310,9 @@ "in_app_review_title": "Rate us!", "forgot_password": "Forgot your password?", - "need_help_contact_us": "Need help? Contact us!", + "need_help": "Need help?", + "actions": "Actions", + "questions_and_answers": "Questions and answers", "universal_code_example": "Ex: AB12345", "my_tickets": "My tickets", "no_ticket": "No ticket", diff --git a/l10n/intl_fr.arb b/l10n/intl_fr.arb index c4b8c0a4d..387817b8c 100644 --- a/l10n/intl_fr.arb +++ b/l10n/intl_fr.arb @@ -264,6 +264,7 @@ "discovery_navbar_more_details": "La page Plus vous permet d'avoir accès aux options supplémentaires de l'application.", "discovery_page_more_report_bug": "L'application n'est pas parfaite et nous sommes toujours ouverts à des suggestions! Si vous voyez un problème ou si vous souhaitez nous faire part d'une idée, vous pouvez utiliser cette fonctionnalité", "discovery_page_more_contributors": "Ici vous trouverez la liste de tous les étudiants qui ont travaillé sur le projet d'un point de vue du code.", + "discovery_page_faq": "Si vous avez des questions, vous pouvez consulter notre FAQ.", "discovery_page_more_settings": "Et finalement ici vous pouvez changer les paramètres généraux de l'application!", "discovery_page_thankyou_message": "Merci encore d'avoir installé ÉTSMobile! Nous espérons que cette application vous sera utile! Plein de nouvelles fonctionnalités vont arriver dans les mois à venir donc penser à mettre à jour l'application!!", @@ -307,9 +308,11 @@ }, "progress_bar_suffix": "jours", - "in_app_review_title": "Évaluez-nous!", + "in_app_review_title": "Évaluez-nous!", + "need_help": "Besoin d'aide?", + "questions_and_answers": "Questions et réponses", + "actions": "Actions", "forgot_password": "Mot de passe oublié?", - "need_help_contact_us": "Besoin d'aide? Contactez-nous!", "universal_code_example": "Ex: AB12345", "my_tickets": "Mes billets", "no_ticket": "Aucun billet", diff --git a/lib/core/constants/discovery_ids.dart b/lib/core/constants/discovery_ids.dart index c14e683dc..2212de49e 100644 --- a/lib/core/constants/discovery_ids.dart +++ b/lib/core/constants/discovery_ids.dart @@ -23,6 +23,7 @@ class DiscoveryIds { static const String detailsMoreBugReport = "page_more_bug_report"; static const String detailsMoreContributors = "page_more_contributors"; + static const String detailsMoreFaq = "page_more_faq"; static const String detailsMoreSettings = "page_more_settings"; static const String detailsMoreThankYou = "page_more_thank_you"; } diff --git a/lib/core/constants/faq.dart b/lib/core/constants/faq.dart new file mode 100644 index 000000000..05592e81a --- /dev/null +++ b/lib/core/constants/faq.dart @@ -0,0 +1,129 @@ +// FLUTTER / DART / THIRD-PARTIES +import 'package:flutter/material.dart'; + +// MODELS +import 'package:notredame/core/models/faq_actions.dart'; +import 'package:notredame/core/models/faq_questions.dart'; + +// CONSTANTS +import 'package:notredame/core/constants/app_info.dart'; + +class Faq { + List questions = [ + QuestionItem( + title: { + "fr": "Quel mot de passe dois-je utiliser pour me connecter ?", + "en": "What password should I use to log in?" + }, + description: { + "fr": + "Le mot de passe à utiliser correspond à celui utilisé pour la connexion à MonÉTS et les autres systèmes informatiques de l’ÉTS.", + "en": + "The password is the one you use for logging into MonÉTS and other ÉTS computer systems." + }, + ), + QuestionItem( + title: { + "fr": "Je n’ai pas accès au cours et au programme.", + "en": "I don't have access to the courses and program." + }, + description: { + "fr": + "Les nouveaux étudiants pourraient ne pas voir l’horaire et les cours inscrits avant le début de la première session de cours. Cependant, ces informations apparaissent dès le début de la première session de cours.", + "en": + "New students may not see the schedule and courses before the start of the first course session. However, this information becomes available at the beginning of the first course session." + }, + ), + QuestionItem( + title: { + "fr": + "Je suis diplômé de l’ÉTS et je souhaite faire réactiver mon compte.", + "en": "I am an ÉTS graduate, and I want to reactivate my account." + }, + description: { + "fr": "Vous pouvez demander de réactiver votre compte", + "en": "You can request to reactivate your account." + }, + ), + QuestionItem( + title: { + "fr": "Je ne vois plus mes notes de contrôle", + "en": "I can't see my grades anymore." + }, + description: { + "fr": + "Il est possible qu’il s’agit de la période d'évaluation des cours. Vous devez compléter les évaluations sur SignETS. Les notes seront disponibles après avoir répondu aux évaluations.", + "en": + "It is possible that this is the course evaluation period. You need to complete the evaluations on SignETS. Grades will be available after responding to the evaluations." + }, + ), + ]; + + List actions = [ + ActionItem( + title: { + "fr": "Où trouver mon code universel ?", + "en": "Where can I find my universal code?" + }, + description: { + "fr": + "Le code universel se trouve dans la décision d’admission sur le portail de monÉTS.", + "en": + "The universal code can be found in the admission decision on the MonÉTS portal." + }, + type: ActionType.webview, + link: "https://portail.etsmtl.ca/home/Admission", + iconName: Icons.person, + iconColor: const Color(0xFFD5A8F8), + circleColor: const Color(0xFF6939B7), + ), + ActionItem( + title: { + "fr": + "Je suis diplômé de l’ÉTS et je souhaite faire réactiver mon compte.", + "en": "I am an ÉTS graduate, and I want to reactivate my account." + }, + description: { + "fr": "Vous pouvez demander de réactiver votre compte.", + "en": "You can request to reactivate your account." + }, + type: ActionType.webview, + link: "https://formulaires.etsmtl.ca/ReactivationCompte", + iconName: Icons.school, + iconColor: const Color(0xFF78E2BC), + circleColor: const Color(0xFF39B78A), + ), + ActionItem( + title: { + "fr": + "Questions concernant vos conditions d'admission, des inscriptions et des conditions relatives à la poursuite de vos études", + "en": + "Questions about your admission conditions, registrations, and conditions for continuing your studies" + }, + description: { + "fr": "Veuillez contacter le Bureau de la registraire.", + "en": "Please contact the Office of the Registrar." + }, + type: ActionType.email, + link: "accueilbdr@etsmtl.ca", + iconName: Icons.email, + iconColor: const Color(0xFFFCA4A4), + circleColor: const Color(0xFFDA4444), + ), + ActionItem( + title: { + "fr": "Questions concernant l’application ÉTSMobile", + "en": "Questions about the ÉTSMobile app" + }, + description: { + "fr": "Veuillez contacter App|ETS.", + "en": "Please contact App|ETS." + }, + type: ActionType.email, + link: AppInfo.email, + iconName: Icons.install_mobile, + iconColor: const Color(0xFF71D8F7), + circleColor: const Color(0xFF397DB7), + ), + ]; +} diff --git a/lib/core/constants/router_paths.dart b/lib/core/constants/router_paths.dart index 17f874fb5..f62fcf345 100644 --- a/lib/core/constants/router_paths.dart +++ b/lib/core/constants/router_paths.dart @@ -2,6 +2,7 @@ class RouterPaths { static const String startup = "/startup"; static const String login = "/login"; + static const String faq = "/faq"; static const String dashboard = "/dashboard"; static const String schedule = "/schedule"; static const String student = "/student"; diff --git a/lib/core/models/faq_actions.dart b/lib/core/models/faq_actions.dart new file mode 100644 index 000000000..c229ede28 --- /dev/null +++ b/lib/core/models/faq_actions.dart @@ -0,0 +1,27 @@ +// FLUTTER / DART / THIRD-PARTIES +import 'package:flutter/material.dart'; + +class ActionItem { + final Map title; + final Map description; + final ActionType type; + final String link; + final IconData iconName; + final Color iconColor; + final Color circleColor; + + ActionItem({ + @required this.title, + @required this.description, + @required this.type, + @required this.link, + @required this.iconName, + @required this.iconColor, + @required this.circleColor, + }); +} + +enum ActionType { + webview, + email, +} diff --git a/lib/core/models/faq_questions.dart b/lib/core/models/faq_questions.dart new file mode 100644 index 000000000..a96bc41a3 --- /dev/null +++ b/lib/core/models/faq_questions.dart @@ -0,0 +1,12 @@ +// FLUTTER / DART / THIRD-PARTIES +import 'package:flutter/material.dart'; + +class QuestionItem { + final Map title; + final Map description; + + QuestionItem({ + @required this.title, + @required this.description, + }); +} diff --git a/lib/core/viewmodels/faq_viewmodel.dart b/lib/core/viewmodels/faq_viewmodel.dart new file mode 100644 index 000000000..2fba133d9 --- /dev/null +++ b/lib/core/viewmodels/faq_viewmodel.dart @@ -0,0 +1,48 @@ +// FLUTTER / DART / THIRD-PARTIES +import 'package:flutter/material.dart'; +import 'package:notredame/locator.dart'; +import 'package:stacked/stacked.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +// MANAGERS +import 'package:notredame/core/managers/settings_manager.dart'; + +// SERVICES +import 'package:notredame/core/services/launch_url_service.dart'; +import 'package:notredame/core/services/analytics_service.dart'; + +// CONSTANTS +import 'package:notredame/core/constants/app_info.dart'; + +class FaqViewModel extends BaseViewModel { + final SettingsManager _settingsManager = locator(); + + final LaunchUrlService _launchUrlService = locator(); + + Locale get locale => _settingsManager.locale; + + String mailtoStr(String email, String subject) { + return 'mailto:$email?subject=$subject'; + } + + Future launchWebsite(String link, Brightness brightness) async { + await _launchUrlService.launchInBrowser(link, brightness); + } + + Future openMail(String addressEmail, BuildContext context) async { + var email = ""; + if (addressEmail == AppInfo.email) { + email = mailtoStr(addressEmail, AppIntl.of(context).email_subject); + } else { + email = mailtoStr(addressEmail, ""); + } + + final urlLaunchable = await _launchUrlService.canLaunch(email); + + if (urlLaunchable) { + await _launchUrlService.launch(email); + } else { + locator().logError("login_view", "Cannot send email."); + } + } +} diff --git a/lib/core/viewmodels/login_viewmodel.dart b/lib/core/viewmodels/login_viewmodel.dart index 09a8cb47b..0061bda40 100644 --- a/lib/core/viewmodels/login_viewmodel.dart +++ b/lib/core/viewmodels/login_viewmodel.dart @@ -90,8 +90,4 @@ class LoginViewModel extends BaseViewModel { return _appIntl.login_error_invalid_credentials; } - - String mailtoStr(String email, String subject) { - return 'mailto:$email?subject=$subject'; - } } diff --git a/lib/ui/router.dart b/lib/ui/router.dart index 53c973372..b7739e1c0 100644 --- a/lib/ui/router.dart +++ b/lib/ui/router.dart @@ -13,6 +13,7 @@ import 'package:notredame/ui/views/feedback_view.dart'; // VIEWS import 'package:notredame/ui/views/login_view.dart'; +import 'package:notredame/ui/views/faq_view.dart'; import 'package:notredame/ui/views/not_found_view.dart'; import 'package:notredame/ui/views/more_view.dart'; import 'package:notredame/ui/views/outage_view.dart'; @@ -48,6 +49,11 @@ Route generateRoute(RouteSettings routeSettings) { return MaterialPageRoute( settings: RouteSettings(name: routeSettings.name), builder: (_) => LoginView()); + case RouterPaths.faq: + return MaterialPageRoute( + settings: RouteSettings(name: routeSettings.name), + builder: (_) => + FaqView(backgroundColor: routeSettings.arguments as Color)); case RouterPaths.dashboard: final code = (routeSettings.arguments as UpdateCode) ?? UpdateCode.none; return PageRouteBuilder( diff --git a/lib/ui/utils/discovery_components.dart b/lib/ui/utils/discovery_components.dart index d5655dd46..b7236cf6a 100644 --- a/lib/ui/utils/discovery_components.dart +++ b/lib/ui/utils/discovery_components.dart @@ -367,6 +367,29 @@ List discoveryComponents(BuildContext context) { ), ), ), + Discovery( + path: null, + featureId: DiscoveryIds.detailsMoreFaq, + title: AppIntl.of(context).need_help, + details: ConstrainedBox( + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.2), + child: Column( + children: [ + Expanded( + child: ListView( + padding: EdgeInsets.zero, + children: [ + _buildSkipDiscoveryButton(context), + Text(AppIntl.of(context).discovery_page_faq, + textAlign: TextAlign.justify), + ], + ), + ), + ], + ), + ), + ), Discovery( path: null, featureId: DiscoveryIds.detailsMoreSettings, diff --git a/lib/ui/views/faq_view.dart b/lib/ui/views/faq_view.dart new file mode 100644 index 000000000..e088180b9 --- /dev/null +++ b/lib/ui/views/faq_view.dart @@ -0,0 +1,286 @@ +// FLUTTER / DART / THIRD-PARTIES +import 'package:carousel_slider/carousel_slider.dart'; +import 'package:flutter/material.dart'; +import 'package:stacked/stacked.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +// VIEWMODEL +import 'package:notredame/core/viewmodels/faq_viewmodel.dart'; + +// MODELS +import 'package:notredame/core/models/faq_actions.dart'; + +// CONSTANTS +import 'package:notredame/core/constants/faq.dart'; + +class FaqView extends StatefulWidget { + final Color backgroundColor; + + const FaqView({this.backgroundColor}); + + @override + State createState() => _FaqViewState(); +} + +class _FaqViewState extends State { + final Faq faq = Faq(); + + @override + Widget build(BuildContext context) => ViewModelBuilder.reactive( + viewModelBuilder: () => FaqViewModel(), + builder: (context, model, child) { + return Scaffold( + backgroundColor: widget.backgroundColor, + body: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + getTitle(), + getSubtitle(AppIntl.of(context).questions_and_answers), + Padding( + padding: const EdgeInsets.only(left: 15.0, right: 15.0), + child: CarouselSlider( + options: CarouselOptions( + height: 250.0, + ), + items: faq.questions.asMap().entries.map((entry) { + final int index = entry.key; + final question = faq.questions[index]; + + return Builder( + builder: (BuildContext context) { + return Container( + width: MediaQuery.of(context).size.width, + margin: const EdgeInsets.symmetric(horizontal: 5.0), + decoration: BoxDecoration( + color: Theme.of(context).brightness == + Brightness.light + ? const Color.fromARGB(255, 240, 238, 238) + : const Color.fromARGB(255, 40, 40, 40), + borderRadius: + const BorderRadius.all(Radius.circular(8.0)), + ), + child: getQuestionCard( + question.title[model.locale.languageCode], + question.description[model.locale.languageCode], + ), + ); + }, + ); + }).toList(), + ), + ), + getSubtitle(AppIntl.of(context).actions), + Expanded( + child: ListView.builder( + padding: const EdgeInsets.only(top: 1.0), + itemCount: faq.actions.length, + itemBuilder: (context, index) { + final action = faq.actions[index]; + + return getActionCard( + action.title[model.locale.languageCode], + action.description[model.locale.languageCode], + action.type, + action.link, + action.iconName, + action.iconColor, + action.circleColor, + context, + model); + }, + ), + ) + ], + ), + ); + }, + ); + + Padding getTitle() { + return Padding( + padding: const EdgeInsets.only(top: 60.0), + child: Row( + children: [ + Padding( + padding: const EdgeInsets.only(left: 5.0), + child: GestureDetector( + onTap: () { + Navigator.of(context).pop(); + }, + child: Padding( + padding: const EdgeInsets.only(left: 10.0), + child: Icon( + Icons.arrow_back, + color: widget.backgroundColor == Colors.white + ? Colors.black + : Colors.white, + ), + ), + ), + ), + const SizedBox(width: 8.0), + Expanded( + child: Text( + AppIntl.of(context).need_help, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.headline5.copyWith( + color: widget.backgroundColor == Colors.white + ? Colors.black + : Colors.white, + ), + ), + ), + ], + ), + ); + } + + Padding getSubtitle(String subtitle) { + return Padding( + padding: const EdgeInsets.only(left: 18.0, top: 18.0, bottom: 10.0), + child: Text( + subtitle, + style: Theme.of(context).textTheme.headline5.copyWith( + color: widget.backgroundColor == Colors.white + ? Colors.black + : Colors.white, + ), + ), + ); + } + + Padding getQuestionCard(String title, String description) { + return Padding( + padding: const EdgeInsets.only(top: 20.0, left: 20.0, right: 20.0), + child: Align( + alignment: Alignment.topLeft, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + textScaleFactor: 1.0, + style: Theme.of(context).textTheme.bodyText2.copyWith( + fontSize: 20, + color: Theme.of(context).brightness == Brightness.light + ? Colors.black + : Colors.white, + ), + textAlign: TextAlign.justify, + ), + const SizedBox(height: 20.0), + Text( + description, + textScaleFactor: 1.0, + style: Theme.of(context).textTheme.bodyText2.copyWith( + fontSize: 16, + color: Theme.of(context).brightness == Brightness.light + ? Colors.black + : Colors.white, + ), + textAlign: TextAlign.justify, + ), + ], + ), + ), + ); + } + + Padding getActionCard( + String title, + String description, + ActionType type, + String link, + IconData iconName, + Color iconColor, + Color circleColor, + BuildContext context, + FaqViewModel model) { + return Padding( + padding: const EdgeInsets.fromLTRB(15.0, 0.0, 15.0, 15.0), + child: ElevatedButton( + onPressed: () { + if (type.name == ActionType.webview.name) { + openWebview(model, link); + } else if (type.name == ActionType.email.name) { + openMail(model, context, link); + } + }, + style: ButtonStyle( + elevation: MaterialStateProperty.all(8.0), + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + )), + child: getActionCardInfo( + context, + title, + description, + iconName, + iconColor, + circleColor, + ), + ), + ); + } + + Row getActionCardInfo(BuildContext context, String title, String description, + IconData iconName, Color iconColor, Color circleColor) { + return Row( + children: [ + Column( + children: [ + CircleAvatar( + backgroundColor: circleColor, + radius: 25, + child: Icon(iconName, color: iconColor), + ), + ], + ), + Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(15, 15, 0, 15), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: Theme.of(context).textTheme.bodyText2.copyWith( + fontSize: 18, + color: Theme.of(context).brightness == Brightness.light + ? Colors.black + : Colors.white, + ), + textAlign: TextAlign.left, + ), + const SizedBox(height: 10.0), + Text( + description, + style: Theme.of(context).textTheme.bodyText2.copyWith( + fontSize: 16, + color: Theme.of(context).brightness == Brightness.light + ? Colors.black + : Colors.white, + ), + textAlign: TextAlign.justify, + ) + ], + ), + ), + ) + ], + ); + } + + Future openWebview(FaqViewModel model, String link) async { + model.launchWebsite(link, Theme.of(context).brightness); + } + + Future openMail( + FaqViewModel model, BuildContext context, String addressEmail) async { + model.openMail(addressEmail, context); + } +} diff --git a/lib/ui/views/login_view.dart b/lib/ui/views/login_view.dart index db999169a..8135e44b6 100644 --- a/lib/ui/views/login_view.dart +++ b/lib/ui/views/login_view.dart @@ -6,7 +6,7 @@ import 'package:fluttertoast/fluttertoast.dart'; import 'package:stacked/stacked.dart'; // SERVICE -import 'package:notredame/core/services/analytics_service.dart'; +import 'package:notredame/core/services/navigation_service.dart'; import 'package:notredame/core/services/launch_url_service.dart'; // UTILS @@ -20,8 +20,8 @@ import 'package:notredame/core/viewmodels/login_viewmodel.dart'; import 'package:notredame/ui/widgets/password_text_field.dart'; // CONSTANTS -import 'package:notredame/core/constants/app_info.dart'; import 'package:notredame/core/constants/urls.dart'; +import 'package:notredame/core/constants/router_paths.dart'; // OTHER import 'package:notredame/ui/utils/app_theme.dart'; @@ -37,6 +37,8 @@ class _LoginViewState extends State { final FocusScopeNode _focusNode = FocusScopeNode(); + final NavigationService _navigationService = locator(); + final LaunchUrlService _launchUrlService = locator(); /// Unique key of the login form form @@ -207,13 +209,18 @@ class _LoginViewState extends State { padding: const EdgeInsets.only(top: 24), child: InkWell( child: Text( - AppIntl.of(context).need_help_contact_us, + AppIntl.of(context).need_help, style: const TextStyle( decoration: TextDecoration.underline, color: Colors.white), ), onTap: () async { - sendEmail(model); + _navigationService.pushNamed( + RouterPaths.faq, + arguments: Utils.getColorByBrightness( + context, + AppTheme.etsLightRed, + AppTheme.primaryDark)); }, ), ), @@ -260,16 +267,4 @@ class _LoginViewState extends State { Color get submitTextColor => Utils.getColorByBrightness(context, AppTheme.etsLightRed, Colors.white); - - Future sendEmail(LoginViewModel model) async { - final clubEmail = - model.mailtoStr(AppInfo.email, AppIntl.of(context).email_subject); - final urlLaunchable = await _launchUrlService.canLaunch(clubEmail); - - if (urlLaunchable) { - await _launchUrlService.launch(clubEmail); - } else { - locator().logError("login_view", "Cannot send email."); - } - } } diff --git a/lib/ui/views/more_view.dart b/lib/ui/views/more_view.dart index 93c49a001..8ed7f33d3 100644 --- a/lib/ui/views/more_view.dart +++ b/lib/ui/views/more_view.dart @@ -160,6 +160,19 @@ class _MoreViewState extends State { opaque: false, )); }), + ListTile( + title: Text(AppIntl.of(context).need_help), + leading: _buildDiscoveryFeatureDescriptionWidget( + context, + getProperIconAccordingToTheme(Icons.question_answer), + DiscoveryIds.detailsMoreFaq, + model), + onTap: () { + _analyticsService.logEvent(tag, "FAQ clicked"); + model.navigationService.pushNamed(RouterPaths.faq, + arguments: Utils.getColorByBrightness( + context, Colors.white, AppTheme.primaryDark)); + }), ListTile( title: Text(AppIntl.of(context).settings_title), leading: _buildDiscoveryFeatureDescriptionWidget( diff --git a/pubspec.lock b/pubspec.lock index bcc47f26a..54a5724b1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -85,6 +85,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.1" + carousel_slider: + dependency: "direct main" + description: + name: carousel_slider + url: "https://pub.dartlang.org" + source: hosted + version: "4.2.1" characters: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index acc62915d..3f58d02db 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,7 +5,7 @@ description: The 4th generation of ÉTSMobile, the main gateway between the Éco # pub.dev using `pub publish`. This is preferred for private packages. publish_to: 'none' # Remove this line if you wish to publish to pub.dev -version: 4.26.0+1 +version: 4.27.0+1 environment: sdk: ">=2.10.0 <3.0.0" @@ -71,6 +71,7 @@ dependencies: auto_size_text: ^3.0.0 easter_egg_trigger: ^1.0.1 calendar_view: ^1.0.1 + carousel_slider: ^4.2.1 reorderable_grid_view: ^2.2.6 dev_dependencies: diff --git a/test/ui/views/faq_view_test.dart b/test/ui/views/faq_view_test.dart new file mode 100644 index 000000000..057f61b6d --- /dev/null +++ b/test/ui/views/faq_view_test.dart @@ -0,0 +1,85 @@ +// FLUTTER / DART / THIRD-PARTIES +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +// MANAGERS +import 'package:notredame/core/managers/settings_manager.dart'; +import '../../mock/managers/settings_manager_mock.dart'; + +// VIEW +import 'package:notredame/ui/views/faq_view.dart'; + +// HELPERS +import '../../helpers.dart'; + +// CONSTANTS +import 'package:notredame/core/constants/faq.dart'; + +void main() { + group('FaqView - ', () { + AppIntl appIntl; + + SettingsManager settingsManager; + + setUp(() async { + setupLaunchUrlServiceMock(); + settingsManager = setupSettingsManagerMock(); + appIntl = await setupAppIntl(); + }); + + tearDown(() {}); + + group('UI - ', () { + testWidgets('has x ElevatedButton', (WidgetTester tester) async { + SettingsManagerMock.stubLocale(settingsManager as SettingsManagerMock); + + await tester.pumpWidget(localizedWidget(child: const FaqView())); + await tester.pumpAndSettle(); + + final elevatedButton = find.byType(ElevatedButton, skipOffstage: false); + + final Faq faq = Faq(); + final numberOfButtons = faq.actions.length; + expect(elevatedButton, findsNWidgets(numberOfButtons)); + }); + + testWidgets('has 2 subtitles', (WidgetTester tester) async { + SettingsManagerMock.stubLocale(settingsManager as SettingsManagerMock); + + await tester.pumpWidget(localizedWidget(child: const FaqView())); + await tester.pumpAndSettle(); + + final subtitle1 = find.text(appIntl.actions); + expect(subtitle1, findsNWidgets(1)); + + final subtitle2 = find.text(appIntl.questions_and_answers); + expect(subtitle2, findsNWidgets(1)); + }); + + testWidgets('has 1 title', (WidgetTester tester) async { + SettingsManagerMock.stubLocale(settingsManager as SettingsManagerMock); + + await tester.pumpWidget(localizedWidget(child: const FaqView())); + await tester.pumpAndSettle(); + + final title = find.text(appIntl.need_help); + + expect(title, findsOneWidget); + }); + }); + + group("golden - ", () { + testWidgets("default view", (WidgetTester tester) async { + SettingsManagerMock.stubLocale(settingsManager as SettingsManagerMock); + tester.binding.window.physicalSizeTestValue = const Size(1800, 2410); + + await tester.pumpWidget(localizedWidget(child: const FaqView())); + await tester.pumpAndSettle(); + + await expectLater(find.byType(FaqView), + matchesGoldenFile(goldenFilePath("FaqView_1"))); + }); + }); + }); +} diff --git a/test/ui/views/goldenFiles/FaqView_1.png b/test/ui/views/goldenFiles/FaqView_1.png new file mode 100644 index 000000000..e75a99a40 Binary files /dev/null and b/test/ui/views/goldenFiles/FaqView_1.png differ diff --git a/test/ui/views/goldenFiles/moreView_1.png b/test/ui/views/goldenFiles/moreView_1.png index 09f80b04d..5f323d1f3 100644 Binary files a/test/ui/views/goldenFiles/moreView_1.png and b/test/ui/views/goldenFiles/moreView_1.png differ diff --git a/test/ui/views/login_view_test.dart b/test/ui/views/login_view_test.dart index 1e80795c3..a55332483 100644 --- a/test/ui/views/login_view_test.dart +++ b/test/ui/views/login_view_test.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:mockito/mockito.dart'; // MANAGERS / SERVICES import 'package:notredame/core/managers/user_repository.dart'; @@ -25,8 +24,6 @@ import '../../helpers.dart'; void main() { AppIntl intl; - LaunchUrlServiceMock launchUrlService; - AnalyticsServiceMock analyticsService; group('LoginView - ', () { setUp(() async { @@ -35,8 +32,8 @@ void main() { setupNavigationServiceMock(); setupSettingsManagerMock(); setupPreferencesServiceMock(); - launchUrlService = setupLaunchUrlServiceMock() as LaunchUrlServiceMock; - analyticsService = setupAnalyticsServiceMock() as AnalyticsServiceMock; + setupLaunchUrlServiceMock() as LaunchUrlServiceMock; + setupAnalyticsServiceMock() as AnalyticsServiceMock; }); tearDown(() { @@ -70,50 +67,6 @@ void main() { isA() .having((source) => source.onPressed, 'onPressed', isNull)); }); - - testWidgets('should open emails', (WidgetTester tester) async { - const url = 'mailto:applets@ens.etsmtl.ca?subject=ÉTSMobile Problem'; - LaunchUrlServiceMock.stubCanLaunchUrl(launchUrlService, url); - LaunchUrlServiceMock.stubLaunchUrl(launchUrlService, url); - - await tester.pumpWidget(localizedWidget(child: LoginView())); - await tester.pumpAndSettle(); - - await tester - .tap(find.widgetWithText(InkWell, intl.need_help_contact_us)); - - // Rebuild the widget after the state has changed. - await tester.pump(); - - verify(launchUrlService.canLaunch(url)).called(1); - verify(launchUrlService.launch(url)).called(1); - verifyNoMoreInteractions(launchUrlService); - }); - - testWidgets('cannot launch email on this platform', - (WidgetTester tester) async { - const url = 'mailto:applets@ens.etsmtl.ca?subject=ÉTSMobile Problem'; - LaunchUrlServiceMock.stubCanLaunchUrl(launchUrlService, url, - toReturn: false); - LaunchUrlServiceMock.stubLaunchUrl(launchUrlService, url, - toReturn: false); - - await tester.pumpWidget(localizedWidget(child: LoginView())); - await tester.pumpAndSettle(); - - await tester - .tap(find.widgetWithText(InkWell, intl.need_help_contact_us)); - - // Rebuild the widget after the state has changed. - await tester.pumpAndSettle(); - - verify(launchUrlService.canLaunch(url)).called(1); - verifyNever(launchUrlService.launch(url)); - verifyNoMoreInteractions(launchUrlService); - - verify(analyticsService.logError(any, any)).called(1); - verifyNoMoreInteractions(analyticsService); - }); }); }); } diff --git a/test/ui/views/more_view_test.dart b/test/ui/views/more_view_test.dart index a4221b97c..d7cc7387f 100644 --- a/test/ui/views/more_view_test.dart +++ b/test/ui/views/more_view_test.dart @@ -48,7 +48,7 @@ void main() { }); group('UI - ', () { - testWidgets('has 1 listView and 6 listTiles', + testWidgets('has 1 listView and 8 listTiles', (WidgetTester tester) async { await tester.pumpWidget( localizedWidget(child: FeatureDiscovery(child: MoreView()))); @@ -58,7 +58,7 @@ void main() { expect(listview, findsOneWidget); final listTile = find.byType(ListTile); - expect(listTile, findsNWidgets(7)); + expect(listTile, findsNWidgets(8)); }); group('navigation - ', () { @@ -147,6 +147,21 @@ void main() { expect(find.byType(AboutDialog), findsOneWidget); }); + testWidgets('need help', (WidgetTester tester) async { + await tester.pumpWidget( + localizedWidget(child: FeatureDiscovery(child: MoreView()))); + await tester.pumpAndSettle(const Duration(seconds: 1)); + + // Tap the button. + await tester.tap(find.widgetWithText(ListTile, intl.need_help)); + + // Rebuild the widget after the state has changed. + await tester.pump(); + + verify(navigation.pushNamed(RouterPaths.faq, arguments: Colors.white)) + .called(1); + }); + testWidgets('settings', (WidgetTester tester) async { await tester.pumpWidget( localizedWidget(child: FeatureDiscovery(child: MoreView()))); diff --git a/test/viewmodels/faq_viewmodel_test.dart b/test/viewmodels/faq_viewmodel_test.dart new file mode 100644 index 000000000..52519f29a --- /dev/null +++ b/test/viewmodels/faq_viewmodel_test.dart @@ -0,0 +1,53 @@ +// FLUTTER / DART / THIRD-PARTIES +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; + +// SERVICES / MANAGERS +import 'package:notredame/core/services/launch_url_service.dart'; +import 'package:notredame/core/managers/settings_manager.dart'; +import '../mock/services/launch_url_service_mock.dart'; + +// VIEW MODEL +import 'package:notredame/core/viewmodels/faq_viewmodel.dart'; + +// OTHER +import '../helpers.dart'; + +void main() { + LaunchUrlServiceMock launchUrlService; + + FaqViewModel viewModel; + + group('FaqViewModel - ', () { + setUp(() async { + launchUrlService = setupLaunchUrlServiceMock() as LaunchUrlServiceMock; + setupSettingsManagerMock(); + + viewModel = FaqViewModel(); + }); + + tearDown(() { + unregister(); + unregister(); + }); + + group('Emails - ', () { + test('Has the right mailto', () { + final str = viewModel.mailtoStr("email", "subject"); + + expect(str, "mailto:email?subject=subject"); + }); + }); + + group('Webview - ', () { + test('Calls launchInBrowser', () { + viewModel.launchWebsite("https://clubapplets.ca/", Brightness.light); + + verify(launchUrlService.launchInBrowser( + "https://clubapplets.ca/", Brightness.light)) + .called(1); + }); + }); + }); +} diff --git a/test/viewmodels/login_viewmodel_test.dart b/test/viewmodels/login_viewmodel_test.dart index aabdf381f..04a9b619d 100644 --- a/test/viewmodels/login_viewmodel_test.dart +++ b/test/viewmodels/login_viewmodel_test.dart @@ -136,13 +136,5 @@ void main() { expect(viewModel.password, ""); }); }); - - group('Emails - ', () { - test('Has the right mailto', () { - final str = viewModel.mailtoStr("email", "subject"); - - expect(str, "mailto:email?subject=subject"); - }); - }); }); }