Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: app error handling #693

Open
wants to merge 18 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 14 additions & 1 deletion app/lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -213,5 +213,18 @@
}
},
"app_outdated_message": "A new version of the StudyU App is available. Please update to get the latest features and improvements. Thank you for your support!",
"update_now": "Update now"
"update_now": "Update now",

"error_missing_study": "Could not login and retrieve the study subject. \nOne reason for this might be that the study subject is no longer available and only resides in app backup",
"join_new_study": "Join a new study",
"join_new_study_description": "Join a new study to continue using the app. Your previous study data will be deleted.",
"delete_app_cache": "Delete App Cache",
"delete_app_cache_description": "Delete application cache for a clean setup. Your previous study data will be deleted.",
"retry": "Retry",
"retry_description": "Try again",
"error_no_internet": "No internet connection",
"current_report": "Current report",
"delete_data_error": "An error occured while deleting data. Please try again later",
"generic_error_try_again": "An error occured. Please try again later",
"preparing_study_error": "An error occurred while preparing the study for you."
}
28 changes: 28 additions & 0 deletions app/lib/models/app_error.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
enum AppErrorTypes {
retrieveSubject,
storeSubject,
network,
notification,
unknown,
}

class ErrorAction {
final String actionText;
final String? actionDescription;
final Future<void> Function() callback;

ErrorAction(this.actionText, this.callback, {this.actionDescription});
}

class AppError {
final AppErrorTypes type;
final String message;
final List<ErrorAction>? actions;

AppError(this.type, this.message, {this.actions});

@override
String toString() {
return '$message, type: $type';
}
}
61 changes: 55 additions & 6 deletions app/lib/screens/app_onboarding/loading_screen.dart
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import 'dart:io';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:provider/provider.dart';
import 'package:studyu_app/models/app_error.dart';
import 'package:studyu_app/models/app_state.dart';
import 'package:studyu_app/routes.dart';
import 'package:studyu_app/screens/app_onboarding/iframe_helper.dart';
import 'package:studyu_app/screens/app_onboarding/preview.dart';
import 'package:studyu_app/screens/study/onboarding/eligibility_screen.dart';
import 'package:studyu_app/screens/study/tasks/task_screen.dart';
import 'package:studyu_app/util/cache.dart';
import 'package:studyu_app/util/error_handler.dart';
import 'package:studyu_app/util/schedule_notifications.dart';
import 'package:studyu_core/core.dart';
import 'package:studyu_flutter_common/studyu_flutter_common.dart';
Expand Down Expand Up @@ -43,7 +46,20 @@ class _LoadingScreenState extends State<LoadingScreen> {
await noSubjectFound();
return;
}
StudySubject? subject = await _retrieveSubject(selectedSubjectId);

StudySubject? subject;

try {
subject = await _retrieveSubject(selectedSubjectId);
} catch (error) {
if (error is AppError && error.type == AppErrorTypes.retrieveSubject) {
if (!mounted) return;
await ErrorHandler.handleError(context, error);

return;
}
}

if (!mounted) return;
if (subject != null) {
subject = await Cache.synchronize(subject);
Expand Down Expand Up @@ -86,9 +102,10 @@ class _LoadingScreenState extends State<LoadingScreen> {
subject = await _fetchRemoteSubject(selectedStudyObjectId);
}
} catch (exception) {
debugPrint(
StudyULogger.debug(
"Could not login and retrieve the study subject: $exception",
);

if (exception is SocketException) {
subject = await Cache.loadSubject();
StudyULogger.info("Offline mode with cached subject: $subject");
Expand All @@ -101,10 +118,42 @@ class _LoadingScreenState extends State<LoadingScreen> {
// 4. Open the app but do not join a study
// 5. Restart the app. Either only this error shows up, worst case is
// app hangs and is unresponsive
StudyULogger.fatal('Could not login and retrieve the study subject. '
'One reason for this might be that the study subject is no '
'longer available and only resides in app backup');
throw Exception("Remote subject not found");

throw AppError(
AppErrorTypes.retrieveSubject,
AppLocalizations.of(context)!.error_missing_study,
actions: [
ErrorAction(
AppLocalizations.of(context)!.delete_app_cache,
() async {
await ErrorHandler.deleteCacheDir();
await ErrorHandler.deleteAppDir();
await SecureStorage.deleteAll();
if (context.mounted) {
ErrorHandler.showSnackbar(
context,
'App reset successfully! Please restart the app.',
);
await Future.delayed(const Duration(seconds: 1));
await SystemChannels.platform
.invokeMethod('SystemNavigator.pop');
}
},
actionDescription:
AppLocalizations.of(context)!.delete_app_cache_description,
),
ErrorAction(
AppLocalizations.of(context)!.retry,
() async {
Navigator.of(context).pop();

await initStudy();
},
actionDescription:
AppLocalizations.of(context)!.retry_description,
),
],
);
}
}
}
Expand Down
44 changes: 30 additions & 14 deletions app/lib/screens/app_onboarding/preview.dart
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,17 @@ class Preview {
return queryParameters!.containsKey(key) && queryParameters![key] == value;
}

Future<StudySubject?> _fetchRemoteSubject(String selectedStudyObjectId) {
return SupabaseQuery.getById<StudySubject>(
selectedStudyObjectId,
selectedColumns: [
'*',
'study!study_subject_studyId_fkey(*)',
'subject_progress(*)',
],
);
}

/// createSubject: If true, the method will return a new StudySubject if none can be found. Otherwise, null is returned
Future<StudySubject?> getStudySubject(
AppState state, {
Expand Down Expand Up @@ -158,23 +169,27 @@ class Preview {
// User is already subscribed to a study
return subject;
}
subject = await SupabaseQuery.getById<StudySubject>(
selectedStudyObjectId!,
selectedColumns: [
'*',
'study!study_subject_studyId_fkey(*)',
'subject_progress(*)',
],
);
if (subject != null && subject!.studyId == study!.id) {
subject = await _fetchRemoteSubject(selectedStudyObjectId!);

if (subject != null && subject!.studyId == study!.id) return subject;
} catch (e) {
try {
StudyULogger.info(
'[PreviewApp]: Failed fetching subject: $e. Trying to sign in participant',);

if (await signInParticipant()) {
subject = await _fetchRemoteSubject(selectedStudyObjectId!);
}

// User is already subscribed to the study
return subject;
if (subject != null && subject!.studyId == study!.id) return subject;
} catch (e) {
StudyULogger.error(
'[PreviewApp]: Failed fetching subject after sign in: $e',);
}
} catch (e) {
print('[PreviewApp]: Failed fetching subject: $e');
// todo try sign in again if token expired see loading screen
}
}

if (createSubject) {
// Create a new study subject
subject = await _createFakeSubject(state);
Expand Down Expand Up @@ -209,9 +224,10 @@ class Preview {
await storeActiveSubjectId(subject!.id);
// print("[PreviewApp]: Saved subject");
} catch (e) {
print('[PreviewApp]: Failed creating subject: $e');
StudyULogger.error('[PreviewApp]: Failed creating subject: $e');
}
}

return subject;
}

Expand Down
2 changes: 1 addition & 1 deletion app/lib/screens/study/dashboard/dashboard.dart
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ class _DashboardScreenState extends State<DashboardScreen>
},
),
IconButton(
tooltip: 'Current report', // todo tr
tooltip: AppLocalizations.of(context)!.current_report,
icon: Icon(MdiIcons.chartBar),
onPressed: () => Navigator.push(
context,
Expand Down
41 changes: 33 additions & 8 deletions app/lib/screens/study/dashboard/settings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
import 'package:provider/provider.dart';
import 'package:studyu_app/models/app_error.dart';
import 'package:studyu_app/models/app_state.dart';
import 'package:studyu_app/routes.dart';
import 'package:studyu_app/util/app_analytics.dart';
import 'package:studyu_app/util/error_handler.dart';
import 'package:studyu_app/util/localization.dart';
import 'package:studyu_app/util/schedule_notifications.dart';
import 'package:studyu_core/core.dart';
Expand Down Expand Up @@ -186,14 +188,26 @@ class OptOutAlertDialog extends StatelessWidget {
label: Text(AppLocalizations.of(context)!.opt_out),
style: ElevatedButton.styleFrom(backgroundColor: Colors.orange[800]),
onPressed: () async {
await subject!.softDelete();
await deleteActiveStudyReference();
if (context.mounted) await cancelNotifications(context);
if (context.mounted) {
Navigator.pushNamedAndRemoveUntil(
try {
await subject!.softDelete();
await deleteActiveStudyReference();
if (context.mounted) await cancelNotifications(context);
if (context.mounted) {
Navigator.pushNamedAndRemoveUntil(
context,
Routes.studySelection,
(_) => false,
);
}
} on SocketException catch (_) {
ErrorHandler.showSnackbar(context, AppLocalizations.of(context)!.error_no_internet);
} on AppError catch (e) {
ErrorHandler.showSnackbar(context, e.message);
} catch (e) {
StudyULogger.error(e.toString());
ErrorHandler.showSnackbar(
context,
Routes.studySelection,
(_) => false,
AppLocalizations.of(context)!.generic_error_try_again,
);
}
},
Expand Down Expand Up @@ -231,7 +245,18 @@ class DeleteAlertDialog extends StatelessWidget {
(_) => false,
);
}
} on SocketException catch (_) {}
} on SocketException catch (_) {
ErrorHandler.showSnackbar(context, AppLocalizations.of(context)!.error_no_internet);
} on AppError catch (e) {
ErrorHandler.showSnackbar(context, e.message);
} catch (e) {
StudyULogger.error(e.toString());

ErrorHandler.showSnackbar(
context,
AppLocalizations.of(context)!.delete_data_error,
);
}
},
),
],
Expand Down
31 changes: 31 additions & 0 deletions app/lib/screens/study/onboarding/kickoff.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import 'dart:io';

import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
import 'package:provider/provider.dart';
import 'package:studyu_app/models/app_error.dart';
import 'package:studyu_app/models/app_state.dart';
import 'package:studyu_app/routes.dart';
import 'package:studyu_app/util/cache.dart';
import 'package:studyu_app/util/error_handler.dart';
import 'package:studyu_core/core.dart';
import 'package:studyu_flutter_common/studyu_flutter_common.dart';

Expand Down Expand Up @@ -37,7 +41,34 @@ class _KickoffScreen extends State<KickoffScreen> {
Routes.dashboard,
(_) => false,
);
} on SocketException catch (e) {
ErrorHandler.showSnackbar(
context,
AppLocalizations.of(context)!.error_no_internet,
);

StudyULogger.fatal('Failed creating subject: $e');
} catch (e) {
ErrorHandler.handleError(
context,
AppError(AppErrorTypes.storeSubject,
AppLocalizations.of(context)!.preparing_study_error,
actions: [
ErrorAction(AppLocalizations.of(context)!.retry,
() => _storeUserStudy(context),
actionDescription:
AppLocalizations.of(context)!.retry_description,),
ErrorAction(
AppLocalizations.of(context)!.join_new_study,
() => Navigator.pushNamedAndRemoveUntil(
context,
Routes.studySelection,
(_) => false,
),
actionDescription:
AppLocalizations.of(context)!.join_new_study_description,
),
],),);
StudyULogger.fatal('Failed creating subject: $e');
}
}
Expand Down
4 changes: 2 additions & 2 deletions app/lib/screens/study/onboarding/study_selection.dart
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ class _InviteCodeDialogState extends State<InviteCodeDialog> {
.select()
.single();
} on PostgrestException catch (error) {
print(error.message);
StudyULogger.error(error.message);
setState(() {
_errorMessage = error.message;
});
Expand All @@ -272,7 +272,7 @@ class _InviteCodeDialogState extends State<InviteCodeDialog> {
params: {'invite_code': _controller.text},
).single();
} on PostgrestException catch (error) {
print(error.message);
StudyULogger.error(error.message);
setState(() {
_errorMessage = error.message;
});
Expand Down
Loading
Loading