1
0
mirror of https://gitlab.com/comunic/comunicmobile synced 2024-12-28 13:58:51 +00:00
comunicmobile/lib/ui/routes/forgot_password_route.dart

263 lines
8.1 KiB
Dart

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/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.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<String> _questions;
var _questionsControllers = <TextEditingController>[];
List<String> 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: <Widget>[
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: <Widget>[
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: <Widget>[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: <Widget>[]
..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: <Widget>[
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<void> _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,
);
}
}