diff --git a/app/lib/app.dart b/app/lib/app.dart index 8fadfc14..6c59547e 100644 --- a/app/lib/app.dart +++ b/app/lib/app.dart @@ -15,20 +15,23 @@ class PharMeApp extends StatelessWidget { @override Widget build(BuildContext context) { - return LifecycleObserver( + return ErrorHandler( appRouter: _appRouter, - child: MaterialApp.router( - debugShowCheckedModeBanner: false, - routeInformationParser: _appRouter.defaultRouteParser(), - routerDelegate: _appRouter.delegate(deepLinkBuilder: getInitialRoute), - theme: PharMeTheme.light, - localizationsDelegates: [ - AppLocalizations.delegate, - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - supportedLocales: [Locale('en', '')], + child: LifecycleObserver( + appRouter: _appRouter, + child: MaterialApp.router( + debugShowCheckedModeBanner: false, + routeInformationParser: _appRouter.defaultRouteParser(), + routerDelegate: _appRouter.delegate(deepLinkBuilder: getInitialRoute), + theme: PharMeTheme.light, + localizationsDelegates: [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: [Locale('en', '')], + ), ), ); } diff --git a/app/lib/common/constants.dart b/app/lib/common/constants.dart index b00839ee..f04b8dcd 100644 --- a/app/lib/common/constants.dart +++ b/app/lib/common/constants.dart @@ -10,11 +10,14 @@ Uri keycloakUrl([String slug = '']) => // Note that sending emails will not work on the iPhone Simulator since it does // not have any email application installed. String _mailContact = 'pgx-app-validation-study@lists.myhpi.de'; -Future sendEmail({String subject = ''}) async { +Future sendEmail({String subject = '', String body = ''}) async { await launchUrl(Uri( scheme: 'mailto', path: _mailContact, - queryParameters: {'subject': subject})); + queryParameters: { + 'subject': subject, + 'body': Uri.encodeComponent(body), + })); } final cpicMaxCacheTime = Duration(days: 90); diff --git a/app/lib/common/routing/router.dart b/app/lib/common/routing/router.dart index e540eb76..2b5fbfb0 100644 --- a/app/lib/common/routing/router.dart +++ b/app/lib/common/routing/router.dart @@ -1,5 +1,6 @@ import '../../drug/module.dart'; import '../../drug_selection/module.dart'; +import '../../error/module.dart'; import '../../faq/module.dart'; import '../../login/module.dart'; import '../../main/module.dart'; @@ -19,6 +20,7 @@ class AppRouter extends _$AppRouter { @override List get routes => [ drugSelectionRoute(), + errorRoute(), loginRoute(), mainRoute( children: [ diff --git a/app/lib/common/widgets/error_handler.dart b/app/lib/common/widgets/error_handler.dart new file mode 100644 index 00000000..1c937d16 --- /dev/null +++ b/app/lib/common/widgets/error_handler.dart @@ -0,0 +1,43 @@ +import '../module.dart'; + +class ErrorHandler extends StatefulWidget { + const ErrorHandler({ + super.key, + required this.appRouter, + required this.child, + }); + + final AppRouter appRouter; + final Widget child; + + @override + State createState() => ErrorHandlerState(); +} + +class ErrorHandlerState extends State { + Future _handleError({ + required Object exception, + StackTrace? stackTrace, + }) async { + debugPrint(exception.toString()); + debugPrintStack(stackTrace: stackTrace); + await widget.appRouter.push(ErrorRoute(error: exception.toString())); + } + + @override + void initState() { + FlutterError.onError = (details) { + _handleError(exception: details.exception, stackTrace: details.stack); + }; + WidgetsBinding.instance.platformDispatcher.onError = + (exception, stackTrace) { + _handleError(exception: exception, stackTrace: stackTrace); + return true; + }; + super.initState(); + } + @override + Widget build(BuildContext context) { + return widget.child; + } +} \ No newline at end of file diff --git a/app/lib/common/widgets/module.dart b/app/lib/common/widgets/module.dart index 16c80e06..ee92ae42 100644 --- a/app/lib/common/widgets/module.dart +++ b/app/lib/common/widgets/module.dart @@ -5,7 +5,9 @@ export 'dialog_content_text.dart'; export 'dialog_wrapper.dart'; export 'drug_list/builder.dart'; export 'drug_list/cubit.dart'; +export 'error_handler.dart'; export 'filter_menu.dart'; +export 'full_width_button.dart'; export 'headings.dart'; export 'indicators.dart'; export 'lifecycle_observer.dart'; diff --git a/app/lib/common/widgets/pharme_logo_page.dart b/app/lib/common/widgets/pharme_logo_page.dart index 44595806..72bb1324 100644 --- a/app/lib/common/widgets/pharme_logo_page.dart +++ b/app/lib/common/widgets/pharme_logo_page.dart @@ -4,9 +4,11 @@ class PharMeLogoPage extends StatelessWidget { const PharMeLogoPage({ super.key, this.child, + this.greyscale = false, }); final Widget? child; + final bool greyscale; @override Widget build(BuildContext context) { @@ -17,11 +19,23 @@ class PharMeLogoPage extends StatelessWidget { Expanded( child: Container( alignment: Alignment.center, - child: SvgPicture.asset( - 'assets/images/logo.svg', + child: greyscale + ? ColorFiltered( + colorFilter: ColorFilter.mode( + PharMeTheme.backgroundColor, + BlendMode.softLight, + ), + child: ColorFiltered( + colorFilter: ColorFilter.mode( + PharMeTheme.backgroundColor, + BlendMode.saturation, + ), + child: _buildLogo(context), + ), + ) + : _buildLogo(context), ), ), - ), Container( alignment: Alignment.center, child: child ?? SizedBox.shrink(), @@ -30,4 +44,8 @@ class PharMeLogoPage extends StatelessWidget { ), ); } + + Widget _buildLogo(BuildContext context) { + return SvgPicture.asset('assets/images/logo.svg'); + } } \ No newline at end of file diff --git a/app/lib/drug_selection/pages/drug_selection.dart b/app/lib/drug_selection/pages/drug_selection.dart index ff5b692a..a7c64f40 100644 --- a/app/lib/drug_selection/pages/drug_selection.dart +++ b/app/lib/drug_selection/pages/drug_selection.dart @@ -4,7 +4,6 @@ import '../../common/models/metadata.dart'; import '../../common/module.dart' hide MetaData; import '../../common/widgets/drug_list/drug_items/drug_checkbox_list.dart'; import '../../common/widgets/drug_search.dart'; -import '../../common/widgets/full_width_button.dart'; import '../cubit.dart'; @RoutePage() diff --git a/app/lib/error/module.dart b/app/lib/error/module.dart new file mode 100644 index 00000000..6b7faa3a --- /dev/null +++ b/app/lib/error/module.dart @@ -0,0 +1,10 @@ +import '../common/module.dart'; + +// For generated routes +export 'pages/error.dart'; + +CustomRoute errorRoute() => CustomRoute( + path: '/error', + page: ErrorRoute.page, + transitionsBuilder: TransitionsBuilders.fadeIn, +); diff --git a/app/lib/error/pages/error.dart b/app/lib/error/pages/error.dart new file mode 100644 index 00000000..c628840c --- /dev/null +++ b/app/lib/error/pages/error.dart @@ -0,0 +1,81 @@ +import 'dart:io'; + +import 'package:flutter/gestures.dart'; +import 'package:flutter/services.dart'; + +import '../../common/module.dart'; + +@RoutePage() +class ErrorPage extends StatelessWidget { + const ErrorPage({required this.error, super.key}); + + final String error; + + @override + Widget build(BuildContext context) { + return PopScope( + canPop: false, + child: PharMeLogoPage( + greyscale: true, + child: Column( + children: [ + Text( + context.l10n.error_title, + style: PharMeTheme.textTheme.headlineMedium, + ), + SizedBox(height: PharMeTheme.mediumSpace), + RichText( + textAlign: TextAlign.center, + text: TextSpan( + style: PharMeTheme.textTheme.bodyLarge, + children: [ + TextSpan( + text: context.l10n.error_uncaught_message_first_part, + ), + TextSpan( + text: context.l10n.error_uncaught_message_bold_part, + style: TextStyle(fontWeight: FontWeight.bold), + ) + ], + ), + ), + SizedBox(height: PharMeTheme.mediumSpace), + RichText( + textAlign: TextAlign.center, + text: TextSpan( + style: PharMeTheme.textTheme.bodyLarge, + children: [ + TextSpan(text: context.l10n.error_uncaught_message_contact), + TextSpan( + text: context.l10n.error_contact_link_text, + style: TextStyle( + color: PharMeTheme.secondaryColor, + decoration: TextDecoration.underline, + ), + recognizer: TapGestureRecognizer()..onTap = + () { + sendEmail( + subject: context.l10n.error_mail_subject, + body: context.l10n.error_mail_body(error), + ); + }, + ), + TextSpan( + text: context.l10n.error_uncaught_message_after_link, + ), + ], + ), + ), + SizedBox(height: PharMeTheme.mediumSpace), + FullWidthButton(context.l10n.error_close_app, () async { + if (Platform.isIOS) { + exit(0); + } + await SystemChannels.platform.invokeMethod('SystemNavigator.pop'); + }), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index df557a23..15826fe5 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -4,6 +4,24 @@ "action_continue": "Continue", "action_back_to_app": "Back to app", + "error_title": "Something went wrong", + "error_uncaught_message_first_part": "PharMe has encountered an unknown error. ", + "error_uncaught_message_bold_part": "Please close the app and open it again.", + "error_uncaught_message_contact": "The error has been logged for our technical staff; however, please ", + "error_contact_link_text": "contact us", + "error_uncaught_message_after_link": " if this problem persists.", + "error_close_app": "Close app", + "error_mail_subject": "Unknown PharMe Error Report", + "error_mail_body": "Error: {error} (please keep this line)\n\n", + "@error_mail_body": { + "placeholders": { + "error": { + "type": "String", + "example": "Exception" + } + } + }, + "auth_welcome": "Welcome to PharMe", "auth_choose_lab": "Please select your data provider", "auth_sign_in": "Get data", diff --git a/app/lib/login/pages/login.dart b/app/lib/login/pages/login.dart index 8027eef8..f5081e30 100644 --- a/app/lib/login/pages/login.dart +++ b/app/lib/login/pages/login.dart @@ -2,7 +2,6 @@ import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:provider/provider.dart'; import '../../../common/module.dart'; -import '../../common/widgets/full_width_button.dart'; import '../cubit.dart'; import '../models/lab.dart';