import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; import 'package:sxchnge_analytics/sxchnge_analytics.dart'; import 'package:sxchnge_auth/sxchnge_auth.dart'; import '../auth/brand_panel.dart'; import '../auth/validation.dart'; import '../routing/routes.dart'; import '../generated/i18n/app_localizations.dart'; class VerificationPage extends StatefulWidget { const VerificationPage({super.key, this.email, this.token, this.delivery}); final String? email; final String? token; final VerificationDelivery? delivery; @override State createState() => _VerificationPageState(); } class _VerificationPageState extends State { final _form = GlobalKey(); late final _email = TextEditingController( text: widget.delivery?.email ?? widget.email, ); late final _provider = VerificationProvider( context.read(), delivery: widget.delivery, ); final _emailFocus = FocusNode(); String? _recipient; bool _initialized = false; bool _lastWasSend = false; bool _validated = false; @override void didChangeDependencies() { super.didChangeDependencies(); if (!_initialized) { _initialized = true; WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; if (widget.token != null) { _provider.confirm(widget.token!); } else if (widget.delivery == null && _email.text.isNotEmpty) { _send(); } }); } else if (_validated) { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) _form.currentState?.validate(); }); } } @override void didUpdateWidget(covariant VerificationPage oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.token == widget.token && oldWidget.email == widget.email && identical(oldWidget.delivery, widget.delivery)) { return; } if (oldWidget.token != widget.token) { context.read().abandon(JourneyExit.navigation); } _provider.reset(delivery: widget.delivery); _lastWasSend = false; _validated = false; _recipient = null; _email.text = widget.delivery?.email ?? widget.email ?? ''; WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; if (widget.token != null) { _provider.confirm(widget.token!); } else if (widget.delivery == null && _email.text.isNotEmpty) { _send(); } }); } void _send() { if (_provider.state.isLoading) return; _validated = true; if (_provider.state.status != VerificationStatus.sent && !(_form.currentState?.validate() ?? false)) { return; } _recipient = _email.text.trim(); FocusScope.of(context).unfocus(); _lastWasSend = true; _provider.send( email: _email.text, locale: Localizations.localeOf(context).languageCode, ); } void _changeEmail() { _lastWasSend = true; _validated = false; _provider.reset(); WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) _emailFocus.requestFocus(); }); } @override void dispose() { _provider.dispose(); _email.dispose(); _emailFocus.dispose(); super.dispose(); } @override Widget build(BuildContext context) => ListenableBuilder( listenable: _provider, builder: (context, _) { final l = AppLocalizations.of(context)!; final theme = Theme.of(context); final colors = theme.colorScheme; final state = _provider.state; final status = state.status; final failed = [ VerificationStatus.expired, VerificationStatus.invalid, VerificationStatus.error, ].contains(status); final verified = status == VerificationStatus.verified; final demo = (_provider.service is SimulationInfo && (_provider.service as SimulationInfo).isSimulated) || state.isSimulated; final sent = status == VerificationStatus.sent; final needsAssistance = state.error == VerificationError.needsAssistance; final retryConfirmation = status == VerificationStatus.error && !needsAssistance && !_lastWasSend && widget.token != null; final title = switch (status) { VerificationStatus.sent => l.checkInbox, VerificationStatus.sending => l.verificationSending, VerificationStatus.checking => l.verificationChecking, VerificationStatus.verified => l.verificationVerified, VerificationStatus.expired => l.verificationExpired, VerificationStatus.invalid => l.verificationInvalid, VerificationStatus.error => l.verificationInternal, VerificationStatus.idle => widget.token != null && !_lastWasSend ? l.verificationChecking : l.verificationRequest, }; final body = needsAssistance ? l.authSignupNeedsAssistance : switch (status) { VerificationStatus.sent => l.verificationSent, VerificationStatus.verified => l.verificationVerifiedBody, VerificationStatus.expired => l.verificationExpiredBody, VerificationStatus.invalid => l.verificationInvalidBody, VerificationStatus.error => l.verificationInternalBody, _ => null, }; return Scaffold( body: SafeArea( child: SingleChildScrollView( padding: const EdgeInsets.all(24), child: Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 520), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const Align( alignment: Alignment.centerLeft, child: BrandWordmark(), ), const SizedBox(height: 48), Align( alignment: Alignment.centerLeft, child: Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: failed ? colors.errorContainer : colors.primaryContainer, borderRadius: BorderRadius.circular(24), ), child: state.isLoading ? const SizedBox( width: 32, height: 32, child: CircularProgressIndicator(), ) : Icon( verified ? Icons.mark_email_read_outlined : failed ? Icons.error_outline : Icons.mark_email_unread_outlined, size: 32, color: failed ? colors.onErrorContainer : colors.onPrimaryContainer, ), ), ), const SizedBox(height: 28), Text( l.verificationStep, style: theme.textTheme.labelLarge?.copyWith( color: colors.primary, ), ), const SizedBox(height: 12), Semantics( liveRegion: true, header: true, child: Text( title, key: ValueKey('verification-${status.name}'), style: theme.textTheme.displaySmall, ), ), if (body != null) ...[ const SizedBox(height: 20), Text( body, style: theme.textTheme.bodyLarge?.copyWith( color: colors.onSurfaceVariant, ), ), ], if (sent) ...[ const SizedBox(height: 20), Text( l.verificationRecipient, style: theme.textTheme.labelLarge, ), Text( _recipient ?? _email.text, key: const ValueKey('verification-recipient'), ), TextButton( key: const ValueKey('verification-change-email'), onPressed: _changeEmail, child: Text(l.verificationChangeEmail), ), if (demo) FilledButton( key: const ValueKey('demo-confirmation'), onPressed: () => context.goNamed( AppRoutes.verifyEmail, queryParameters: {'token': 'demo-valid'}, ), child: Text( l.verificationDemoLink, textAlign: TextAlign.center, ), ), const SizedBox(height: 12), OutlinedButton( key: const ValueKey('verification-resend'), onPressed: _send, child: Text( l.verificationResend, textAlign: TextAlign.center, ), ), if (!demo) ...[ const SizedBox(height: 16), Text( l.verificationHint, style: theme.textTheme.bodyMedium, ), ], ], if (!verified && !sent && !retryConfirmation && status != VerificationStatus.checking) ...[ const SizedBox(height: 28), Form( key: _form, child: TextFormField( key: const ValueKey('verification-email'), controller: _email, focusNode: _emailFocus, autocorrect: false, enabled: !state.isLoading, keyboardType: TextInputType.emailAddress, autofillHints: const [AutofillHints.email], textInputAction: TextInputAction.send, validator: (value) => AuthValidation.email(value, l), onFieldSubmitted: (_) => _send(), decoration: InputDecoration( labelText: l.email, hintText: l.emailHint, errorMaxLines: 3, prefixIcon: const Icon(Icons.alternate_email), ), ), ), const SizedBox(height: 16), FilledButton( key: const ValueKey('verification-resend'), onPressed: state.isLoading ? null : _send, child: Text( state.isLoading ? l.authWorking : l.verificationRequest, textAlign: TextAlign.center, ), ), ], if (retryConfirmation) ...[ const SizedBox(height: 12), FilledButton( key: const ValueKey('verification-retry'), onPressed: () => _provider.confirm(widget.token!), child: Text(l.verificationRetry), ), TextButton( onPressed: _changeEmail, child: Text(l.verificationRequest), ), ], if (verified) ...[ const SizedBox(height: 16), FilledButton( key: const ValueKey('verification-continue'), onPressed: () => context.goNamed( AppRoutes.login, extra: state.result?.verifiedEmail, ), child: Text(l.verificationContinue), ), ], if (!verified) ...[ const SizedBox(height: 16), TextButton( onPressed: () => context.goNamed(AppRoutes.login), child: Text(l.verificationBack), ), ], ], ), ), ), ), ), ); }, ); }