sneaky email verification fix
This commit is contained in:
111
frontend/pweb/lib/pages/verification/content.dart
Normal file
111
frontend/pweb/lib/pages/verification/content.dart
Normal file
@@ -0,0 +1,111 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:provider/provider.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/pages/verification/controller.dart';
|
||||
import 'package:pweb/pages/verification/resend_dialog.dart';
|
||||
import 'package:pweb/utils/snackbar.dart';
|
||||
import 'package:pweb/widgets/error/snackbar.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
|
||||
class AccountVerificationContent extends StatefulWidget {
|
||||
const AccountVerificationContent();
|
||||
|
||||
@override
|
||||
State<AccountVerificationContent> createState() =>
|
||||
AccountVerificationContentState();
|
||||
}
|
||||
|
||||
class AccountVerificationContentState
|
||||
extends State<AccountVerificationContent> {
|
||||
Future<void> _resendVerificationEmail() async {
|
||||
final controller = context.read<AccountVerificationController>();
|
||||
if (controller.isResending) return;
|
||||
final locs = AppLocalizations.of(context)!;
|
||||
final email = await requestVerificationEmail(context, locs);
|
||||
if (!mounted || email == null) return;
|
||||
if (email.isEmpty) {
|
||||
notifyUser(context, locs.errorEmailMissing);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await controller.resendVerificationEmail(email);
|
||||
if (!mounted) return;
|
||||
await notifyUser(context, locs.signupConfirmationResent(email));
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
await postNotifyUserOfErrorX(
|
||||
context: context,
|
||||
errorSituation: locs.signupConfirmationResendError,
|
||||
exception: e,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final locs = AppLocalizations.of(context)!;
|
||||
final controller = context.watch<AccountVerificationController>();
|
||||
final action = OutlinedButton.icon(
|
||||
onPressed: () => navigateAndReplace(context, Pages.login),
|
||||
icon: const Icon(Icons.login),
|
||||
label: Text(locs.login),
|
||||
);
|
||||
|
||||
Widget content;
|
||||
if (controller.isLoading) {
|
||||
content = const Center(child: CircularProgressIndicator());
|
||||
} else if (controller.isSuccess) {
|
||||
content = StatusPageSuccess(
|
||||
successMessage: locs.accountVerified,
|
||||
successDescription: locs.accountVerifiedDescription,
|
||||
action: action,
|
||||
);
|
||||
} else {
|
||||
content = exceptionToErrorPage(
|
||||
context: context,
|
||||
title: locs.verificationFailed,
|
||||
errorMessage: locs.accountVerificationFailed,
|
||||
exception:
|
||||
controller.error ?? Exception(locs.accountVerificationFailed),
|
||||
action: controller.canResend
|
||||
? OutlinedButton.icon(
|
||||
onPressed: controller.isResending
|
||||
? null
|
||||
: _resendVerificationEmail,
|
||||
icon: controller.isResending
|
||||
? const SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.mark_email_unread_outlined),
|
||||
label: Text(locs.signupConfirmationResend),
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
return PageWithFooter(
|
||||
appBar: AppBar(
|
||||
title: Text(locs.verifyAccount),
|
||||
centerTitle: true,
|
||||
actions: [
|
||||
const LocaleChangerDropdown(
|
||||
availableLocales: AppLocalizations.supportedLocales,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: content,
|
||||
);
|
||||
}
|
||||
}
|
||||
64
frontend/pweb/lib/pages/verification/controller.dart
Normal file
64
frontend/pweb/lib/pages/verification/controller.dart
Normal file
@@ -0,0 +1,64 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pshared/provider/account.dart';
|
||||
import 'package:pshared/provider/email_verification.dart';
|
||||
|
||||
import 'package:pweb/models/flow_status.dart';
|
||||
|
||||
|
||||
class AccountVerificationController extends ChangeNotifier {
|
||||
AccountVerificationController({
|
||||
required AccountProvider accountProvider,
|
||||
required EmailVerificationProvider verificationProvider,
|
||||
}) : _accountProvider = accountProvider,
|
||||
_verificationProvider = verificationProvider {
|
||||
_verificationProvider.addListener(_onVerificationChanged);
|
||||
}
|
||||
|
||||
final AccountProvider _accountProvider;
|
||||
final EmailVerificationProvider _verificationProvider;
|
||||
|
||||
FlowStatus _resendStatus = FlowStatus.idle;
|
||||
String? _verificationToken;
|
||||
|
||||
bool get isLoading => _verificationProvider.isLoading;
|
||||
bool get isSuccess => _verificationProvider.isSuccess;
|
||||
Exception? get error => _verificationProvider.error;
|
||||
bool get canResend => _verificationProvider.canResendVerification;
|
||||
bool get isResending => _resendStatus == FlowStatus.resending;
|
||||
|
||||
void startVerification(String token) {
|
||||
final trimmed = token.trim();
|
||||
if (trimmed.isEmpty || trimmed == _verificationToken) return;
|
||||
_verificationToken = trimmed;
|
||||
_verificationProvider.verify(trimmed);
|
||||
}
|
||||
|
||||
Future<void> resendVerificationEmail(String email) async {
|
||||
final trimmed = email.trim();
|
||||
if (trimmed.isEmpty || isResending) return;
|
||||
_setResendStatus(FlowStatus.resending);
|
||||
try {
|
||||
await _accountProvider.resendVerificationEmail(trimmed);
|
||||
_setResendStatus(FlowStatus.idle);
|
||||
} catch (_) {
|
||||
_setResendStatus(FlowStatus.error);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
void _onVerificationChanged() {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _setResendStatus(FlowStatus status) {
|
||||
_resendStatus = status;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_verificationProvider.removeListener(_onVerificationChanged);
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -2,81 +2,26 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pshared/provider/account.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';
|
||||
import 'package:pweb/pages/verification/content.dart';
|
||||
import 'package:pweb/pages/verification/controller.dart';
|
||||
|
||||
|
||||
class AccountVerificationPage extends StatefulWidget {
|
||||
class AccountVerificationPage extends StatelessWidget {
|
||||
final String token;
|
||||
|
||||
const AccountVerificationPage({super.key, required this.token});
|
||||
|
||||
@override
|
||||
State<AccountVerificationPage> createState() => _AccountVerificationPageState();
|
||||
}
|
||||
|
||||
class _AccountVerificationPageState extends State<AccountVerificationPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
context.read<EmailVerificationProvider>().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<EmailVerificationProvider>();
|
||||
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,
|
||||
return ChangeNotifierProvider(
|
||||
create: (context) => AccountVerificationController(
|
||||
accountProvider: context.read<AccountProvider>(),
|
||||
verificationProvider: context.read<EmailVerificationProvider>(),
|
||||
)..startVerification(token),
|
||||
child: AccountVerificationContent(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
41
frontend/pweb/lib/pages/verification/resend_dialog.dart
Normal file
41
frontend/pweb/lib/pages/verification/resend_dialog.dart
Normal file
@@ -0,0 +1,41 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
|
||||
Future<String?> requestVerificationEmail(
|
||||
BuildContext context,
|
||||
AppLocalizations locs,
|
||||
) async {
|
||||
final controller = TextEditingController();
|
||||
final email = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: Text(locs.signupConfirmationResend),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
autofocus: true,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
decoration: InputDecoration(
|
||||
labelText: locs.username,
|
||||
hintText: locs.usernameHint,
|
||||
),
|
||||
onSubmitted: (_) =>
|
||||
Navigator.of(dialogContext).pop(controller.text.trim()),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||
child: Text(locs.cancel),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () =>
|
||||
Navigator.of(dialogContext).pop(controller.text.trim()),
|
||||
child: Text(locs.signupConfirmationResend),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
controller.dispose();
|
||||
return email?.trim();
|
||||
}
|
||||
Reference in New Issue
Block a user