2020-05-03 12:22:06 +00:00
|
|
|
import 'package:comunic/helpers/account_helper.dart';
|
2020-05-03 14:55:00 +00:00
|
|
|
import 'package:comunic/ui/routes/password_reset_route.dart';
|
2020-05-03 12:33:26 +00:00
|
|
|
import 'package:comunic/ui/widgets/dialogs/cancel_dialog_button.dart';
|
2020-05-03 12:22:06 +00:00
|
|
|
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';
|
2020-05-03 13:35:07 +00:00
|
|
|
import 'package:flutter/rendering.dart';
|
2020-05-03 12:22:06 +00:00
|
|
|
|
|
|
|
/// Reset password route
|
|
|
|
///
|
|
|
|
/// @author Pierre Hubert
|
|
|
|
|
2021-02-18 17:28:57 +00:00
|
|
|
class ForgotPasswordRoute extends StatelessWidget {
|
2020-05-03 12:22:06 +00:00
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
return Scaffold(
|
|
|
|
appBar: AppBar(
|
|
|
|
title: Text(tr("Password forgotten")),
|
|
|
|
),
|
2020-05-03 13:48:10 +00:00
|
|
|
body: Padding(
|
|
|
|
padding: const EdgeInsets.all(8.0),
|
|
|
|
child: Center(
|
|
|
|
child: ConstrainedBox(
|
|
|
|
constraints: BoxConstraints(maxWidth: 300),
|
|
|
|
child: SingleChildScrollView(child: _ResetPasswordBody()),
|
|
|
|
),
|
2020-05-03 12:22:06 +00:00
|
|
|
),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-03 13:35:07 +00:00
|
|
|
enum _SelectedOption { NONE, SECURITY_QUESTIONS }
|
2020-05-03 12:33:26 +00:00
|
|
|
|
2020-05-03 12:22:06 +00:00
|
|
|
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;
|
|
|
|
|
2020-05-03 12:33:26 +00:00
|
|
|
var _selectedOption = _SelectedOption.NONE;
|
|
|
|
|
2020-05-03 12:22:06 +00:00
|
|
|
void _setLoading(bool loading) => setState(() => _loading = loading);
|
|
|
|
|
2020-05-03 13:35:07 +00:00
|
|
|
/// Step 3b - Answer security questions
|
|
|
|
List<String> _questions;
|
2021-03-13 14:28:34 +00:00
|
|
|
var _questionsControllers = <TextEditingController>[];
|
2020-05-03 13:35:07 +00:00
|
|
|
|
2020-05-03 13:48:10 +00:00
|
|
|
List<String> get _answers =>
|
|
|
|
_questionsControllers.map((f) => f.text).toList();
|
|
|
|
|
|
|
|
bool get _canSubmitAnswers => _answers.every((f) => f.isNotEmpty);
|
|
|
|
|
2020-05-03 12:22:06 +00:00
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
if (_loading) return buildCenteredProgressBar();
|
|
|
|
|
|
|
|
if (_emailAddress == null) return _buildEnterEmailAddressScreen();
|
2020-05-03 12:33:26 +00:00
|
|
|
|
|
|
|
switch (_selectedOption) {
|
|
|
|
case _SelectedOption.NONE:
|
|
|
|
return _buildOptionsScreen();
|
2020-05-03 13:35:07 +00:00
|
|
|
|
|
|
|
case _SelectedOption.SECURITY_QUESTIONS:
|
|
|
|
return _buildSecurityQuestionsScreen();
|
2020-05-03 12:33:26 +00:00
|
|
|
}
|
2020-05-03 14:55:00 +00:00
|
|
|
|
|
|
|
throw Exception("Unreachable statement!");
|
2020-05-03 12:22:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
Widget _buildEnterEmailAddressScreen() {
|
|
|
|
return Column(
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
children: <Widget>[
|
2020-05-03 12:33:26 +00:00
|
|
|
Text(tr("Please enter your email address to reset your password:")),
|
2020-05-03 12:22:06 +00:00
|
|
|
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");
|
2021-02-18 17:28:57 +00:00
|
|
|
showSimpleSnack(context,
|
|
|
|
tr("An error occurred while checking your recovery options !"));
|
2020-05-03 12:22:06 +00:00
|
|
|
_setLoading(false);
|
|
|
|
}
|
|
|
|
}
|
2020-05-03 12:33:26 +00:00
|
|
|
|
|
|
|
/// 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:")),
|
2020-05-03 13:35:07 +00:00
|
|
|
_Spacer(),
|
2021-03-13 14:14:54 +00:00
|
|
|
OutlinedButton.icon(
|
2020-05-03 12:33:26 +00:00
|
|
|
onPressed: _openSendEmailDialog,
|
|
|
|
icon: Icon(Icons.email),
|
|
|
|
label: Text(tr("Send us an email to ask for help")),
|
|
|
|
),
|
2020-05-03 13:35:07 +00:00
|
|
|
_Spacer(visible: _hasSecurityQuestions),
|
|
|
|
_hasSecurityQuestions
|
2021-03-13 14:14:54 +00:00
|
|
|
? OutlinedButton.icon(
|
2020-05-03 13:35:07 +00:00
|
|
|
onPressed: _loadSecurityQuestions,
|
|
|
|
icon: Icon(Icons.help_outline),
|
|
|
|
label: Text(tr("Answer your security questions")),
|
|
|
|
)
|
|
|
|
: Container(),
|
2020-05-03 12:33:26 +00:00
|
|
|
],
|
|
|
|
);
|
|
|
|
|
|
|
|
/// 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()],
|
|
|
|
));
|
|
|
|
}
|
2020-05-03 13:35:07 +00:00
|
|
|
|
|
|
|
/// 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())
|
2021-03-13 14:14:54 +00:00
|
|
|
..add(OutlinedButton(
|
2020-05-03 13:48:10 +00:00
|
|
|
onPressed: _canSubmitAnswers ? _submitSecurityAnswers : null,
|
2020-05-03 13:35:07 +00:00
|
|
|
child: Text(tr("Submit")),
|
|
|
|
)),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-05-03 13:48:10 +00:00
|
|
|
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,
|
2020-05-03 14:21:36 +00:00
|
|
|
labelText:
|
|
|
|
tr("Answer %num%", args: {"num": (id + 1).toString()})),
|
2020-05-03 13:48:10 +00:00
|
|
|
),
|
|
|
|
_Spacer()
|
|
|
|
],
|
2020-05-03 13:35:07 +00:00
|
|
|
);
|
2020-05-03 13:48:10 +00:00
|
|
|
|
|
|
|
/// Submit security answers
|
2020-05-03 14:21:36 +00:00
|
|
|
Future<void> _submitSecurityAnswers() async {
|
|
|
|
_setLoading(true);
|
|
|
|
try {
|
|
|
|
final token = await AccountHelper.checkAnswers(_emailAddress, _answers);
|
2020-05-03 14:55:00 +00:00
|
|
|
|
|
|
|
_useResetToken(token);
|
2020-05-03 14:21:36 +00:00
|
|
|
} catch (e, s) {
|
|
|
|
print("Could not submit security answers! $e\n$s");
|
|
|
|
showSimpleSnack(
|
|
|
|
context, tr("Could not validate these security answers!"));
|
|
|
|
}
|
|
|
|
_setLoading(false);
|
|
|
|
}
|
2020-05-03 14:55:00 +00:00
|
|
|
|
|
|
|
/// 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)));
|
2020-05-03 13:35:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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,
|
|
|
|
);
|
|
|
|
}
|
2020-05-03 12:22:06 +00:00
|
|
|
}
|