Skip to content

Commit

Permalink
Report error to sentry if initialization of userCodeModel fails.
Browse files Browse the repository at this point in the history
Also, if initialization fails, do not implicitly use empty array of user codes, but show error.

Also throw Errors when using UserCodeModel when it is not properly initialized.

Also add a loading spinner and some error popup to deeplink activation.
  • Loading branch information
michael-markl committed Oct 1, 2024
1 parent f2f96eb commit 0124851
Show file tree
Hide file tree
Showing 11 changed files with 190 additions and 92 deletions.
3 changes: 2 additions & 1 deletion frontend/assets/l10n/app_de.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@
"openSettings": "Einstellungen öffnen",
"previous": "Zurück",
"settings": "Einstellungen",
"tryAgain": "Erneut versuchen"
"tryAgain": "Erneut versuchen",
"unknownError": "Ein unbekannter Fehler ist aufgetreten."
},
"deeplinkActivation": {
"headline": "Karte aktivieren",
Expand Down
3 changes: 2 additions & 1 deletion frontend/assets/l10n/app_en.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@
"openSettings": "Open settings",
"previous": "Back",
"settings": "Settings",
"tryAgain": "Try again"
"tryAgain": "Try again",
"unknownError": "An unknown error occurred."
},
"identification": {
"activate": "Activate",
Expand Down
12 changes: 6 additions & 6 deletions frontend/lib/about/backend_switch_dialog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -74,18 +74,18 @@ class BackendSwitchDialogState extends State<BackendSwitchDialog> {
);
}

void switchBackendUrl(BuildContext context) {
Future<void> switchBackendUrl(BuildContext context) async {
final settings = Provider.of<SettingsModel>(context, listen: false);
final updatedEnableStaging = !settings.enableStaging;
clearData();
settings.setEnableStaging(enabled: updatedEnableStaging);
await clearData();
await settings.setEnableStaging(enabled: updatedEnableStaging);
Navigator.of(context, rootNavigator: true).pop();
}

void clearData() {
Future<void> clearData() async {
final settings = Provider.of<SettingsModel>(context, listen: false);
final userCodesModel = Provider.of<UserCodeModel>(context, listen: false);
settings.clearSettings();
userCodesModel.removeCodes();
await settings.clearSettings();
await userCodesModel.removeCodes();
}
}
2 changes: 1 addition & 1 deletion frontend/lib/about/dev_settings_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ class DevSettingsView extends StatelessWidget {
}

Future<void> _resetEakData(BuildContext context, UserCodeModel userCodesModel) async {
userCodesModel.removeCodes();
await userCodesModel.removeCodes();
}

DynamicUserCode _determineUserCode(String projectId) {
Expand Down
136 changes: 95 additions & 41 deletions frontend/lib/activation/deeplink_activation.dart
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,38 @@ enum DeepLinkActivationStatus {
}
}

class DeepLinkActivation extends StatelessWidget {
class DeepLinkActivation extends StatefulWidget {
final String base64qrcode;

const DeepLinkActivation({super.key, required this.base64qrcode});

@override
State<DeepLinkActivation> createState() => _DeepLinkActivationState();
}

enum _State {
waiting,
loading,
success,
}

class _DeepLinkActivationState extends State<DeepLinkActivation> {
_State _state = _State.waiting;

@override
Widget build(BuildContext context) {
final t = context.t;
DynamicActivationCode? activationCode = getActivationCode(context, base64qrcode);
DynamicActivationCode? activationCode = getActivationCode(context, widget.base64qrcode);
CardInfo? cardInfo = activationCode?.info;
final userCodeModel = Provider.of<UserCodeModel>(context, listen: false);
final userCodeModel = Provider.of<UserCodeModel>(context);

if (!userCodeModel.isInitialized) {
if (userCodeModel.initializationFailed) {
return SafeArea(child: Center(child: Text(t.common.unknownError, textAlign: TextAlign.center)));
}
return Container();
}

final status = DeepLinkActivationStatus.from(userCodeModel, activationCode);
final regionId = cardInfo?.extensions.extensionRegion.regionId ?? -1;
final regionsQuery = GetRegionsByIdQuery(
Expand All @@ -60,9 +81,6 @@ class DeepLinkActivation extends StatelessWidget {
ids: [regionId],
),
);

final String cardsInUse = userCodeModel.userCodes.length.toString();
final String maxCardAmount = buildConfig.maxCardAmount.toString();
final theme = Theme.of(context);

return Query(
Expand Down Expand Up @@ -97,34 +115,56 @@ class DeepLinkActivation extends StatelessWidget {
)),
Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
...switch (status) {
DeepLinkActivationStatus.invalidLink => [_WarningText(t.deeplinkActivation.invalidCode)],
DeepLinkActivationStatus.limitReached => [
_WarningText('${t.deeplinkActivation.limitReached} ($cardsInUse/$maxCardAmount)')
],
DeepLinkActivationStatus.alreadyExists => [
_WarningText(t.deeplinkActivation.alreadyExists)
],
DeepLinkActivationStatus.valid => [
ElevatedButton(
onPressed: activationCode != null
? () {
activateCard(
context,
() => GoRouter.of(context)
.pushReplacement('$homeRouteName/$identityTabIndex'),
activationCode);
}
: null,
child: Text(t.deeplinkActivation.buttonText),
child: Column(mainAxisSize: MainAxisSize.min, children: [
if (_state == _State.waiting) _WarningText(status, userCodeModel),
ElevatedButton.icon(
onPressed: activationCode != null &&
_state == _State.waiting &&
status == DeepLinkActivationStatus.valid
? () async {
setState(() {
_state = _State.loading;
});
try {
final activated = await activateCard(context, activationCode);
if (activated) {
GoRouter.of(context).pushReplacement('$homeRouteName/$identityTabIndex');
setState(() {
_state = _State.success;
});
} else {
setState(() {
_state = _State.waiting;
});
}
} catch (_) {
setState(() {
_state = _State.waiting;
});
// TODO 1656: Improve error handling!!
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: Theme.of(context).colorScheme.primary,
content: Text(t.common.unknownError),
),
);
rethrow;
}
}
: null,
icon: _state != _State.waiting
? Container(
width: 24,
height: 24,
padding: const EdgeInsets.all(2.0),
child: const CircularProgressIndicator(
strokeWidth: 3,
),
)
],
},
],
),
: const Icon(Icons.add_card),
label: Text(t.deeplinkActivation.buttonText),
)
]),
),
],
)),
Expand All @@ -134,18 +174,32 @@ class DeepLinkActivation extends StatelessWidget {
}

class _WarningText extends StatelessWidget {
final String text;
final DeepLinkActivationStatus status;
final UserCodeModel userCodeModel;

const _WarningText(this.text);
const _WarningText(this.status, this.userCodeModel);

@override
Widget build(BuildContext context) {
return Column(
children: [
Icon(Icons.warning, color: Theme.of(context).colorScheme.secondary),
Text(text, textAlign: TextAlign.center)
],
);
final String cardsInUse = userCodeModel.userCodes.length.toString();
final String maxCardAmount = buildConfig.maxCardAmount.toString();
final text = switch (status) {
DeepLinkActivationStatus.invalidLink => t.deeplinkActivation.activationInvalid,
DeepLinkActivationStatus.limitReached => '${t.deeplinkActivation.limitReached} ($cardsInUse/$maxCardAmount)',
DeepLinkActivationStatus.alreadyExists => t.deeplinkActivation.alreadyExists,
DeepLinkActivationStatus.valid => '',
};
if (status == DeepLinkActivationStatus.valid) {
return Container();
}
return Padding(
padding: EdgeInsets.symmetric(vertical: 8),
child: Column(
children: [
Icon(Icons.warning, color: Theme.of(context).colorScheme.secondary),
Text(text, textAlign: TextAlign.center)
],
));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@ class ActivationCodeScannerPage extends StatelessWidget {
try {
final activationCode = const ActivationCodeParser().parseQrCodeContent(code);

await activateCard(context, moveToLastCard, activationCode);
final activated = await activateCard(context, activationCode);
if (activated) {
moveToLastCard();
}
} on ActivationDidNotOverwriteExisting catch (_) {
await showError(t.identification.cardAlreadyActivated, null);
} on QrCodeFieldMissingException catch (e) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import 'package:flutter/material.dart';

import 'package:ehrenamtskarte/l10n/translations.g.dart';

class ActivationExistingCardDialog extends StatelessWidget {
const ActivationExistingCardDialog({super.key});

Expand All @@ -18,21 +20,20 @@ class ActivationExistingCardDialog extends StatelessWidget {
),
actions: <Widget>[
TextButton(
child: const Text('Ok'),
child: Text(context.t.common.ok),
onPressed: () {
Navigator.of(context).pop(false);
Navigator.of(context).pop();
},
)
],
);
}

/// Returns true, if the user wants to activate an existing card
static Future<bool> showExistingCardDialog(BuildContext context) async {
return await showDialog<bool>(
context: context,
builder: (context) => ActivationExistingCardDialog(),
) ??
false;
static Future<void> showExistingCardDialog(BuildContext context) async {
return await showDialog(
context: context,
builder: (context) => ActivationExistingCardDialog(),
);
}
}
4 changes: 4 additions & 0 deletions frontend/lib/identification/identification_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import 'package:ehrenamtskarte/identification/qr_code_scanner/qr_code_camera_per
import 'package:ehrenamtskarte/identification/user_code_model.dart';
import 'package:ehrenamtskarte/identification/verification_workflow/dialogs/remove_card_confirmation_dialog.dart';
import 'package:ehrenamtskarte/identification/verification_workflow/verification_workflow.dart';
import 'package:ehrenamtskarte/l10n/translations.g.dart';
import 'package:ehrenamtskarte/proto/card.pb.dart';
import 'package:ehrenamtskarte/routing.dart';
import 'package:flutter/material.dart';
Expand All @@ -34,6 +35,9 @@ class IdentificationPageState extends State<IdentificationPage> {
return Consumer<UserCodeModel>(
builder: (context, userCodeModel, child) {
if (!userCodeModel.isInitialized) {
if (userCodeModel.initializationFailed) {
return SafeArea(child: Center(child: Text(context.t.common.unknownError, textAlign: TextAlign.center)));
}
return Container();
}

Expand Down
Loading

0 comments on commit 0124851

Please sign in to comment.