| @@ -1,4 +1,6 @@ | |||
| import 'package:dio/dio.dart'; | |||
| import 'package:farm_tpf/models/account.dart'; | |||
| import 'package:farm_tpf/models/password.dart'; | |||
| import 'package:farm_tpf/models/user.dart'; | |||
| import 'package:farm_tpf/models/user_request.dart'; | |||
| import 'package:retrofit/retrofit.dart'; | |||
| @@ -11,4 +13,19 @@ abstract class RestClient { | |||
| @POST("/api/authenticate") | |||
| Future<User> login(@Body() UserRequest userRequest); | |||
| @GET("/api/account") | |||
| Future<Account> getMe(); | |||
| @POST("/api/account/reset-password/init") | |||
| Future<void> forgotPassword(@Body() String email); | |||
| @POST("/api/account/reset-password/finish") | |||
| Future<void> resetPassword(@Body() Password password); | |||
| @POST("/api/account/change-password") | |||
| Future<void> changePassword(@Body() Password password); | |||
| @PUT("/api/update-my-profile") | |||
| Future<Account> updateProfile(@Body() Account account); | |||
| } | |||
| @@ -35,4 +35,95 @@ class _RestClient implements RestClient { | |||
| final value = User.fromJson(_result.data); | |||
| return value; | |||
| } | |||
| @override | |||
| getMe() async { | |||
| const _extra = <String, dynamic>{}; | |||
| final queryParameters = <String, dynamic>{}; | |||
| final _data = <String, dynamic>{}; | |||
| final Response<Map<String, dynamic>> _result = await _dio.request( | |||
| '/api/account', | |||
| queryParameters: queryParameters, | |||
| options: RequestOptions( | |||
| method: 'GET', | |||
| headers: <String, dynamic>{}, | |||
| extra: _extra, | |||
| baseUrl: baseUrl), | |||
| data: _data); | |||
| final value = Account.fromJson(_result.data); | |||
| return value; | |||
| } | |||
| @override | |||
| forgotPassword(email) async { | |||
| ArgumentError.checkNotNull(email, 'email'); | |||
| const _extra = <String, dynamic>{}; | |||
| final queryParameters = <String, dynamic>{}; | |||
| final _data = email; | |||
| await _dio.request<void>('/api/account/reset-password/init', | |||
| queryParameters: queryParameters, | |||
| options: RequestOptions( | |||
| method: 'POST', | |||
| headers: <String, dynamic>{}, | |||
| extra: _extra, | |||
| baseUrl: baseUrl), | |||
| data: _data); | |||
| return null; | |||
| } | |||
| @override | |||
| resetPassword(password) async { | |||
| ArgumentError.checkNotNull(password, 'password'); | |||
| const _extra = <String, dynamic>{}; | |||
| final queryParameters = <String, dynamic>{}; | |||
| final _data = <String, dynamic>{}; | |||
| _data.addAll(password?.toJson() ?? <String, dynamic>{}); | |||
| await _dio.request<void>('/api/account/reset-password/finish', | |||
| queryParameters: queryParameters, | |||
| options: RequestOptions( | |||
| method: 'POST', | |||
| headers: <String, dynamic>{}, | |||
| extra: _extra, | |||
| baseUrl: baseUrl), | |||
| data: _data); | |||
| return null; | |||
| } | |||
| @override | |||
| changePassword(password) async { | |||
| ArgumentError.checkNotNull(password, 'password'); | |||
| const _extra = <String, dynamic>{}; | |||
| final queryParameters = <String, dynamic>{}; | |||
| final _data = <String, dynamic>{}; | |||
| _data.addAll(password?.toJson() ?? <String, dynamic>{}); | |||
| await _dio.request<void>('/api/account/change-password', | |||
| queryParameters: queryParameters, | |||
| options: RequestOptions( | |||
| method: 'POST', | |||
| headers: <String, dynamic>{}, | |||
| extra: _extra, | |||
| baseUrl: baseUrl), | |||
| data: _data); | |||
| return null; | |||
| } | |||
| @override | |||
| updateProfile(account) async { | |||
| ArgumentError.checkNotNull(account, 'account'); | |||
| const _extra = <String, dynamic>{}; | |||
| final queryParameters = <String, dynamic>{}; | |||
| final _data = <String, dynamic>{}; | |||
| _data.addAll(account?.toJson() ?? <String, dynamic>{}); | |||
| final Response<Map<String, dynamic>> _result = await _dio.request( | |||
| '/api/update-my-profile', | |||
| queryParameters: queryParameters, | |||
| options: RequestOptions( | |||
| method: 'PUT', | |||
| headers: <String, dynamic>{}, | |||
| extra: _extra, | |||
| baseUrl: baseUrl), | |||
| data: _data); | |||
| final value = Account.fromJson(_result.data); | |||
| return value; | |||
| } | |||
| } | |||
| @@ -0,0 +1,28 @@ | |||
| import 'package:farm_tpf/data/api/dio_provider.dart'; | |||
| import 'package:farm_tpf/data/api/rest_client.dart'; | |||
| import 'package:farm_tpf/models/account.dart'; | |||
| import 'package:farm_tpf/models/password.dart'; | |||
| class UserRepository { | |||
| final dio = DioProvider.instance(); | |||
| Future<Account> getUser() { | |||
| final client = RestClient(dio); | |||
| return client.getMe(); | |||
| } | |||
| Future<void> forgotPassword(String email) { | |||
| final client = RestClient(dio); | |||
| return client.forgotPassword(email); | |||
| } | |||
| Future<void> changePassword(Password password) { | |||
| final client = RestClient(dio); | |||
| return client.changePassword(password); | |||
| } | |||
| Future<Account> updateProfile(Account account) { | |||
| final client = RestClient(dio); | |||
| return client.updateProfile(account); | |||
| } | |||
| } | |||
| @@ -0,0 +1,29 @@ | |||
| import 'package:json_annotation/json_annotation.dart'; | |||
| part 'account.g.dart'; | |||
| @JsonSerializable() | |||
| class Account { | |||
| Account(); | |||
| num id; | |||
| String login; | |||
| String firstName; | |||
| String lastName; | |||
| String midleName; | |||
| String fullName; | |||
| String telephone; | |||
| String address; | |||
| String avartar; | |||
| num vaiTroId; | |||
| String tenVaiTro; | |||
| num donViCanhTacId; | |||
| String tenDonVi; | |||
| String email; | |||
| String imageUrl; | |||
| bool activated; | |||
| List authorities; | |||
| factory Account.fromJson(Map<String,dynamic> json) => _$AccountFromJson(json); | |||
| Map<String, dynamic> toJson() => _$AccountToJson(this); | |||
| } | |||
| @@ -0,0 +1,48 @@ | |||
| // GENERATED CODE - DO NOT MODIFY BY HAND | |||
| part of 'account.dart'; | |||
| // ************************************************************************** | |||
| // JsonSerializableGenerator | |||
| // ************************************************************************** | |||
| Account _$AccountFromJson(Map<String, dynamic> json) { | |||
| return Account() | |||
| ..id = json['id'] as num | |||
| ..login = json['login'] as String | |||
| ..firstName = json['firstName'] as String | |||
| ..lastName = json['lastName'] as String | |||
| ..midleName = json['midleName'] as String | |||
| ..fullName = json['fullName'] as String | |||
| ..telephone = json['telephone'] as String | |||
| ..address = json['address'] as String | |||
| ..avartar = json['avartar'] as String | |||
| ..vaiTroId = json['vaiTroId'] as num | |||
| ..tenVaiTro = json['tenVaiTro'] as String | |||
| ..donViCanhTacId = json['donViCanhTacId'] as num | |||
| ..tenDonVi = json['tenDonVi'] as String | |||
| ..email = json['email'] as String | |||
| ..imageUrl = json['imageUrl'] as String | |||
| ..activated = json['activated'] as bool | |||
| ..authorities = json['authorities'] as List; | |||
| } | |||
| Map<String, dynamic> _$AccountToJson(Account instance) => <String, dynamic>{ | |||
| 'id': instance.id, | |||
| 'login': instance.login, | |||
| 'firstName': instance.firstName, | |||
| 'lastName': instance.lastName, | |||
| 'midleName': instance.midleName, | |||
| 'fullName': instance.fullName, | |||
| 'telephone': instance.telephone, | |||
| 'address': instance.address, | |||
| 'avartar': instance.avartar, | |||
| 'vaiTroId': instance.vaiTroId, | |||
| 'tenVaiTro': instance.tenVaiTro, | |||
| 'donViCanhTacId': instance.donViCanhTacId, | |||
| 'tenDonVi': instance.tenDonVi, | |||
| 'email': instance.email, | |||
| 'imageUrl': instance.imageUrl, | |||
| 'activated': instance.activated, | |||
| 'authorities': instance.authorities, | |||
| }; | |||
| @@ -0,0 +1,15 @@ | |||
| import 'package:json_annotation/json_annotation.dart'; | |||
| part 'password.g.dart'; | |||
| @JsonSerializable() | |||
| class Password { | |||
| Password(); | |||
| String key; | |||
| String currentPassword; | |||
| String newPassword; | |||
| factory Password.fromJson(Map<String,dynamic> json) => _$PasswordFromJson(json); | |||
| Map<String, dynamic> toJson() => _$PasswordToJson(this); | |||
| } | |||
| @@ -0,0 +1,20 @@ | |||
| // GENERATED CODE - DO NOT MODIFY BY HAND | |||
| part of 'password.dart'; | |||
| // ************************************************************************** | |||
| // JsonSerializableGenerator | |||
| // ************************************************************************** | |||
| Password _$PasswordFromJson(Map<String, dynamic> json) { | |||
| return Password() | |||
| ..key = json['key'] as String | |||
| ..currentPassword = json['currentPassword'] as String | |||
| ..newPassword = json['newPassword'] as String; | |||
| } | |||
| Map<String, dynamic> _$PasswordToJson(Password instance) => <String, dynamic>{ | |||
| 'key': instance.key, | |||
| 'currentPassword': instance.currentPassword, | |||
| 'newPassword': instance.newPassword, | |||
| }; | |||
| @@ -0,0 +1,27 @@ | |||
| import 'package:flutter/material.dart'; | |||
| class WidgetToast extends StatelessWidget { | |||
| String message; | |||
| WidgetToast({@required this.message}); | |||
| @override | |||
| Widget build(BuildContext context) { | |||
| return Container( | |||
| padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 12.0), | |||
| decoration: BoxDecoration( | |||
| borderRadius: BorderRadius.circular(25.0), | |||
| color: Colors.greenAccent, | |||
| ), | |||
| child: Row( | |||
| mainAxisSize: MainAxisSize.min, | |||
| children: [ | |||
| Icon(Icons.check), | |||
| SizedBox( | |||
| width: 12.0, | |||
| ), | |||
| Text(message), | |||
| ], | |||
| ), | |||
| ); | |||
| } | |||
| } | |||
| @@ -0,0 +1,117 @@ | |||
| import 'package:farm_tpf/data/repository/user_repository.dart'; | |||
| import 'package:farm_tpf/presentation/custom_widgets/widget_loading.dart'; | |||
| import 'package:farm_tpf/presentation/custom_widgets/widget_toast.dart'; | |||
| import 'package:farm_tpf/utils/const_color.dart'; | |||
| import 'package:farm_tpf/utils/validators.dart'; | |||
| import 'package:flutter/material.dart'; | |||
| import 'package:fluttertoast/fluttertoast.dart'; | |||
| import 'package:keyboard_dismisser/keyboard_dismisser.dart'; | |||
| class ForgotPasswordScreen extends StatefulWidget { | |||
| @override | |||
| _ForgotPasswordScreenState createState() => _ForgotPasswordScreenState(); | |||
| } | |||
| class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> { | |||
| UserRepository _userRepository = UserRepository(); | |||
| final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>(); | |||
| GlobalKey<FormState> _formKey = GlobalKey(); | |||
| bool _autoValidate = false; | |||
| TextEditingController _emailController = TextEditingController(); | |||
| String _email = ""; | |||
| FlutterToast flutterToast; | |||
| @override | |||
| void initState() { | |||
| super.initState(); | |||
| flutterToast = FlutterToast(context); | |||
| } | |||
| _validateInputs() async { | |||
| if (_formKey.currentState.validate()) { | |||
| _formKey.currentState.save(); | |||
| LoadingDialog.showLoadingDialog(context); | |||
| _userRepository.forgotPassword(_email).then((value) { | |||
| LoadingDialog.hideLoadingDialog(context); | |||
| flutterToast.showToast( | |||
| child: WidgetToast(message: "Gửi email thành công.")); | |||
| Navigator.pop(context); | |||
| }).catchError((error) { | |||
| _scaffoldKey.currentState.showSnackBar(SnackBar( | |||
| content: Row( | |||
| mainAxisAlignment: MainAxisAlignment.spaceBetween, | |||
| children: <Widget>[ | |||
| Text('Email không tồn tại.'), | |||
| Icon(Icons.error), | |||
| ], | |||
| ), | |||
| backgroundColor: Colors.red, | |||
| duration: Duration(seconds: 3), | |||
| )); | |||
| LoadingDialog.hideLoadingDialog(context); | |||
| }); | |||
| } else { | |||
| _autoValidate = true; | |||
| } | |||
| } | |||
| Widget _emailField() { | |||
| return TextFormField( | |||
| keyboardType: TextInputType.text, | |||
| decoration: InputDecoration(labelText: "Email"), | |||
| controller: _emailController, | |||
| validator: (String value) { | |||
| return Validators.validateEmail(value); | |||
| }, | |||
| onSaved: (newValue) { | |||
| _email = newValue; | |||
| }, | |||
| ); | |||
| } | |||
| Widget _btnSubmit() { | |||
| return SizedBox( | |||
| width: double.infinity, | |||
| height: 55, | |||
| child: FlatButton( | |||
| onPressed: () { | |||
| FocusScopeNode currentFocus = FocusScope.of(context); | |||
| if (!currentFocus.hasPrimaryFocus) { | |||
| currentFocus.unfocus(); | |||
| } | |||
| _validateInputs(); | |||
| }, | |||
| color: COLOR_CONST.DEFAULT, | |||
| shape: RoundedRectangleBorder( | |||
| borderRadius: new BorderRadius.circular(7.0), | |||
| ), | |||
| child: Text( | |||
| 'Gửi'.toUpperCase(), | |||
| ), | |||
| ), | |||
| ); | |||
| } | |||
| @override | |||
| Widget build(BuildContext context) => KeyboardDismisser( | |||
| child: Scaffold( | |||
| key: _scaffoldKey, | |||
| appBar: AppBar( | |||
| title: Text("Gửi email khôi phục mật khẩu"), | |||
| ), | |||
| body: Form( | |||
| key: _formKey, | |||
| autovalidate: _autoValidate, | |||
| child: SingleChildScrollView( | |||
| padding: EdgeInsets.all(8.0), | |||
| child: Column( | |||
| children: <Widget>[ | |||
| _emailField(), | |||
| SizedBox( | |||
| height: 16.0, | |||
| ), | |||
| _btnSubmit() | |||
| ], | |||
| ), | |||
| )))); | |||
| } | |||
| @@ -1,6 +1,7 @@ | |||
| import 'package:farm_tpf/authentication/authentication.dart'; | |||
| import 'package:farm_tpf/data/repository/authentication_repository.dart'; | |||
| import 'package:farm_tpf/presentation/custom_widgets/widget_loading.dart'; | |||
| import 'package:farm_tpf/presentation/screens/forgot_password/sc_forgot_password.dart'; | |||
| import 'package:farm_tpf/presentation/screens/login/bloc/login_bloc.dart'; | |||
| import 'package:farm_tpf/utils/const_color.dart'; | |||
| import 'package:flutter/material.dart'; | |||
| @@ -20,7 +21,15 @@ class LoginForm extends StatelessWidget { | |||
| Scaffold.of(context) | |||
| ..hideCurrentSnackBar() | |||
| ..showSnackBar( | |||
| const SnackBar(content: Text('Authentication Failure')), | |||
| SnackBar( | |||
| content: Row( | |||
| mainAxisAlignment: MainAxisAlignment.spaceBetween, | |||
| children: <Widget>[ | |||
| Text('Tài khoản hoặc mật khẩu không đúng.'), | |||
| Icon(Icons.error), | |||
| ], | |||
| ), | |||
| backgroundColor: Colors.red), | |||
| ); | |||
| } | |||
| if (state.status.isSubmissionSuccess) { | |||
| @@ -43,7 +52,7 @@ class LoginForm extends StatelessWidget { | |||
| const Padding(padding: EdgeInsets.all(16)), | |||
| _LoginButton(), | |||
| const Padding(padding: EdgeInsets.all(6)), | |||
| _forgotPasswordButton(), | |||
| _FogotPasswordButton(), | |||
| _registerButton() | |||
| ], | |||
| ), | |||
| @@ -52,14 +61,20 @@ class LoginForm extends StatelessWidget { | |||
| } | |||
| } | |||
| Widget _forgotPasswordButton() { | |||
| return Align( | |||
| alignment: Alignment.centerRight, | |||
| child: FlatButton( | |||
| child: Text( | |||
| 'Quên mật khẩu ?', | |||
| ), | |||
| onPressed: () {})); | |||
| class _FogotPasswordButton extends StatelessWidget { | |||
| @override | |||
| Widget build(BuildContext context) { | |||
| return Align( | |||
| alignment: Alignment.centerRight, | |||
| child: FlatButton( | |||
| child: Text( | |||
| 'Quên mật khẩu ?', | |||
| ), | |||
| onPressed: () { | |||
| Navigator.of(context).push( | |||
| MaterialPageRoute(builder: (_) => ForgotPasswordScreen())); | |||
| })); | |||
| } | |||
| } | |||
| Widget _registerButton() { | |||
| @@ -0,0 +1,52 @@ | |||
| import 'dart:developer'; | |||
| import 'package:intl/intl.dart'; | |||
| extension HHmm on Duration { | |||
| String formatHHmm() { | |||
| //1:34:00.000000 | |||
| final str = this.toString(); | |||
| final texts = str.split(":"); | |||
| final textHour = texts[0].padLeft(2, '0'); | |||
| final textMinute = texts[1].padLeft(2, '0'); | |||
| return "${textHour}h ${textMinute}m"; | |||
| } | |||
| } | |||
| extension FormatNumber on int { | |||
| String formatDecimalThousand() { | |||
| //1403 -> 1,403 | |||
| var f = new NumberFormat.decimalPattern("en_US"); | |||
| return f.format(this); | |||
| } | |||
| } | |||
| extension FormatDate on int { | |||
| String MMM_dd_yyyy() { | |||
| return DateFormat("MMM dd, yyyy") | |||
| .format(DateTime.fromMillisecondsSinceEpoch(this * 1000)); | |||
| } | |||
| } | |||
| extension DoubleParsing on String { | |||
| double parseDoubleThousand() { | |||
| var newValue = this.replaceAll(",", ""); | |||
| //TODO: CHECK again | |||
| if (newValue.endsWith(".0")) { | |||
| newValue = newValue.substring(0, newValue.length - 2); | |||
| } | |||
| return double.tryParse(newValue); | |||
| } | |||
| } | |||
| extension IntParsing on String { | |||
| int parseIntThousand() { | |||
| var newValue = this.replaceAll(",", ""); | |||
| if (newValue.endsWith(".0")) { | |||
| newValue = newValue.substring(0, newValue.length - 2); | |||
| } | |||
| return int.tryParse(newValue); | |||
| } | |||
| } | |||
| @@ -0,0 +1,76 @@ | |||
| import 'formatter.dart'; | |||
| class Validators { | |||
| static final RegExp _emailRegExp = RegExp( | |||
| r'^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$', | |||
| ); | |||
| static final RegExp _passwordRegExp = RegExp( | |||
| r'^.{4,8}$', | |||
| ); | |||
| static isValidEmail(String email) { | |||
| return _emailRegExp.hasMatch(email); | |||
| } | |||
| static isValidPassword(String password) { | |||
| return _passwordRegExp.hasMatch(password); | |||
| } | |||
| static isValidName(String name) { | |||
| return name.isNotEmpty; | |||
| } | |||
| static String validateNotNullOrEmpty(String value, String errorMessage) { | |||
| if (value.length == 0) { | |||
| return errorMessage; | |||
| } else { | |||
| return null; | |||
| } | |||
| } | |||
| static String validNumber(String value, String errorMessage) { | |||
| try { | |||
| var doubleValue = value.parseDoubleThousand(); | |||
| if (doubleValue > 0) { | |||
| return null; | |||
| } else { | |||
| return errorMessage; | |||
| } | |||
| } catch (_) { | |||
| return errorMessage; | |||
| } | |||
| } | |||
| static String validateEmail(String value) { | |||
| String pattern = | |||
| r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$'; | |||
| RegExp regExp = new RegExp(pattern); | |||
| if (value.length == 0) { | |||
| return "Nhập email"; | |||
| } else if (!regExp.hasMatch(value)) { | |||
| return "Email không đúng"; | |||
| } else { | |||
| return null; | |||
| } | |||
| } | |||
| String validateNewPassword(String value) { | |||
| if (value.length == 0) { | |||
| return "Nhập mật khẩu mới"; | |||
| } else { | |||
| return null; | |||
| } | |||
| } | |||
| String validateConfirmPassword(String newPassword, String value) { | |||
| if (value.length == 0) { | |||
| return "Nhập lại mật khẩu mới"; | |||
| } else if (newPassword != value) { | |||
| return "Mật khẩu không trùng khớp"; | |||
| } else { | |||
| return null; | |||
| } | |||
| } | |||
| } | |||
| final validators = Validators(); | |||
| @@ -226,6 +226,13 @@ packages: | |||
| description: flutter | |||
| source: sdk | |||
| version: "0.0.0" | |||
| fluttertoast: | |||
| dependency: "direct main" | |||
| description: | |||
| name: fluttertoast | |||
| url: "https://pub.dartlang.org" | |||
| source: hosted | |||
| version: "6.0.1" | |||
| formz: | |||
| dependency: "direct main" | |||
| description: | |||
| @@ -394,6 +401,13 @@ packages: | |||
| url: "https://pub.dartlang.org" | |||
| source: hosted | |||
| version: "1.0.2" | |||
| pattern_formatter: | |||
| dependency: "direct main" | |||
| description: | |||
| name: pattern_formatter | |||
| url: "https://pub.dartlang.org" | |||
| source: hosted | |||
| version: "1.0.2" | |||
| pedantic: | |||
| dependency: transitive | |||
| description: | |||
| @@ -18,6 +18,8 @@ dependencies: | |||
| dio: 3.0.9 | |||
| formz: ^0.3.0 | |||
| keyboard_dismisser: ^1.0.2 | |||
| fluttertoast: ^6.0.1 | |||
| pattern_formatter: ^1.0.2 | |||
| dev_dependencies: | |||
| flutter_test: | |||