| import 'package:dio/dio.dart'; | 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.dart'; | ||||
| import 'package:farm_tpf/models/user_request.dart'; | import 'package:farm_tpf/models/user_request.dart'; | ||||
| import 'package:retrofit/retrofit.dart'; | import 'package:retrofit/retrofit.dart'; | ||||
| @POST("/api/authenticate") | @POST("/api/authenticate") | ||||
| Future<User> login(@Body() UserRequest userRequest); | 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); | |||||
| } | } |
| final value = User.fromJson(_result.data); | final value = User.fromJson(_result.data); | ||||
| return value; | 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; | |||||
| } | |||||
| } | } |
| 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); | |||||
| } | |||||
| } |
| 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); | |||||
| } |
| // 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, | |||||
| }; |
| 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); | |||||
| } |
| // 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, | |||||
| }; |
| 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), | |||||
| ], | |||||
| ), | |||||
| ); | |||||
| } | |||||
| } |
| 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() | |||||
| ], | |||||
| ), | |||||
| )))); | |||||
| } |
| import 'package:farm_tpf/authentication/authentication.dart'; | import 'package:farm_tpf/authentication/authentication.dart'; | ||||
| import 'package:farm_tpf/data/repository/authentication_repository.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/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/presentation/screens/login/bloc/login_bloc.dart'; | ||||
| import 'package:farm_tpf/utils/const_color.dart'; | import 'package:farm_tpf/utils/const_color.dart'; | ||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||
| Scaffold.of(context) | Scaffold.of(context) | ||||
| ..hideCurrentSnackBar() | ..hideCurrentSnackBar() | ||||
| ..showSnackBar( | ..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) { | if (state.status.isSubmissionSuccess) { | ||||
| const Padding(padding: EdgeInsets.all(16)), | const Padding(padding: EdgeInsets.all(16)), | ||||
| _LoginButton(), | _LoginButton(), | ||||
| const Padding(padding: EdgeInsets.all(6)), | const Padding(padding: EdgeInsets.all(6)), | ||||
| _forgotPasswordButton(), | |||||
| _FogotPasswordButton(), | |||||
| _registerButton() | _registerButton() | ||||
| ], | ], | ||||
| ), | ), | ||||
| } | } | ||||
| } | } | ||||
| 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() { | Widget _registerButton() { |
| 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); | |||||
| } | |||||
| } |
| 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(); |
| description: flutter | description: flutter | ||||
| source: sdk | source: sdk | ||||
| version: "0.0.0" | version: "0.0.0" | ||||
| fluttertoast: | |||||
| dependency: "direct main" | |||||
| description: | |||||
| name: fluttertoast | |||||
| url: "https://pub.dartlang.org" | |||||
| source: hosted | |||||
| version: "6.0.1" | |||||
| formz: | formz: | ||||
| dependency: "direct main" | dependency: "direct main" | ||||
| description: | description: | ||||
| url: "https://pub.dartlang.org" | url: "https://pub.dartlang.org" | ||||
| source: hosted | source: hosted | ||||
| version: "1.0.2" | 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: | pedantic: | ||||
| dependency: transitive | dependency: transitive | ||||
| description: | description: |
| dio: 3.0.9 | dio: 3.0.9 | ||||
| formz: ^0.3.0 | formz: ^0.3.0 | ||||
| keyboard_dismisser: ^1.0.2 | keyboard_dismisser: ^1.0.2 | ||||
| fluttertoast: ^6.0.1 | |||||
| pattern_formatter: ^1.0.2 | |||||
| dev_dependencies: | dev_dependencies: | ||||
| flutter_test: | flutter_test: |