import 'package:comunic/helpers/account_helper.dart'; import 'package:comunic/ui/routes/password_reset_route.dart'; import 'package:comunic/ui/widgets/dialogs/cancel_dialog_button.dart'; import 'package:comunic/ui/widgets/login_route_container.dart'; import 'package:comunic/ui/widgets/safe_state.dart'; import 'package:comunic/utils/input_utils.dart'; import 'package:comunic/utils/intl_utils.dart'; import 'package:comunic/utils/ui_utils.dart'; import 'package:flutter/material.dart'; /// Reset password route /// /// @author Pierre Hubert class ForgotPasswordRoute extends StatelessWidget { @override Widget build(BuildContext context) { return LoginRouteContainer( child: Scaffold( appBar: AppBar( title: Text(tr("Password forgotten")!), ), body: Padding( padding: const EdgeInsets.all(8.0), child: Center( child: ConstrainedBox( constraints: BoxConstraints(maxWidth: 300), child: SingleChildScrollView(child: _ResetPasswordBody()), ), ), ), ), ); } } enum _SelectedOption { NONE, SECURITY_QUESTIONS } class _ResetPasswordBody extends StatefulWidget { @override _ResetPasswordBodyState createState() => _ResetPasswordBodyState(); } class _ResetPasswordBodyState extends SafeState<_ResetPasswordBody> { var _loading = false; /// Step 1 - check email address String? _emailAddress; final _emailController = TextEditingController(); String get _inputEmail => _emailController.text; bool get _isEmailValid => _inputEmail.isNotEmpty && validateEmail(_inputEmail); /// Step 2 - Offer options bool? _hasSecurityQuestions; var _selectedOption = _SelectedOption.NONE; void _setLoading(bool loading) => setState(() => _loading = loading); /// Step 3b - Answer security questions List? _questions; var _questionsControllers = []; List get _answers => _questionsControllers.map((f) => f.text).toList(); bool get _canSubmitAnswers => _answers.every((f) => f.isNotEmpty); @override Widget build(BuildContext context) { if (_loading) return buildCenteredProgressBar(); if (_emailAddress == null) return _buildEnterEmailAddressScreen(); switch (_selectedOption) { case _SelectedOption.NONE: return _buildOptionsScreen(); case _SelectedOption.SECURITY_QUESTIONS: return _buildSecurityQuestionsScreen(); } throw Exception("Unreachable statement!"); } Widget _buildEnterEmailAddressScreen() { return Column( mainAxisSize: MainAxisSize.min, children: [ Text(tr("Please enter your email address to reset your password:")!), TextField( controller: _emailController, onChanged: (s) => setState(() {}), onSubmitted: _isEmailValid ? (s) => _checkEmail() : null, textInputAction: TextInputAction.done, keyboardType: TextInputType.emailAddress, decoration: InputDecoration( icon: Icon(Icons.email), alignLabelWithHint: true, labelText: tr("Email address..."), suffixIcon: IconButton( icon: Icon(Icons.check), onPressed: _isEmailValid ? _checkEmail : null, ), errorText: _inputEmail.isEmpty || _isEmailValid ? null : tr("Invalid email address!"), ), ), ], ); } /// Check given email address void _checkEmail() async { try { _setLoading(true); // Check if email address exists or not if (!await AccountHelper.existsMailAccount(_inputEmail) ) { _setLoading(false); showSimpleSnack(context, tr("Specified email address was not found!")!); return; } _hasSecurityQuestions = await AccountHelper.hasSecurityQuestions(_inputEmail); // We retain email address only if everything went well _emailAddress = _inputEmail; _setLoading(false); } catch (e, s) { print("Could not check given email! $e\n$s"); showSimpleSnack(context, tr("An error occurred while checking your recovery options !")!); _setLoading(false); } } /// Offer the user the options he has to reset his account Widget _buildOptionsScreen() => Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text(tr("Here are your options to reset your account:")!), _Spacer(), OutlinedButton.icon( onPressed: _openSendEmailDialog, icon: Icon(Icons.email), label: Flexible( child: Text(tr("Send us an email to ask for help")!))), _Spacer(visible: _hasSecurityQuestions!), _hasSecurityQuestions! ? OutlinedButton.icon( onPressed: _loadSecurityQuestions, icon: Icon(Icons.help_outline), label: Text(tr("Answer your security questions")!), ) : Container(), ], ); /// Show a dialog with our email address void _openSendEmailDialog() { showDialog( context: context, builder: (c) => AlertDialog( title: Text("Contact us"), content: Text(tr("You can reach us at contact@communiquons.org")!), actions: [CancelDialogButton()], )); } /// Load security questions void _loadSecurityQuestions() async { _setLoading(true); try { _questions = await AccountHelper.getSecurityQuestions(_emailAddress); _questionsControllers = List.generate(_questions!.length, (i) => TextEditingController()); _selectedOption = _SelectedOption.SECURITY_QUESTIONS; } catch (e, s) { print("Could not load security questions! $e\n$s"); showSimpleSnack(context, tr("Could not load your security questions!")!); } _setLoading(false); } /// Show a screen to prompt security questions of the user Widget _buildSecurityQuestionsScreen() { return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [] ..add(Text(tr("Please answer now your security questions:")!)) ..add(_Spacer()) ..addAll(List.generate(_questions!.length, _buildSecurityQuestionField)) ..add(_Spacer()) ..add(OutlinedButton( onPressed: _canSubmitAnswers ? _submitSecurityAnswers : null, child: Text(tr("Submit")!), )), ); } Widget _buildSecurityQuestionField(int id) => Column( children: [ Text(_questions![id]), TextField( controller: _questionsControllers[id], onChanged: (s) => setState(() {}), onSubmitted: _canSubmitAnswers ? (s) => _submitSecurityAnswers() : null, decoration: InputDecoration( alignLabelWithHint: false, labelText: tr("Answer %num%", args: {"num": (id + 1).toString()})), ), _Spacer() ], ); /// Submit security answers Future _submitSecurityAnswers() async { _setLoading(true); try { final token = await AccountHelper.checkAnswers(_emailAddress, _answers); _useResetToken(token); } catch (e, s) { print("Could not submit security answers! $e\n$s"); showSimpleSnack( context, tr("Could not validate these security answers!")!); } _setLoading(false); } /// Call this method whenever the user managed to get a password reset token void _useResetToken(String? token) => Navigator.of(context).pushReplacement( MaterialPageRoute(builder: (c) => PasswordResetRoute(token: token!))); } class _Spacer extends StatelessWidget { final bool visible; const _Spacer({Key? key, this.visible = true}) : assert(visible != null), super(key: key); @override Widget build(BuildContext context) { return Container( height: visible ? 35 : null, ); } }