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

Draft
wants to merge 15 commits into
base: dev
Choose a base branch
from
Draft
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
9 changes: 8 additions & 1 deletion app/lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -213,5 +213,12 @@
}
},
"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.",
"retry": "Retry",
"retry_description": "Try again",
"error_no_internet": "No internet connection"
}
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';
}
}
50 changes: 45 additions & 5 deletions app/lib/screens/app_onboarding/loading_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import 'dart:io';
import 'package:flutter/material.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 +45,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 @@ -89,6 +104,7 @@ class _LoadingScreenState extends State<LoadingScreen> {
debugPrint(
"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 +117,34 @@ 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)!.join_new_study,
() async {
await cancelNotifications(context);
await deleteActiveStudyReference();
if (!mounted) return;
Navigator.pushReplacementNamed(context, Routes.welcome);
},
actionDescription:
AppLocalizations.of(context)!.join_new_study_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
42 changes: 34 additions & 8 deletions app/lib/screens/study/dashboard/settings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@ import 'package:provider/provider.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';
import 'package:studyu_flutter_common/studyu_flutter_common.dart';

import '../../../models/app_error.dart';

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

Expand Down Expand Up @@ -186,14 +189,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, "Connection error");
} on AppError catch (e) {
ErrorHandler.showSnackbar(context, e.message);
} catch (e) {
StudyULogger.error(e.toString());
ErrorHandler.showSnackbar(
context,
Routes.studySelection,
(_) => false,
"An error occured while opting out. Please try again later",
);
}
},
Expand Down Expand Up @@ -231,7 +246,18 @@ class DeleteAlertDialog extends StatelessWidget {
(_) => false,
);
}
} on SocketException catch (_) {}
} on SocketException catch (_) {
ErrorHandler.showSnackbar(context, "Connection error");
} on AppError catch (e) {
ErrorHandler.showSnackbar(context, e.message);
} catch (e) {
StudyULogger.error(e.toString());

ErrorHandler.showSnackbar(
context,
"An error occured while deleting data. Please try again later",
);
}
},
),
],
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,
'An error occurred while preparing the study for you.',
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
19 changes: 16 additions & 3 deletions app/lib/screens/study/tasks/task_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import 'dart:io';
import 'package:flutter/material.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/screens/study/tasks/intervention/checkmark_task_widget.dart';
import 'package:studyu_app/screens/study/tasks/observation/questionnaire_task_widget.dart';
import 'package:studyu_app/util/cache.dart';
import 'package:studyu_app/util/error_handler.dart';
import 'package:studyu_app/widgets/html_text.dart';
import 'package:studyu_core/core.dart';

Expand Down Expand Up @@ -95,9 +97,20 @@ Future<void> handleTaskCompletion(
} on SocketException {
await Cache.storeSubject(activeSubject);
} catch (exception) {
debugPrint("Could not save results");
StudyULogger.error(exception);
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
ErrorHandler.handleError(
context,
AppError(
AppErrorTypes.unknown,
AppLocalizations.of(context)!.could_not_save_results,
actions: [
ErrorAction(AppLocalizations.of(context)!.retry,
() => handleTaskCompletion(context, completionCallback)),
],
),
);
/*ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context)!.could_not_save_results),
duration: const Duration(seconds: 10),
Expand All @@ -106,7 +119,7 @@ Future<void> handleTaskCompletion(
onPressed: () => handleTaskCompletion(context, completionCallback),
),
),
);
);*/
rethrow;
}
}
Loading
Loading