diff --git a/frontend/pshared/lib/provider/account.dart b/frontend/pshared/lib/provider/account.dart index 9b88dced..07f9dabc 100644 --- a/frontend/pshared/lib/provider/account.dart +++ b/frontend/pshared/lib/provider/account.dart @@ -191,6 +191,15 @@ class AccountProvider extends ChangeNotifier { } } + Future resendVerificationEmail(String email) async { + try { + await AccountService.resendVerificationEmail(email); + } catch (e) { + _setResource(_resource.copyWith(error: toException(e))); + rethrow; + } + } + Future logout() async { _authState = AuthState.empty; _setResource(_resource.copyWith(isLoading: true, error: null)); diff --git a/frontend/pshared/lib/provider/email_verification.dart b/frontend/pshared/lib/provider/email_verification.dart new file mode 100644 index 00000000..3d1f21f0 --- /dev/null +++ b/frontend/pshared/lib/provider/email_verification.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; + +import 'package:pshared/provider/resource.dart'; +import 'package:pshared/service/account.dart'; +import 'package:pshared/utils/exception.dart'; + + +class EmailVerificationProvider extends ChangeNotifier { + Resource _resource = Resource(data: null, isLoading: false); + String? _token; + + Resource get resource => _resource; + bool get isLoading => _resource.isLoading; + bool get isSuccess => _resource.data == true; + Exception? get error => _resource.error; + + Future verify(String token) async { + final trimmed = token.trim(); + if (trimmed.isEmpty) { + _setResource( + Resource( + data: null, + isLoading: false, + error: Exception('Email verification token is empty.'), + ), + ); + return; + } + if (_token == trimmed && _resource.isLoading) return; + _token = trimmed; + _setResource(Resource(data: null, isLoading: true)); + try { + await AccountService.verifyEmail(trimmed); + _setResource(Resource(data: true, isLoading: false)); + } catch (e) { + _setResource( + Resource( + data: null, + isLoading: false, + error: toException(e), + ), + ); + } + } + + void _setResource(Resource resource) { + _resource = resource; + notifyListeners(); + } +} diff --git a/frontend/pshared/lib/service/account.dart b/frontend/pshared/lib/service/account.dart index ec0b97d7..f3a2f203 100644 --- a/frontend/pshared/lib/service/account.dart +++ b/frontend/pshared/lib/service/account.dart @@ -57,6 +57,16 @@ class AccountService { await getPUTResponse(_objectType, 'password', ForgotPasswordRequest.build(login: email).toJson()); } + static Future resendVerificationEmail(String email) async { + _logger.fine('Resending verification email'); + await getPUTResponse(_objectType, 'email', {'login': email}); + } + + static Future verifyEmail(String token) async { + _logger.fine('Verifying email'); + await getGETResponse(_objectType, 'verify/$token'); + } + static Future resetPassword(String accountRef, String token, String newPassword) async { _logger.fine('Resetting password for account: $accountRef'); await getPOSTResponse(_objectType, 'password/reset/$accountRef/$token', ResetPasswordRequest.build(password: newPassword).toJson()); diff --git a/frontend/pweb/lib/app/router/pages.dart b/frontend/pweb/lib/app/router/pages.dart index a7e0005f..c5e9ee8b 100644 --- a/frontend/pweb/lib/app/router/pages.dart +++ b/frontend/pweb/lib/app/router/pages.dart @@ -9,6 +9,7 @@ enum Pages { methods, verify, signup, + signupConfirm, settings, dashboard, profile, @@ -57,4 +58,4 @@ void navigateNamedAndReplace(BuildContext context, Pages page, {String? objectRe void navigateNext(BuildContext context, Pages page, {Object? extra}) { WidgetsBinding.instance.addPostFrameCallback((_) => navigate(context, page, extra: extra)); -} \ No newline at end of file +} diff --git a/frontend/pweb/lib/app/router/router.dart b/frontend/pweb/lib/app/router/router.dart index 7a0f81d3..cc961aab 100644 --- a/frontend/pweb/lib/app/router/router.dart +++ b/frontend/pweb/lib/app/router/router.dart @@ -8,6 +8,8 @@ import 'package:pweb/pages/2fa/page.dart'; import 'package:pweb/pages/errors/not_found.dart'; import 'package:pweb/pages/login/page.dart'; import 'package:pweb/pages/signup/page.dart'; +import 'package:pweb/pages/signup/confirmation/args.dart'; +import 'package:pweb/pages/signup/confirmation/page.dart'; import 'package:pweb/pages/verification/page.dart'; @@ -36,6 +38,15 @@ GoRouter createRouter() => GoRouter( path: routerPage(Pages.signup), builder: (_, _) => const SignUpPage(), ), + GoRoute( + name: Pages.signupConfirm.name, + path: routerPage(Pages.signupConfirm), + builder: (_, state) => SignUpConfirmationPage( + email: state.extra is SignupConfirmationArgs + ? (state.extra as SignupConfirmationArgs).email + : null, + ), + ), GoRoute( name: Pages.verify.name, path: '${routerPage(Pages.verify)}${routerAddParam(PageParams.token)}', diff --git a/frontend/pweb/lib/l10n/en.arb b/frontend/pweb/lib/l10n/en.arb index 3f58ce6d..9ee98a49 100644 --- a/frontend/pweb/lib/l10n/en.arb +++ b/frontend/pweb/lib/l10n/en.arb @@ -41,6 +41,30 @@ "goToSignUp": "Go to Sign Up", "signupError": "Failed to signup: {error}", "signupSuccess": "Email confirmation message has been sent to {email}. Please, open it and click link to activate your account.", + "signupConfirmationTitle": "Confirm your email", + "signupConfirmationDescription": "We sent a confirmation link to {email}. Open it and click the link to activate your account.", + "@signupConfirmationDescription": { + "placeholders": { + "email": {} + } + }, + "signupConfirmationDescriptionNoEmail": "We sent a confirmation link to your email. Open it and click the link to activate your account.", + "signupConfirmationResend": "Resend email", + "signupConfirmationResendCooldown": "Resend in {time}", + "@signupConfirmationResendCooldown": { + "placeholders": { + "time": {} + } + }, + "signupConfirmationResent": "Verification email resent to {email}.", + "@signupConfirmationResent": { + "placeholders": { + "email": {} + } + }, + "signupConfirmationResendError": "Failed to resend verification email", + "signupConfirmationLoginTitle": "Log in after confirmation", + "signupConfirmationLoginHint": "Once your email is confirmed, you can sign in below.", "connectivityError": "Cannot reach the server at {serverAddress}. Check your network and try again.", "errorAccountNotVerified": "Your account hasn't been verified yet. Please check your email to complete the verification", "errorLoginUnauthorized": "Login or password is incorrect. Please try again", diff --git a/frontend/pweb/lib/l10n/ru.arb b/frontend/pweb/lib/l10n/ru.arb index 829cb907..12f7bcbc 100644 --- a/frontend/pweb/lib/l10n/ru.arb +++ b/frontend/pweb/lib/l10n/ru.arb @@ -41,6 +41,30 @@ "goToSignUp": "Перейти к регистрации", "signupError": "Не удалось зарегистрироваться: {error}", "signupSuccess": "Письмо с подтверждением email отправлено на {email}. Пожалуйста, откройте его и перейдите по ссылке для активации вашего аккаунта.", + "signupConfirmationTitle": "Подтвердите email", + "signupConfirmationDescription": "Мы отправили ссылку для подтверждения на {email}. Откройте письмо и перейдите по ссылке, чтобы активировать аккаунт.", + "@signupConfirmationDescription": { + "placeholders": { + "email": {} + } + }, + "signupConfirmationDescriptionNoEmail": "Мы отправили ссылку для подтверждения на ваш email. Откройте письмо и перейдите по ссылке, чтобы активировать аккаунт.", + "signupConfirmationResend": "Отправить письмо снова", + "signupConfirmationResendCooldown": "Повторная отправка через {time}", + "@signupConfirmationResendCooldown": { + "placeholders": { + "time": {} + } + }, + "signupConfirmationResent": "Письмо с подтверждением повторно отправлено на {email}.", + "@signupConfirmationResent": { + "placeholders": { + "email": {} + } + }, + "signupConfirmationResendError": "Не удалось повторно отправить письмо с подтверждением", + "signupConfirmationLoginTitle": "Войдите после подтверждения", + "signupConfirmationLoginHint": "После подтверждения email вы можете войти ниже.", "connectivityError": "Не удается связаться с сервером {serverAddress}. Проверьте ваше интернет-соединение и попробуйте снова.", "errorAccountNotVerified": "Ваш аккаунт еще не подтвержден. Пожалуйста, проверьте вашу электронную почту для завершения верификации", "errorLoginUnauthorized": "Неверный логин или пароль. Пожалуйста, попробуйте снова", diff --git a/frontend/pweb/lib/main.dart b/frontend/pweb/lib/main.dart index ae9e0278..2c6816c1 100644 --- a/frontend/pweb/lib/main.dart +++ b/frontend/pweb/lib/main.dart @@ -17,6 +17,7 @@ import 'package:pshared/provider/organizations.dart'; import 'package:pshared/provider/accounts/employees.dart'; import 'package:pshared/provider/recipient/pmethods.dart'; import 'package:pshared/provider/recipient/provider.dart'; +import 'package:pshared/provider/email_verification.dart'; import 'package:pshared/provider/ledger.dart'; import 'package:pshared/provider/payment/wallets.dart'; import 'package:pshared/provider/invitations.dart'; @@ -77,6 +78,8 @@ void main() async { create: (_) => EmployeesProvider(), update: (context, organizations, provider) => provider!..updateProviders(organizations), ), + ChangeNotifierProvider(create: (_) => EmailVerificationProvider()), + ChangeNotifierProvider( create: (_) => UploadHistoryProvider(service: MockUploadHistoryService())..load(), ), diff --git a/frontend/pweb/lib/pages/login/form.dart b/frontend/pweb/lib/pages/login/form.dart index 29b1dce3..28224fb9 100644 --- a/frontend/pweb/lib/pages/login/form.dart +++ b/frontend/pweb/lib/pages/login/form.dart @@ -22,7 +22,9 @@ import 'package:pweb/generated/i18n/app_localizations.dart'; class LoginForm extends StatefulWidget { - const LoginForm({super.key}); + final String? initialEmail; + + const LoginForm({super.key, this.initialEmail}); @override State createState() => _LoginFormState(); @@ -37,6 +39,16 @@ class _LoginFormState extends State { final ValueNotifier _isUsernameAcceptable = ValueNotifier(false); final ValueNotifier _isPasswordAcceptable = ValueNotifier(false); + @override + void initState() { + super.initState(); + final initialEmail = widget.initialEmail?.trim(); + if (initialEmail != null && initialEmail.isNotEmpty) { + _usernameController.text = initialEmail; + _isUsernameAcceptable.value = true; + } + } + Future _login(BuildContext context, VoidCallback onLogin, void Function(Object e) onError) async { final provider = Provider.of(context, listen: false); diff --git a/frontend/pweb/lib/pages/settings/widgets/base.dart b/frontend/pweb/lib/pages/settings/widgets/base.dart index 674e7fd9..51d5c8a8 100644 --- a/frontend/pweb/lib/pages/settings/widgets/base.dart +++ b/frontend/pweb/lib/pages/settings/widgets/base.dart @@ -81,17 +81,17 @@ class _BaseEditTileBodyState extends State<_BaseEditTileBody> { return; } _stateNotifier.value = EditState.saving; - final sms = ScaffoldMessenger.of(context); + final scaffoldMessenger = ScaffoldMessenger.of(context); final locs = AppLocalizations.of(context)!; try { await widget.delegate.valueSetter(newValue); - sms.showSnackBar(SnackBar( + scaffoldMessenger.showSnackBar(SnackBar( content: Text(locs.settingsSuccessfullyUpdated), duration: const Duration(milliseconds: 1200), )); } catch (e) { - notifyUserOfErrorX( - scaffoldMessenger: sms, + showErrorSnackBar( + scaffoldMessenger: scaffoldMessenger, errorSituation: widget.delegate.errorSituation, appLocalizations: locs, exception: e, @@ -123,6 +123,7 @@ class _BaseEditTileBodyState extends State<_BaseEditTileBody> { ); }, ); + if (!mounted) return; if (result != null) await _performSave(result); } diff --git a/frontend/pweb/lib/pages/settings/widgets/image.dart b/frontend/pweb/lib/pages/settings/widgets/image.dart index ed076bb9..139f1c86 100644 --- a/frontend/pweb/lib/pages/settings/widgets/image.dart +++ b/frontend/pweb/lib/pages/settings/widgets/image.dart @@ -41,8 +41,8 @@ class ImageTile extends AbstractSettingsTile { Future _pickImage(BuildContext context) async { final picker = ImagePicker(); + final scaffoldMessenger = ScaffoldMessenger.of(context); final locs = AppLocalizations.of(context)!; - final sm = ScaffoldMessenger.of(context); final picked = await picker.pickImage( source: ImageSource.gallery, maxWidth: maxWidth, @@ -56,10 +56,10 @@ class ImageTile extends AbstractSettingsTile { CachedNetworkImage.evictFromCache(imageUrl!); } } catch (e) { - notifyUserOfErrorX( - scaffoldMessenger: sm, - errorSituation: imageUpdateError ?? locs.settingsImageUpdateError, - exception: e, + showErrorSnackBar( + scaffoldMessenger: scaffoldMessenger, + errorSituation: imageUpdateError ?? locs.settingsImageUpdateError, + exception: e, appLocalizations: locs, ); } diff --git a/frontend/pweb/lib/pages/signup/confirmation/args.dart b/frontend/pweb/lib/pages/signup/confirmation/args.dart new file mode 100644 index 00000000..712d1987 --- /dev/null +++ b/frontend/pweb/lib/pages/signup/confirmation/args.dart @@ -0,0 +1,5 @@ +class SignupConfirmationArgs { + final String? email; + + const SignupConfirmationArgs({this.email}); +} diff --git a/frontend/pweb/lib/pages/signup/confirmation/card/badge.dart b/frontend/pweb/lib/pages/signup/confirmation/card/badge.dart new file mode 100644 index 00000000..7e69cbde --- /dev/null +++ b/frontend/pweb/lib/pages/signup/confirmation/card/badge.dart @@ -0,0 +1,39 @@ +part of 'card.dart'; + + +class _SignupConfirmationEmailBadge extends StatelessWidget { + final String email; + + const _SignupConfirmationEmailBadge({required this.email}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + decoration: BoxDecoration( + color: colorScheme.surfaceContainerHighest.withValues(alpha: 0.6), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.alternate_email, + size: 18, + color: colorScheme.primary, + ), + const SizedBox(width: 8), + SelectableText( + email, + style: theme.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ); + } +} diff --git a/frontend/pweb/lib/pages/signup/confirmation/card/card.dart b/frontend/pweb/lib/pages/signup/confirmation/card/card.dart new file mode 100644 index 00000000..c5b8b817 --- /dev/null +++ b/frontend/pweb/lib/pages/signup/confirmation/card/card.dart @@ -0,0 +1,33 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + +import 'package:provider/provider.dart'; + +import 'package:pshared/provider/account.dart'; + +import 'package:pweb/utils/snackbar.dart'; +import 'package:pweb/widgets/error/snackbar.dart'; +import 'package:pweb/widgets/vspacer.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + +part 'state.dart'; +part 'content.dart'; +part 'badge.dart'; +part 'resend_button.dart'; + + +class SignupConfirmationCard extends StatefulWidget { + final String? email; + final bool isEmbedded; + + const SignupConfirmationCard({ + super.key, + this.email, + this.isEmbedded = false, + }); + + @override + State createState() => _SignupConfirmationCardState(); +} diff --git a/frontend/pweb/lib/pages/signup/confirmation/card/content.dart b/frontend/pweb/lib/pages/signup/confirmation/card/content.dart new file mode 100644 index 00000000..552ca61c --- /dev/null +++ b/frontend/pweb/lib/pages/signup/confirmation/card/content.dart @@ -0,0 +1,77 @@ +part of 'card.dart'; + + +class _SignupConfirmationContent extends StatelessWidget { + final String? email; + final String description; + final bool canResend; + final String resendLabel; + final bool isResending; + final VoidCallback onResend; + + const _SignupConfirmationContent({ + required this.email, + required this.description, + required this.canResend, + required this.resendLabel, + required this.isResending, + required this.onResend, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + final locs = AppLocalizations.of(context)!; + + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: colorScheme.primary.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(14), + ), + child: Icon( + Icons.mark_email_read_outlined, + color: colorScheme.primary, + ), + ), + const VSpacer(), + Text( + locs.signupConfirmationTitle, + textAlign: TextAlign.center, + style: theme.textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.w700, + ), + ), + const VSpacer(), + Text( + description, + textAlign: TextAlign.center, + style: theme.textTheme.bodyMedium, + ), + if (email != null && email!.isNotEmpty) ...[ + const VSpacer(), + _SignupConfirmationEmailBadge(email: email!), + ], + const VSpacer(multiplier: 1.5), + Wrap( + spacing: 12, + runSpacing: 12, + alignment: WrapAlignment.center, + children: [ + _SignupConfirmationResendButton( + canResend: canResend, + isResending: isResending, + resendLabel: resendLabel, + onResend: onResend, + ), + ], + ), + ], + ); + } +} diff --git a/frontend/pweb/lib/pages/signup/confirmation/card/resend_button.dart b/frontend/pweb/lib/pages/signup/confirmation/card/resend_button.dart new file mode 100644 index 00000000..dbd6b050 --- /dev/null +++ b/frontend/pweb/lib/pages/signup/confirmation/card/resend_button.dart @@ -0,0 +1,36 @@ +part of 'card.dart'; + + +class _SignupConfirmationResendButton extends StatelessWidget { + final bool canResend; + final bool isResending; + final String resendLabel; + final VoidCallback onResend; + + const _SignupConfirmationResendButton({ + required this.canResend, + required this.isResending, + required this.resendLabel, + required this.onResend, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return ElevatedButton.icon( + onPressed: canResend ? onResend : null, + icon: isResending + ? SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + color: theme.colorScheme.onPrimary, + ), + ) + : const Icon(Icons.mark_email_read_outlined), + label: Text(resendLabel), + ); + } +} diff --git a/frontend/pweb/lib/pages/signup/confirmation/card/state.dart b/frontend/pweb/lib/pages/signup/confirmation/card/state.dart new file mode 100644 index 00000000..4c8d28ee --- /dev/null +++ b/frontend/pweb/lib/pages/signup/confirmation/card/state.dart @@ -0,0 +1,122 @@ +part of 'card.dart'; + + +class _SignupConfirmationCardState extends State { + static const int _defaultCooldownSeconds = 60; + + Timer? _cooldownTimer; + int _cooldownRemainingSeconds = 0; + bool _isResending = false; + + @override + void initState() { + super.initState(); + _startCooldown(_defaultCooldownSeconds); + } + + @override + void dispose() { + _cooldownTimer?.cancel(); + super.dispose(); + } + + bool get _isCooldownActive => _cooldownRemainingSeconds > 0; + + void _startCooldown(int seconds) { + _cooldownTimer?.cancel(); + if (seconds <= 0) { + setState(() => _cooldownRemainingSeconds = 0); + return; + } + setState(() => _cooldownRemainingSeconds = seconds); + _cooldownTimer = Timer.periodic(const Duration(seconds: 1), (timer) { + if (!mounted) { + timer.cancel(); + return; + } + if (_cooldownRemainingSeconds <= 1) { + timer.cancel(); + setState(() => _cooldownRemainingSeconds = 0); + return; + } + setState(() => _cooldownRemainingSeconds -= 1); + }); + } + + Future _resendVerificationEmail() async { + final email = widget.email?.trim(); + final locs = AppLocalizations.of(context)!; + if (email == null || email.isEmpty) { + notifyUser(context, locs.errorEmailMissing); + return; + } + if (_isResending || _isCooldownActive) return; + + setState(() => _isResending = true); + try { + await context.read().resendVerificationEmail(email); + if (!mounted) return; + notifyUser(context, locs.signupConfirmationResent(email)); + _startCooldown(_defaultCooldownSeconds); + } catch (e) { + if (!mounted) return; + postNotifyUserOfErrorX( + context: context, + errorSituation: locs.signupConfirmationResendError, + exception: e, + ); + } finally { + if (mounted) { + setState(() => _isResending = false); + } + } + } + + String _formatCooldown(int seconds) { + final minutes = seconds ~/ 60; + final remainingSeconds = seconds % 60; + if (minutes > 0) { + return '$minutes:${remainingSeconds.toString().padLeft(2, '0')}'; + } + return remainingSeconds.toString(); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final locs = AppLocalizations.of(context)!; + final email = widget.email?.trim(); + final description = (email != null && email.isNotEmpty) + ? locs.signupConfirmationDescription(email) + : locs.signupConfirmationDescriptionNoEmail; + final canResend = !_isResending && !_isCooldownActive && email != null && email.isNotEmpty; + final resendLabel = _isCooldownActive + ? locs.signupConfirmationResendCooldown(_formatCooldown(_cooldownRemainingSeconds)) + : locs.signupConfirmationResend; + + final content = _SignupConfirmationContent( + email: email, + description: description, + canResend: canResend, + resendLabel: resendLabel, + isResending: _isResending, + onResend: _resendVerificationEmail, + ); + + if (widget.isEmbedded) return content; + + return Card( + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + side: BorderSide( + color: theme.dividerColor.withValues(alpha: 0.6), + ), + ), + child: Padding( + padding: const EdgeInsets.all(28), + child: content, + ), + ); + } +} diff --git a/frontend/pweb/lib/pages/signup/confirmation/login_prompt.dart b/frontend/pweb/lib/pages/signup/confirmation/login_prompt.dart new file mode 100644 index 00000000..43ac5b8d --- /dev/null +++ b/frontend/pweb/lib/pages/signup/confirmation/login_prompt.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/app/router/pages.dart'; +import 'package:pweb/widgets/vspacer.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class SignupConfirmationLoginPrompt extends StatelessWidget { + final bool isEmbedded; + + const SignupConfirmationLoginPrompt({ + super.key, + this.isEmbedded = false, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + final locs = AppLocalizations.of(context)!; + + final content = Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: colorScheme.secondary.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + Icons.lock_open_outlined, + color: colorScheme.secondary, + ), + ), + const VSpacer(), + Text( + locs.signupConfirmationLoginTitle, + textAlign: TextAlign.center, + style: theme.textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.w700, + ), + ), + const VSpacer(), + Text( + locs.signupConfirmationLoginHint, + textAlign: TextAlign.center, + style: theme.textTheme.bodyMedium, + ), + const VSpacer(multiplier: 1.5), + OutlinedButton.icon( + onPressed: () => navigate(context, Pages.login), + icon: const Icon(Icons.login), + label: Text(locs.login), + ), + ], + ); + + if (isEmbedded) return content; + + return Card( + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(18), + side: BorderSide( + color: theme.dividerColor.withValues(alpha: 0.5), + ), + ), + child: Padding( + padding: const EdgeInsets.all(24), + child: content, + ), + ); + } +} diff --git a/frontend/pweb/lib/pages/signup/confirmation/page.dart b/frontend/pweb/lib/pages/signup/confirmation/page.dart new file mode 100644 index 00000000..2d24331c --- /dev/null +++ b/frontend/pweb/lib/pages/signup/confirmation/page.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/pages/login/app_bar.dart'; +import 'package:pweb/pages/signup/confirmation/card/card.dart'; +import 'package:pweb/pages/signup/confirmation/login_prompt.dart'; +import 'package:pweb/pages/with_footer.dart'; +import 'package:pweb/widgets/vspacer.dart'; + + +class SignUpConfirmationPage extends StatefulWidget { + final String? email; + + const SignUpConfirmationPage({super.key, this.email}); + + @override + State createState() => _SignUpConfirmationPageState(); +} + +class _SignUpConfirmationPageState extends State { + @override + Widget build(BuildContext context) { + final email = widget.email?.trim(); + + return PageWithFooter( + appBar: const LoginAppBar(), + child: LayoutBuilder( + builder: (context, constraints) { + final isWide = constraints.maxWidth >= 980; + + return ListView( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 32), + children: [ + Center( + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: isWide ? 980 : 720), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Card( + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(22), + side: BorderSide( + color: Theme.of(context).dividerColor.withValues(alpha: 0.6), + ), + ), + child: Padding( + padding: const EdgeInsets.all(28), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SignupConfirmationCard( + email: email, + isEmbedded: true, + ), + const VSpacer(multiplier: 2), + Divider( + height: 1, + color: Theme.of(context).dividerColor.withValues(alpha: 0.6), + ), + const VSpacer(multiplier: 2), + const SignupConfirmationLoginPrompt(isEmbedded: true), + ], + ), + ), + ), + ], + ), + ), + ), + ], + ); + }, + ), + ); + } +} diff --git a/frontend/pweb/lib/pages/signup/form/feilds.dart b/frontend/pweb/lib/pages/signup/form/feilds.dart index d62ce67b..750ad917 100644 --- a/frontend/pweb/lib/pages/signup/form/feilds.dart +++ b/frontend/pweb/lib/pages/signup/form/feilds.dart @@ -39,16 +39,16 @@ class SignUpFormFields extends StatelessWidget { const VSpacer(), NotEmptyTextFormField( controller: controllers.firstName, - labelText: AppLocalizations.of(context)!.lastName, + labelText: AppLocalizations.of(context)!.firstName, readOnly: false, - error: AppLocalizations.of(context)!.enterLastName, + error: AppLocalizations.of(context)!.enterFirstName, ), const VSpacer(), NotEmptyTextFormField( controller: controllers.lastName, - labelText: AppLocalizations.of(context)!.firstName, + labelText: AppLocalizations.of(context)!.lastName, readOnly: false, - error: AppLocalizations.of(context)!.enterFirstName, + error: AppLocalizations.of(context)!.enterLastName, ), const VSpacer(), EmailField(controller: controllers.email), diff --git a/frontend/pweb/lib/pages/signup/form/state.dart b/frontend/pweb/lib/pages/signup/form/state.dart index 27d3b87c..91b93247 100644 --- a/frontend/pweb/lib/pages/signup/form/state.dart +++ b/frontend/pweb/lib/pages/signup/form/state.dart @@ -15,7 +15,7 @@ import 'package:pweb/app/router/pages.dart'; import 'package:pweb/pages/signup/form/content.dart'; import 'package:pweb/pages/signup/form/controllers.dart'; import 'package:pweb/pages/signup/form/form.dart'; -import 'package:pweb/utils/snackbar.dart'; +import 'package:pweb/pages/signup/confirmation/args.dart'; import 'package:pweb/widgets/error/snackbar.dart'; import 'package:pweb/generated/i18n/app_localizations.dart'; @@ -91,12 +91,12 @@ class SignUpFormState extends State { void handleSignUp() => signUp( context, () { - final locs = AppLocalizations.of(context)!; - notifyUser( - context, - locs.signupSuccess(controllers.email.text.trim()), + context.goNamed( + Pages.signupConfirm.name, + extra: SignupConfirmationArgs( + email: controllers.email.text.trim(), + ), ); - context.goNamed(Pages.login.name); }, (e) => postNotifyUserOfErrorX( context: context, diff --git a/frontend/pweb/lib/pages/status/failure.dart b/frontend/pweb/lib/pages/status/failure.dart index 25b99706..43269b1e 100644 --- a/frontend/pweb/lib/pages/status/failure.dart +++ b/frontend/pweb/lib/pages/status/failure.dart @@ -8,7 +8,14 @@ import 'package:pweb/generated/i18n/app_localizations.dart'; class StatusPageFailure extends StatelessWidget { final String errorMessage; final Object error; - const StatusPageFailure({ super.key, required this.errorMessage, required this.error }); + final Widget? action; + + const StatusPageFailure({ + super.key, + required this.errorMessage, + required this.error, + this.action, + }); @override Widget build(BuildContext context) => exceptionToErrorPage( diff --git a/frontend/pweb/lib/pages/status/page.dart b/frontend/pweb/lib/pages/status/page.dart index 45a85643..65011b45 100644 --- a/frontend/pweb/lib/pages/status/page.dart +++ b/frontend/pweb/lib/pages/status/page.dart @@ -16,6 +16,7 @@ class StatusPage extends StatefulWidget { final String successMessage; final String successDescription; final Widget? successAction; + final Widget? failureAction; const StatusPage({ super.key, @@ -25,6 +26,7 @@ class StatusPage extends StatefulWidget { required this.successMessage, required this.successDescription, this.successAction, + this.failureAction, }); @override @@ -58,6 +60,7 @@ class _StatusPageState extends State> { return StatusPageFailure( errorMessage: widget.errorMessage, error: snapshot.error!, + action: widget.failureAction, ); } @@ -75,4 +78,3 @@ class _StatusPageState extends State> { ), ); } - diff --git a/frontend/pweb/lib/pages/verification/page.dart b/frontend/pweb/lib/pages/verification/page.dart index a8a35a7f..baf5fcb2 100644 --- a/frontend/pweb/lib/pages/verification/page.dart +++ b/frontend/pweb/lib/pages/verification/page.dart @@ -1,21 +1,81 @@ import 'package:flutter/material.dart'; -import 'package:pweb/pages/status/page.dart'; -import 'package:pweb/services/verification.dart'; +import 'package:provider/provider.dart'; +import 'package:pshared/provider/email_verification.dart'; +import 'package:pshared/widgets/locale.dart'; + +import 'package:pweb/app/router/pages.dart'; +import 'package:pweb/pages/errors/error.dart'; +import 'package:pweb/pages/status/success.dart'; +import 'package:pweb/pages/with_footer.dart'; import 'package:pweb/generated/i18n/app_localizations.dart'; -class AccountVerificationPage extends StatelessWidget { +class AccountVerificationPage extends StatefulWidget { final String token; const AccountVerificationPage({super.key, required this.token}); @override - Widget build(BuildContext context) => StatusPage( - operation: () async => VerificationService.verify(token), - errorMessage: AppLocalizations.of(context)!.accountVerificationFailed, - successMessage: AppLocalizations.of(context)!.accountVerified, - successDescription: AppLocalizations.of(context)!.accountVerifiedDescription, - ); + State createState() => _AccountVerificationPageState(); +} + +class _AccountVerificationPageState extends State { + @override + void initState() { + super.initState(); + context.read().verify(widget.token); + } + + @override + Widget build(BuildContext context) { + return const _AccountVerificationContent(); + } +} + +class _AccountVerificationContent extends StatelessWidget { + const _AccountVerificationContent(); + + @override + Widget build(BuildContext context) { + final locs = AppLocalizations.of(context)!; + final provider = context.watch(); + final action = OutlinedButton.icon( + onPressed: () => navigateAndReplace(context, Pages.login), + icon: const Icon(Icons.login), + label: Text(locs.login), + ); + + Widget content; + if (provider.isLoading) { + content = const Center(child: CircularProgressIndicator()); + } else if (provider.isSuccess) { + content = StatusPageSuccess( + successMessage: locs.accountVerified, + successDescription: locs.accountVerifiedDescription, + action: action, + ); + } else { + content = exceptionToErrorPage( + context: context, + title: locs.verificationFailed, + errorMessage: locs.accountVerificationFailed, + exception: provider.error ?? Exception(locs.accountVerificationFailed), + ); + } + + return PageWithFooter( + appBar: AppBar( + title: Text(locs.verifyAccount), + centerTitle: true, + actions: [ + const LocaleChangerDropdown( + availableLocales: AppLocalizations.supportedLocales, + ), + ], + ), + child: content, + ); + } } diff --git a/frontend/pweb/lib/utils/error/snackbar.dart b/frontend/pweb/lib/utils/error/snackbar.dart index e75c993c..23df4c86 100644 --- a/frontend/pweb/lib/utils/error/snackbar.dart +++ b/frontend/pweb/lib/utils/error/snackbar.dart @@ -1,28 +1,53 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; -import 'package:pshared/utils/snackbar.dart'; import 'package:pweb/utils/error/handler.dart'; +import 'package:pweb/utils/snackbar.dart'; import 'package:pweb/widgets/error/content.dart'; import 'package:pweb/generated/i18n/app_localizations.dart'; -ScaffoldFeatureController notifyUserOfErrorX({ +Future notifyUserOfErrorX({ + required BuildContext context, + required String errorSituation, + required Object exception, + required AppLocalizations appLocalizations, + int delaySeconds = 3, +}) async { + if (!context.mounted) return; + final localizedError = ErrorHandler.handleErrorLocs(appLocalizations, exception); + final technicalDetails = exception.toString(); + final scaffoldMessenger = ScaffoldMessenger.maybeOf(context); + + if (scaffoldMessenger != null) { + final snackBar = _buildMainErrorSnackBar( + errorSituation: errorSituation, + localizedError: localizedError, + technicalDetails: technicalDetails, + loc: appLocalizations, + scaffoldMessenger: scaffoldMessenger, + delaySeconds: delaySeconds, + ); + scaffoldMessenger.showSnackBar(snackBar); + return; + } + + await _showErrorDialog( + context, + title: errorSituation, + message: localizedError, + ); +} + +void showErrorSnackBar({ required ScaffoldMessengerState scaffoldMessenger, required String errorSituation, required Object exception, required AppLocalizations appLocalizations, int delaySeconds = 3, }) { - // A. Localized user-friendly error message - final String localizedError = ErrorHandler.handleErrorLocs(appLocalizations, exception); - - // B. Technical details for advanced reference - final String technicalDetails = exception.toString(); - - // C. Build the snack bar + final localizedError = ErrorHandler.handleErrorLocs(appLocalizations, exception); + final technicalDetails = exception.toString(); final snackBar = _buildMainErrorSnackBar( errorSituation: errorSituation, localizedError: localizedError, @@ -31,23 +56,22 @@ ScaffoldFeatureController notifyUserOfErrorX({ scaffoldMessenger: scaffoldMessenger, delaySeconds: delaySeconds, ); - - // D. Show it - return scaffoldMessenger.showSnackBar(snackBar); + scaffoldMessenger.showSnackBar(snackBar); } -ScaffoldFeatureController notifyUserOfError({ +Future notifyUserOfError({ required BuildContext context, required String errorSituation, required Object exception, int delaySeconds = 3, -}) => notifyUserOfErrorX( - scaffoldMessenger: ScaffoldMessenger.of(context), - errorSituation: errorSituation, - exception: exception, - appLocalizations: AppLocalizations.of(context)!, - delaySeconds: delaySeconds, -); +}) => + notifyUserOfErrorX( + context: context, + errorSituation: errorSituation, + exception: exception, + appLocalizations: AppLocalizations.of(context)!, + delaySeconds: delaySeconds, + ); Future executeActionWithNotification({ required BuildContext context, @@ -56,19 +80,17 @@ Future executeActionWithNotification({ String? successMessage, int delaySeconds = 3, }) async { - final scaffoldMessenger = ScaffoldMessenger.of(context); final localizations = AppLocalizations.of(context)!; try { final result = await action(); if (successMessage != null) { - notifyUser(context, successMessage, delaySeconds: delaySeconds); + await notifyUser(context, successMessage, delaySeconds: delaySeconds); } return result; } catch (e) { - // Report the error using your existing notifier. - notifyUserOfErrorX( - scaffoldMessenger: scaffoldMessenger, + await notifyUserOfErrorX( + context: context, errorSituation: errorMessage, exception: e, appLocalizations: localizations, @@ -78,43 +100,36 @@ Future executeActionWithNotification({ return null; } -Future> postNotifyUserOfError({ - required ScaffoldMessengerState scaffoldMessenger, +Future postNotifyUserOfError({ + required BuildContext context, required String errorSituation, required Object exception, required AppLocalizations appLocalizations, int delaySeconds = 3, -}) { - - final completer = Completer>(); - - WidgetsBinding.instance.addPostFrameCallback((_) => completer.complete(notifyUserOfErrorX( - scaffoldMessenger: scaffoldMessenger, +}) => + notifyUserOfErrorX( + context: context, errorSituation: errorSituation, exception: exception, appLocalizations: appLocalizations, delaySeconds: delaySeconds, - )), - ); + ); - return completer.future; -} - -Future> postNotifyUserOfErrorX({ +Future postNotifyUserOfErrorX({ required BuildContext context, required String errorSituation, required Object exception, int delaySeconds = 3, -}) => postNotifyUserOfError( - scaffoldMessenger: ScaffoldMessenger.of(context), - errorSituation: errorSituation, - exception: exception, - appLocalizations: AppLocalizations.of(context)!, - delaySeconds: delaySeconds, -); +}) => + postNotifyUserOfError( + context: context, + errorSituation: errorSituation, + exception: exception, + appLocalizations: AppLocalizations.of(context)!, + delaySeconds: delaySeconds, + ); -/// 2) A helper function that returns the main SnackBar widget SnackBar _buildMainErrorSnackBar({ required String errorSituation, required String localizedError, @@ -122,17 +137,42 @@ SnackBar _buildMainErrorSnackBar({ required AppLocalizations loc, required ScaffoldMessengerState scaffoldMessenger, int delaySeconds = 3, -}) => SnackBar( - duration: Duration(seconds: delaySeconds), - content: ErrorSnackBarContent( - situation: errorSituation, - localizedError: localizedError, - ), - action: SnackBarAction( - label: loc.showDetailsAction, - onPressed: () => scaffoldMessenger.showSnackBar(SnackBar( - content: Text(technicalDetails), - duration: const Duration(seconds: 6), - )), - ), -); +}) => + SnackBar( + duration: Duration(seconds: delaySeconds), + content: ErrorSnackBarContent( + situation: errorSituation, + localizedError: localizedError, + ), + action: SnackBarAction( + label: loc.showDetailsAction, + onPressed: () => scaffoldMessenger.showSnackBar( + SnackBar( + content: Text(technicalDetails), + duration: const Duration(seconds: 6), + ), + ), + ), + ); + +Future _showErrorDialog( + BuildContext context, { + required String title, + required String message, +}) async { + if (!context.mounted) return; + final loc = AppLocalizations.of(context)!; + await showDialog( + context: context, + builder: (dialogContext) => AlertDialog( + title: Text(title), + content: Text(message), + actions: [ + TextButton( + onPressed: () => Navigator.of(dialogContext).pop(), + child: Text(loc.ok), + ), + ], + ), + ); +} diff --git a/frontend/pweb/lib/utils/notify.dart b/frontend/pweb/lib/utils/notify.dart index 3703111a..77f69cec 100644 --- a/frontend/pweb/lib/utils/notify.dart +++ b/frontend/pweb/lib/utils/notify.dart @@ -13,19 +13,18 @@ Future invokeAndNotify( void Function(Object)? onError, void Function(T)? onSuccess, }) async { - final sm = ScaffoldMessenger.of(context); final locs = AppLocalizations.of(context)!; try { final res = await operation(); if (operationSuccess != null) { - notifyUserX(sm, operationSuccess); + await notifyUser(context, operationSuccess); } if (onSuccess != null) { onSuccess(res); } } catch (e) { notifyUserOfErrorX( - scaffoldMessenger: sm, + context: context, errorSituation: operationError ?? locs.errorInternalError, exception: e, appLocalizations: locs, diff --git a/frontend/pweb/lib/utils/snackbar.dart b/frontend/pweb/lib/utils/snackbar.dart index a8524ed7..7daa8866 100644 --- a/frontend/pweb/lib/utils/snackbar.dart +++ b/frontend/pweb/lib/utils/snackbar.dart @@ -2,28 +2,67 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:pweb/generated/i18n/app_localizations.dart'; -ScaffoldFeatureController notifyUserX( - ScaffoldMessengerState sm, - String message, - { int delaySeconds = 3 } -) => sm.showSnackBar(SnackBar(content: Text(message), duration: Duration(seconds: delaySeconds))); -ScaffoldFeatureController notifyUser( - BuildContext context, - String message, - { int delaySeconds = 3 } -) => notifyUserX(ScaffoldMessenger.of(context), message, delaySeconds: delaySeconds); +Future notifyUserX( + BuildContext context, + String message, { + int delaySeconds = 3, +}) async { + final scaffoldMessenger = ScaffoldMessenger.maybeOf(context); + if (scaffoldMessenger != null) { + scaffoldMessenger.showSnackBar( + SnackBar( + content: Text(message), + duration: Duration(seconds: delaySeconds), + ), + ); + return; + } -Future> postNotifyUser( - BuildContext context, String message, {int delaySeconds = 3}) { + await _showMessageDialog(context, message); +} - final completer = Completer>(); +Future notifyUser( + BuildContext context, + String message, { + int delaySeconds = 3, +}) => + notifyUserX(context, message, delaySeconds: delaySeconds); - WidgetsBinding.instance.addPostFrameCallback((_) { - final controller = notifyUser(context, message, delaySeconds: delaySeconds); - completer.complete(controller); +Future postNotifyUser( + BuildContext context, + String message, { + int delaySeconds = 3, +}) { + final completer = Completer(); + + WidgetsBinding.instance.addPostFrameCallback((_) async { + if (!context.mounted) { + completer.complete(); + return; + } + await notifyUser(context, message, delaySeconds: delaySeconds); + completer.complete(); }); return completer.future; } + +Future _showMessageDialog(BuildContext context, String message) async { + if (!context.mounted) return; + final loc = AppLocalizations.of(context)!; + await showDialog( + context: context, + builder: (dialogContext) => AlertDialog( + title: Text(message), + actions: [ + TextButton( + onPressed: () => Navigator.of(dialogContext).pop(), + child: Text(loc.ok), + ), + ], + ), + ); +} diff --git a/frontend/pweb/lib/widgets/error/snackbar.dart b/frontend/pweb/lib/widgets/error/snackbar.dart index fd37120c..0344fa49 100644 --- a/frontend/pweb/lib/widgets/error/snackbar.dart +++ b/frontend/pweb/lib/widgets/error/snackbar.dart @@ -4,50 +4,56 @@ import 'package:flutter/material.dart'; import 'package:pweb/utils/error_handler.dart'; import 'package:pweb/utils/snackbar.dart'; -import 'package:pweb/widgets/error/content.dart'; +import 'package:pweb/widgets/error/content.dart'; import 'package:pweb/generated/i18n/app_localizations.dart'; -ScaffoldFeatureController notifyUserOfErrorX({ - required ScaffoldMessengerState scaffoldMessenger, +Future notifyUserOfErrorX({ + required BuildContext context, required String errorSituation, required Object exception, required AppLocalizations appLocalizations, int delaySeconds = 3, -}) { - // A. Localized user-friendly error message - final String localizedError = ErrorHandler.handleErrorLocs(appLocalizations, exception); +}) async { + if (!context.mounted) return; + final localizedError = ErrorHandler.handleErrorLocs(appLocalizations, exception); + final technicalDetails = exception.toString(); + final scaffoldMessenger = ScaffoldMessenger.maybeOf(context); - // B. Technical details for advanced reference - final String technicalDetails = exception.toString(); + if (scaffoldMessenger != null) { + final snackBar = _buildMainErrorSnackBar( + errorSituation: errorSituation, + localizedError: localizedError, + technicalDetails: technicalDetails, + loc: appLocalizations, + scaffoldMessenger: scaffoldMessenger, + delaySeconds: delaySeconds, + ); + scaffoldMessenger.showSnackBar(snackBar); + return; + } - // C. Build the snack bar - final snackBar = _buildMainErrorSnackBar( - errorSituation: errorSituation, - localizedError: localizedError, - technicalDetails: technicalDetails, - loc: appLocalizations, - scaffoldMessenger: scaffoldMessenger, - delaySeconds: delaySeconds, + await _showErrorDialog( + context, + title: errorSituation, + message: localizedError, ); - - // D. Show it - return scaffoldMessenger.showSnackBar(snackBar); } -ScaffoldFeatureController notifyUserOfError({ +Future notifyUserOfError({ required BuildContext context, required String errorSituation, required Object exception, int delaySeconds = 3, -}) => notifyUserOfErrorX( - scaffoldMessenger: ScaffoldMessenger.of(context), - errorSituation: errorSituation, - exception: exception, - appLocalizations: AppLocalizations.of(context)!, - delaySeconds: delaySeconds, -); +}) => + notifyUserOfErrorX( + context: context, + errorSituation: errorSituation, + exception: exception, + appLocalizations: AppLocalizations.of(context)!, + delaySeconds: delaySeconds, + ); Future executeActionWithNotification({ required BuildContext context, @@ -56,19 +62,17 @@ Future executeActionWithNotification({ String? successMessage, int delaySeconds = 3, }) async { - final scaffoldMessenger = ScaffoldMessenger.of(context); final localizations = AppLocalizations.of(context)!; try { final res = await action(); if (successMessage != null) { - notifyUserX(scaffoldMessenger, successMessage, delaySeconds: delaySeconds); + await notifyUser(context, successMessage, delaySeconds: delaySeconds); } return res; } catch (e) { - // Report the error using your existing notifier. - notifyUserOfErrorX( - scaffoldMessenger: scaffoldMessenger, + await notifyUserOfErrorX( + context: context, errorSituation: errorMessage, exception: e, appLocalizations: localizations, @@ -78,43 +82,48 @@ Future executeActionWithNotification({ return null; } -Future> postNotifyUserOfError({ - required ScaffoldMessengerState scaffoldMessenger, +Future postNotifyUserOfError({ + required BuildContext context, required String errorSituation, required Object exception, required AppLocalizations appLocalizations, int delaySeconds = 3, }) { + final completer = Completer(); - final completer = Completer>(); - - WidgetsBinding.instance.addPostFrameCallback((_) => completer.complete(notifyUserOfErrorX( - scaffoldMessenger: scaffoldMessenger, + WidgetsBinding.instance.addPostFrameCallback((_) async { + if (!context.mounted) { + completer.complete(); + return; + } + await notifyUserOfErrorX( + context: context, errorSituation: errorSituation, exception: exception, appLocalizations: appLocalizations, delaySeconds: delaySeconds, - )), - ); + ); + completer.complete(); + }); return completer.future; } -Future> postNotifyUserOfErrorX({ +Future postNotifyUserOfErrorX({ required BuildContext context, required String errorSituation, required Object exception, int delaySeconds = 3, -}) => postNotifyUserOfError( - scaffoldMessenger: ScaffoldMessenger.of(context), - errorSituation: errorSituation, - exception: exception, - appLocalizations: AppLocalizations.of(context)!, - delaySeconds: delaySeconds, -); +}) => + postNotifyUserOfError( + context: context, + errorSituation: errorSituation, + exception: exception, + appLocalizations: AppLocalizations.of(context)!, + delaySeconds: delaySeconds, + ); -/// 2) A helper function that returns the main SnackBar widget SnackBar _buildMainErrorSnackBar({ required String errorSituation, required String localizedError, @@ -122,17 +131,42 @@ SnackBar _buildMainErrorSnackBar({ required AppLocalizations loc, required ScaffoldMessengerState scaffoldMessenger, int delaySeconds = 3, -}) => SnackBar( - duration: Duration(seconds: delaySeconds), - content: ErrorSnackBarContent( - situation: errorSituation, - localizedError: localizedError, - ), - action: SnackBarAction( - label: loc.showDetailsAction, - onPressed: () => scaffoldMessenger.showSnackBar(SnackBar( - content: Text(technicalDetails), - duration: const Duration(seconds: 6), - )), - ), -); +}) => + SnackBar( + duration: Duration(seconds: delaySeconds), + content: ErrorSnackBarContent( + situation: errorSituation, + localizedError: localizedError, + ), + action: SnackBarAction( + label: loc.showDetailsAction, + onPressed: () => scaffoldMessenger.showSnackBar( + SnackBar( + content: Text(technicalDetails), + duration: const Duration(seconds: 6), + ), + ), + ), + ); + +Future _showErrorDialog( + BuildContext context, { + required String title, + required String message, +}) async { + if (!context.mounted) return; + final loc = AppLocalizations.of(context)!; + await showDialog( + context: context, + builder: (dialogContext) => AlertDialog( + title: Text(title), + content: Text(message), + actions: [ + TextButton( + onPressed: () => Navigator.of(dialogContext).pop(), + child: Text(loc.ok), + ), + ], + ), + ); +}