From d3fe9c4a029e0c82e17f0718d23081c3f5a1b98b Mon Sep 17 00:00:00 2001 From: Sikander Saleem Date: Wed, 17 Mar 2021 15:33:55 +0300 Subject: [PATCH] forgot password, otp & change password api added. --- lib/api/api_client.dart | 2 +- lib/api/user_api_client.dart | 19 ++++ lib/models/general_response_model.dart | 21 ++++ lib/ui/dialogs/change_password_dialog.dart | 102 ++++++++++++++++++ .../{opt_dialog.dart => otp_dialog.dart} | 25 +++-- lib/ui/screens/forgot_password_screen.dart | 86 ++++++++++++--- lib/ui/screens/login_screen.dart | 3 +- lib/widgets/otp_widget.dart | 35 +++--- 8 files changed, 241 insertions(+), 52 deletions(-) create mode 100644 lib/models/general_response_model.dart create mode 100644 lib/ui/dialogs/change_password_dialog.dart rename lib/ui/dialogs/{opt_dialog.dart => otp_dialog.dart} (87%) diff --git a/lib/api/api_client.dart b/lib/api/api_client.dart index b2296a7..332d7ad 100644 --- a/lib/api/api_client.dart +++ b/lib/api/api_client.dart @@ -99,7 +99,7 @@ class ApiClient { var queryString = new Uri(queryParameters: queryParameters).query; url = url + '?' + queryString; } - var response = await _post(Uri.parse(url), body: requestBody, headers: _headers).timeout(Duration(seconds: 12)); + var response = await _post(Uri.parse(url), body: requestBody, headers: _headers).timeout(Duration(seconds: 15)); if (response.statusCode >= 200 && response.statusCode < 300) { return response; diff --git a/lib/api/user_api_client.dart b/lib/api/user_api_client.dart index b11e9c6..b13eaca 100644 --- a/lib/api/user_api_client.dart +++ b/lib/api/user_api_client.dart @@ -1,8 +1,27 @@ import 'package:tangheem/classes/consts.dart'; +import 'package:tangheem/models/general_response_model.dart'; import 'api_client.dart'; class UserApiClient { static final UserApiClient _instance = UserApiClient._internal(); UserApiClient._internal(); factory UserApiClient() => _instance; + + Future forgotPassword(String _email) async { + String url = "${ApiConsts.user}ForgotPassword"; + var postParams = {"email": _email}; + return await ApiClient().postJsonForObject((json) => GeneralResponseModel.fromJson(json), url, postParams); + } + + Future verifyOTP(String _email, int _otp) async { + String url = "${ApiConsts.user}OTPVerification"; + var postParams = {"email": _email, "opt": _otp}; + return await ApiClient().postJsonForObject((json) => GeneralResponseModel.fromJson(json), url, postParams); + } + + Future updatePassword(String _email, int _otp, String _password) async { + String url = "${ApiConsts.user}UpdatePassword"; + var postParams = {"email": _email, "opt": _otp, "newPassword": _password, "confirmPassword": _password}; + return await ApiClient().postJsonForObject((json) => GeneralResponseModel.fromJson(json), url, postParams); + } } diff --git a/lib/models/general_response_model.dart b/lib/models/general_response_model.dart new file mode 100644 index 0000000..6fea371 --- /dev/null +++ b/lib/models/general_response_model.dart @@ -0,0 +1,21 @@ +class GeneralResponseModel { + int statusCode; + String message; + Null result; + + GeneralResponseModel({this.statusCode, this.message, this.result}); + + GeneralResponseModel.fromJson(Map json) { + statusCode = json['statusCode']; + message = json['message']; + result = json['result']; + } + + Map toJson() { + final Map data = new Map(); + data['statusCode'] = this.statusCode; + data['message'] = this.message; + data['result'] = this.result; + return data; + } +} diff --git a/lib/ui/dialogs/change_password_dialog.dart b/lib/ui/dialogs/change_password_dialog.dart new file mode 100644 index 0000000..588b845 --- /dev/null +++ b/lib/ui/dialogs/change_password_dialog.dart @@ -0,0 +1,102 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:tangheem/api/user_api_client.dart'; +import 'package:tangheem/classes/colors.dart'; +import 'package:tangheem/classes/utils.dart'; +import 'package:tangheem/widgets/common_textfield_widget.dart'; +import 'package:tangheem/widgets/otp_widget.dart'; + +class ChangePasswordDialog extends StatefulWidget { + final Function(String) onPassword; + ChangePasswordDialog({Key key, this.onPassword}) : super(key: key); + + @override + _ChangePasswordDialogState createState() { + return _ChangePasswordDialogState(); + } +} + +class _ChangePasswordDialogState extends State { + final TextEditingController _passwordController = TextEditingController(); + final TextEditingController _confirmPasswordController = TextEditingController(); + + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Dialog( + insetPadding: EdgeInsets.symmetric(horizontal: 60.0, vertical: 24.0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 0, + backgroundColor: Colors.transparent, + child: Directionality( + textDirection: TextDirection.rtl, + child: Container( + width: double.infinity, + decoration: BoxDecoration( + color: ColorConsts.primaryBlue, + borderRadius: BorderRadius.circular(16), + ), + padding: EdgeInsets.symmetric(vertical: 32, horizontal: 16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "تغيير كلمة المرور", + textAlign: TextAlign.center, + style: TextStyle(color: Colors.white, fontSize: 22), + ), + SizedBox(height: 16), + CommonTextFieldWidget(hint: "الايميل المسجل", controller: _passwordController, prefixIcon: "assets/icons/password.svg"), + SizedBox(height: 8), + CommonTextFieldWidget(hint: "تأكيد كلمة المرور", controller: _confirmPasswordController, prefixIcon: "assets/icons/password.svg"), + SizedBox(height: 16), + SizedBox( + width: double.infinity, + height: 40, + child: TextButton( + onPressed: () { + if (_passwordController.text.length < 1) { + Utils.showToast("Password is empty."); + return; + } + if (_confirmPasswordController.text.length < 1) { + Utils.showToast("Confirm password is empty."); + return; + } + if (_passwordController.text != _confirmPasswordController.text) { + Utils.showToast("Password incorrect"); + return; + } + widget.onPassword(_passwordController.text); + }, + style: TextButton.styleFrom( + primary: Colors.white, + padding: EdgeInsets.all(2), + backgroundColor: ColorConsts.secondaryPink, + textStyle: TextStyle(fontSize: 14, fontFamily: "DroidKufi"), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6.0), + ), + ), + child: Text("تحديث"), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/ui/dialogs/opt_dialog.dart b/lib/ui/dialogs/otp_dialog.dart similarity index 87% rename from lib/ui/dialogs/opt_dialog.dart rename to lib/ui/dialogs/otp_dialog.dart index 8305a4b..8a20344 100644 --- a/lib/ui/dialogs/opt_dialog.dart +++ b/lib/ui/dialogs/otp_dialog.dart @@ -1,11 +1,14 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; +import 'package:tangheem/api/user_api_client.dart'; import 'package:tangheem/classes/colors.dart'; +import 'package:tangheem/classes/utils.dart'; import 'package:tangheem/widgets/otp_widget.dart'; class OTPDialog extends StatefulWidget { - OTPDialog({Key key}) : super(key: key); + final Function(int) onOTP; + OTPDialog({Key key, this.onOTP}) : super(key: key); @override _OTPDialogState createState() { @@ -15,10 +18,10 @@ class OTPDialog extends StatefulWidget { class _OTPDialogState extends State { final TextEditingController _pinPutController = TextEditingController(); - final FocusNode _pinPutFocusNode = FocusNode(); bool hasError = false; String errorMessage; + String otpMessage = ""; @override void initState() { @@ -74,11 +77,11 @@ class _OTPDialogState extends State { }); }, pinBoxColor: ColorConsts.secondaryWhite.withOpacity(0.2), - onDone: (text) {}, + onDone: (text) => otpMessage = text, textBorderColor: Colors.transparent, pinBoxWidth: 40, pinBoxHeight: 40, - pinTextStyle: TextStyle(fontSize: 20.0, color: Colors.white,height: .2), + pinTextStyle: TextStyle(fontSize: 20.0, color: Colors.white), pinTextAnimatedSwitcherTransition: ProvidedPinBoxTextAnimation.scalingTransition, pinTextAnimatedSwitcherDuration: Duration(milliseconds: 300), keyboardType: TextInputType.number, @@ -89,7 +92,11 @@ class _OTPDialogState extends State { height: 40, child: TextButton( onPressed: () { - Navigator.pop(context); + if (otpMessage.length < 4) { + Utils.showToast("Invalid OTP"); + return; + } + widget.onOTP(int.parse(otpMessage)); }, style: TextButton.styleFrom( primary: Colors.white, @@ -108,12 +115,4 @@ class _OTPDialogState extends State { ), ); } - - BoxDecoration get _pinPutDecoration { - return BoxDecoration( - color: ColorConsts.secondaryWhite.withOpacity(0.2), - //border: Border.all(color: Colors.deepPurpleAccent), - //borderRadius: BorderRadius.circular(0.0), - ); - } } diff --git a/lib/ui/screens/forgot_password_screen.dart b/lib/ui/screens/forgot_password_screen.dart index 582f7c9..2052cea 100644 --- a/lib/ui/screens/forgot_password_screen.dart +++ b/lib/ui/screens/forgot_password_screen.dart @@ -1,9 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:tangheem/api/user_api_client.dart'; import 'package:tangheem/classes/colors.dart'; import 'package:tangheem/classes/utils.dart'; -import 'package:tangheem/ui/dialogs/opt_dialog.dart'; +import 'package:tangheem/models/general_response_model.dart'; +import 'package:tangheem/ui/dialogs/change_password_dialog.dart'; +import 'package:tangheem/ui/dialogs/otp_dialog.dart'; import 'package:tangheem/widgets/common_textfield_widget.dart'; +import 'package:tangheem/extensions/email_validator.dart'; class ForgotPasswordScreen extends StatefulWidget { static const String routeName = "/forgot_password"; @@ -17,9 +21,8 @@ class ForgotPasswordScreen extends StatefulWidget { } class _ForgotPasswordScreenState extends State { - TextEditingController _usernameController = TextEditingController(); TextEditingController _emailController = TextEditingController(); - + GeneralResponseModel _generalResponse; @override void initState() { super.initState(); @@ -30,6 +33,52 @@ class _ForgotPasswordScreenState extends State { super.dispose(); } + void forgotPassword(String email) async { + Utils.showLoading(context); + try { + _generalResponse = await UserApiClient().forgotPassword(email); + Utils.showToast("OTP sent"); + } catch (ex, tr) { + Utils.handleException(ex, null); + Utils.hideLoading(context); + return; + } finally { + Utils.hideLoading(context); + } + getOTP(email); + } + + void verifyOTP(String email, int otp) async { + Utils.showLoading(context); + try { + _generalResponse = await UserApiClient().verifyOTP(email, otp); + } catch (ex, tr) { + Utils.handleException(ex, null); + Utils.hideLoading(context); + return; + } finally { + Utils.hideLoading(context); + } + Navigator.pop(context); + changePassword(email, otp); + } + + void updatePassword(String email, int otp, String password) async { + Utils.showLoading(context); + try { + _generalResponse = await UserApiClient().updatePassword(email, otp, password); + } catch (ex, tr) { + Utils.handleException(ex, null); + Utils.hideLoading(context); + return; + } finally { + Utils.hideLoading(context); + } + Navigator.pop(context); + Utils.showToast("Password changed successfully."); + Navigator.pop(context); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -66,12 +115,6 @@ class _ForgotPasswordScreenState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - CommonTextFieldWidget(hint: "اسم المستخدم", controller: _usernameController, prefixIcon: "assets/icons/user.svg"), - SizedBox(height: 8), - Text( - "-أو-", - style: TextStyle(color: Colors.white), - ), SizedBox(height: 8), CommonTextFieldWidget(hint: "الايميل المسجل", controller: _emailController, prefixIcon: "assets/icons/email.svg"), SizedBox(height: 12), @@ -80,15 +123,14 @@ class _ForgotPasswordScreenState extends State { height: 50, child: TextButton( onPressed: () { - if (_usernameController.text.length < 1) { - Utils.showToast("Username is empty."); - return; - } if (_emailController.text.length < 1) { Utils.showToast("Email is empty."); return; + } else if (!_emailController.text.isValidEmail()) { + Utils.showToast("Invalid email."); + return; } - getOTP(); + forgotPassword(_emailController.text); }, style: TextButton.styleFrom( primary: Colors.white, @@ -111,11 +153,23 @@ class _ForgotPasswordScreenState extends State { ); } - getOTP() { + void getOTP(String email) { + showDialog( + context: context, + barrierColor: ColorConsts.secondaryWhite.withOpacity(0.8), + builder: (BuildContext context) => OTPDialog( + onOTP: (otp) => verifyOTP(email, otp), + ), + ); + } + + void changePassword(String email, int otp) { showDialog( context: context, barrierColor: ColorConsts.secondaryWhite.withOpacity(0.8), - builder: (BuildContext context) => OTPDialog(), + builder: (BuildContext context) => ChangePasswordDialog( + onPassword: (password) => updatePassword(email, otp, password), + ), ); } } diff --git a/lib/ui/screens/login_screen.dart b/lib/ui/screens/login_screen.dart index 682a43d..18846fa 100644 --- a/lib/ui/screens/login_screen.dart +++ b/lib/ui/screens/login_screen.dart @@ -82,8 +82,7 @@ class _LoginScreenState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - // todo @discuss wiht haroon to change hint to email, because auth depends on email - CommonTextFieldWidget(hint: "اسم المستخدم", controller: _emailController, prefixIcon: "assets/icons/user.svg"), + CommonTextFieldWidget(hint: "الايميل المسجل", controller: _emailController, prefixIcon: "assets/icons/email.svg"), SizedBox(height: 16), CommonTextFieldWidget(hint: "كلمة المرور", controller: _passwordController, isPassword: true, prefixIcon: "assets/icons/password.svg"), SizedBox(height: 16), diff --git a/lib/widgets/otp_widget.dart b/lib/widgets/otp_widget.dart index d26ff8c..5e8ca11 100644 --- a/lib/widgets/otp_widget.dart +++ b/lib/widgets/otp_widget.dart @@ -109,7 +109,7 @@ class OTPWidgetState extends State with SingleTickerProviderStateMixi } } - _calculateStrList() async { + _calculateStrList() { if (strList.length > widget.maxLength) { strList.length = widget.maxLength; } @@ -330,27 +330,22 @@ class OTPWidgetState extends State with SingleTickerProviderStateMixi } else { insets = widget.pinBoxOuterPadding; } - return Padding( - padding: insets, - child: Container( - key: ValueKey("container$i"), - child: Padding( - padding: EdgeInsets.symmetric(vertical: 1.0, horizontal: 1.0), - child: Center( - child: _animatedTextBox(strList[i], i), - ), - ), - decoration: BoxDecoration( - border: Border.all( - color: borderColor, - width: widget.pinBoxBorderWidth, - ), - color: pinBoxColor, - borderRadius: BorderRadius.circular(widget.pinBoxRadius), + return Container( + key: ValueKey("container$i"), + alignment: Alignment.center, + padding: EdgeInsets.symmetric(vertical: 4.0, horizontal: 1.0), + margin: insets, + child: _animatedTextBox(strList[i], i), + decoration: BoxDecoration( + border: Border.all( + color: borderColor, + width: widget.pinBoxBorderWidth, ), - width: widget.pinBoxWidth, - height: widget.pinBoxHeight, + color: pinBoxColor, + borderRadius: BorderRadius.circular(widget.pinBoxRadius), ), + width: widget.pinBoxWidth, + height: widget.pinBoxHeight, ); }