Email confirmation page after SignUp

This commit is contained in:
Arseni
2026-01-13 15:31:13 +03:00
parent dedde76dd7
commit 5d330c8ccc
12 changed files with 343 additions and 9 deletions

View File

@@ -0,0 +1,5 @@
class SignupConfirmationArgs {
final String? email;
const SignupConfirmationArgs({this.email});
}

View File

@@ -0,0 +1,163 @@
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';
class SignupConfirmationCard extends StatefulWidget {
final String? email;
const SignupConfirmationCard({super.key, this.email});
@override
State<SignupConfirmationCard> createState() => _SignupConfirmationCardState();
}
class _SignupConfirmationCardState extends State<SignupConfirmationCard> {
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<void> _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<AccountProvider>().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;
return Card(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
locs.signupConfirmationTitle,
style: theme.textTheme.headlineSmall,
),
const VSpacer(),
Text(description, style: theme.textTheme.bodyMedium),
if (email != null && email.isNotEmpty) ...[
const VSpacer(),
SelectableText(
email,
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
],
const VSpacer(multiplier: 1.5),
Row(
children: [
ElevatedButton.icon(
onPressed: canResend ? _resendVerificationEmail : 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),
),
],
),
],
),
),
);
}
}

View File

@@ -0,0 +1,31 @@
import 'package:flutter/material.dart';
import 'package:pweb/widgets/vspacer.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class SignupConfirmationLoginPrompt extends StatelessWidget {
const SignupConfirmationLoginPrompt({super.key});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final locs = AppLocalizations.of(context)!;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
locs.signupConfirmationLoginTitle,
style: theme.textTheme.titleLarge,
),
const VSpacer(),
Text(
locs.signupConfirmationLoginHint,
style: theme.textTheme.bodyMedium,
),
],
);
}
}

View File

@@ -0,0 +1,49 @@
import 'package:flutter/material.dart';
import 'package:pweb/pages/login/app_bar.dart';
import 'package:pweb/pages/login/form.dart';
import 'package:pweb/pages/signup/confirmation/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<SignUpConfirmationPage> createState() => _SignUpConfirmationPageState();
}
class _SignUpConfirmationPageState extends State<SignUpConfirmationPage> {
@override
Widget build(BuildContext context) {
final email = widget.email?.trim();
return PageWithFooter(
appBar: const LoginAppBar(),
child: ListView(
padding: const EdgeInsets.all(16),
children: [
Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 520),
child: SignupConfirmationCard(email: email),
),
),
const VSpacer(multiplier: 2),
Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 520),
child: const SignupConfirmationLoginPrompt(),
),
),
const VSpacer(multiplier: 1.5),
LoginForm(initialEmail: email),
],
),
);
}
}